skip to Main Content

My goal is to create Feature Oriented App. i.e. Feature1, Feature2.

My expected result:

  1. ContentView navigate to Feature1View:
    1. initialize Feature1ViewModel once
    2. navigate to Feature1_1View:
      1. initialize Feature1_1ViewModel once.

The problem:

  1. ContentView navigate to Feature1View:
    1. initialize Feature1ViewModel twice and deinit once.
    2. navigate to Feature1_1View:
      1. initialize Feature1_1ViewModel once.

What I’ve tried:

  1. use static var shared. The problem: How to detect the app have exit Feature1 (.onDisappear() is triggered when navigating from Feature1View to Feature1_1View)
class Feature1ViewModelManager {
    static var shared: Feature1ViewModel! {
        get {
            if sharedClosure == nil {
                sharedClosure = Feature1ViewModel()
            }
            
            return sharedClosure
        }
        set {
            sharedClosure = newValue
        }
    }
    
    private static var sharedClosure: Feature1ViewModel!
}
  1. use the if else statement instead of using NavigationStack in ContentView. The problem is that I can’t navigate back from Feature1View to ContentView

Feature1View.swift

enum Feature1ViewRoute {
    case Feature1_1
}


class Feature1ViewModel: ObservableObject {
    let id = UUID()
    
    @Published var departure: Station?
    
    init() { print("(type(of: self)) (#function) (id.uuidString)") }
    
    deinit { print("(type(of: self)) (#function) (id.uuidString)") }
}

struct Feature1View: View {
    @StateObject private var feature1ViewModel: Feature1ViewModel
    
    init(feature1ViewModel: Feature1ViewModel = Feature1ViewModel()) {
        self._feature1ViewModel = StateObject(wrappedValue: feature1ViewModel)
        print("(type(of: self)) (#function)")
    }
    
    var body: some View {
        VStack {
            Text("Feature1")
            Text("departure (feature1ViewModel.departure?.name ?? "")")
            Button {
                feature1ViewModel.departure = Station(name: "Lebak Bulus Grab")
            } label: {
                Text("set departure")
            }
            NavigationLink(value: Feature1ViewRoute.Feature1_1) {
                Text("Go to Feature1_1")
            }
        }
        .navigationDestination(for: Feature1ViewRoute.self) { route in
            switch route {
            case .Feature1_1:
                Feature1_1View()
            }
        }
    }
}

Feature1_1View.swift

enum Feature1_1ViewRoute {
    case Feature1_1_1
}


class Feature1_1ViewModel: ObservableObject {
    let id = UUID()
    
    init() { print("(type(of: self)) (#function) (id.uuidString)") }
    
    deinit { print("(type(of: self)) (#function) (id.uuidString)") }
}

struct Feature1_1View: View {
    @StateObject private var feature1_1ViewModel: Feature1_1ViewModel = Feature1_1ViewModel()
    
    init() {
        print("(type(of: self)) (#function)")
    }
    
    var body: some View {
        VStack {
            Text("Feature1_1View")
            
            NavigationLink(value: Feature1_1ViewRoute.Feature1_1_1) {
                Text("Go to Feature1_1_1")
            }
        }
        .navigationDestination(for: Feature1_1ViewRoute.self) { route in
            Feature1_1_1View()
        }
    }
}

Feature1_1_1View.swift

class Feature1_1_1ViewModel: ObservableObject {
    let id = UUID()
    
    init() { print("(type(of: self)) (#function) (id.uuidString)") }
    
    deinit { print("(type(of: self)) (#function) (id.uuidString)") }
}


struct Feature1_1_1View: View {
    @StateObject private var feature1_1_1ViewModel: Feature1_1_1ViewModel = Feature1_1_1ViewModel()
    
    init() {
        print("(type(of: self)) (#function)")
    }
    
    var body: some View {
        VStack {
            Text("Feature1_1_1View")
        }
    }
}

2

Answers


  1. Your issue is likely attributable to SwiftUI’s initialization behavior. The navigation stack will recreate the views, which encompasses initializing the @StateObjects each instance.

    One resolution to this is to utilize an @EnvironmentObject as opposed to an @StateObject. This will inhibit the ViewModel from being re-initialized every occasion the view is recreated.

    First, upon the app starting, initialize your ViewModel and pass it to the ContentView via .environmentObject() modifier:

    // AppDelegate.swift or SceneDelegate.swift
    let contentView = ContentView().environmentObject(Feature1ViewModel())
    

    Then, inside your views, you can refer to the ViewModel:

    // Feature1View.swift
    @EnvironmentObject var feature1ViewModel: Feature1ViewModel
    

    In this scenario, the ViewModel will not be reinitialized when the Feature1View is recreated. Additionally, since the ViewModel is being created and passed in at the root of the application, it will not be annihilated until the app itself is destroyed. So this should fulfill your necessity to initialize only once.

    One note on this approach: This is essentially creating a single instance of the ViewModel for the entire lifecycle of the application. It’s a bit akin to using a singleton, and it can have similar pros and cons. The ViewModel’s state will persist between navigations, which can be useful, but it also signifies that if you need to reset the ViewModel’s state when you navigate away from a view, you’ll need to do so manually.

    If you want to make the ViewModel initialization and destruction more explicit, you could contemplate using an architecture pattern like MVVM, which would permit you to manage the ViewModel’s lifecycle in a more granular way. With MVVM, each view has its own ViewModel, and the ViewModel is initialized when the view is created and destroyed when the view is dismantled.

    Login or Signup to reply.
  2. In SwiftUI, a @StateObject is intended to be initialized once and then to uphold its state across re-renders of the view. However, if you’re noticing your @StateObject being initialized multiple times, it’s possible that the parent view itself is getting re-rendered, provoking all of its body to be re-evaluated and consequently the StateObject to be re-initialized.

    You can resolve this issue by delivering your @StateObject through the view hierarchy, conceiving it at the root and conveying it as a @ObservedObject or @EnvironmentObject to offspring views.

    In your case, I would recommend initializing your view models as @StateObject within the ContentView and dispatch them as @ObservedObject to the corresponding feature views.

    Here’s an example of how you can accomplish this:

    struct ContentView: View {
        @StateObject private var feature1ViewModel = Feature1ViewModel()
        @StateObject private var feature1_1ViewModel = Feature1_1ViewModel()
        // ... more view models as required ...
    
        var body: some View {
            NavigationView {
                NavigationLink("Go to Feature1View", destination: Feature1View(feature1ViewModel: feature1ViewModel))
            }
        }
    }
    
    struct Feature1View: View {
        @ObservedObject var feature1ViewModel: Feature1ViewModel
        
        // No need to initialize the view model here again
        init(feature1ViewModel: Feature1ViewModel) {
            self.feature1ViewModel = feature1ViewModel
            print("(type(of: self)) (#function)")
        }
        
        var body: some View {
            // ...
        }
    }
    

    The same pattern can be employed to the other feature views, as well.

    Executing it this way certifies that each view model is initialized only once within ContentView and can be passed through to the feature views without being re-initialized. This approach also carries the advantage of making your state more predictable and easier to administer, since all state is governed at the top level of your application.

    If your app structure is complex and you find it hard to control state and relay down the necessary objects from the root of your app, you might want to contemplate using a state management library or an architectural pattern that assists with this, such as Redux or Flux.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search