Search code examples
swiftmemoryautomatic-ref-countingself

Weakly captured self won't let the view model deallocate until the Task finishes


I am trying to learn ARC and I'm having a hard time with a weakly captured self. My project is using MVVM with SwiftUI. I'm presenting a sheet (AuthenticationLoginView) that has a @StateObject var viewModel = AuthenticationLoginViewModel() property. On dismiss of the presented sheet, I expect that the viewModel will have it's deinit called and so it does until I run an asynchronous function within a Task block.

class AuthenticationLoginViewModel: ObservableObject {

    @Published var isLoggingIn: Bool = false
    
    private var authenticationService: AuthenticationService
    
    private var cancellables: Set<AnyCancellable> = Set()
    private var onLoginTask: Task<Void, Never>?
    
    init(authenticationService: AuthenticationService) {
        self.authenticationService = authenticationService
    }
    
    deinit {
        onLoginTask?.cancel()
        LoggerService.log("deallocated")
    }
    
    public func onLogin() {
        guard !isLoggingIn else { return }
        isLoggingIn = true
        onLoginTask = Task { [weak self] in
            await self?.login()
        }
    }
    
    private func login() async {
        LoggerService.log("Logging in...")
        sleep(2)
        // 
        // self is still allocated here <<<---- ???
        //
        let authResponse = try? await self.authenticationService.authenticate(username: username, password: password)
        LoggerService.log(self.isLoggingIn) // <<--- prints `true`
        handleLoginResponse(authResponse: authResponse)
    }
}

So I have my two cases here:

Case #1

  1. I present the sheet.
  2. I dismiss the sheet.
  3. The deinit function is getting called (app logs: "deallocated")

Case #2

  1. I present the sheet.
  2. I press the login button so the onLogin function is getting called.
  3. I dismiss the sheet before the sleep(2) ends.
  4. ---- I EXPECT the "deallocated" message to be printed from deinit and the logging at LoggerService.log(self.isLoggingIn) to print nil and the self.authenticationService.authenticate(... to never be called as self doesn't exist anymore.
  5. Not expected but happening: the app prints "Logging in", sleeps for 2 seconds, calls the service, prints true, and then deallocates the view model (the view was dismissed 2 seconds ago)

What am I doing wrong?

I'm still learning and I'm pretty much unsure if this is normal or I miss something. Anyway, I expect the view model to be deallocated as the view referencing it was dismissed.


Solution

  • At the time you call onLogin the reference to self is valid and so the Task commences.

    After that, the reference to self in login keeps self alive. The Task has a life of its own, and you did not cancel it.

    Moreover the use of sleep is wrong, as it is not cancellable in any case, so neither is your Task. Use Task.sleep.