From 22e109bf9dfaf3ede442c218d35cbf171ca05548 Mon Sep 17 00:00:00 2001 From: Casper Zandbergen Date: Sat, 27 Sep 2025 21:55:40 +0200 Subject: [PATCH 1/3] fix closure retain cycle --- Sources/DotLottie/Public/DotLottieAnimationView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/DotLottie/Public/DotLottieAnimationView.swift b/Sources/DotLottie/Public/DotLottieAnimationView.swift index c6323e1..2b7288a 100644 --- a/Sources/DotLottie/Public/DotLottieAnimationView.swift +++ b/Sources/DotLottie/Public/DotLottieAnimationView.swift @@ -21,9 +21,9 @@ public class DotLottieAnimationView: UIView, DotLottie { super.init(frame: .zero) - dotLottieViewModel.$framerate.sink { value in - if self.mtkView != nil { - self.mtkView.preferredFramesPerSecond = dotLottieViewModel.framerate + dotLottieViewModel.$framerate.sink { [weak self] value in + if let self, mtkView != nil { + mtkView.preferredFramesPerSecond = dotLottieViewModel.framerate } }.store(in: &cancellableBag) From c5da0d80209cf8c5d6872342ace2f75a16543fe6 Mon Sep 17 00:00:00 2001 From: Casper Zandbergen Date: Sat, 27 Sep 2025 21:57:25 +0200 Subject: [PATCH 2/3] fix memory leak --- Sources/DotLottie/Public/DotLottie.swift | 2 +- .../Public/DotLottieAnimationView.swift | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/DotLottie/Public/DotLottie.swift b/Sources/DotLottie/Public/DotLottie.swift index f2205d6..ed7e186 100644 --- a/Sources/DotLottie/Public/DotLottie.swift +++ b/Sources/DotLottie/Public/DotLottie.swift @@ -8,5 +8,5 @@ import Foundation protocol DotLottie { - var dotLottieViewModel: DotLottieAnimation { get set } + var dotLottieViewModel: DotLottieAnimation { get } } diff --git a/Sources/DotLottie/Public/DotLottieAnimationView.swift b/Sources/DotLottie/Public/DotLottieAnimationView.swift index 2b7288a..31100f6 100644 --- a/Sources/DotLottie/Public/DotLottieAnimationView.swift +++ b/Sources/DotLottie/Public/DotLottieAnimationView.swift @@ -38,7 +38,7 @@ public class DotLottieAnimationView: UIView, DotLottie { private func setupMetalView() { mtkView = MTKView(frame: bounds) - self.coordinator = Coordinator(self, mtkView: mtkView) + self.coordinator = Coordinator(WeakWrapper(self), mtkView: mtkView) if let metalDevice = MTLCreateSystemDefaultDevice() { mtkView.device = metalDevice @@ -72,6 +72,20 @@ public class DotLottieAnimationView: UIView, DotLottie { public func subscribe(observer: Observer) { self.dotLottieViewModel.subscribe(observer: observer) } + + private class WeakWrapper: DotLottie { + var dotLottieViewModel: DotLottieAnimation { + view?.dotLottieViewModel ?? initialDotLottieViewModel + } + + private weak var view: DotLottieAnimationView? + private var initialDotLottieViewModel: DotLottieAnimation + + init(_ view: DotLottieAnimationView) { + self.view = view + self.initialDotLottieViewModel = view.dotLottieViewModel + } + } } #endif From f58f8ec4686f5214951e2e6abbafb16bfa0dd06e Mon Sep 17 00:00:00 2001 From: Casper Zandbergen Date: Sat, 27 Sep 2025 21:57:37 +0200 Subject: [PATCH 3/3] add test --- Tests/DotLottieTests/DotLottieTests.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/DotLottieTests/DotLottieTests.swift b/Tests/DotLottieTests/DotLottieTests.swift index 6e0042b..88ce0b3 100644 --- a/Tests/DotLottieTests/DotLottieTests.swift +++ b/Tests/DotLottieTests/DotLottieTests.swift @@ -2,10 +2,11 @@ import XCTest @testable import DotLottie final class DotLottieTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. -// XCTAssertEqual(DotLottie().text, "Hello, World!") + func testAnimationViewMemoryLeak() throws { + weak var view: DotLottieAnimationView? = testAnimation.view() + XCTAssertNil(view) } } + +/// https://lottiefiles.com/free-animation/swipe-left-arrows-nIIJhfaFd3 +let testAnimation = DotLottieAnimation(animationData: #"{"v":"5.5.9","fr":60,"ip":0,"op":151,"w":500,"h":500,"nm":"scroll_up 2","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[250,468,0],"to":[0,-36.333,0],"ti":[0,36.333,0]},{"t":79,"s":[250,250,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":62,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":108,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":126,"s":[100]},{"t":143,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[243.769,343.23,0],"to":[0,-11.5,0],"ti":[0,11.5,0]},{"t":99,"s":[243.769,274.23,0]}],"ix":2},"a":{"a":0,"k":[63.846,26.538,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0,53.077],[65.384,0],[100.697,30.081],[127.692,53.077]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Path 1","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":32,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":92,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":117,"s":[100]},{"t":132,"s":[1]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-5.231,-51.77,0],"ix":2},"a":{"a":0,"k":[63.846,26.538,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0,53.077],[65.384,0],[100.697,30.081],[127.692,53.077]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"scroll_up","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[249,250,0],"ix":2},"a":{"a":0,"k":[250,250,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":500,"h":500,"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]}"#, config: .init())