From bb54ac1c9973dc01262119f0b0925f67c942264c Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Fri, 1 Jul 2022 15:55:58 +0200 Subject: [PATCH] feat: Add CapWebView (#5715) (#5730) Co-authored-by: Ely Lucas Co-authored-by: Steven Sherry Co-authored-by: Steven Sherry --- .../Capacitor.xcodeproj/project.pbxproj | 4 + ios/Capacitor/Capacitor/CAPWebView.swift | 227 ++++++++++++++++++ ios/Capacitor/Capacitor/JSTypes.swift | 9 + 3 files changed, 240 insertions(+) create mode 100644 ios/Capacitor/Capacitor/CAPWebView.swift diff --git a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj index 75fe641804..b380626c50 100644 --- a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj +++ b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 501CBAA71FC0A723009B0D4D /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 501CBAA61FC0A723009B0D4D /* WebKit.framework */; }; 50503EE91FC08595003606DC /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50503EDF1FC08594003606DC /* Capacitor.framework */; }; 50503EEE1FC08595003606DC /* CapacitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50503EED1FC08595003606DC /* CapacitorTests.swift */; }; + 55F6736A26C371E6001E7AB9 /* CAPWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F6736826C371E6001E7AB9 /* CAPWebView.swift */; }; 6214934725509C3F006C36F9 /* CAPInstanceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6214934625509C3F006C36F9 /* CAPInstanceConfiguration.swift */; }; 621ECCB72542045900D3D615 /* CAPBridgedJSTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 621ECCB42542045900D3D615 /* CAPBridgedJSTypes.m */; }; 621ECCB82542045900D3D615 /* CAPBridgedJSTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 621ECCB62542045900D3D615 /* CAPBridgedJSTypes.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -148,6 +149,7 @@ 50503EDF1FC08594003606DC /* Capacitor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Capacitor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50503EE81FC08595003606DC /* CapacitorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CapacitorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50503EED1FC08595003606DC /* CapacitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapacitorTests.swift; sourceTree = ""; }; + 55F6736826C371E6001E7AB9 /* CAPWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPWebView.swift; sourceTree = ""; }; 6214934625509C3F006C36F9 /* CAPInstanceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CAPInstanceConfiguration.swift; sourceTree = ""; }; 621ECCB42542045900D3D615 /* CAPBridgedJSTypes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CAPBridgedJSTypes.m; sourceTree = ""; }; 621ECCB62542045900D3D615 /* CAPBridgedJSTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAPBridgedJSTypes.h; sourceTree = ""; }; @@ -336,6 +338,7 @@ 623D6913254C7030002D01D1 /* CAPInstanceDescriptor.swift */, 623D691B254C7462002D01D1 /* CAPInstanceConfiguration.h */, 623D691C254C7462002D01D1 /* CAPInstanceConfiguration.m */, + 55F6736826C371E6001E7AB9 /* CAPWebView.swift */, 6214934625509C3F006C36F9 /* CAPInstanceConfiguration.swift */, 62959B082524DA7700A3D7F1 /* CAPLog.swift */, 62959AE72524DA7700A3D7F1 /* CAPFile.swift */, @@ -589,6 +592,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 55F6736A26C371E6001E7AB9 /* CAPWebView.swift in Sources */, A71289E627F380A500DADDF3 /* Router.swift in Sources */, 62959B362524DA7800A3D7F1 /* CAPBridgeViewController.swift in Sources */, 621ECCB72542045900D3D615 /* CAPBridgedJSTypes.m in Sources */, diff --git a/ios/Capacitor/Capacitor/CAPWebView.swift b/ios/Capacitor/Capacitor/CAPWebView.swift new file mode 100644 index 0000000000..f7d3784f62 --- /dev/null +++ b/ios/Capacitor/Capacitor/CAPWebView.swift @@ -0,0 +1,227 @@ +import Foundation +import WebKit +import UIKit + +open class CAPWebView: UIView { + lazy var webView: WKWebView = createWebView( + with: configuration, + assetHandler: assetHandler, + delegationHandler: delegationHandler + ) + + private lazy var capacitorBridge = CapacitorBridge( + with: configuration, + delegate: self, + cordovaConfiguration: configDescriptor.cordovaConfiguration, + assetHandler: assetHandler, + delegationHandler: delegationHandler + ) + + public final var bridge: CAPBridgeProtocol { + return capacitorBridge + } + + private lazy var configDescriptor = instanceDescriptor() + private lazy var configuration = InstanceConfiguration(with: configDescriptor, isDebug: CapacitorBridge.isDevEnvironment) + + private lazy var assetHandler: WebViewAssetHandler = { + let handler = WebViewAssetHandler(router: _Router()) + handler.setAssetPath(configuration.appLocation.path) + return handler + }() + + private lazy var delegationHandler = WebViewDelegationHandler() + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + public init() { + super.init(frame: .zero) + setup() + } + + private func setup() { + CAPLog.enableLogging = configuration.loggingEnabled + logWarnings(for: configDescriptor) + + if configDescriptor.instanceType == .fixed { updateBinaryVersion() } + + addSubview(webView) + webView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: topAnchor), + webView.bottomAnchor.constraint(equalTo: bottomAnchor), + webView.leadingAnchor.constraint(equalTo: leadingAnchor), + webView.trailingAnchor.constraint(equalTo: trailingAnchor) + ]) + + guard FileManager.default.fileExists(atPath: bridge.config.appStartFileURL.path) else { fatalLoadError() } + capacitorDidLoad() + + let url = bridge.config.appStartServerURL + CAPLog.print("⚡️ Loading app at \(url.absoluteString)") + capacitorBridge.webViewDelegationHandler.willLoadWebview(webView) + _ = webView.load(URLRequest(url: url)) + } + + public lazy final var isNewBinary: Bool = { + if let curVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, + let curVersionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + if let lastVersionCode = UserDefaults.standard.string(forKey: "lastBinaryVersionCode"), + let lastVersionName = UserDefaults.standard.string(forKey: "lastBinaryVersionName") { + return (curVersionCode.isEqual(lastVersionCode) == false || curVersionName.isEqual(lastVersionName) == false) + } + } + return false + }() + + open func instanceDescriptor() -> InstanceDescriptor { + let descriptor = InstanceDescriptor.init() + if !isNewBinary && !descriptor.cordovaDeployDisabled { + if let persistedPath = UserDefaults.standard.string(forKey: "serverBasePath"), !persistedPath.isEmpty { + if let libPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first { + descriptor.appLocation = URL(fileURLWithPath: libPath, isDirectory: true) + .appendingPathComponent("NoCloud") + .appendingPathComponent("ionic_built_snapshots") + .appendingPathComponent(URL(fileURLWithPath: persistedPath, isDirectory: true).lastPathComponent) + } + } + } + return descriptor + } + + /** + Allows any additional configuration to be performed. The `webView` and `bridge` properties will be set by this point. + + - Note: This is called before the webview has been added to the view hierarchy. Not all operations may be possible at + this time. + */ + open func capacitorDidLoad() { + } + + open func loadInitialContext(_ userContentController: WKUserContentController) { + CAPLog.print("in loadInitialContext base") + } +} + +extension CAPWebView { + + open func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration { + let webViewConfiguration = WKWebViewConfiguration() + webViewConfiguration.allowsInlineMediaPlayback = true + webViewConfiguration.suppressesIncrementalRendering = false + webViewConfiguration.allowsAirPlayForMediaPlayback = true + webViewConfiguration.mediaTypesRequiringUserActionForPlayback = [] + if let appendUserAgent = instanceConfiguration.appendedUserAgentString { + if let appName = webViewConfiguration.applicationNameForUserAgent { + webViewConfiguration.applicationNameForUserAgent = "\(appName) \(appendUserAgent)" + } else { + webViewConfiguration.applicationNameForUserAgent = appendUserAgent + } + } + return webViewConfiguration + } + + private func createWebView(with configuration: InstanceConfiguration, assetHandler: WebViewAssetHandler, delegationHandler: WebViewDelegationHandler) -> WKWebView { + // set the cookie policy + HTTPCookieStorage.shared.cookieAcceptPolicy = HTTPCookie.AcceptPolicy.always + // setup the web view configuration + let webViewConfig = webViewConfiguration(for: configuration) + webViewConfig.setURLSchemeHandler(assetHandler, forURLScheme: configuration.localURL.scheme ?? InstanceDescriptorDefaults.scheme) + webViewConfig.userContentController = delegationHandler.contentController + // create the web view and set its properties + loadInitialContext(webViewConfig.userContentController) + let webView = WKWebView(frame: .zero, configuration: webViewConfig) + webView.scrollView.bounces = false + webView.scrollView.contentInsetAdjustmentBehavior = configuration.contentInsetAdjustmentBehavior + webView.allowsLinkPreview = configuration.allowLinkPreviews + webView.configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs") + webView.scrollView.isScrollEnabled = configuration.scrollingEnabled + + if let overrideUserAgent = configuration.overridenUserAgentString { + webView.customUserAgent = overrideUserAgent + } + + if let backgroundColor = configuration.backgroundColor { + self.backgroundColor = backgroundColor + webView.backgroundColor = backgroundColor + webView.scrollView.backgroundColor = backgroundColor + } else if #available(iOS 13, *) { + // Use the system background colors if background is not set by user + self.backgroundColor = UIColor.systemBackground + webView.backgroundColor = UIColor.systemBackground + webView.scrollView.backgroundColor = UIColor.systemBackground + } + + // set our delegates + webView.uiDelegate = delegationHandler + webView.navigationDelegate = delegationHandler + return webView + } + + private func logWarnings(for descriptor: InstanceDescriptor) { + if descriptor.warnings.contains(.missingAppDir) { + CAPLog.print("⚡️ ERROR: Unable to find application directory at: \"\(descriptor.appLocation.absoluteString)\"!") + } + if descriptor.instanceType == .fixed { + if descriptor.warnings.contains(.missingFile) { + CAPLog.print("Unable to find capacitor.config.json, make sure it exists and run npx cap copy.") + } + if descriptor.warnings.contains(.invalidFile) { + CAPLog.print("Unable to parse capacitor.config.json. Make sure it's valid JSON.") + } + if descriptor.warnings.contains(.missingCordovaFile) { + CAPLog.print("Unable to find config.xml, make sure it exists and run npx cap copy.") + } + if descriptor.warnings.contains(.invalidCordovaFile) { + CAPLog.print("Unable to parse config.xml. Make sure it's valid XML.") + } + } + } + + private func updateBinaryVersion() { + guard isNewBinary else { + return + } + guard let versionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, + let versionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { + return + } + let prefs = UserDefaults.standard + prefs.set(versionCode, forKey: "lastBinaryVersionCode") + prefs.set(versionName, forKey: "lastBinaryVersionName") + prefs.set("", forKey: "serverBasePath") + prefs.synchronize() + } + + private func fatalLoadError() -> Never { + printLoadError() + exit(1) + } + + private func printLoadError() { + let fullStartPath = capacitorBridge.config.appStartFileURL.path + + CAPLog.print("⚡️ ERROR: Unable to load \(fullStartPath)") + CAPLog.print("⚡️ This file is the root of your web app and must exist before") + CAPLog.print("⚡️ Capacitor can run. Ensure you've run capacitor copy at least") + CAPLog.print("⚡️ or, if embedding, that this directory exists as a resource directory.") + } +} + +extension CAPWebView: CAPBridgeDelegate { + internal var bridgedWebView: WKWebView? { + return webView + } + + internal var bridgedViewController: UIViewController? { + // search for the parent view controller + var object = self.next + while !(object is UIViewController) && object != nil { + object = object?.next + } + return object as? UIViewController + } +} diff --git a/ios/Capacitor/Capacitor/JSTypes.swift b/ios/Capacitor/Capacitor/JSTypes.swift index 592a1d4d4b..7d4ce144f9 100644 --- a/ios/Capacitor/Capacitor/JSTypes.swift +++ b/ios/Capacitor/Capacitor/JSTypes.swift @@ -125,6 +125,15 @@ public protocol JSValueContainer: JSStringContainer, JSBoolContainer, JSIntConta } extension JSValueContainer { + public func getValue(_ key: String) -> JSValue? { + return jsObjectRepresentation[key] + } + + @available(*, message: "All values returned conform to JSValue, use getValue(_:) instead.", renamed: "getValue(_:)") + public func getAny(_ key: String) -> Any? { + return getValue(key) + } + public func getString(_ key: String) -> String? { return jsObjectRepresentation[key] as? String }