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.
This commit is contained in:
Mario Zechner 2025-08-27 11:21:23 +02:00
parent cb62bd70c0
commit ab53d271a4
4 changed files with 43 additions and 1 deletions

View File

@ -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")

View File

@ -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 {

View File

@ -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 {

View File

@ -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