Skip to content

Commit

Permalink
feat: Add CapWebView (#5715) (#5730)
Browse files Browse the repository at this point in the history

Co-authored-by: Ely Lucas <ely@meta-tek.net>
Co-authored-by: Steven Sherry <steven.r.sherry@gmail.com>
Co-authored-by: Steven Sherry <steven.sherry@ionic.io>
  • Loading branch information
4 people committed Jul 1, 2022
1 parent 1e06a19 commit bb54ac1
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 0 deletions.
4 changes: 4 additions & 0 deletions ios/Capacitor/Capacitor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -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 = "<group>"; };
55F6736826C371E6001E7AB9 /* CAPWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPWebView.swift; sourceTree = "<group>"; };
6214934625509C3F006C36F9 /* CAPInstanceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CAPInstanceConfiguration.swift; sourceTree = "<group>"; };
621ECCB42542045900D3D615 /* CAPBridgedJSTypes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CAPBridgedJSTypes.m; sourceTree = "<group>"; };
621ECCB62542045900D3D615 /* CAPBridgedJSTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAPBridgedJSTypes.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
227 changes: 227 additions & 0 deletions ios/Capacitor/Capacitor/CAPWebView.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
9 changes: 9 additions & 0 deletions ios/Capacitor/Capacitor/JSTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit bb54ac1

Please sign in to comment.