Skip to content

Commit

Permalink
chore: WebViewSyncPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
crleona committed Jan 9, 2025
1 parent 98a5f02 commit 9365cdb
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
3A3036482A4B45780004CF0B /* TroubleShootingPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3036472A4B45780004CF0B /* TroubleShootingPlugin.swift */; };
3A42EB922B87CDE30044FD45 /* FilterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A42EB912B87CDE30044FD45 /* FilterPlugin.swift */; };
3A4E19BD2941D885002EA8BC /* IDFACollectionPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4E19BC2941D86E002EA8BC /* IDFACollectionPlugin.swift */; };
4E15DDC42D3040F30061D89B /* WebViewSyncPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E15DDC32D3040EE0061D89B /* WebViewSyncPlugin.swift */; };
58324F75294BF7CF00C71E2E /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58324F74294BF7CF00C71E2E /* WidgetKit.framework */; };
58324F77294BF7CF00C71E2E /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58324F76294BF7CF00C71E2E /* SwiftUI.framework */; };
58324F7A294BF7CF00C71E2E /* iOSWidgetExampleBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58324F79294BF7CF00C71E2E /* iOSWidgetExampleBundle.swift */; };
Expand Down Expand Up @@ -91,6 +92,7 @@
3A3036472A4B45780004CF0B /* TroubleShootingPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TroubleShootingPlugin.swift; sourceTree = "<group>"; };
3A42EB912B87CDE30044FD45 /* FilterPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPlugin.swift; sourceTree = "<group>"; };
3A4E19BC2941D86E002EA8BC /* IDFACollectionPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDFACollectionPlugin.swift; sourceTree = "<group>"; };
4E15DDC32D3040EE0061D89B /* WebViewSyncPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSyncPlugin.swift; sourceTree = "<group>"; };
58324F73294BF7CF00C71E2E /* iOSWidgetExampleExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = iOSWidgetExampleExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
58324F74294BF7CF00C71E2E /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
58324F76294BF7CF00C71E2E /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -205,6 +207,7 @@
3A4E19BB2941D86E002EA8BC /* ExamplePlugins */ = {
isa = PBXGroup;
children = (
4E15DDC32D3040EE0061D89B /* WebViewSyncPlugin.swift */,
BA541E9A2A8B587E0088D841 /* LocationPlugin.swift */,
3A4E19BC2941D86E002EA8BC /* IDFACollectionPlugin.swift */,
3A3036472A4B45780004CF0B /* TroubleShootingPlugin.swift */,
Expand Down Expand Up @@ -397,6 +400,7 @@
3A4E19BD2941D885002EA8BC /* IDFACollectionPlugin.swift in Sources */,
58324F84294BF7CF00C71E2E /* iOSWidgetExample.intentdefinition in Sources */,
3A3036482A4B45780004CF0B /* TroubleShootingPlugin.swift in Sources */,
4E15DDC42D3040F30061D89B /* WebViewSyncPlugin.swift in Sources */,
3A42EB922B87CDE30044FD45 /* FilterPlugin.swift in Sources */,
BA541E9B2A8B587E0088D841 /* LocationPlugin.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,20 @@ class LocationPlugin: NSObject, Plugin, CLLocationManagerDelegate {
let status = locationManager?.authorizationStatus
return status == .authorizedAlways || status == .authorizedWhenInUse
}

func onUserIdChanged(_ userId: String?) {
// no-op
}

func onDeviceIdChanged(_ deviceId: String?) {
// no-op
}

func onSessionIdChanged(_ sessionId: Int64) {
// no-op
}

func onOptOutChanged(_ optOut: Bool) {
// no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
//
// WebViewSyncPlugin.swift
// AmplitudeSwiftUIExample
//
// Created by Chris Leonavicius on 1/9/25.
//

import AmplitudeSwift
import ObjectiveC
import WebKit

class WebViewSyncPlugin: NSObject, Plugin {

let type = PluginType.utility

private let webviews = NSHashTable<WKWebView>.weakObjects()

private static let swizzleWebViewInitializer: Void = {
func swizzle(class cls: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)

guard let originalMethod, let swizzledMethod else { return }

method_exchangeImplementations(originalMethod, swizzledMethod)
}

swizzle(class: WKWebView.self,
originalSelector: #selector(WKWebView.init(frame:configuration:)),
swizzledSelector: #selector(WKWebView.amp_init(frame:configuration:)))

swizzle(class: WKWebView.self,
originalSelector: #selector(WKWebView.init(coder:)),
swizzledSelector: #selector(WKWebView.amp_init(coder:)))
}()

private weak var amplitude: Amplitude?

func setup(amplitude: Amplitude) {
self.amplitude = amplitude

Self.swizzleWebViewInitializer
Self.registerPlugin(self)
DispatchQueue.main.async { [weak self] in
self?.injectConfigInAllWebviews()
}
}

func execute(event: BaseEvent) -> BaseEvent? {
return event
}

@MainActor
func onAttachWebView(webview: WKWebView) {
attach(to: webview)
injectConfig(in: webview)
}

func onUserIdChanged(_ userId: String?) {
DispatchQueue.main.async { [weak self] in
self?.injectConfigInAllWebviews()
}
}

func onDeviceIdChanged(_ deviceId: String?) {
DispatchQueue.main.async { [weak self] in
self?.injectConfigInAllWebviews()
}
}

func onSessionIdChanged(_ sessionId: Int64) {
DispatchQueue.main.async { [weak self] in
self?.injectConfigInAllWebviews()
}
}

func onOptOutChanged(_ optOut: Bool) {
// no-op
}

func teardown() {
let webviews = Self.registeredWebViews
Self.unregisterPlugin(self)
DispatchQueue.main.async {
webviews.forEach {
$0.configuration.userContentController.removeScriptMessageHandler(forName: "amp_webview_config_callback")
}
}
}

deinit {
teardown()
}
}

extension WebViewSyncPlugin: WKScriptMessageHandler {

private func attach(to webView: WKWebView) {
let userContentController = webView.configuration.userContentController
if !userContentController.userScripts.contains(where: { $0 === WebViewSyncPlugin.userScript }) {
userContentController.addUserScript(WebViewSyncPlugin.userScript)
}
userContentController.add(self, name: "amp_webview_config_callback")
}

private static let userScriptSource =
"""
(function () {
if (window.amp_webview_config) {
return;
}
var config = null;
const subscribers = [];
function subscribe(callback) {
subscribers.push(callback);
}
function unsubscribe(callback) {
subscribers = subscribers.filter((sub) => sub !== callback);
}
function getConfig() {
return config;
}
function updateConfig(updatedConfig) {
config = updatedConfig;
subscribers.forEach((callback) => {
callback(config);
});
}
window.amp_webview_config = {
subscribe,
unsubscribe,
getConfig,
updateConfig,
};
})();
window.webkit.messageHandlers.amp_webview_config_callback.postMessage(0);
"""

private static let userScript = WKUserScript(source: userScriptSource,
injectionTime: .atDocumentStart,
forMainFrameOnly: false)

@MainActor
private func injectConfigInAllWebviews() {
WebViewSyncPlugin.webviews.allObjects.forEach { injectConfig(in: $0) }
}

@MainActor
private func injectConfig(in webview: WKWebView, frameInfo: WKFrameInfo? = nil) {
guard let amplitude = amplitude else {
return
}

let config = NSMutableDictionary()
config["api_key"] = amplitude.configuration.apiKey
config["user_id"] = amplitude.getUserId()
config["device_id"] = amplitude.getDeviceId()
config["session_id"] = amplitude.getSessionId()

guard let configJsonData = try? JSONSerialization.data(withJSONObject: config),
let configJsonString = String(data: configJsonData, encoding: .utf8) else {
return
}

let script = "window.amp_webview_config.updateConfig(\(configJsonString));"

if #available(iOS 14.0, *) {
webview.evaluateJavaScript(script, in: frameInfo, in: .page) { result in
switch result {
case .success:
break
case .failure(let error):
print("Error injecting script: \(error)")
}
}
} else {
webview.evaluateJavaScript(script) { result, error in
if let error {
print("Error injecting script: \(error)")
}
}
}
}

func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard let webview = message.webView else {
return
}
injectConfig(in: webview, frameInfo: message.frameInfo)
}
}

extension WebViewSyncPlugin {

private static let lock = NSLock()
private static let webviews = NSHashTable<WKWebView>.weakObjects()
private static var plugins: [WebViewSyncPlugin] = []

private static func registerPlugin(_ plugin: WebViewSyncPlugin) {
lock.withLock {
plugins.append(plugin)
}
}

private static func unregisterPlugin(_ plugin: WebViewSyncPlugin) {
lock.withLock {
plugins.removeAll { $0 === plugin }
}
}

@MainActor
fileprivate static func registerWebview(_ webview: WKWebView) {
var activePlugins: [WebViewSyncPlugin]?
lock.withLock {
activePlugins = plugins
webviews.add(webview)
}

activePlugins?.forEach { $0.onAttachWebView(webview: webview) }
}

private static var registeredWebViews: [WKWebView] {
return lock.withLock {
return webviews.allObjects
}
}
}

extension WKWebView {

@objc func amp_init(coder: NSCoder) -> Self? {
let webview = amp_init(coder: coder)

if let webview {
WebViewSyncPlugin.registerWebview(webview)
}

return webview
}

@objc func amp_init(frame: CGRect, configuration: WKWebViewConfiguration) -> Self {
let webview = amp_init(frame: frame, configuration: configuration)

WebViewSyncPlugin.registerWebview(webview)

return webview
}
}

0 comments on commit 9365cdb

Please sign in to comment.