import SwiftUI import WebKit import UIKit // État partagé entre le WKWebView et l'interface SwiftUI final class WebModel: ObservableObject { @Published var progress: Double = 0 @Published var loaded = false @Published var failed = false var reloadRequested = false var webView: WKWebView? func reload() { failed = false loaded = false progress = 0 if let wv = webView { if wv.url == nil { wv.load(URLRequest(url: Ka.url)) } else { wv.reload() } } } } struct WebView: UIViewRepresentable { @ObservedObject var model: WebModel func makeCoordinator() -> Coordinator { Coordinator(model: model) } func makeUIView(context: Context) -> WKWebView { let cfg = WKWebViewConfiguration() cfg.websiteDataStore = .default() // persiste le cookie de session (reste connecté) cfg.allowsInlineMediaPlayback = true cfg.defaultWebpagePreferences.allowsContentJavaScript = true let wv = WKWebView(frame: .zero, configuration: cfg) wv.navigationDelegate = context.coordinator wv.uiDelegate = context.coordinator wv.allowsBackForwardNavigationGestures = false wv.scrollView.bounces = true wv.scrollView.contentInsetAdjustmentBehavior = .never wv.isOpaque = false wv.backgroundColor = UIColor(Ka.bg) wv.scrollView.backgroundColor = UIColor(Ka.bg) wv.customUserAgent = "AdminKa-iOS/1.0 (Groupe KA console)" // pull-to-refresh natif let rc = UIRefreshControl() rc.tintColor = UIColor(Ka.orange) rc.addTarget(context.coordinator, action: #selector(Coordinator.onRefresh(_:)), for: .valueChanged) wv.scrollView.refreshControl = rc context.coordinator.observe(wv) model.webView = wv wv.load(URLRequest(url: Ka.url)) return wv } func updateUIView(_ uiView: WKWebView, context: Context) {} final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate { let model: WebModel private var obs: NSKeyValueObservation? init(model: WebModel) { self.model = model } func observe(_ wv: WKWebView) { obs = wv.observe(\.estimatedProgress, options: [.new]) { [weak self] wv, _ in DispatchQueue.main.async { self?.model.progress = wv.estimatedProgress } } } @objc func onRefresh(_ sender: UIRefreshControl) { model.webView?.reload() } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { webView.scrollView.refreshControl?.endRefreshing() DispatchQueue.main.async { withAnimation(.easeOut(duration: 0.35)) { self.model.loaded = true } self.model.failed = false } } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { fail(error) } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { fail(error) } private func fail(_ error: Error) { let ns = error as NSError if ns.code == NSURLErrorCancelled { return } DispatchQueue.main.async { self.model.webView?.scrollView.refreshControl?.endRefreshing() self.model.failed = true } } // liens externes (target=_blank / http externes) → Safari func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if let url = navigationAction.request.url { UIApplication.shared.open(url) } return nil } // --- Panneaux JavaScript natifs (indispensables : sans eux confirm() // --- ne s'affiche pas → le mode BYPASS et les confirmations restent morts) --- private func topVC(_ webView: WKWebView) -> UIViewController? { var vc = webView.window?.rootViewController while let p = vc?.presentedViewController { vc = p } return vc } func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { let a = UIAlertController(title: nil, message: message, preferredStyle: .alert) a.addAction(UIAlertAction(title: "OK", style: .default) { _ in completionHandler() }) guard let vc = topVC(webView) else { completionHandler(); return } vc.present(a, animated: true) } func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { let a = UIAlertController(title: nil, message: message, preferredStyle: .alert) a.addAction(UIAlertAction(title: "Annuler", style: .cancel) { _ in completionHandler(false) }) a.addAction(UIAlertAction(title: "OK", style: .default) { _ in completionHandler(true) }) guard let vc = topVC(webView) else { completionHandler(false); return } vc.present(a, animated: true) } func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { let a = UIAlertController(title: nil, message: prompt, preferredStyle: .alert) a.addTextField { $0.text = defaultText } a.addAction(UIAlertAction(title: "Annuler", style: .cancel) { _ in completionHandler(nil) }) a.addAction(UIAlertAction(title: "OK", style: .default) { _ in completionHandler(a.textFields?.first?.text) }) guard let vc = topVC(webView) else { completionHandler(nil); return } vc.present(a, animated: true) } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url, let host = url.host, navigationAction.navigationType == .linkActivated, host != Ka.url.host { UIApplication.shared.open(url) decisionHandler(.cancel) return } decisionHandler(.allow) } } }