Skip to content

Commit ebdcb61

Browse files
authored
iOS: fix memory leak issues (#486)
fix memory leak issues
1 parent 0b074ea commit ebdcb61

File tree

4 files changed

+33
-14
lines changed

4 files changed

+33
-14
lines changed

ios/core/Sources/Player/HeadlessPlayer.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,7 @@ public protocol HeadlessPlayer {
164164
public extension HeadlessPlayer {
165165
/// The current state of Player
166166
var state: BaseFlowState? {
167-
guard
168-
let jsState = jsPlayerReference?.invokeMethod("getState", withArguments: [])
169-
else { return nil }
170-
return BaseFlowState.createInstance(value: jsState)
167+
return jsPlayerReference?.getState()
171168
}
172169

173170
/**
@@ -221,6 +218,10 @@ public extension HeadlessPlayer {
221218
- completion: A completion handler for when the flow has completed
222219
*/
223220
func start(flow: String, completion: @escaping (Result<CompletedState, PlayerError>) -> Void) {
221+
// declare these variables outside and reference them inside the errorHandler to prevent retain cycle
222+
let playerRef = jsPlayerReference
223+
let loggerRef = logger
224+
224225
let promiseHandler: @convention(block) (JSValue?) -> Void = { completedState in
225226
guard
226227
let result = CompletedState.createInstance(from: completedState)
@@ -231,17 +232,17 @@ public extension HeadlessPlayer {
231232
}
232233
let errorHandler: @convention(block) (JSValue?) -> Void = { _ in
233234
guard
234-
let result = self.state as? ErrorState
235-
else {
235+
let result = playerRef?.getState() as? ErrorState else {
236236
return completion(.failure(PlayerError.jsConversionFailure))
237237
}
238-
logger.e(result.error)
238+
239+
loggerRef.e(result.error)
239240
completion(.failure(PlayerError.promiseRejected(error: result)))
240241
}
241242

242243
// Ensure these get created because otherwise we will never know when the flow ends/errors
243244
guard
244-
let context = jsPlayerReference?.context,
245+
let context = playerRef?.context,
245246
let flowObject = context.evaluateScript("(\(flow))"),
246247
!flowObject.isUndefined,
247248
let callback = JSValue(object: promiseHandler, in: context),
@@ -251,7 +252,7 @@ public extension HeadlessPlayer {
251252
}
252253

253254
// Should not be possible due to fatalError in constructor, but just for handling optionals safely
254-
guard let player = jsPlayerReference else { return completion(.failure(PlayerError.playerNotInstantiated)) }
255+
guard let player = playerRef else { return completion(.failure(PlayerError.playerNotInstantiated)) }
255256
player
256257
.invokeMethod("start", withArguments: [flowObject])
257258
.invokeMethod("then", withArguments: [callback])
@@ -347,3 +348,12 @@ internal extension JSContext {
347348
return value
348349
}
349350
}
351+
352+
private extension JSValue {
353+
// put getState in extension so it can be accessed by the computed property in HeadlessPlayer.state and also inside the ErrorHandler by the playerReference
354+
func getState() -> BaseFlowState? {
355+
guard let jsState = self.invokeMethod("getState", withArguments: [])
356+
else { return nil }
357+
return BaseFlowState.createInstance(value: jsState)
358+
}
359+
}

ios/swiftui/Sources/ManagedPlayer/ManagedPlayer.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ internal struct ManagedPlayer14<Loading: View, Fallback: View>: View {
147147

148148
public var body: some View {
149149
bodyContent(viewModel.stateTransition.call() ?? .identity)
150+
.onDisappear {
151+
context.clearExceptionHandler()
152+
}
150153
}
151154

152155
private func bodyContent(_ transitionInfo: PlayerViewTransition) -> some View {

ios/swiftui/Sources/ManagedPlayer/ManagedPlayerViewModel.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ public class ManagedPlayerViewModel: ObservableObject, NativePlugin {
114114
}
115115

116116
public func apply<P>(player: P) where P: HeadlessPlayer {
117-
player.hooks?.state.tap({ (state) in
117+
player.hooks?.state.tap({ [weak self] (state) in
118118
guard let inProgress = state as? InProgressState else {
119-
self.currentState = nil
119+
self?.currentState = nil
120120
return
121121
}
122-
self.currentState = inProgress
122+
self?.currentState = inProgress
123123
})
124124
}
125125

ios/swiftui/Sources/SwiftUIPlayer.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ public struct SwiftUIPlayer: View, HeadlessPlayer {
8484
self.onViewController(controller)
8585
}
8686

87-
hooks.state.tap { newState in
88-
self.state = newState
87+
hooks.state.tap { [weak self] newState in
88+
self?.state = newState
8989
}
9090

9191
guard !flow.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
@@ -109,6 +109,12 @@ public struct SwiftUIPlayer: View, HeadlessPlayer {
109109
registry.resetView()
110110
}
111111

112+
/// Clear the exceptionHandler of the context to remove reference to the logger
113+
/// should be called when ManagedPlayer gets tore down
114+
public func clearExceptionHandler() {
115+
player?.context.exceptionHandler = nil
116+
}
117+
112118
/// Returns `player` but asserts that it is not nil. Used from methods that should not be called
113119
/// when we are unloaded.
114120
private var expectedPlayer: JSValue? {

0 commit comments

Comments
 (0)