From 612cfc548d734d52dc8ddb4bbbf8061bee55708c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 26 Jul 2023 16:20:31 +0200 Subject: [PATCH 01/26] Ignore `*.xcuserstate`. --- .gitignore | 2 ++ .../UserInterfaceState.xcuserstate | Bin 71403 -> 0 bytes 2 files changed, 2 insertions(+) delete mode 100644 Strada.xcodeproj/project.xcworkspace/xcuserdata/denissvara.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/.gitignore b/.gitignore index 85fc3d8..3b8d4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ build node_modules *.log + +*.xcuserstate diff --git a/Strada.xcodeproj/project.xcworkspace/xcuserdata/denissvara.xcuserdatad/UserInterfaceState.xcuserstate b/Strada.xcodeproj/project.xcworkspace/xcuserdata/denissvara.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 598d9b3844a5f809937735a4c60af466f69ad112..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71403 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 bS5d1@uN(dljBQwdmf!TJRBZkE^`rj*oL;jg From 21d366923f980fd27815ab94521eefd9f643cce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 27 Jul 2023 17:53:49 +0200 Subject: [PATCH 02/26] Define `BridgeComponent` protocol. Start implementing `BridgeDelegate`. --- Source/Bridge.swift | 29 +++--- Source/BridgeComponent.swift | 25 +++++ Source/BridgeDelegate.swift | 120 +++++++++++++++++++++++ Strada.xcodeproj/project.pbxproj | 12 +++ Tests/BridgeDelegateTests.swift | 161 +++++++++++++++++++++++++++++++ 5 files changed, 336 insertions(+), 11 deletions(-) create mode 100644 Source/BridgeComponent.swift create mode 100644 Source/BridgeDelegate.swift create mode 100644 Tests/BridgeDelegateTests.swift diff --git a/Source/Bridge.swift b/Source/Bridge.swift index f1eca46..90bcb1b 100644 --- a/Source/Bridge.swift +++ b/Source/Bridge.swift @@ -1,18 +1,20 @@ import Foundation import WebKit -public protocol BridgeDelegate: AnyObject { - func bridgeDidInitialize() - func bridgeDidReceiveMessage(_ message: Message) -} - public enum BridgeError: Error { case missingWebView } +public protocol Bridgable: AnyObject { + func register(component: String) + func register(components: [String]) + func unregister(component: String) + func send(_ message: Message) +} + /// `Bridge` is the object for configuring a web view and /// the channel for sending/receiving messages -public final class Bridge { +public final class Bridge: Bridgable { public typealias CompletionHandler = (_ result: Any?, _ error: Error?) -> Void public var webView: WKWebView? { @@ -22,7 +24,7 @@ public final class Bridge { } } - public weak var delegate: BridgeDelegate? + public weak var delegate: BridgeDelegate? = nil /// This needs to match whatever the JavaScript file uses private let bridgeGlobal = "window.nativeBridge" @@ -149,12 +151,17 @@ public final class Bridge { extension Bridge: ScriptMessageHandlerDelegate { func scriptMessageHandlerDidReceiveMessage(_ scriptMessage: WKScriptMessage) { - if let event = scriptMessage.body as? String, event == "ready" { + if let event = scriptMessage.body as? String, + event == "ready" { delegate?.bridgeDidInitialize() - } else if let message = InternalMessage(scriptMessage: scriptMessage) { + return + } + + if let message = InternalMessage(scriptMessage: scriptMessage) { delegate?.bridgeDidReceiveMessage(message.toMessage()) - } else { - debugLog("Unhandled message received: \(scriptMessage.body)") + return } + + debugLog("Unhandled message received: \(scriptMessage.body)") } } diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift new file mode 100644 index 0000000..e660486 --- /dev/null +++ b/Source/BridgeComponent.swift @@ -0,0 +1,25 @@ +import Foundation + +public protocol BridgeComponent: AnyObject { + static var name: String { get } + var delegate: BridgeDelegate? { get set } + + init() + func handle(message: Message) + func onStart() + func onStop() +} + +public extension BridgeComponent { + func send(message: Message) { + guard let bridge = delegate?.bridge else { + debugLog("bridgeMessageFailedToSend: bridge is not available") + return + } + + bridge.send(message) + } + + func onStart() {} + func onStop() {} +} diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift new file mode 100644 index 0000000..a7b6789 --- /dev/null +++ b/Source/BridgeDelegate.swift @@ -0,0 +1,120 @@ +import Foundation +import WebKit + +public protocol BridgeDestination: AnyObject { + func bridgeWebViewIsReady() -> Bool + var supportedComponents: [BridgeComponent.Type] { get } +} + +public final class BridgeDelegate { + public let location: String + public let destination: BridgeDestination + weak var bridge: Bridgable? + + public init(location: String, + destination: BridgeDestination) { + self.location = location + self.destination = destination + } + // + // func onColdBootPageCompleted() { + // bridge?.load() + // } + // + // func onColdBootPageStarted() { + // bridge?.reset() + // } + // + // func onWebViewAttached(_ webView: WKWebView) { + // bridge = Bridge.getBridgeFor(webView) + // bridge?.delegate = self + // + // if bridge != nil { + // if shouldReloadBridge() { + // bridge?.load() + // } + // } else { + // logEvent("bridgeNotInitializedForWebView", location) + // } + // } + // + // func onWebViewDetached() { + // bridge?.delegate = nil + // bridge = nil + // } + // + func bridgeDidInitialize() { + let componentNames = destination.supportedComponents.map { $0.name } + bridge?.register(components: componentNames) + } + + @discardableResult + func bridgeDidReceiveMessage(_ message: Message) -> Bool { + guard destinationIsActive, + location == message.metadata?.url else { + debugLog("bridgeDidIgnoreMessage: \(message)") + return false + } + + debugLog("bridgeDidReceiveMessage: \(message)") + getOrCreateComponent(name: message.component)?.handle(message: message) + + return true + } + + // MARK: - Destination lifecycle + + public func onStart() { + debugLog("bridgeDestinationDidStart: \(location)") + destinationIsActive = true + activeComponents.forEach { $0.onStart() } + } + + public func onStop() { + activeComponents.forEach { $0.onStop() } + destinationIsActive = false + debugLog("bridgeDestinationDidStop: \(location)") + } + + public func onDestroy() { + destinationIsActive = false + debugLog("bridgeDestinationDidDestroy: \(location)") + } + + // MARK: Retrieve component(s) by type + + func component() -> C? { + return activeComponents.compactMap { $0 as? C }.first + } + + func forEachComponent(action: (C) -> Void) { + activeComponents.compactMap { $0 as? C }.forEach { action($0) } + } + + // MARK: Private + + private var initializedComponents: [String: BridgeComponent] = [:] + private var destinationIsActive = false + + private var activeComponents: [BridgeComponent] { + return initializedComponents.values.filter { _ in destinationIsActive } + } + + private func getOrCreateComponent(name: String) -> BridgeComponent? { + if let component = initializedComponents[name] { + return component + } + + guard let componentType = destination.supportedComponents.first(where: { $0.name == name }) else { + return nil + } + + let component = componentType.init() + component.delegate = self + + initializedComponents[name] = component + + return component + } +} + diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index ffa585b..8c0f4ec 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -24,6 +24,9 @@ E20978492A71366B00CDEEE5 /* Data+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978482A71366B00CDEEE5 /* Data+Utils.swift */; }; E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784A2A714D4E00CDEEE5 /* String+JSON.swift */; }; E209784D2A714F1900CDEEE5 /* Dictionary+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */; }; + E2DB15912A7163B0001EE08C /* BridgeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */; }; + E2DB15932A7282CF001EE08C /* BridgeComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15922A7282CF001EE08C /* BridgeComponent.swift */; }; + E2DB15952A72B0A8001EE08C /* BridgeDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +60,9 @@ E20978482A71366B00CDEEE5 /* Data+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utils.swift"; sourceTree = ""; }; E209784A2A714D4E00CDEEE5 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = ""; }; E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = ""; }; + E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegate.swift; sourceTree = ""; }; + E2DB15922A7282CF001EE08C /* BridgeComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeComponent.swift; sourceTree = ""; }; + E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegateTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -109,6 +115,8 @@ 9274F1FF2229970D003E85F4 /* strada.js */, 9274F1E92229963B003E85F4 /* Info.plist */, E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */, + E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */, + E2DB15922A7282CF001EE08C /* BridgeComponent.swift */, ); path = Source; sourceTree = ""; @@ -121,6 +129,7 @@ C1EB05252588133D00933244 /* MessageTests.swift */, 9274F1F52229963B003E85F4 /* Info.plist */, E20978432A6EAF3600CDEEE5 /* InternalMessageTests.swift */, + E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */, ); path = Tests; sourceTree = ""; @@ -250,8 +259,10 @@ E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */, C11349A62587EFFB000A6E56 /* ScriptMessageHandler.swift in Sources */, E20978472A7135E700CDEEE5 /* Encodable+Utils.swift in Sources */, + E2DB15912A7163B0001EE08C /* BridgeDelegate.swift in Sources */, C11349B22587F31E000A6E56 /* JavaScript.swift in Sources */, 9274F20222299715003E85F4 /* Bridge.swift in Sources */, + E2DB15932A7282CF001EE08C /* BridgeComponent.swift in Sources */, E20978492A71366B00CDEEE5 /* Data+Utils.swift in Sources */, 9274F20422299738003E85F4 /* Message.swift in Sources */, E20978422A6E9E6B00CDEEE5 /* InternalMessage.swift in Sources */, @@ -265,6 +276,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E2DB15952A72B0A8001EE08C /* BridgeDelegateTests.swift in Sources */, C11349C2258801F6000A6E56 /* JavaScriptTests.swift in Sources */, C1EB052E2588201600933244 /* BridgeTests.swift in Sources */, E20978442A6EAF3600CDEEE5 /* InternalMessageTests.swift in Sources */, diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift new file mode 100644 index 0000000..baf5ede --- /dev/null +++ b/Tests/BridgeDelegateTests.swift @@ -0,0 +1,161 @@ +import Foundation +import XCTest +import WebKit +@testable import Strada + +class BridgeDelegateTests: XCTestCase { + private var delegate: BridgeDelegate! + private var destination: BridgeDestinationSpy! + private var bridge: BridgeSpy! + private let json = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + + override func setUp() async throws { + destination = BridgeDestinationSpy() + delegate = BridgeDelegate(location: "https://37signals.com", + destination: destination) + + bridge = BridgeSpy() + delegate.bridge = bridge + delegate.onStart() + } + + func testBridgeDidInitialize() { + delegate.bridgeDidInitialize() + + XCTAssertTrue(bridge.registerComponentsWasCalled) + XCTAssertEqual(bridge.registerComponentsArg, ["one", "two"]) + + // Registered components are lazy initialized. + let componentOne: TwoBridgeComponent? = delegate.component() + let componentTwo: TwoBridgeComponent? = delegate.component() + XCTAssertNil(componentOne) + XCTAssertNil(componentTwo) + } + + func testBridgeDidReceiveMessage() { + let json = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + let message = Message(id: "1", + component: "two", + event: "connect", + metadata: .init(url: "https://37signals.com"), + jsonData: json) + + var component: TwoBridgeComponent? = delegate.component() + + XCTAssertNil(component) + XCTAssertTrue(delegate.bridgeDidReceiveMessage(message)) + + component = delegate.component() + + XCTAssertNotNil(component) + // Make sure the component has delegate set, and did receive the message. + XCTAssertTrue(component!.handleMessageWasCalled) + XCTAssertEqual(component?.handleMessageArg, message) + XCTAssertNotNil(component?.delegate) + } + + func testBridgeDidReceiveMessageIgnored() { + let json = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: .init(url: "https://37signals.com/another_url"), + jsonData: json) + + XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) + } + + func testDestinationIsInactive() { + let message = Message(id: "1", + component: "one", + event: "connect", + metadata: .init(url: "https://37signals.com"), + jsonData: json) + + XCTAssertTrue(delegate.bridgeDidReceiveMessage(message)) + + var component: OneBridgeComponent? = delegate.component() + XCTAssertNotNil(component) + + delegate.onStop() + XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) + component = delegate.component() + XCTAssertNil(component) + } +} + +private class BridgeDestinationSpy: BridgeDestination { + func bridgeWebViewIsReady() -> Bool { + return true + } + + var supportedComponents: [Strada.BridgeComponent.Type] = [OneBridgeComponent.self, TwoBridgeComponent.self] +} + +private class OneBridgeComponent: BridgeComponent { + static var name: String = "one" + + required init() {} + + weak var delegate: Strada.BridgeDelegate? + + func handle(message: Strada.Message) { + + } +} + +private class TwoBridgeComponent: BridgeComponent { + static var name: String = "two" + + var handleMessageWasCalled = false + var handleMessageArg: Message? + + required init() {} + + weak var delegate: Strada.BridgeDelegate? + + func handle(message: Strada.Message) { + handleMessageWasCalled = true + handleMessageArg = message + } +} + +private class BridgeSpy: Bridgable { + var registerComponentWasCalled = false + var registerComponentArg: String? = nil + + var registerComponentsWasCalled = false + var registerComponentsArg: [String]? = nil + + var unregisterComponentWasCalled = false + var unregisterComponentArg: String? = nil + + var sendMessageWasCalled = false + var sendMessageArg: Message? = nil + + func register(component: String) { + registerComponentWasCalled = true + registerComponentArg = component + } + + func register(components: [String]) { + registerComponentsWasCalled = true + registerComponentsArg = components + } + + func unregister(component: String) { + unregisterComponentWasCalled = true + unregisterComponentArg = component + } + + func send(_ message: Strada.Message) { + sendMessageWasCalled = true + sendMessageArg = message + } +} From 50aaaf2f80160a5cba72b0d469af0d979592ec2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Mon, 31 Jul 2023 15:10:01 +0200 Subject: [PATCH 03/26] Fix broken Bridge test. --- Tests/BridgeTests.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Tests/BridgeTests.swift b/Tests/BridgeTests.swift index 23ac4bc..2580ba2 100644 --- a/Tests/BridgeTests.swift +++ b/Tests/BridgeTests.swift @@ -54,9 +54,19 @@ class BridgeTests: XCTestCase { let bridge = Bridge(webView: webView) XCTAssertNil(webView.lastEvaluatedJavaScript) -// let message = Message(id: "1", component: "test", event: "send", data: ["title": "testing"]) -// bridge.send(message) -// XCTAssertEqual(webView.lastEvaluatedJavaScript, "window.nativeBridge.send({\"component\":\"test\",\"event\":\"send\",\"data\":{\"title\":\"testing\"},\"id\":\"1\"})") + let data = """ + {"title":"Page-title"} + """ + let metadata = Message.Metadata(url: "https://37signals.com") + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: metadata, + jsonData: data) + + + bridge.send(message) + XCTAssertEqual(webView.lastEvaluatedJavaScript, "window.nativeBridge.send({\"component\":\"page\",\"event\":\"connect\",\"data\":{\"title\":\"Page-title\"},\"id\":\"1\"})") } func testEvaluateJavaScript() { From e38b09da88df691219cedf89caf0c8fd02f4b8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Mon, 31 Jul 2023 16:18:37 +0200 Subject: [PATCH 04/26] Make retrieve component function public. --- Source/BridgeDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index a7b6789..da73e99 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -83,7 +83,7 @@ public final class BridgeDelegate { // MARK: Retrieve component(s) by type - func component() -> C? { + public func component() -> C? { return activeComponents.compactMap { $0 as? C }.first } From 502c40659c2596cd582c5061c41f3d94c54eb3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Mon, 31 Jul 2023 18:04:50 +0200 Subject: [PATCH 05/26] Add iOS specific view's lifecycle function to `BridgeDelegate` and `BridgeComponent`. Update tests. --- Source/BridgeComponent.swift | 11 +++++++---- Source/BridgeDelegate.swift | 29 ++++++++++++++++------------- Tests/BridgeDelegateTests.swift | 9 ++++----- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index e660486..9fd2ed1 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -6,8 +6,10 @@ public protocol BridgeComponent: AnyObject { init() func handle(message: Message) - func onStart() - func onStop() + + func onViewDidLoad() + func onViewWillAppear() + func onViewWillDisappear() } public extension BridgeComponent { @@ -20,6 +22,7 @@ public extension BridgeComponent { bridge.send(message) } - func onStart() {} - func onStop() {} + func onViewDidLoad() {} + func onViewWillAppear() {} + func onViewWillDisappear() {} } diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index da73e99..c0abec8 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -3,7 +3,6 @@ import WebKit public protocol BridgeDestination: AnyObject { func bridgeWebViewIsReady() -> Bool - var supportedComponents: [BridgeComponent.Type] { get } } public final class BridgeDelegate { @@ -12,9 +11,11 @@ public final class BridgeDelegate { weak var bridge: Bridgable? public init(location: String, - destination: BridgeDestination) { + destination: BridgeDestination, + componentTypes: [BridgeComponent.Type]) { self.location = location self.destination = destination + self.componentTypes = componentTypes } // // func onColdBootPageCompleted() { @@ -44,7 +45,7 @@ public final class BridgeDelegate { // } // func bridgeDidInitialize() { - let componentNames = destination.supportedComponents.map { $0.name } + let componentNames = componentTypes.map { $0.name } bridge?.register(components: componentNames) } @@ -64,21 +65,22 @@ public final class BridgeDelegate { // MARK: - Destination lifecycle - public func onStart() { - debugLog("bridgeDestinationDidStart: \(location)") + public func onViewDidLoad() { + debugLog("bridgeDestinationViewDidLoad: \(location)") destinationIsActive = true - activeComponents.forEach { $0.onStart() } + activeComponents.forEach { $0.onViewDidLoad() } } - public func onStop() { - activeComponents.forEach { $0.onStop() } - destinationIsActive = false - debugLog("bridgeDestinationDidStop: \(location)") + public func onViewWillAppear() { + debugLog("bridgeDestinationViewWillAppear: \(location)") + destinationIsActive = true + activeComponents.forEach { $0.onViewWillAppear() } } - public func onDestroy() { + public func onViewWillDisappear() { + activeComponents.forEach { $0.onViewWillDisappear() } destinationIsActive = false - debugLog("bridgeDestinationDidDestroy: \(location)") + debugLog("bridgeDestinationViewWillDisappear: \(location)") } // MARK: Retrieve component(s) by type @@ -95,6 +97,7 @@ public final class BridgeDelegate { private var initializedComponents: [String: BridgeComponent] = [:] private var destinationIsActive = false + private let componentTypes: [BridgeComponent.Type] private var activeComponents: [BridgeComponent] { return initializedComponents.values.filter { _ in destinationIsActive } @@ -105,7 +108,7 @@ public final class BridgeDelegate { return component } - guard let componentType = destination.supportedComponents.first(where: { $0.name == name }) else { + guard let componentType = componentTypes.first(where: { $0.name == name }) else { return nil } diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index baf5ede..ffdbf40 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -14,11 +14,12 @@ class BridgeDelegateTests: XCTestCase { override func setUp() async throws { destination = BridgeDestinationSpy() delegate = BridgeDelegate(location: "https://37signals.com", - destination: destination) + destination: destination, + componentTypes: [OneBridgeComponent.self, TwoBridgeComponent.self]) bridge = BridgeSpy() delegate.bridge = bridge - delegate.onStart() + delegate.onViewDidLoad() } func testBridgeDidInitialize() { @@ -83,7 +84,7 @@ class BridgeDelegateTests: XCTestCase { var component: OneBridgeComponent? = delegate.component() XCTAssertNotNil(component) - delegate.onStop() + delegate.onViewWillDisappear() XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) component = delegate.component() XCTAssertNil(component) @@ -94,8 +95,6 @@ private class BridgeDestinationSpy: BridgeDestination { func bridgeWebViewIsReady() -> Bool { return true } - - var supportedComponents: [Strada.BridgeComponent.Type] = [OneBridgeComponent.self, TwoBridgeComponent.self] } private class OneBridgeComponent: BridgeComponent { From 19fb2dbff6e47689b800235ef76cb74edf890ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 1 Aug 2023 14:35:45 +0200 Subject: [PATCH 06/26] Link `BridgeDelegate`'s bridge when it is set to a `Bridge` instance. --- Source/Bridge.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Bridge.swift b/Source/Bridge.swift index 90bcb1b..0a1bb96 100644 --- a/Source/Bridge.swift +++ b/Source/Bridge.swift @@ -24,7 +24,11 @@ public final class Bridge: Bridgable { } } - public weak var delegate: BridgeDelegate? = nil + public weak var delegate: BridgeDelegate? = nil { + didSet { + delegate?.bridge = self + } + } /// This needs to match whatever the JavaScript file uses private let bridgeGlobal = "window.nativeBridge" From b4db251f34e6a1dfe7409a070b3fea237984b810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 1 Aug 2023 15:35:33 +0200 Subject: [PATCH 07/26] Initialize `BridgeComponent` with a destination. --- Source/BridgeComponent.swift | 2 +- Source/BridgeDelegate.swift | 2 +- Tests/BridgeDelegateTests.swift | 14 +++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index 9fd2ed1..a16a5ed 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -4,7 +4,7 @@ public protocol BridgeComponent: AnyObject { static var name: String { get } var delegate: BridgeDelegate? { get set } - init() + init(destination: BridgeDestination) func handle(message: Message) func onViewDidLoad() diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index c0abec8..e5b4399 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -112,7 +112,7 @@ public final class BridgeDelegate { return nil } - let component = componentType.init() + let component = componentType.init(destination: destination) component.delegate = self initializedComponents[name] = component diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index ffdbf40..95979dd 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -99,25 +99,21 @@ private class BridgeDestinationSpy: BridgeDestination { private class OneBridgeComponent: BridgeComponent { static var name: String = "one" - - required init() {} - weak var delegate: Strada.BridgeDelegate? - func handle(message: Strada.Message) { - - } + required init(destination: Strada.BridgeDestination) {} + + func handle(message: Strada.Message) {} } private class TwoBridgeComponent: BridgeComponent { static var name: String = "two" + weak var delegate: Strada.BridgeDelegate? var handleMessageWasCalled = false var handleMessageArg: Message? - required init() {} - - weak var delegate: Strada.BridgeDelegate? + required init(destination: Strada.BridgeDestination) {} func handle(message: Strada.Message) { handleMessageWasCalled = true From 974dd47227f4a0caca1eb8110553ef8fcdf16d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 2 Aug 2023 12:21:06 +0200 Subject: [PATCH 08/26] Ignore breakpoint files. --- .gitignore | 2 ++ .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 Strada.xcodeproj/xcuserdata/denissvara.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist diff --git a/.gitignore b/.gitignore index 3b8d4a9..419f575 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules *.log *.xcuserstate + +*.xcbkptlist diff --git a/Strada.xcodeproj/xcuserdata/denissvara.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Strada.xcodeproj/xcuserdata/denissvara.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist deleted file mode 100644 index c309098..0000000 --- a/Strada.xcodeproj/xcuserdata/denissvara.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ /dev/null @@ -1,6 +0,0 @@ - - - From 662396134162251c34392e90b2e060c294cde4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 3 Aug 2023 13:46:53 +0200 Subject: [PATCH 09/26] Only expose a static initializer on `Bridge` that takes a web view instance. --- Source/Bridge.swift | 66 +++++++++++++++++++++++------------------ Tests/BridgeTests.swift | 15 ++++++---- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/Source/Bridge.swift b/Source/Bridge.swift index 0a1bb96..16d1514 100644 --- a/Source/Bridge.swift +++ b/Source/Bridge.swift @@ -5,7 +5,9 @@ public enum BridgeError: Error { case missingWebView } -public protocol Bridgable: AnyObject { +protocol Bridgable: AnyObject { + var delegate: BridgeDelegate? { get set } + func register(component: String) func register(components: [String]) func unregister(component: String) @@ -17,60 +19,47 @@ public protocol Bridgable: AnyObject { public final class Bridge: Bridgable { public typealias CompletionHandler = (_ result: Any?, _ error: Error?) -> Void - public var webView: WKWebView? { - didSet { - guard webView != oldValue else { return } - loadIntoWebView() - } - } - - public weak var delegate: BridgeDelegate? = nil { - didSet { - delegate?.bridge = self - } - } - - /// This needs to match whatever the JavaScript file uses - private let bridgeGlobal = "window.nativeBridge" - - /// The webkit.messageHandlers name - private let scriptHandlerName = "strada" + weak var delegate: BridgeDelegate? + weak var webView: WKWebView? deinit { webView?.configuration.userContentController.removeScriptMessageHandler(forName: scriptHandlerName) } - /// Create a new Bridge object for calling methods on this web view with a delegate - /// for receiving messages - public init(webView: WKWebView? = nil, delegate: BridgeDelegate? = nil) { + public static func initialize(_ webView: WKWebView) { + if getBridgeFor(webView) == nil { + initialize(Bridge(webView: webView)) + } + } + + init(webView: WKWebView) { self.webView = webView - self.delegate = delegate loadIntoWebView() } - // MARK: - API + // MARK: - Internal API /// Register a single component /// - Parameter component: Name of a component to register support for - public func register(component: String) { + func register(component: String) { callBridgeFunction("register", arguments: [component]) } /// Register multiple components /// - Parameter components: Array of component names to register - public func register(components: [String]) { + func register(components: [String]) { callBridgeFunction("register", arguments: [components]) } /// Unregister support for a single component /// - Parameter component: Component name - public func unregister(component: String) { + func unregister(component: String) { callBridgeFunction("unregister", arguments: [component]) } /// Send a message through the bridge to the web application /// - Parameter message: Message to send - public func send(_ message: Message) { + func send(_ message: Message) { let internalMessage = InternalMessage(from: message) callBridgeFunction("send", arguments: [internalMessage.toJSON()]) } @@ -83,6 +72,24 @@ public final class Bridge: Bridgable { // let replyMessage = message.replacing(data: data) // callBridgeFunction("send", arguments: [replyMessage.toJSON()]) // } + + static func initialize(_ bridge: Bridge) { + instances.append(bridge) + instances.removeAll { $0.webView == nil } + } + + static func getBridgeFor(_ webView: WKWebView) -> Bridge? { + return instances.first { $0.webView == webView } + } + + // MARK: Private + + private static var instances: [Bridge] = [] + /// This needs to match whatever the JavaScript file uses + private let bridgeGlobal = "window.nativeBridge" + + /// The webkit.messageHandlers name + private let scriptHandlerName = "strada" private func callBridgeFunction(_ function: String, arguments: [Any]) { let js = JavaScript(object: bridgeGlobal, functionName: function, arguments: arguments) @@ -100,7 +107,8 @@ public final class Bridge: Bridgable { configuration.userContentController.addUserScript(userScript) } - configuration.userContentController.add(ScriptMessageHandler(delegate: self), name: scriptHandlerName) + let scriptMessageHandler = ScriptMessageHandler(delegate: self) + configuration.userContentController.add(scriptMessageHandler, name: scriptHandlerName) } private func makeUserScript() -> WKUserScript? { diff --git a/Tests/BridgeTests.swift b/Tests/BridgeTests.swift index 2580ba2..16f0820 100644 --- a/Tests/BridgeTests.swift +++ b/Tests/BridgeTests.swift @@ -3,22 +3,24 @@ import WebKit @testable import Strada class BridgeTests: XCTestCase { - func testInitAutomaticallyLoadsIntoWebView() { + func testInitWithANewWebViewAutomaticallyLoadsIntoWebView() { let webView = WKWebView() let userContentController = webView.configuration.userContentController XCTAssertTrue(userContentController.userScripts.isEmpty) - _ = Bridge(webView: webView) + Bridge.initialize(webView) XCTAssertEqual(userContentController.userScripts.count, 1) } - func testLoadIntoConfiguration() { + func testInitWithTheSameWebViewDoesNotLoadTwice() { let webView = WKWebView() let userContentController = webView.configuration.userContentController XCTAssertTrue(userContentController.userScripts.isEmpty) - let bridge = Bridge() - bridge.webView = webView + Bridge.initialize(webView) + XCTAssertEqual(userContentController.userScripts.count, 1) + + Bridge.initialize(webView) XCTAssertEqual(userContentController.userScripts.count, 1) } @@ -79,7 +81,8 @@ class BridgeTests: XCTestCase { } func testEvaluateJavaScriptReturnsErrorForNoWebView() { - let bridge = Bridge() + let bridge = Bridge(webView: WKWebView()) + bridge.webView = nil let expectation = self.expectation(description: "error handler") bridge.evaluate(function: "test", arguments: []) { (result, error) in From cf2373428221193e242bf3b37fdb880c46501803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 3 Aug 2023 13:48:32 +0200 Subject: [PATCH 10/26] Expose access to web view through bridge delegate. Implement logic when a web view is attached and detached. --- Source/BridgeDelegate.swift | 80 +++++++++++++++------------------ Tests/BridgeDelegateTests.swift | 2 + 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index e5b4399..7c626d0 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -8,6 +8,8 @@ public protocol BridgeDestination: AnyObject { public final class BridgeDelegate { public let location: String public let destination: BridgeDestination + public weak var webView: WKWebView? + weak var bridge: Bridgable? public init(location: String, @@ -17,50 +19,23 @@ public final class BridgeDelegate { self.destination = destination self.componentTypes = componentTypes } - // - // func onColdBootPageCompleted() { - // bridge?.load() - // } - // - // func onColdBootPageStarted() { - // bridge?.reset() - // } - // - // func onWebViewAttached(_ webView: WKWebView) { - // bridge = Bridge.getBridgeFor(webView) - // bridge?.delegate = self - // - // if bridge != nil { - // if shouldReloadBridge() { - // bridge?.load() - // } - // } else { - // logEvent("bridgeNotInitializedForWebView", location) - // } - // } - // - // func onWebViewDetached() { - // bridge?.delegate = nil - // bridge = nil - // } - // - func bridgeDidInitialize() { - let componentNames = componentTypes.map { $0.name } - bridge?.register(components: componentNames) - } - @discardableResult - func bridgeDidReceiveMessage(_ message: Message) -> Bool { - guard destinationIsActive, - location == message.metadata?.url else { - debugLog("bridgeDidIgnoreMessage: \(message)") - return false - } + public func onWebViewAttached(_ webView: WKWebView) { + bridge = Bridge.getBridgeFor(webView) + bridge?.delegate = self - debugLog("bridgeDidReceiveMessage: \(message)") - getOrCreateComponent(name: message.component)?.handle(message: message) + guard bridge != nil else { + debugLog("bridgeNotInitializedForWebView") + return + } - return true + self.webView = webView + } + + public func onWebViewDetached() { + bridge?.delegate = nil + bridge = nil + webView = nil } // MARK: - Destination lifecycle @@ -83,14 +58,31 @@ public final class BridgeDelegate { debugLog("bridgeDestinationViewWillDisappear: \(location)") } - // MARK: Retrieve component(s) by type + // MARK: Retrieve component by type public func component() -> C? { return activeComponents.compactMap { $0 as? C }.first } - func forEachComponent(action: (C) -> Void) { - activeComponents.compactMap { $0 as? C }.forEach { action($0) } + // MARK: Internal + + func bridgeDidInitialize() { + let componentNames = componentTypes.map { $0.name } + bridge?.register(components: componentNames) + } + + @discardableResult + func bridgeDidReceiveMessage(_ message: Message) -> Bool { + guard destinationIsActive, + location == message.metadata?.url else { + debugLog("bridgeDidIgnoreMessage: \(message)") + return false + } + + debugLog("bridgeDidReceiveMessage: \(message)") + getOrCreateComponent(name: message.component)?.handle(message: message) + + return true } // MARK: Private diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index 95979dd..3ab65a2 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -122,6 +122,8 @@ private class TwoBridgeComponent: BridgeComponent { } private class BridgeSpy: Bridgable { + var delegate: Strada.BridgeDelegate? = nil + var registerComponentWasCalled = false var registerComponentArg: String? = nil From f34652f45b9ae541dfdf33d41176d970131f96e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 3 Aug 2023 13:58:32 +0200 Subject: [PATCH 11/26] Add two additional lifecycle function to bridge component. --- Source/BridgeComponent.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index a16a5ed..f8af271 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -9,7 +9,9 @@ public protocol BridgeComponent: AnyObject { func onViewDidLoad() func onViewWillAppear() + func onViewDidAppear() func onViewWillDisappear() + func onViewDidDisappear() } public extension BridgeComponent { @@ -24,5 +26,7 @@ public extension BridgeComponent { func onViewDidLoad() {} func onViewWillAppear() {} + func onViewDidAppear() {} func onViewWillDisappear() {} + func onViewDidDisappear() {} } From e611f12aae87a8c7ad9c64b7f1dd4ff99606a0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 3 Aug 2023 15:44:59 +0200 Subject: [PATCH 12/26] Mark destination in bridge delegate as an unowned reference. --- Source/BridgeDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 7c626d0..9c28b13 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -7,7 +7,7 @@ public protocol BridgeDestination: AnyObject { public final class BridgeDelegate { public let location: String - public let destination: BridgeDestination + public unowned let destination: BridgeDestination public weak var webView: WKWebView? weak var bridge: Bridgable? From 3e4e5bd383a31597eebcc09e98399ceb5eb1cebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 09:08:06 +0200 Subject: [PATCH 13/26] Rename web view activated/deactivated functions following the typical naming convention for delegate methods. --- Source/BridgeDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 9c28b13..6d84876 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -20,7 +20,7 @@ public final class BridgeDelegate { self.componentTypes = componentTypes } - public func onWebViewAttached(_ webView: WKWebView) { + public func webViewDidBecomeActive(_ webView: WKWebView) { bridge = Bridge.getBridgeFor(webView) bridge?.delegate = self @@ -32,7 +32,7 @@ public final class BridgeDelegate { self.webView = webView } - public func onWebViewDetached() { + public func webViewDidBecomeDeactivated() { bridge?.delegate = nil bridge = nil webView = nil From adbd2b930ab9d8ea30feb4cc4a1072c7a1211a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 09:12:31 +0200 Subject: [PATCH 14/26] Access webView through the bridge instance. --- Source/Bridge.swift | 1 + Source/BridgeDelegate.swift | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Source/Bridge.swift b/Source/Bridge.swift index 16d1514..dfee7c4 100644 --- a/Source/Bridge.swift +++ b/Source/Bridge.swift @@ -7,6 +7,7 @@ public enum BridgeError: Error { protocol Bridgable: AnyObject { var delegate: BridgeDelegate? { get set } + var webView: WKWebView? { get } func register(component: String) func register(components: [String]) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 6d84876..53f9110 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -8,7 +8,9 @@ public protocol BridgeDestination: AnyObject { public final class BridgeDelegate { public let location: String public unowned let destination: BridgeDestination - public weak var webView: WKWebView? + public var webView: WKWebView? { + bridge?.webView + } weak var bridge: Bridgable? @@ -24,18 +26,14 @@ public final class BridgeDelegate { bridge = Bridge.getBridgeFor(webView) bridge?.delegate = self - guard bridge != nil else { + if bridge == nil { debugLog("bridgeNotInitializedForWebView") - return } - - self.webView = webView } public func webViewDidBecomeDeactivated() { bridge?.delegate = nil bridge = nil - webView = nil } // MARK: - Destination lifecycle From face82f9cc15fb07233ec0117c35116448395813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 09:23:54 +0200 Subject: [PATCH 15/26] Don't expose bridge's js functions publicly. --- Source/Bridge.swift | 58 ++++++++++++++++----------------- Tests/BridgeDelegateTests.swift | 1 + 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/Source/Bridge.swift b/Source/Bridge.swift index dfee7c4..ad1be95 100644 --- a/Source/Bridge.swift +++ b/Source/Bridge.swift @@ -18,15 +18,11 @@ protocol Bridgable: AnyObject { /// `Bridge` is the object for configuring a web view and /// the channel for sending/receiving messages public final class Bridge: Bridgable { - public typealias CompletionHandler = (_ result: Any?, _ error: Error?) -> Void + typealias CompletionHandler = (_ result: Any?, _ error: Error?) -> Void weak var delegate: BridgeDelegate? weak var webView: WKWebView? - deinit { - webView?.configuration.userContentController.removeScriptMessageHandler(forName: scriptHandlerName) - } - public static func initialize(_ webView: WKWebView) { if getBridgeFor(webView) == nil { initialize(Bridge(webView: webView)) @@ -37,6 +33,10 @@ public final class Bridge: Bridgable { self.webView = webView loadIntoWebView() } + + deinit { + webView?.configuration.userContentController.removeScriptMessageHandler(forName: scriptHandlerName) + } // MARK: - Internal API @@ -74,6 +74,29 @@ public final class Bridge: Bridgable { // callBridgeFunction("send", arguments: [replyMessage.toJSON()]) // } + /// Evaluates javaScript string directly as passed in sending through the web view + func evaluate(javaScript: String, completion: CompletionHandler? = nil) { + guard let webView = webView else { + completion?(nil, BridgeError.missingWebView) + return + } + + webView.evaluateJavaScript(javaScript) { result, error in + if let error = error { + debugLog("Error evaluating JavaScript: \(error)") + } + + completion?(result, error) + } + } + + /// Evaluates a JavaScript function with optional arguments by encoding the arguments + /// Function should not include the parens + /// Usage: evaluate(function: "console.log", arguments: ["test"]) + func evaluate(function: String, arguments: [Any] = [], completion: CompletionHandler? = nil) { + evaluate(javaScript: JavaScript(functionName: function, arguments: arguments), completion: completion) + } + static func initialize(_ bridge: Bridge) { instances.append(bridge) instances.removeAll { $0.webView == nil } @@ -126,31 +149,8 @@ public final class Bridge: Bridgable { return nil } } - - // MARK: - JavaScript Evaluation - - /// Evaluates javaScript string directly as passed in sending through the web view - public func evaluate(javaScript: String, completion: CompletionHandler? = nil) { - guard let webView = webView else { - completion?(nil, BridgeError.missingWebView) - return - } - - webView.evaluateJavaScript(javaScript) { result, error in - if let error = error { - debugLog("Error evaluating JavaScript: \(error)") - } - - completion?(result, error) - } - } - /// Evaluates a JavaScript function with optional arguments by encoding the arguments - /// Function should not include the parens - /// Usage: evaluate(function: "console.log", arguments: ["test"]) - public func evaluate(function: String, arguments: [Any] = [], completion: CompletionHandler? = nil) { - evaluate(javaScript: JavaScript(functionName: function, arguments: arguments), completion: completion) - } + // MARK: - JavaScript Evaluation private func evaluate(javaScript: JavaScript, completion: CompletionHandler? = nil) { do { diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index 3ab65a2..8745500 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -123,6 +123,7 @@ private class TwoBridgeComponent: BridgeComponent { private class BridgeSpy: Bridgable { var delegate: Strada.BridgeDelegate? = nil + var webView: WKWebView? = nil var registerComponentWasCalled = false var registerComponentArg: String? = nil From c26374771584365f4909bd43d9718ddc354f66f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 09:45:51 +0200 Subject: [PATCH 16/26] Add two more destination lifecycle events to bridge delegate. Update tests. --- Source/BridgeDelegate.swift | 12 +++++ Tests/BridgeDelegateTests.swift | 89 ++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 53f9110..084e677 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -50,12 +50,24 @@ public final class BridgeDelegate { activeComponents.forEach { $0.onViewWillAppear() } } + public func onViewDidAppear() { + debugLog("bridgeDestinationViewDidAppear: \(location)") + destinationIsActive = true + activeComponents.forEach { $0.onViewDidAppear() } + } + public func onViewWillDisappear() { activeComponents.forEach { $0.onViewWillDisappear() } destinationIsActive = false debugLog("bridgeDestinationViewWillDisappear: \(location)") } + public func onViewDidDisappear() { + activeComponents.forEach { $0.onViewDidDisappear() } + destinationIsActive = false + debugLog("bridgeDestinationViewDidDisappear: \(location)") + } + // MARK: Retrieve component by type public func component() -> C? { diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index 8745500..fbffb84 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -15,7 +15,7 @@ class BridgeDelegateTests: XCTestCase { destination = BridgeDestinationSpy() delegate = BridgeDelegate(location: "https://37signals.com", destination: destination, - componentTypes: [OneBridgeComponent.self, TwoBridgeComponent.self]) + componentTypes: [OneBridgeComponent.self, BridgeComponentSpy.self]) bridge = BridgeSpy() delegate.bridge = bridge @@ -29,8 +29,8 @@ class BridgeDelegateTests: XCTestCase { XCTAssertEqual(bridge.registerComponentsArg, ["one", "two"]) // Registered components are lazy initialized. - let componentOne: TwoBridgeComponent? = delegate.component() - let componentTwo: TwoBridgeComponent? = delegate.component() + let componentOne: BridgeComponentSpy? = delegate.component() + let componentTwo: BridgeComponentSpy? = delegate.component() XCTAssertNil(componentOne) XCTAssertNil(componentTwo) } @@ -45,7 +45,7 @@ class BridgeDelegateTests: XCTestCase { metadata: .init(url: "https://37signals.com"), jsonData: json) - var component: TwoBridgeComponent? = delegate.component() + var component: BridgeComponentSpy? = delegate.component() XCTAssertNil(component) XCTAssertTrue(delegate.bridgeDidReceiveMessage(message)) @@ -59,7 +59,7 @@ class BridgeDelegateTests: XCTestCase { XCTAssertNotNil(component?.delegate) } - func testBridgeDidReceiveMessageIgnored() { + func testBridgeIgnoresMessageForUnknownComponent() { let json = """ {"title":"Page-title","subtitle":"Page-subtitle"} """ @@ -72,7 +72,7 @@ class BridgeDelegateTests: XCTestCase { XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) } - func testDestinationIsInactive() { + func testInactiveDestinationIgnoresMessage() { let message = Message(id: "1", component: "one", event: "connect", @@ -86,9 +86,58 @@ class BridgeDelegateTests: XCTestCase { delegate.onViewWillDisappear() XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) + component = delegate.component() XCTAssertNil(component) } + + func testDestinationForwardsViewWillAppearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewWillAppear() + XCTAssertTrue(component!.onViewWillAppearWasCalled) + } + + func testDestinationForwardsViewDidAppearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewDidAppear() + XCTAssertTrue(component!.onViewDidAppearWasCalled) + } + + func testDestinationForwardsViewWillDisappearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewWillDisappear() + XCTAssertTrue(component!.onViewWillDisappearWasCalled) + } + + func testDestinationForwardsViewDidDisappearToComponents() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewDidDisappear() + XCTAssertTrue(component!.onViewDidDisappearWasCalled) + } + + private func testMessage() -> Message { + return Message(id: "1", + component: "two", + event: "connect", + metadata: .init(url: "https://37signals.com"), + jsonData: json) + } } private class BridgeDestinationSpy: BridgeDestination { @@ -106,19 +155,45 @@ private class OneBridgeComponent: BridgeComponent { func handle(message: Strada.Message) {} } -private class TwoBridgeComponent: BridgeComponent { +private class BridgeComponentSpy: BridgeComponent { static var name: String = "two" weak var delegate: Strada.BridgeDelegate? var handleMessageWasCalled = false var handleMessageArg: Message? + var onViewDidLoadWasCalled = false + var onViewWillAppearWasCalled = false + var onViewDidAppearWasCalled = false + var onViewWillDisappearWasCalled = false + var onViewDidDisappearWasCalled = false + required init(destination: Strada.BridgeDestination) {} func handle(message: Strada.Message) { handleMessageWasCalled = true handleMessageArg = message } + + func onViewDidLoad() { + onViewDidLoadWasCalled = true + } + + func onViewWillAppear() { + onViewWillAppearWasCalled = true + } + + func onViewDidAppear() { + onViewDidAppearWasCalled = true + } + + func onViewWillDisappear() { + onViewWillDisappearWasCalled = true + } + + func onViewDidDisappear() { + onViewDidDisappearWasCalled = true + } } private class BridgeSpy: Bridgable { From 45836c92038efce9b8a717eb02295c9417cdc145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 09:46:52 +0200 Subject: [PATCH 17/26] Remove unused property from `BridgeDestination` protocol. --- Source/BridgeDelegate.swift | 4 +--- Tests/BridgeDelegateTests.swift | 6 +----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 084e677..2aa7854 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -1,9 +1,7 @@ import Foundation import WebKit -public protocol BridgeDestination: AnyObject { - func bridgeWebViewIsReady() -> Bool -} +public protocol BridgeDestination: AnyObject {} public final class BridgeDelegate { public let location: String diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index fbffb84..3db1aeb 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -140,11 +140,7 @@ class BridgeDelegateTests: XCTestCase { } } -private class BridgeDestinationSpy: BridgeDestination { - func bridgeWebViewIsReady() -> Bool { - return true - } -} +private class BridgeDestinationSpy: BridgeDestination {} private class OneBridgeComponent: BridgeComponent { static var name: String = "one" From b2ebd6f207bfbfe086270085f84d139c13187f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 09:49:59 +0200 Subject: [PATCH 18/26] Rename tests. --- Tests/BridgeDelegateTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index 3db1aeb..68996e7 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -72,7 +72,7 @@ class BridgeDelegateTests: XCTestCase { XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) } - func testInactiveDestinationIgnoresMessage() { + func testBridgeIgnoresMessageForInactiveDestination() { let message = Message(id: "1", component: "one", event: "connect", @@ -91,7 +91,7 @@ class BridgeDelegateTests: XCTestCase { XCTAssertNil(component) } - func testDestinationForwardsViewWillAppearToComponents() { + func testBridgeForwardsViewWillAppearToComponents() { delegate.bridgeDidReceiveMessage(testMessage()) let component: BridgeComponentSpy? = delegate.component() @@ -101,7 +101,7 @@ class BridgeDelegateTests: XCTestCase { XCTAssertTrue(component!.onViewWillAppearWasCalled) } - func testDestinationForwardsViewDidAppearToComponents() { + func testBridgeForwardsViewDidAppearToComponents() { delegate.bridgeDidReceiveMessage(testMessage()) let component: BridgeComponentSpy? = delegate.component() @@ -111,7 +111,7 @@ class BridgeDelegateTests: XCTestCase { XCTAssertTrue(component!.onViewDidAppearWasCalled) } - func testDestinationForwardsViewWillDisappearToComponents() { + func testBridgeForwardsViewWillDisappearToComponents() { delegate.bridgeDidReceiveMessage(testMessage()) let component: BridgeComponentSpy? = delegate.component() @@ -121,7 +121,7 @@ class BridgeDelegateTests: XCTestCase { XCTAssertTrue(component!.onViewWillDisappearWasCalled) } - func testDestinationForwardsViewDidDisappearToComponents() { + func testBridgeForwardsViewDidDisappearToComponents() { delegate.bridgeDidReceiveMessage(testMessage()) let component: BridgeComponentSpy? = delegate.component() From 9b363a8105e6b53050dea52e10d6ef0ee7101a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 11:28:35 +0200 Subject: [PATCH 19/26] Update to recommended Xcode settings. --- Strada.xcodeproj/project.pbxproj | 11 +++++++++-- .../xcshareddata/xcschemes/Strada.xcscheme | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index 8c0f4ec..e51f3da 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 53; objects = { /* Begin PBXBuildFile section */ @@ -200,8 +200,9 @@ 9274F1DC2229963B003E85F4 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1010; - LastUpgradeCheck = 1210; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = Basecamp; TargetAttributes = { 9274F1E42229963B003E85F4 = { @@ -430,6 +431,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -437,6 +439,8 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = dev.hotwired.strada; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -457,6 +461,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -464,6 +469,8 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = dev.hotwired.strada; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme b/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme index 28e3342..a43b636 100644 --- a/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme +++ b/Strada.xcodeproj/xcshareddata/xcschemes/Strada.xcscheme @@ -1,6 +1,6 @@ Date: Fri, 4 Aug 2023 11:36:28 +0200 Subject: [PATCH 20/26] Set min deployment target to iOS 13. --- Strada.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index e51f3da..a6a0874 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -349,7 +349,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -408,7 +408,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; From 16c8c2161cc0ec27d18be0ff4b4ba2b97baf5961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 11:50:15 +0200 Subject: [PATCH 21/26] Increase expectations timeout to 2 seconds. --- Tests/BridgeTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/BridgeTests.swift b/Tests/BridgeTests.swift index 16f0820..50231dc 100644 --- a/Tests/BridgeTests.swift +++ b/Tests/BridgeTests.swift @@ -90,7 +90,7 @@ class BridgeTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 2) } func testEvaluateFunction() { @@ -112,7 +112,7 @@ class BridgeTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 2) } } From a06c4842dd557ed26d7bf4aaaf1c1c1ae9b90deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 16:06:53 +0200 Subject: [PATCH 22/26] Make `BridgeComponent` an "abstract" class. --- Source/BridgeComponent.swift | 41 +++++++++++++++++++++++---------- Source/BridgeDelegate.swift | 4 +--- Tests/BridgeDelegateTests.swift | 28 +++++++++++----------- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index f8af271..a78af3e 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -1,12 +1,13 @@ import Foundation -public protocol BridgeComponent: AnyObject { +protocol BridgingComponent: AnyObject { static var name: String { get } - var delegate: BridgeDelegate? { get set } + var delegate: BridgeDelegate { get } - init(destination: BridgeDestination) - func handle(message: Message) + init(destination: BridgeDestination, + delegate: BridgeDelegate) + func handle(message: Message) func onViewDidLoad() func onViewWillAppear() func onViewDidAppear() @@ -14,19 +15,35 @@ public protocol BridgeComponent: AnyObject { func onViewDidDisappear() } -public extension BridgeComponent { +open class BridgeComponent: BridgingComponent { + class var name: String { + fatalError("BridgeComponent subclass must provide a unique 'name'") + } + + unowned var delegate: BridgeDelegate + + required public init(destination: BridgeDestination, delegate: BridgeDelegate) { + self.delegate = delegate + } + + public func handle(message: Message) { + fatalError("BridgeComponent subclass must handle incoming messages") + } + + public func onViewDidLoad() {} + public func onViewWillAppear() {} + public func onViewDidAppear() {} + public func onViewWillDisappear() {} + public func onViewDidDisappear() {} +} + +extension BridgingComponent { func send(message: Message) { - guard let bridge = delegate?.bridge else { + guard let bridge = delegate.bridge else { debugLog("bridgeMessageFailedToSend: bridge is not available") return } bridge.send(message) } - - func onViewDidLoad() {} - func onViewWillAppear() {} - func onViewDidAppear() {} - func onViewWillDisappear() {} - func onViewDidDisappear() {} } diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 2aa7854..89fac91 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -112,9 +112,7 @@ public final class BridgeDelegate { return nil } - let component = componentType.init(destination: destination) - component.delegate = self - + let component = componentType.init(destination: destination, delegate: self) initializedComponents[name] = component return component diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index 68996e7..9f0502e 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -143,17 +143,17 @@ class BridgeDelegateTests: XCTestCase { private class BridgeDestinationSpy: BridgeDestination {} private class OneBridgeComponent: BridgeComponent { - static var name: String = "one" - weak var delegate: Strada.BridgeDelegate? + static override var name: String { "one" } - required init(destination: Strada.BridgeDestination) {} + required init(destination: BridgeDestination, delegate: BridgeDelegate) { + super.init(destination: destination, delegate: delegate) + } - func handle(message: Strada.Message) {} + override func handle(message: Strada.Message) {} } private class BridgeComponentSpy: BridgeComponent { - static var name: String = "two" - weak var delegate: Strada.BridgeDelegate? + static override var name: String { "two" } var handleMessageWasCalled = false var handleMessageArg: Message? @@ -164,30 +164,32 @@ private class BridgeComponentSpy: BridgeComponent { var onViewWillDisappearWasCalled = false var onViewDidDisappearWasCalled = false - required init(destination: Strada.BridgeDestination) {} + required init(destination: BridgeDestination, delegate: BridgeDelegate) { + super.init(destination: destination, delegate: delegate) + } - func handle(message: Strada.Message) { + override func handle(message: Strada.Message) { handleMessageWasCalled = true handleMessageArg = message } - func onViewDidLoad() { + override func onViewDidLoad() { onViewDidLoadWasCalled = true } - func onViewWillAppear() { + override func onViewWillAppear() { onViewWillAppearWasCalled = true } - func onViewDidAppear() { + override func onViewDidAppear() { onViewDidAppearWasCalled = true } - func onViewWillDisappear() { + override func onViewWillDisappear() { onViewWillDisappearWasCalled = true } - func onViewDidDisappear() { + override func onViewDidDisappear() { onViewDidDisappearWasCalled = true } } From c18fc10feeb3f0081c5258fce51578f23a7b6eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 16:16:55 +0200 Subject: [PATCH 23/26] Make all functions and properties of `BridgeComponent` public. --- Source/BridgeComponent.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index a78af3e..8480501 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -16,11 +16,11 @@ protocol BridgingComponent: AnyObject { } open class BridgeComponent: BridgingComponent { - class var name: String { + public class var name: String { fatalError("BridgeComponent subclass must provide a unique 'name'") } - unowned var delegate: BridgeDelegate + public unowned let delegate: BridgeDelegate required public init(destination: BridgeDestination, delegate: BridgeDelegate) { self.delegate = delegate @@ -38,7 +38,7 @@ open class BridgeComponent: BridgingComponent { } extension BridgingComponent { - func send(message: Message) { + public func send(message: Message) { guard let bridge = delegate.bridge else { debugLog("bridgeMessageFailedToSend: bridge is not available") return From 506dcaea26e09a23d0ac9cbf09e6a997b885ffe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 16:21:22 +0200 Subject: [PATCH 24/26] Make relevant functions and properties of `BridgeComponent` open. --- Source/BridgeComponent.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index 8480501..302ea47 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -16,7 +16,7 @@ protocol BridgingComponent: AnyObject { } open class BridgeComponent: BridgingComponent { - public class var name: String { + open class var name: String { fatalError("BridgeComponent subclass must provide a unique 'name'") } @@ -26,18 +26,10 @@ open class BridgeComponent: BridgingComponent { self.delegate = delegate } - public func handle(message: Message) { + open func handle(message: Message) { fatalError("BridgeComponent subclass must handle incoming messages") } - public func onViewDidLoad() {} - public func onViewWillAppear() {} - public func onViewDidAppear() {} - public func onViewWillDisappear() {} - public func onViewDidDisappear() {} -} - -extension BridgingComponent { public func send(message: Message) { guard let bridge = delegate.bridge else { debugLog("bridgeMessageFailedToSend: bridge is not available") @@ -46,4 +38,10 @@ extension BridgingComponent { bridge.send(message) } + + open func onViewDidLoad() {} + open func onViewWillAppear() {} + open func onViewDidAppear() {} + open func onViewWillDisappear() {} + open func onViewDidDisappear() {} } From c92caf53ad10b902b8bfc6f54b3db494202eef27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 16:52:21 +0200 Subject: [PATCH 25/26] Set min deployment target to iOS 14. --- Strada.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index a6a0874..1788947 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -349,7 +349,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -408,7 +408,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; From c41a37eb003b5a3e107fb9a83247d21da8d8e96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Fri, 4 Aug 2023 16:57:32 +0200 Subject: [PATCH 26/26] Mark destination inactive only when view disappears. --- Source/BridgeDelegate.swift | 1 - Tests/BridgeDelegateTests.swift | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Source/BridgeDelegate.swift b/Source/BridgeDelegate.swift index 89fac91..00ca33a 100644 --- a/Source/BridgeDelegate.swift +++ b/Source/BridgeDelegate.swift @@ -56,7 +56,6 @@ public final class BridgeDelegate { public func onViewWillDisappear() { activeComponents.forEach { $0.onViewWillDisappear() } - destinationIsActive = false debugLog("bridgeDestinationViewWillDisappear: \(location)") } diff --git a/Tests/BridgeDelegateTests.swift b/Tests/BridgeDelegateTests.swift index 9f0502e..d1ad6d7 100644 --- a/Tests/BridgeDelegateTests.swift +++ b/Tests/BridgeDelegateTests.swift @@ -84,7 +84,7 @@ class BridgeDelegateTests: XCTestCase { var component: OneBridgeComponent? = delegate.component() XCTAssertNotNil(component) - delegate.onViewWillDisappear() + delegate.onViewDidDisappear() XCTAssertFalse(delegate.bridgeDidReceiveMessage(message)) component = delegate.component() @@ -131,6 +131,16 @@ class BridgeDelegateTests: XCTestCase { XCTAssertTrue(component!.onViewDidDisappearWasCalled) } + func testBridgeDestinationIsActiveAfterViewWillDisappearIsCalled() { + delegate.bridgeDidReceiveMessage(testMessage()) + + let component: BridgeComponentSpy? = delegate.component() + XCTAssertNotNil(component) + + delegate.onViewWillDisappear() + XCTAssertTrue(delegate.bridgeDidReceiveMessage(testMessage())) + } + private func testMessage() -> Message { return Message(id: "1", component: "two",