Bladeren bron

feat(spine-ios): Add memory leak detection and fix PMA handling

- Enable debug extension on app startup for leak detection
- Add reportLeaks() calls when example views disappear
- Fix PMA flag handling by reading it from atlas page instead of hardcoding to false
- Add manual dispose() method to SpineController for explicit cleanup if needed

Note: SwiftUI view caching may show false positive leaks when views disappear,
as SwiftUI keeps views in memory for performance until they're truly no longer needed.
Mario Zechner 2 weken geleden
bovenliggende
commit
ab53d271a4

+ 26 - 0
spine-ios/Example/Spine iOS Example/MainView.swift

@@ -28,35 +28,60 @@
  *****************************************************************************/
 
 import SpineiOS
+import SpineSwift
 import SwiftUI
 
+// View modifier to report memory leaks when view disappears
+struct LeakReporter: ViewModifier {
+    func body(content: Content) -> some View {
+        content
+            .onDisappear {
+                reportLeaks()
+            }
+    }
+}
+
+extension View {
+    func reportLeaksOnDisappear() -> some View {
+        modifier(LeakReporter())
+    }
+}
+
 struct MainView: View {
     var body: some View {
         List {
             Section {
                 NavigationLink("Simple Animation") {
                     SimpleAnimation()
+                        .reportLeaksOnDisappear()
                 }
                 NavigationLink("Play/Pause") {
                     PlayPauseAnimation()
+                        .reportLeaksOnDisappear()
                 }
                 NavigationLink("Animation State Listener") {
                     AnimationStateEvents()
+                        .reportLeaksOnDisappear()
                 }
                 NavigationLink("Debug Rendering") {
                     DebugRendering()
+                        .reportLeaksOnDisappear()
                 }
                 NavigationLink("Dress Up") {
                     DressUp()
+                        .reportLeaksOnDisappear()
                 }
                 NavigationLink("IK Following") {
                     IKFollowing()
+                        .reportLeaksOnDisappear()
                 }
                 NavigationLink("Physics") {
                     Physics()
+                        .reportLeaksOnDisappear()
                 }
                 NavigationLink("Disable Rendering") {
                     DisableRendering()
+                        .reportLeaksOnDisappear()
                 }
             } header: {
                 Text("Swift + SwiftUI")
@@ -66,6 +91,7 @@ struct MainView: View {
                     SimpleAnimationViewControllerRepresentable()
                         .navigationTitle("Simple Animation")
                         .navigationBarTitleDisplayMode(.inline)
+                        .reportLeaksOnDisappear()
                 }
             } header: {
                 Text("ObjC + UIKit")

+ 6 - 0
spine-ios/Example/Spine iOS Example/SpineExampleApp.swift

@@ -28,11 +28,17 @@
  *****************************************************************************/
 
 import SpineiOS
+import SpineSwift
 import SwiftUI
 
 @main
 struct SpineExampleApp: App {
 
+    init() {
+        // Enable debug extension for memory leak detection
+        enableDebugExtension(true)
+    }
+
     var body: some Scene {
         WindowGroup {
             NavigationView {

+ 7 - 0
spine-ios/Sources/SpineiOS/SpineController.swift

@@ -110,6 +110,13 @@ public final class SpineController: NSObject, ObservableObject {
             drawable?.dispose()  // TODO move drawable out of view?
         }
     }
+    
+    /// Manually dispose the drawable. Call this when you know the controller is no longer needed.
+    /// This is useful in SwiftUI where views may be cached and deinit may be delayed.
+    public func dispose() {
+        drawable?.dispose()
+        drawable = nil
+    }
 
     /// The ``Atlas`` from which images to render the skeleton are sourced.
     public var atlas: Atlas {

+ 4 - 1
spine-ios/Sources/SpineiOS/SpineUIView.swift

@@ -247,12 +247,15 @@ extension SpineUIView {
     }
 
     private func initRenderer(atlasPages: [UIImage]) throws {
+        // Get PMA flag from first atlas page if available
+        let pmaFlag = controller.atlas.pages.count > 0 ? (controller.atlas.pages[0]?.pma ?? false) : false
+        
         renderer = try SpineRenderer(
             device: SpineObjects.shared.device,
             commandQueue: SpineObjects.shared.commandQueue,
             pixelFormat: colorPixelFormat,
             atlasPages: atlasPages,
-            pma: false  // TODO: Get PMA flag from atlas when API is available
+            pma: pmaFlag
         )
         renderer?.delegate = controller
         renderer?.dataSource = controller