blob: 1e507b5fa8ebf74bd45fa339557e75e07cc0ffb0 [file] [log] [blame]
/*
* 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<Dialog?>? = 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<Dialog?>? = 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<T>(dynamicMember keyPath: ReferenceWritableKeyPath<WebViewState, T>) -> 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)
}
}