diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index 3399f340a..59a337860 100644 --- a/ios/core/Sources/Player/HeadlessPlayer.swift +++ b/ios/core/Sources/Player/HeadlessPlayer.swift @@ -164,10 +164,7 @@ public protocol HeadlessPlayer { public extension HeadlessPlayer { /// The current state of Player var state: BaseFlowState? { - guard - let jsState = jsPlayerReference?.invokeMethod("getState", withArguments: []) - else { return nil } - return BaseFlowState.createInstance(value: jsState) + return jsPlayerReference?.getState() } /** @@ -221,6 +218,10 @@ public extension HeadlessPlayer { - completion: A completion handler for when the flow has completed */ func start(flow: String, completion: @escaping (Result) -> Void) { + // declare these variables outside and reference them inside the errorHandler to prevent retain cycle + let playerRef = jsPlayerReference + let loggerRef = logger + let promiseHandler: @convention(block) (JSValue?) -> Void = { completedState in guard let result = CompletedState.createInstance(from: completedState) @@ -231,17 +232,17 @@ public extension HeadlessPlayer { } let errorHandler: @convention(block) (JSValue?) -> Void = { _ in guard - let result = self.state as? ErrorState - else { + let result = playerRef?.getState() as? ErrorState else { return completion(.failure(PlayerError.jsConversionFailure)) } - logger.e(result.error) + + loggerRef.e(result.error) completion(.failure(PlayerError.promiseRejected(error: result))) } // Ensure these get created because otherwise we will never know when the flow ends/errors guard - let context = jsPlayerReference?.context, + let context = playerRef?.context, let flowObject = context.evaluateScript("(\(flow))"), !flowObject.isUndefined, let callback = JSValue(object: promiseHandler, in: context), @@ -251,7 +252,7 @@ public extension HeadlessPlayer { } // Should not be possible due to fatalError in constructor, but just for handling optionals safely - guard let player = jsPlayerReference else { return completion(.failure(PlayerError.playerNotInstantiated)) } + guard let player = playerRef else { return completion(.failure(PlayerError.playerNotInstantiated)) } player .invokeMethod("start", withArguments: [flowObject]) .invokeMethod("then", withArguments: [callback]) @@ -347,3 +348,12 @@ internal extension JSContext { return value } } + +private extension JSValue { + // put getState in extension so it can be accessed by the computed property in HeadlessPlayer.state and also inside the ErrorHandler by the playerReference + func getState() -> BaseFlowState? { + guard let jsState = self.invokeMethod("getState", withArguments: []) + else { return nil } + return BaseFlowState.createInstance(value: jsState) + } +} diff --git a/ios/swiftui/Sources/ManagedPlayer/ManagedPlayer.swift b/ios/swiftui/Sources/ManagedPlayer/ManagedPlayer.swift index ba8f05ed7..27b0b00c8 100644 --- a/ios/swiftui/Sources/ManagedPlayer/ManagedPlayer.swift +++ b/ios/swiftui/Sources/ManagedPlayer/ManagedPlayer.swift @@ -147,6 +147,9 @@ internal struct ManagedPlayer14: View { public var body: some View { bodyContent(viewModel.stateTransition.call() ?? .identity) + .onDisappear { + context.clearExceptionHandler() + } } private func bodyContent(_ transitionInfo: PlayerViewTransition) -> some View { diff --git a/ios/swiftui/Sources/ManagedPlayer/ManagedPlayerViewModel.swift b/ios/swiftui/Sources/ManagedPlayer/ManagedPlayerViewModel.swift index 7ce14d861..19a488231 100644 --- a/ios/swiftui/Sources/ManagedPlayer/ManagedPlayerViewModel.swift +++ b/ios/swiftui/Sources/ManagedPlayer/ManagedPlayerViewModel.swift @@ -114,12 +114,12 @@ public class ManagedPlayerViewModel: ObservableObject, NativePlugin { } public func apply

(player: P) where P: HeadlessPlayer { - player.hooks?.state.tap({ (state) in + player.hooks?.state.tap({ [weak self] (state) in guard let inProgress = state as? InProgressState else { - self.currentState = nil + self?.currentState = nil return } - self.currentState = inProgress + self?.currentState = inProgress }) } diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index fb7e66d9f..d80c288ec 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -84,8 +84,8 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { self.onViewController(controller) } - hooks.state.tap { newState in - self.state = newState + hooks.state.tap { [weak self] newState in + self?.state = newState } guard !flow.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { @@ -109,6 +109,12 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { registry.resetView() } + /// Clear the exceptionHandler of the context to remove reference to the logger + /// should be called when ManagedPlayer gets tore down + public func clearExceptionHandler() { + player?.context.exceptionHandler = nil + } + /// Returns `player` but asserts that it is not nil. Used from methods that should not be called /// when we are unloaded. private var expectedPlayer: JSValue? {