/* * Copyright (C) 2020 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ import SwiftUI import WebKit /// A view that displays interactive web content. /// /// A `WebView` is useful in any scenario that requires rendering HTML, such as for a web browser, /// ePub reader, or simply to display local HTML resources. It provides basic behaviors for handling /// web content by default, which can be customized to provide alternate behaviors. public struct WebView : View { /// Checks whether or not WebView can handle the given URL by default. public static func canHandle(_ url: URL) -> Bool { return url.scheme.map(WKWebView.handlesURLScheme(_:)) ?? false } fileprivate var state: WebViewState @State private var defaultDialog: Dialog? = nil private var customDialog: Binding? = nil fileprivate var dialog: Dialog? { get { (self.customDialog ?? self.$defaultDialog).wrappedValue } nonmutating set { (self.customDialog ?? self.$defaultDialog).wrappedValue = newValue } } private var useInternalDialogHandling = true /// Create a new `WebView` instance. /// /// In order to store the state of webpages, a `WebViewState` instance must be provided by the /// caller. Once the web view is displayed its state can be both read and modified from the /// `WebViewState`, such as determining whether the `WebView` contains back-forward history, or /// to reload the current page. /// /// - Parameters: /// - state: State to be associated with this web view. /// - dialog: A binding representing any dialog that the webpage needs displayed. A custom /// binding should only be provided if you need to customize the display of dialogs, otherwise /// `WebView` will manage display. public init(state: WebViewState, dialog: Binding? = nil) { self.state = state self.customDialog = dialog self.useInternalDialogHandling = dialog == nil } public var body: some View { _WebView(owner: self) .overlay(dialogView) } @ViewBuilder private var dialogView: some View { if useInternalDialogHandling, let configuration = dialog?.configuration { switch configuration { case let .javaScriptAlert(message, completion): JavaScriptAlert(message: message, completion: { dialog = nil completion() }) case let .javaScriptConfirm(message, completion): JavaScriptConfirm(message: message, completion: { dialog = nil completion($0) }) case let .javaScriptPrompt(message, defaultText, completion): JavaScriptPrompt(message: message, defaultText: defaultText, completion: { dialog = nil completion($0) }) } } else { EmptyView().hidden() } } } private struct _WebView { let owner: WebView func makeView(coordinator: Coordinator, environment: EnvironmentValues) -> WKWebView { let view = WKWebView() view.navigationDelegate = coordinator view.uiDelegate = coordinator coordinator.webView = view coordinator.environment = environment if let request = coordinator.initialRequest { view.load(request) } return view } func updateView(_ view: WKWebView, coordinator: Coordinator, environment: EnvironmentValues) { coordinator.environment = environment if let flag = environment.allowsBackForwardNavigationGestures { view.allowsBackForwardNavigationGestures = flag } } static func dismantleView(_ view: WKWebView, coordinator: Coordinator) { coordinator.webView = nil } func makeCoordinator() -> Coordinator { Coordinator(owner: owner) } } #if os(macOS) extension _WebView : NSViewRepresentable { func makeNSView(context: Context) -> WKWebView { makeView(coordinator: context.coordinator, environment: context.environment) } func updateNSView(_ nsView: WKWebView, context: Context) { updateView(nsView, coordinator: context.coordinator, environment: context.environment) } static func dismantleNSView(_ nsView: WKWebView, coordinator: Coordinator) { dismantleView(nsView, coordinator: coordinator) } } #else extension _WebView : UIViewRepresentable { func makeUIView(context: Context) -> WKWebView { makeView(coordinator: context.coordinator, environment: context.environment) } func updateUIView(_ uiView: WKWebView, context: Context) { updateView(uiView, coordinator: context.coordinator, environment: context.environment) } static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) { dismantleView(uiView, coordinator: coordinator) } } #endif @dynamicMemberLookup private final class Coordinator : NSObject, WKNavigationDelegate, WKUIDelegate { private var owner: WebView fileprivate var environment: EnvironmentValues? init(owner: WebView) { self.owner = owner } // MARK: WebViewState Proxy var webView: WKWebView? { get { owner.state.webView } set { owner.state.webView = newValue } } subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> T { get { owner.state[keyPath: keyPath] } set { owner.state[keyPath: keyPath] = newValue } } // MARK: Navigation func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void ) { if let decider = environment?.navigationActionDecider { let action = NavigationAction( navigationAction, webpagePreferences: preferences, reply: decisionHandler) decider(action, owner.state) } else { decisionHandler(.allow, preferences) } } func webView( _ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void ) { if let decider = environment?.navigationResponseDecider { let response = NavigationResponse(navigationResponse, reply: decisionHandler) decider(response, owner.state) } else { decisionHandler(.allow) } } // MARK: Dialogs func webView( _ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void ) { owner.dialog = .javaScriptAlert(message, completion: completionHandler) } func webView( _ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void ) { owner.dialog = .javaScriptConfirm(message, completion: completionHandler) } func webView( _ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void ) { owner.dialog = .javaScriptPrompt( prompt, defaultText: defaultText ?? "", completion: completionHandler) } }