Search code examples
swiftuicombine

SwiftUI not releasing model instances


Having a SwiftUI project generated by Xcode, and adding a custom MyView with a MyViewModel. The ContentView just renders MyView.

The problem:

  1. When the ContentView gets reloaded (the reload button changes its state), MyViewModel gets somehow disconnected from MyView (the MyView counter stops incrementing in the UI when the button is clicked), but the console logs show the incrementation works.
  2. If the model subscribes to a publisher, it does not get unsubscribed because the instance is not released. Therefore, the instances still process incoming messages and alter the app's data and state.

Looking at the instance counters and memory addresses in the console:

  1. Every time the ContentView gets refreshed, a new MyView and MyViewModel instances get created. However, the counter incrementation uses the original first-created model instance.
  2. Some model instances did not get released.

EDIT: The model needs to be recreated every time MyView is recreated.

import SwiftUI

struct ContentView: View {
    @State
    private var reloadCounter = 0

    var body: some View {
        VStack {
            Button(action: { self.reloadCounter += 1 },
                   label: { Text("Reload view") })

            Text("Reload counter: \(reloadCounter)")

            MyView().environmentObject(MyViewModel())
        }
    }
}
import SwiftUI

struct MyView: View {
    @EnvironmentObject
    private var model: MyViewModel

    var body: some View {
        VStack {
            Button(action: { self.model.counter += 1 },
                   label:  { Text("Increment counter") })

            Text("Counter value: \(model.counter)")
        }
        .frame(width: 480, height: 300)
    }

    init() { withUnsafePointer(to: self) { print("Initialising MyView struct instance \(String(format: "%p", $0))") }}
}
import Combine

class MyViewModel: ObservableObject {
    private static var instanceCount: Int = 0 { didSet {
        print("SettingsViewModel: \(instanceCount) instances")
    }}

    @Published
    var counter: Int = 0 { didSet {
        print("Model counter: \(counter), self: \(Unmanaged.passUnretained(self).toOpaque())")
    }}

    init() { print("Initialising MyViewModel class instance \(Unmanaged.passUnretained(self).toOpaque())"); Self.instanceCount += 1 }
    deinit { print("Deinitialising MyViewModel class instance \(Unmanaged.passUnretained(self).toOpaque())"); Self.instanceCount -= 1 }
}

Any clue what did I do wrong?

The image below depicts the app's UI after all the actions in the logs were performed.

enter image description here


Solution

  • In the latest Xcode (tested with Xcode 14.3.1) the model leaking does not happen anymore.

    Also, there is the @StateObject property wrapper available that allows views to instantiate the models.