Skip to content

Commit 496fe78

Browse files
committed
Update WebView
1 parent 16ce608 commit 496fe78

File tree

1 file changed

+229
-13
lines changed

1 file changed

+229
-13
lines changed

Sources/SwiftUIComponents/UIKitWrapper/WebView.swift

Lines changed: 229 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,25 @@ import WebKit
1616
public 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 {
3646
public 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

207256
class ObserableWebView: ObservableObject {
208257
@Published var webView: WKWebView?
209258
}
210259

211260
struct 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

Comments
 (0)