Home > Software engineering >  How to pass by reference in Swift - number incrementing app using MVVM
How to pass by reference in Swift - number incrementing app using MVVM

Time:01-31

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
}

CodePudding user response:

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.

CodePudding user response:

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, i.e. involving view data.

When it comes to async networking use the new .task modifier and you can avoid @StateObject.

  • Related