3

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

vigdora
  • 319
  • 4
  • 11
  • To use SwiftUI effectively you'll need to learn to use the View struct for your view data instead of a view model object – malhal Apr 14 '23 at 14:33
  • @malhal thanks for the input, but why using view model object is incorrect?. and how it is related to the issue? – vigdora Apr 14 '23 at 16:30
  • It's because you mention memory issues that are typical of objects and StateObject is not designed for a view model object. It's for when you need a reference type in a State. Since you are using async/await you don't need an object, best stick to value types like the built-in `View` struct for your view data. Use the View struct's `.task` modifier to get an async context. – malhal Apr 14 '23 at 17:01
  • I see what you are saying, but take into an account that this is a very narrow example that it not representing my real code. I am just trying to understand swift behavior under the hood in relation to weak references. and regardles, I think there are plenty examples of viewmodels using state object out there. – vigdora Apr 14 '23 at 17:06
  • @malhal btw, I tried the same without the stateObject - changing it to a normal class and the issue stil persists. so you can still address the original question - why the [weak viewmodel] in the first example doesnt work – vigdora Apr 16 '23 at 07:52
  • Yeh a normal class won't work. In SwiftUI use structs like the View struct with @State with struct or value – malhal Apr 16 '23 at 09:46

2 Answers2

4

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.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • first, vm and action are not the same type, so you got me already confused right there (; second, why the weak viewmodel reference has to be nil for it to work? why action is "insisting" on keeping the strong reference? I need a bit more explanation (or should I review the topic somewhere?) – vigdora Apr 13 '23 at 16:43
  • 1
    “vm and action are not the same type”—Sorry, that was an error. I have fixed it. As for action “insisting” on keeping the strong reference, that is just the way Swift works when you say `viewmodel?.someAsyncAction`. You either get nil, or you get a closure that holds a strong reference, depending on whether the weak reference is nil at the moment the expression is evaluated. If you wish to preserve the weakness of the reference, you must create the closure explicitly, as you do in your second version. – rob mayoff Apr 13 '23 at 17:05
  • hey rob, still scrathing my head over this... I replaced the stateObject with normal class - issue still persists (first example), leaving multiple viewmodels after dismissing the screen. although you managed to break down the differences between the two examples, I can't completely comprehend why the viewmodel is kept as strong reference in the first example - why the action would have to be nil as a pre-requisite? weak reference protects you from being nil in the future, not prior – vigdora Apr 16 '23 at 08:01
  • also I have found this post related - https://stackoverflow.com/questions/50582360/how-to-weak-reference-a-function-passed-as-a-parameter. specially the last answer. – vigdora Apr 16 '23 at 08:24
0

Using rob's answer and doing some extra reading I think I managed to shed some more light over this (at least for me, as rob's answer is ofcourse correct):
first of all, the weak-reference concept is a compiler game - meaning, the complier runs over the first example and translate it to: (as rob described)

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)

which makes sense...

the missing part for me was understanding the next sentence by rob:

action will always have a strong reference to the TestViewModel

so, there is another step where the compiler translate action to be a closure like so (very abstract):

{
   viewmodel.action // implicit viewmodel
}

and hands it over to onAsyncAction argument. in other words, the returned closure from evaluating action is holding another implicit viewmodel reference. the compiler can't conclude the explicit and the implicit viewmodels are related, thus the weakness is not applied to the later

vigdora
  • 319
  • 4
  • 11