/* * 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 Combine import SwiftUI import WebKit /// Holds the dynamic state of a `WebView`. final public class WebViewState : ObservableObject { var initialRequest: URLRequest? var webViewObservations: [NSKeyValueObservation] = [] var webView: WKWebView? { didSet { webViewObservations.forEach { $0.invalidate() } guard let webView = webView else { webViewObservations.removeAll() return } func register( _ keyPath: KeyPath ) -> NSKeyValueObservation where T : Equatable { webView.observe( keyPath, options: [.prior, .old, .new], changeHandler: webView(_:didChangeKeyPath:)) } // TODO: Evaluate the performance impact of pre-registering all of these observations. // We can't know what changes will trigger view updates, so we need to observe every // property that get's republished below; however, there may be more efficient ways to // achieve this. webViewObservations = [ register(\.canGoBack), register(\.canGoForward), register(\.title), register(\.url), register(\.isLoading), register(\.estimatedProgress), ] } } public let objectWillChange = ObservableObjectPublisher() /// Creates a new `WebViewState`. /// - Parameters: /// - initialURL: The `URL` to navigate to immediatly upon `WebView` creation. /// - configuration: Configuration for the `WebView`. public convenience init( initialURL: URL? = nil, configuration: WKWebViewConfiguration = .init() ) { self.init( initialRequest: initialURL.map { URLRequest(url: $0) }, configuration: configuration) } /// Creates a new `WebViewState`. /// - Parameters: /// - initialRequest: The `URLRequest` to navigate to immediatly upon `WebView` creation. /// - configuration: Configuration for the `WebView`. public init(initialRequest: URLRequest?, configuration: WKWebViewConfiguration = .init()) { self.initialRequest = initialRequest } func webView( _: WKWebView, didChangeKeyPath change: NSKeyValueObservedChange ) where Value : Equatable { if change.isPrior && change.oldValue != change.newValue { objectWillChange.send() } } // NOTE: I'm eliding documentation comments here. These properties and methods related 1:1 with // their `WKWebView` counterparts. public var canGoBack: Bool { webView?.canGoBack ?? false } public var canGoForward: Bool { webView?.canGoForward ?? false } public var title: String { webView?.title ?? "" } public var url: URL? { webView?.url } public var isLoading: Bool { webView?.isLoading ?? false } public var estimatedProgress: Double? { isLoading ? webView?.estimatedProgress : nil } public var hasOnlySecureContent: Bool { webView?.hasOnlySecureContent ?? false } public func load(_ url: URL) { load(URLRequest(url: url)) } public func load(_ request: URLRequest) { webView?.load(request) } public func goBack() { webView?.goBack() } public func goForward() { webView?.goForward() } public func reload() { webView?.reload() } public func stopLoading() { webView?.stopLoading() } func createPDF( configuration: WKPDFConfiguration = .init(), completion: @escaping (Result) -> Void ) { if let webView = webView { webView.createPDF(configuration: configuration, completionHandler: completion) } else { completion(.failure(WKError(.unknown))) } } }