swiftswiftuimemory-leaksweak-referencesretain-cycle

Weak reference doesn't work as expected when passing it as a method reference


I am already aware of the strong/weak reference concept in swift.
yet after running the next code, and tapping on the Button (and dismissing the screen), the TestViewModel stayed in memory! I was expecting that using [weak viewmodel] will be enough to prevent it. in the second example I managed to fix it - but I don't understand why it worked

import SwiftUI
import Resolver

struct TestScreen: View {
    
    @StateObject var viewmodel = TestViewModel()
    @Injected var testStruct: TestStruct
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        NavigationView {
            
            VStack(spacing: 0) {
                
                Button("go back") { [weak viewmodel] in
                        testStruct.saveActionGlobaly(onAsyncAction:  viewmodel?.someAsyncAction )
                        presentationMode.wrappedValue.dismiss()
                    }
            }
        }
    }
}


import Foundation
import Resolver
import SwiftUI

public class TestStruct {
   var onAsyncAction: (() async throws -> Void)?
    
    public func saveActionGlobaly(onAsyncAction: (() async throws -> Void)?) {
        self.onAsyncAction = onAsyncAction
    } 
}

EXAMPLE 2:
I managed to prevent the leak by changing the code this way: (notice the changes in the callback passed to onAsyncAction)

import Resolver

struct TestScreen: View {
    
    @StateObject var viewmodel = TestViewModel()
    @Injected var testStruct: TestStruct
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        NavigationView {
            
            VStack(spacing: 0) {
                
                Button("go back") { [weak viewmodel] in
                        testStruct.saveActionGlobaly(onAsyncAction:  { await viewmodel?.someAsyncAction() } )
                        presentationMode.wrappedValue.dismiss()
                    }
            }
        }
    }
}

I dont understand why the second TestScreen managed to apply the weak reference and the first one didn't, thanks (:

environment: swift 5 xcode 14.2


Solution

  • Your first version:

    testStruct.saveActionGlobaly(onAsyncAction:  viewmodel?.someAsyncAction )
    

    is equivalent to this:

    let action: (() async throws -> Void)?
    if let vm = viewmodel {
        // vm is a strong non-nil reference, so this closure
        // has a strong non-nil reference to a TestViewModel.
        action = vm.someAsyncAction
    } else {
        action = nil
    }
    testStruct.saveActionGlobaly(onAsyncAction: action)
    

    SwiftUI holds on to your @StateObject for as long as TestScreen is part of the view hierarchy, which is as long as the Button is part of the view hierarchy. So SwiftUI maintains a strong reference to your TestViewModel until after it has called your Button's action. So in your first version, your weak viewmodel reference inside the Button's action will never be nil. Therefore vm will never be nil, action will never be nil, and action will always have a strong reference to the TestViewModel.

    Your second version:

    testStruct.saveActionGlobaly(onAsyncAction:  { await viewmodel?.someAsyncAction() } )
    

    preserves the weakness of the viewmodel variable. It only creates a strong reference to the TestViewModel momentarily, each time it is invoked, and discards the strong reference immediately after someAsyncAction returns.