@@ -16,15 +16,25 @@ import WebKit
1616public struct WebView : View {
1717 let url : String
1818 let preferredContentMode : WKWebpagePreferences . ContentMode
19+ let isInspectable : Bool
1920
2021 public init ( url: String , preferredContentMode: WKWebpagePreferences . ContentMode = . mobile) {
2122 self . url = url
2223 self . preferredContentMode = preferredContentMode
24+ self . isInspectable = false
25+ }
26+
27+ @available ( iOS 16 . 4 , * )
28+ public init ( url: String , preferredContentMode: WKWebpagePreferences . ContentMode = . mobile, isInspectable: Bool = false ) {
29+ self . url = url
30+ self . preferredContentMode = preferredContentMode
31+ self . isInspectable = isInspectable
2332 }
2433
2534 public var body : some View {
2635 WebKitWebView ( url: url,
2736 preferredContentMode: preferredContentMode,
37+ isInspectable: isInspectable,
2838 webViewObject: ObserableWebView ( ) )
2939 }
3040}
@@ -36,6 +46,7 @@ public struct WebView: View {
3646public struct NaviWebView : View {
3747 let url : String
3848 let preferredContentMode : WKWebpagePreferences . ContentMode
49+ let isInspectable : Bool
3950
4051 @State private var canGoBack : Bool = false
4152 @State private var canGoForward : Bool = false
@@ -44,6 +55,14 @@ public struct NaviWebView: View {
4455 public init ( url: String , preferredContentMode: WKWebpagePreferences . ContentMode = . mobile) {
4556 self . url = url
4657 self . preferredContentMode = preferredContentMode
58+ self . isInspectable = false
59+ }
60+
61+ @available ( iOS 16 . 4 , * )
62+ public init ( url: String , preferredContentMode: WKWebpagePreferences . ContentMode = . mobile, isInspectable: Bool = false ) {
63+ self . url = url
64+ self . preferredContentMode = preferredContentMode
65+ self . isInspectable = isInspectable
4766 }
4867
4968 public var body : some View {
@@ -82,6 +101,7 @@ public struct NaviWebView: View {
82101
83102 WebKitWebView ( url: url,
84103 preferredContentMode: preferredContentMode,
104+ isInspectable: isInspectable,
85105 webViewObject: webViewObject)
86106 }
87107 }
@@ -96,6 +116,7 @@ public struct NaviSheetWebView: View {
96116
97117 let url : String
98118 let preferredContentMode : WKWebpagePreferences . ContentMode
119+ let isInspectable : Bool
99120
100121 @State private var canGoBack : Bool = false
101122 @State private var canGoForward : Bool = false
@@ -104,12 +125,21 @@ public struct NaviSheetWebView: View {
104125 public init ( url: String , preferredContentMode: WKWebpagePreferences . ContentMode = . mobile) {
105126 self . url = url
106127 self . preferredContentMode = preferredContentMode
128+ self . isInspectable = false
129+ }
130+
131+ @available ( iOS 16 . 4 , * )
132+ public init ( url: String , preferredContentMode: WKWebpagePreferences . ContentMode = . mobile, isInspectable: Bool = false ) {
133+ self . url = url
134+ self . preferredContentMode = preferredContentMode
135+ self . isInspectable = isInspectable
107136 }
108137
109138 public var body : some View {
110139 NavigationView {
111140 WebKitWebView ( url: url,
112141 preferredContentMode: preferredContentMode,
142+ isInspectable: isInspectable,
113143 webViewObject: webViewObject)
114144 . toolbar {
115145 if #available( iOS 26 . 0 , * ) {
@@ -203,14 +233,34 @@ struct WebView_Previews: PreviewProvider {
203233
204234// MARK: - UIWebView : UIViewRepresentable
205235
236+ private let windowOpenOverrideJS = """
237+ (function() {
238+ if (window.__popupIntercepted__) { return; }
239+ window.__popupIntercepted__ = true;
240+
241+ const originalOpen = window.open;
242+ window.open = function(url, name, specs) {
243+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.windowOpen) {
244+ window.webkit.messageHandlers.windowOpen.postMessage({
245+ url: url,
246+ name: name || null,
247+ specs: specs || null
248+ });
249+ return null;
250+ }
251+ return originalOpen.apply(window, arguments);
252+ };
253+ })();
254+ """
206255
207256class ObserableWebView : ObservableObject {
208257 @Published var webView : WKWebView ?
209258}
210259
211260struct WebKitWebView : UIViewRepresentable {
212- var url : String
261+ let url : String
213262 let preferredContentMode : WKWebpagePreferences . ContentMode
263+ let isInspectable : Bool
214264 @ObservedObject var webViewObject : ObserableWebView
215265
216266 func makeCoordinator( ) -> Coordinator {
@@ -228,11 +278,27 @@ struct WebKitWebView: UIViewRepresentable {
228278 pref. preferredContentMode = preferredContentMode
229279 configuration. defaultWebpagePreferences = pref
230280
281+ let contentController = WKUserContentController ( )
282+ let script = WKUserScript (
283+ source: windowOpenOverrideJS,
284+ injectionTime: . atDocumentStart,
285+ forMainFrameOnly: false
286+ )
287+
288+ contentController. addUserScript ( script)
289+ contentController. add ( context. coordinator, name: " windowOpen " )
290+
291+ configuration. userContentController = contentController
292+ configuration. preferences. javaScriptCanOpenWindowsAutomatically = true
293+
231294 let webView = WKWebView ( frame: CGRect . zero, configuration: configuration)
232295 webView. uiDelegate = context. coordinator
233296 webView. navigationDelegate = context. coordinator
234297 webView. allowsBackForwardNavigationGestures = true
235298 webView. scrollView. isScrollEnabled = true
299+ if #available( iOS 16 . 4 , * ) {
300+ webView. isInspectable = isInspectable
301+ }
236302
237303 if let url = URL ( string: url) {
238304 webView. load ( URLRequest ( url: url) )
@@ -245,29 +311,179 @@ struct WebKitWebView: UIViewRepresentable {
245311
246312 }
247313
248- class Coordinator : NSObject , WKNavigationDelegate , WKUIDelegate {
314+ static func dismantleUIView( _ uiView: WKWebView , coordinator: Coordinator ) {
315+ coordinator. cleanupPendingJSDialogs ( )
316+ uiView. stopLoading ( )
317+ uiView. uiDelegate = nil
318+ uiView. navigationDelegate = nil
319+ }
320+
321+ class Coordinator : NSObject {
322+ enum JSDialog {
323+ case alert( ( ) -> Void )
324+ case confirm( ( Bool ) -> Void )
325+ case prompt( ( String ? ) -> Void )
326+ }
249327
250- var parent : WebKitWebView
328+ private var pendingDialog : JSDialog ?
329+ private var parent : WebKitWebView
251330
252331 init ( _ parent: WebKitWebView ) {
253332 self . parent = parent
254333 }
255334
256- func webView( _ webView: WKWebView ,
257- decidePolicyFor navigationAction: WKNavigationAction ,
258- decisionHandler: @escaping ( WKNavigationActionPolicy ) -> Void ) {
335+ func cleanupPendingJSDialogs( ) {
336+ switch pendingDialog {
337+ case . alert( let handler) :
338+ handler ( )
339+ case . confirm( let handler) :
340+ handler ( false )
341+ case . prompt( let handler) :
342+ handler ( nil )
343+ case . none:
344+ break
345+ }
346+ pendingDialog = nil
347+ }
348+ }
349+ }
350+
351+ extension WebKitWebView . Coordinator : WKNavigationDelegate {
352+ func webView( _ webView: WKWebView ,
353+ decidePolicyFor navigationAction: WKNavigationAction ,
354+ decisionHandler: @escaping ( WKNavigationActionPolicy ) -> Void ) {
355+ if navigationAction. targetFrame == nil , let url = navigationAction. request. url {
356+ webView. load ( URLRequest ( url: url) )
357+ decisionHandler ( . cancel)
259358
260- decisionHandler ( . allow )
359+ return
261360 }
262361
263- func webView( _ webView: WKWebView , didFinish navigation: WKNavigation ! ) {
264- parent. webViewObject. webView = webView
362+ decisionHandler ( . allow)
363+ }
364+
365+ func webView( _ webView: WKWebView , didFinish navigation: WKNavigation ! ) {
366+ parent. webViewObject. webView = webView
367+ }
368+ }
369+
370+ extension WebKitWebView . Coordinator : WKUIDelegate {
371+ func webView( _ webView: WKWebView , createWebViewWith configuration: WKWebViewConfiguration , for navigationAction: WKNavigationAction , windowFeatures: WKWindowFeatures ) -> WKWebView ? {
372+ webView. load ( navigationAction. request)
373+
374+ return nil
375+ }
376+
377+ func webView(
378+ _ webView: WKWebView ,
379+ runJavaScriptAlertPanelWithMessage message: String ,
380+ initiatedByFrame frame: WKFrameInfo ,
381+ completionHandler: @escaping ( ) -> Void
382+ ) {
383+ pendingDialog = . alert( completionHandler)
384+
385+ let alert = UIAlertController ( title: nil , message: message, preferredStyle: . alert)
386+ alert. addAction ( UIAlertAction ( title: nil , style: . default) { [ weak self] _ in
387+ completionHandler ( )
388+ self ? . pendingDialog = nil
389+ } )
390+
391+ UIApplication . shared. topViewController ( ) ? . present ( alert, animated: true )
392+ }
393+
394+ func webView(
395+ _ webView: WKWebView ,
396+ runJavaScriptConfirmPanelWithMessage message: String ,
397+ initiatedByFrame frame: WKFrameInfo ,
398+ completionHandler: @escaping ( Bool ) -> Void
399+ ) {
400+ pendingDialog = . confirm( completionHandler)
401+
402+ let alert = UIAlertController ( title: nil , message: message, preferredStyle: . alert)
403+
404+ alert. addAction ( UIAlertAction ( title: nil , style: . cancel) { [ weak self] _ in
405+ completionHandler ( false )
406+ self ? . pendingDialog = nil
407+ } )
408+
409+ alert. addAction ( UIAlertAction ( title: nil , style: . default) { [ weak self] _ in
410+ completionHandler ( true )
411+ self ? . pendingDialog = nil
412+ } )
413+
414+ UIApplication . shared. topViewController ( ) ? . present ( alert, animated: true )
415+ }
416+
417+ func webView(
418+ _ webView: WKWebView ,
419+ runJavaScriptTextInputPanelWithPrompt prompt: String ,
420+ defaultText: String ? ,
421+ initiatedByFrame frame: WKFrameInfo ,
422+ completionHandler: @escaping ( String ? ) -> Void
423+ ) {
424+ pendingDialog = . prompt( completionHandler)
425+
426+ let alert = UIAlertController ( title: nil , message: prompt, preferredStyle: . alert)
427+ alert. addTextField { $0. text = defaultText }
428+
429+ alert. addAction ( UIAlertAction ( title: nil , style: . cancel) { [ weak self] _ in
430+ completionHandler ( nil )
431+ self ? . pendingDialog = nil
432+ } )
433+
434+ alert. addAction ( UIAlertAction ( title: nil , style: . default) { [ weak self] _ in
435+ completionHandler ( alert. textFields? . first? . text)
436+ self ? . pendingDialog = nil
437+ } )
438+
439+ UIApplication . shared. topViewController ( ) ? . present ( alert, animated: true )
440+ }
441+ }
442+
443+ extension WebKitWebView . Coordinator : WKScriptMessageHandler {
444+ func userContentController( _ userContentController: WKUserContentController , didReceive message: WKScriptMessage ) {
445+ guard
446+ message. name == " windowOpen " ,
447+ let body = message. body as? [ String : Any ] ,
448+ let urlString = body [ " url " ] as? String ,
449+ let url = URL ( string: urlString)
450+ else { return }
451+
452+ if let webView = message. webView {
453+ webView. load ( URLRequest ( url: url) )
265454 }
455+ }
456+ }
457+
458+ fileprivate extension UIApplication {
459+ func topViewController(
460+ base: UIViewController ? = nil
461+ ) -> UIViewController ? {
266462
267- func webView( _ webView: WKWebView , createWebViewWith configuration: WKWebViewConfiguration , for navigationAction: WKNavigationAction , windowFeatures: WKWindowFeatures ) -> WKWebView ? {
268- webView. load ( navigationAction. request)
269-
270- return nil
463+ let baseVC : UIViewController ?
464+
465+ if let base = base {
466+ baseVC = base
467+ } else {
468+ baseVC = connectedScenes
469+ . compactMap { $0 as? UIWindowScene }
470+ . flatMap { $0. windows }
471+ . first { $0. isKeyWindow } ?
472+ . rootViewController
473+ }
474+
475+ if let nav = baseVC as? UINavigationController {
476+ return topViewController ( base: nav. visibleViewController)
477+ }
478+
479+ if let tab = baseVC as? UITabBarController {
480+ return topViewController ( base: tab. selectedViewController)
481+ }
482+
483+ if let presented = baseVC? . presentedViewController {
484+ return topViewController ( base: presented)
271485 }
486+
487+ return baseVC
272488 }
273489}
0 commit comments