Swift 100%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// SiteBrowserView.swift — les sites du Groupe KA DANS l'app : navigateur3// WKWebView intégré aux couleurs de l'univers (progression, retour/avancer,4// recharger, partager, ouvrir dans Safari). Utilisé en plein écran depuis5// chaque univers et inline pour les univers sans liste native (hub).6import SwiftUI7import WebKit89// MARK: - Modèle observable du WKWebView1011final class SiteWebModel: NSObject, ObservableObject, WKNavigationDelegate {12 let webView: WKWebView13 @Published var progress: Double = 014 @Published var loading = false15 @Published var canGoBack = false16 @Published var canGoForward = false17 @Published var currentURL: URL1819 private var observers: [NSKeyValueObservation] = []2021 init(start: URL) {22 currentURL = start23 let config = WKWebViewConfiguration()24 config.allowsInlineMediaPlayback = true25 config.defaultWebpagePreferences.allowsContentJavaScript = true26 webView = WKWebView(frame: .zero, configuration: config)27 super.init()28 webView.navigationDelegate = self29 webView.allowsBackForwardNavigationGestures = true30 webView.isOpaque = false31 observers = [32 webView.observe(\.estimatedProgress) { [weak self] w, _ in33 DispatchQueue.main.async { self?.progress = w.estimatedProgress }34 },35 webView.observe(\.isLoading) { [weak self] w, _ in36 DispatchQueue.main.async { self?.loading = w.isLoading }37 },38 webView.observe(\.canGoBack) { [weak self] w, _ in39 DispatchQueue.main.async { self?.canGoBack = w.canGoBack }40 },41 webView.observe(\.canGoForward) { [weak self] w, _ in42 DispatchQueue.main.async { self?.canGoForward = w.canGoForward }43 },44 webView.observe(\.url) { [weak self] w, _ in45 DispatchQueue.main.async { if let u = w.url { self?.currentURL = u } }46 },47 ]48 webView.load(URLRequest(url: start))49 }5051 // http(s) reste dans la vue ; tel:/mailto:/itms… → le système52 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,53 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {54 if let u = navigationAction.request.url,55 let scheme = u.scheme?.lowercased(),56 !["http", "https", "about", "blob", "data"].contains(scheme) {57 UIApplication.shared.open(u)58 decisionHandler(.cancel)59 return60 }61 decisionHandler(.allow)62 }6364 // les target="_blank" s'ouvrent dans la même vue65 func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,66 for navigationAction: WKNavigationAction,67 windowFeatures: WKWindowFeatures) -> WKWebView? {68 if let u = navigationAction.request.url { webView.load(URLRequest(url: u)) }69 return nil70 }71}7273private struct WebViewContainer: UIViewRepresentable {74 let webView: WKWebView75 func makeUIView(context: Context) -> WKWebView { webView }76 func updateUIView(_ uiView: WKWebView, context: Context) {}77}7879// MARK: - Écran navigateur (embarquable : web + progression + barre de contrôle)8081struct SiteBrowserScreen: View {82 let accent: Color83 @ObservedObject var model: SiteWebModel84 @Environment(\.colorScheme) private var scheme8586 var body: some View {87 VStack(spacing: 0) {88 WebViewContainer(webView: model.webView)89 .overlay(alignment: .top) {90 if model.loading {91 ProgressView(value: max(model.progress, 0.06))92 .progressViewStyle(.linear)93 .tint(accent)94 }95 }96 controls97 }98 .background(KATheme.paper(scheme))99 }100101 private var controls: some View {102 HStack(spacing: 26) {103 Button { Haptics.tap(); model.webView.goBack() } label: {104 Image(systemName: "chevron.backward")105 }106 .disabled(!model.canGoBack)107 .accessibilityLabel("Page précédente")108 Button { Haptics.tap(); model.webView.goForward() } label: {109 Image(systemName: "chevron.forward")110 }111 .disabled(!model.canGoForward)112 .accessibilityLabel("Page suivante")113 Spacer()114 Text(model.currentURL.host()?.replacingOccurrences(of: "www.", with: "") ?? "")115 .font(.system(.caption2, design: .monospaced).weight(.bold))116 .foregroundStyle(.secondary)117 .lineLimit(1)118 Spacer()119 ShareLink(item: model.currentURL) { Image(systemName: "square.and.arrow.up") }120 .accessibilityLabel("Partager la page")121 Button {122 Haptics.tap()123 if model.loading { model.webView.stopLoading() } else { model.webView.reload() }124 } label: {125 Image(systemName: model.loading ? "xmark" : "arrow.clockwise")126 }127 .accessibilityLabel(model.loading ? "Arrêter" : "Recharger")128 Link(destination: model.currentURL) { Image(systemName: "safari") }129 .accessibilityLabel("Ouvrir dans Safari")130 }131 .font(.body.weight(.semibold))132 .tint(accent)133 .padding(.horizontal, 18)134 .padding(.vertical, 10)135 .background(.bar)136 }137}138139// MARK: - Site embarqué inline (univers sans liste native, ex. le hub)140141struct EmbeddedSite: View {142 let universe: Universe143 @StateObject private var model: SiteWebModel144145 init(universe: Universe) {146 self.universe = universe147 _model = StateObject(wrappedValue: SiteWebModel(start: universe.baseURL))148 }149150 var body: some View {151 SiteBrowserScreen(accent: universe.accent, model: model)152 }153}154155// MARK: - Plein écran (couverture) : titre + logo + fermer156157struct SiteBrowserCover: View {158 let title: String159 let accent: Color160 let logoID: String161 @StateObject private var model: SiteWebModel162 @Environment(\.dismiss) private var dismiss163164 init(universe: Universe, path: String? = nil) {165 title = universe.name166 accent = universe.accent167 logoID = universe.id168 let url = path.map { universe.baseURL.appendingPathComponent($0) } ?? universe.baseURL169 _model = StateObject(wrappedValue: SiteWebModel(start: url))170 }171172 init(title: String, accent: Color, logoID: String, url: URL) {173 self.title = title174 self.accent = accent175 self.logoID = logoID176 _model = StateObject(wrappedValue: SiteWebModel(start: url))177 }178179 var body: some View {180 NavigationStack {181 SiteBrowserScreen(accent: accent, model: model)182 .toolbar {183 ToolbarItem(placement: .topBarLeading) {184 Button { dismiss() } label: {185 Image(systemName: "xmark")186 }187 .accessibilityLabel("Fermer le site")188 }189 ToolbarItem(placement: .principal) {190 HStack(spacing: 8) {191 KALogo(assetID: logoID, size: 24)192 Text(title).font(.headline)193 }194 }195 }196 .navigationBarTitleDisplayMode(.inline)197 .toolbarBackground(.visible, for: .navigationBar)198 }199 .tint(accent)200 }201}202