skip to Main Content

I’ve just started learning swift and was going to build this number-incrementing sample app to understand MVVM. I don’t understand why is my number on the view not updating upon clicking the button.

I tried to update the view everytime user clicks the button but the count stays at zero.

The View

import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = CounterViewModel()

    var body: some View {
        VStack {
            Text("(viewModel.model.count)")
            Button(action: {
                self.viewModel.increment()
            }) {
                Text("Increment")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The ViewModel

import SwiftUI
class CounterViewModel: ObservableObject {
    @ObservedObject var model = Model()

    func increment() {
        self.model.count += 1
    }
}

The Model

import Foundation
class Model : ObservableObject{
    @Published var count = 0
}

2

Answers


  1. Following should work:

    import SwiftUI
    
    struct Model {
        var count = 0
    }
    
    class CounterViewModel: ObservableObject {
       @Published var model = Model()
    
        func increment() {
            self.model.count += 1
        }
    }
    
    struct ContentView: View {
        @ObservedObject var viewModel = CounterViewModel()
    
        var body: some View {
            VStack {
                Text("(viewModel.model.count)")
                Button(action: {
                    self.viewModel.increment()
                }) {
                    Text("Increment")
                }
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    Please note:
    ObservableObject and @Published are designed to work together.
    Only a value, that is in an observed object gets published and so the view updated.
    A distinction between model and view model is not always necessary and the terms are somewhat misleading. You can just put the count var in the ViewModel. Like:

     @Published var count = 1
    

    It makes sense to have an own model struct (or class), when fx you fetch a record from a database or via a network request, than your Model would take the complete record.

    Something like:

    struct Adress {
       let name: String
       let street: String
       let place: String
       let email: String
    }
    

    Please also note the advantages (and disadvantages) of having immutable structs as a model. But this is another topic.

    Login or Signup to reply.
  2. Hi it’s a bad idea to use MVVM in SwiftUI because Swift is designed to take advantage of fast value types for view data like structs whereas MVVM uses slow objects for view data which leads to the kind of consistency bugs that SwiftUI’s use of value types is designed to eliminate. It’s a shame so many MVVM UIKit developers (and Harvard lecturers) have tried to push their MVVM garbage onto SwiftUI instead of learning it properly. Fortunately some of them are changing their ways.

    When learning SwiftUI I believe it’s best to learn value semantics first (where any value change to a struct is also a change to the struct itself), then the View struct (i.e. when body is called), then @Binding, then @State. e.g. have a play around with this:

    // use a config struct like this for view data to group related vars
    struct ContentViewConfig {
       var count = 0 {
           didSet {
               // could do validation here, e.g. isValid = count < 10
           }
       }
       // include other vars that are all related, e.g. you could have searchText and searchResults.
    
       // use mutating func for logic that affects multiple vars
        mutating func increment() {
            count += 1
            //othervar += 1
        }
    }
    
    struct ContentView: View {
        @State var config = ContentViewConfig() // normally structs are immutable, but @State makes it mutable like magic, so its like have a view model object right here, but better.
    
        var body: some View {
            VStack {
                ContentView2(count: config.count)
                ContentView3(config: $config)
            }
        }
    }
    
    // when designing a View first ask yourself what data does this need to do its job?
    struct ContentView2: View {
        let count: Int
    
        // body is only called if count is different from the last time this was init.
        var body: some View {
            Text(count, format: .number)
        }
    }
    
    struct ContentView3: View {
        @Binding var config: ContentViewConfig
    
        var body: some View {
            Button(action: {
                config.increment()
                }) {
                    Text("Increment")
                }
            }
        }
    }
    

    Then once you are comfortable with view data you can move on to model data which is when ObservableObject and singletons come into play, e.g.

    struct Item: Identifiable {
        let id = UUID()
        var text = ""
    }
    
    class MyStore: ObservableObject {
        @Published var items: [Item] = []
    
        static var shared = MyStore()
        static var preview = MyStore(preview: true)
    
        init(preview: Bool = false) {
            if preview {
                items = [Item(text: "Test Item")]
            }
        }
    }
    
    @main
    struct TestApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environmentObject(MyStore.shared)
            }
        }
    }
    
    struct ContentView: View {
        @EnvironmentObject store: MyStore
    
        var body: some View {
            List($store.items) { $item in
                TextField("Item", $item.text)
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
                .environmentObject(MyStore.preview)
        }
    }
    
    

    Note we use singletons because it would be dangerous to use @StateObject for model data because its lifetime is tied to something on screen we could accidentally lose all our model data which should have lifetime tied to the app running. Best to think of @StateObject when you need a reference type in a @State, e.g. when working asynchronously on view data when the View struct might be init multiple times and you want to take advantage of the object being deinit when the view disappears for implementing cancellation.

    When it comes to async features like network downloads try the new .task modifier. It has cancellation support built-in and with .task(id:) it will even cancel and restart when the id value changes. Much simpler than implementing these features yourself in an @StateObject.

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