ios – SwiftUI Memory Leak with multiple closures kept in @State keeping reference to @StatEObject


I’ve encountered very odd memory leak in code. It is caused mainly by usage of @escaping closure and nesting this closure. I’ve created minimal reproducible example to demonstrate this memory leak.

import SwiftUI

struct IdentifiableAction: Identifiable, Hashable {
    
    public let id: UUID
    public let action: () -> Void
    
    public init(action: @escaping () -> Void, id: UUID = .init()) {
        self.id = id
        self.action = action
    }
    
    public func callAsFunction() {
        action()
    }
    
    public static func == (lhs: IdentifiableAction, rhs: IdentifiableAction) -> Bool {
        lhs.id == rhs.id
    }
    
    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

extension View {
    func check(action: Binding<IdentifiableAction?>, label: String) -> some View {
        self
            .onChange(of: action.wrappedValue) { _ in
                guard action.wrappedValue != nil else { return }
                print("[DEBUG] Check action \(label)")
                action.wrappedValue?()
            }
    }
}

struct ContentView: View {
    
    var body: some View {
        TabView {
            NavigationView {
                VStack {
                    NavigationLink("DetailView") {
                        DetailView()
                    }
                }
                .padding()
            }
            .tabItem { Text("Tab 1") }
            
            
            Text("Page 2")
                .tabItem { Text("Tab 2") }
            Text("Page 3")
                .tabItem { Text("Tab 3") }
        }
    }
}

class DetailViewModel: ObservableObject {
    
    init() { 
        print("[DEBUG]: init DetailViewModel")
    }
    
    deinit {
        print("[DEBUG]: deinit DetailViewModel")
    }
    
    func action() {
        print("[DEBUG]: view model action")
    }
}

struct DetailView: View {
    
    @State private var actionCheck1: IdentifiableAction?
    @State private var actionCheck2: IdentifiableAction?
    @State private var actionCheck3: IdentifiableAction?
    
    @StateObject private var viewModel = DetailViewModel()
    
    var body: some View {
        VStack {
            
            Text("Detail view!")
            
            Button("Execute checked action") {
                actionCheck1 = IdentifiableAction {
                    actionCheck2 = IdentifiableAction {
                        actionCheck3 = IdentifiableAction {
                            viewModel.action()
                        }
                    }
                }
            }
        }
        .padding()
        .check(action: $actionCheck1, label: "1")
        .check(action: $actionCheck2, label: "2")
        .check(action: $actionCheck3, label: "3")
    }
}

Generally I want to have actions that is executed after doing some “checks”. Here this checks are simple print statements, by in app it can be showing some view, dialog, alert with different condition. But to keep example simple and minimal I just print statments about checks being done, and then execute action in view model. Something like below:

[DEBUG] Check action 1
[DEBUG] Check action 2
[DEBUG] Check action 3
[DEBUG]: view model action

I want each check to be something like view modifier or function on view extension (a bit similar to sheet(item: $presentedItem). So I have view extension function .check(action: $actionCheck1) To be able to use action as such trigger I need to wrap each action in IdentifiableAction struct that implements Identifiable protocol and Hashable protocol, I am appending to each action unique UUID each time the action is created.

Than I use this view modifiers this way:

.check(action: $actionCheck1, label: "1")
.check(action: $actionCheck2, label: "2")
.check(action: $actionCheck3, label: "3")

Ok and now exection of action that is intercepted by multiple check being action (IdentifiableAction) that must execute and pass to execute proper action viewModel.action I do:

Button("Execute checked action") {
                actionCheck1 = IdentifiableAction {
                    actionCheck2 = IdentifiableAction {
                        actionCheck3 = IdentifiableAction {
                            viewModel.action()
                        }
                    }
                }
            }

Above code sadly causes memory leak. Theoretically we are inside View that is struct, we use IdentifiableAction that is struct, and the only types that are reference types are DetailsViewModel class and closures passed to IdentifiableAction.

So the problem with memory leak are this nested closures used inside IdentifiableActions.

I’ve tired to use capture lists like [weak viewModel]. But it only works for single IdentifiableAction.

Button("Execute checked action") {
    actionCheck1 = IdentifiableAction { [weak viewModel] in
         viewModel?.action()
    }
}

So above code prevents memory leak.

But adding additional [weak viewModel] capture lists in nested closures doesn’t change anything, and memory leak prevails.

Button("Execute checked action") {
       actionCheck1 = IdentifiableAction { [weak viewModel] in
           actionCheck2 = IdentifiableAction { [weak viewModel] in
              actionCheck3 = IdentifiableAction { [weak viewModel] in
                  viewModel?.action()
             }
          }
      }
}

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img