6

I'm considered newby in SwiftUI and I have the below ViewModel. But I'm not sure MyViewModel should be singleton. Is that usage is right? And what is the best practice/usage for conforms ObservableObject?

class MyViewModel: ObservableObject {
    static let shared: MyViewModel = MyViewModel()
    
    @Published var result: String = ""
    
    private init() { }
    
    // some functions
}

struct ContentView: View {
    @ObservedObject private var vm = MyViewModel.shared
    
    var body: some View {
        Text(vm.result)
    }
}
Kenan Nur
  • 383
  • 6
  • 17

2 Answers2

4

Why do you think a viewmodel should be a singleton? And especially, why should an ObservableObject conformant class need a singleton instance? That's a bad idea.

Not only is this absolutely unnecessary, this would also mean you cannot have several instances of the same view on the screen without them having shared state. This is especially bad on iPad if you want to support split screen and running 2 scenes of your app on the screen at the same time.

Don't make anything a singleton, unless you absolutely have to.

The only important thing to keep in mind with storing @ObservedObjects on SwiftUI Views is that they should never be initialised inside the view. When an @ObservedObject changes (or one of its @Published properties change), the View storing it will be reloaded. This means that if you create the object inside the View, whenever the object updates, the view itself will create a new instance of said object.

So this is a bad idea and won't work:

struct ContentView: View {
    // Never do this
    @ObservedObject private var vm = MyViewModel()
    
    var body: some View {
        Text(vm.result)
    }
}

Instead, you need to inject the viewmodel into your View (by creating it in the parent view or in a coordinator, etc, wherever you create your ContentView from).

struct ParentView: View {
    @State private var childVM = MyViewModel()

    var body: some View {
        ContentView(vm: childVM)
    }
}


struct ContentView: View {
    @ObservedObject private var vm: MyViewModel
 
    // Proper way of injecting the view model
    init(vm: MyViewModel) {
        self.vm = vm
    }
   
    var body: some View {
        Text(vm.result)
    }
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • I'am gonna use ContentView in NavigationLink(). And I think if I don't use singleton instance of MyViewModel it creates again and again. To prevent this situation I used singleton. Is this way(injecting VM) also suitable for usage in NavigationLink?? – Kenan Nur Sep 18 '20 at 12:49
  • @KenanNur a singleton does solve the problem of recreation, but it isn't the right solution. Yes, injection is suitable for usage in `NavigationLink` too. – Dávid Pásztor Sep 18 '20 at 13:14
  • 5
    Just to point that with iOS 14, you can now use `@StateObject` if you want to have a view model not to be injected by the parent. – Rico Crescenzio Sep 18 '20 at 14:16
  • @DávidPásztor I added my new scenario. Can you take a look? Thanks – Kenan Nur Sep 18 '20 at 14:19
  • Question!, what about if I have a Cart that needs to be shared across.. ? I have always used a singleton for this before.. how would be. the approach using swift way ??? – lorenzo gonzalez Aug 29 '22 at 16:34
  • @lorenzogonzalez there are other (and better, more testable) ways of ensuring that only a single instance exists for the lifecycle of the app rather than using a singleton pattern with a `static let shared` instance. However, explaining those is not in scope for this question, nor can be done in the comments here. – Dávid Pásztor Aug 30 '22 at 08:22
1

I implemented my scenario by that way. Can we say this is right way?

struct RootTabView: View {
    @State var tabSelection = 0
    @State private var listVM = ListViewModel()
    
    var body: some View {
        TabView(selection: $tabSelection) {
            ListView(vm: listVM).tabItem({
                Text("Tab 1")
            }).tag(0)
            
            //Some other tabs
        }
    }
}

struct ListView: View {
    @ObservedObject var vm: ListViewModel
    
    var body: some View {
        NavigationView {
            List(vm.toDoList, id: \.self) { toDo in
                NavigationLink(destination: DetailView(vm: vm)) {
                    Text(toDo)
                }
            }
        }
        .onAppear {
            vm.getList()
        }
    }
}

struct DetailView: View {
    @ObservedObject var vm: ListViewModel
    
    var body: some View {
        Text(vm.toDoItem)
            .onAppear {
                vm.getDetail()
            }
    }
}

class ListViewModel: ObservableObject {
    @Published var toDoList: [String] = []
    @Published var toDoItem: String = ""
    
    func getList() {
        toDoList = ["a", "b", "c"]
    }
    
    func getDetail() {
        // do some stuffs
        toDoItem = "A"
    }
}
Ajith Renjala
  • 4,934
  • 5
  • 34
  • 42
Kenan Nur
  • 383
  • 6
  • 17
  • Yes, this looks fine. One piece of advice unrelated to the original question: the `DetailView` using the same `ListViewModel` as its parent view, `ListView` means that you won't be able to decouple your child view from its parent view. I would suggest creating a `DetailViewModel`, then your `ListViewModel` could store an array of `DetailViewModel`s and inject that into each `DetailView`. This will enable better reusability and decoupling of `DetailView`, since you will be able to create one without having to create a `ListView`. – Dávid Pásztor Sep 18 '20 at 14:22
  • @DávidPásztor I would love to decouple but then I also need my list to recognize when my detail view modified its item. Is there a way to get around coupled view models for that quite common case? – jeanmartin Jan 23 '21 at 16:56
  • @jeanmartin that's quite easy, you can just create a subscription to the `@Published` properties of your detail VM and react to them changing from the list VM. The list VM knowing about the detail VM is completely fine, only the detail VM should not know about the list VM. – Dávid Pásztor Jan 25 '21 at 09:41