Home > Mobile >  MVVM model in SwiftUI
MVVM model in SwiftUI

Time:02-22

I want to separate view from view model according to MVVM. How would I create a model in SwiftUI? I read that one should use struct rather than class.

As an example I have a model for a park where you can plant trees in:

// View Model
struct Park {
  var numberOfTrees = 0
  func plantTree() {
    numberOfTrees  = 1 // Cannot assign to property: 'self' is immutable
  }
}

// View
struct ParkView: View {
  var park: Park
  var body: some View {
    // …
  }
}

Read things about @State in such things, that make structs somewhat mutable, so I tried:

struct Park {
  @State var numberOfTrees = 0 // Enum 'State' cannot be used as an attribute
  func plantTree() {
    numberOfTrees  = 1 // Cannot assign to property: 'self' is immutable
  }
}

I did use @State successfully directly in a View. This doesn’t help with separating the view model code though.

I could use class:

class Park: ObservableObject {
  var numberOfTrees = 0
  func plantTree() {
    numberOfTrees  = 1
  }
}

…but then I would have trouble using this view model nested in another one, say City:

struct City {
  @ObservedObject var centerPark: Park
}

Changes in centerPark wouldn’t be published as Park now is reference type (at least not in my tests or here). Also, I would like to know how you solve this using a struct.

CodePudding user response:

as a starting point:

// Model
struct Park {
    var numberOfTrees = 0
    mutating func plantTree() {  // `mutating`gets rid of your error
        numberOfTrees  = 1
    }
}

// View Model
class CityVM: ObservableObject {
    
    @Published var park = Park() // creates a Park and publishes it to the views
    
    // ... other @Published things ...
    
    // Intents:
    func plantTree() {
        park.plantTree()
    }
}


// View
struct ParkView: View {
    
    // create the ViewModel, which creates the model(s)
    // usually you would do this in the App struct and make available to all views by .environmentObject
    @StateObject var city = CityVM()
    
    var body: some View {
        VStack {
            Text("My city has \(city.park.numberOfTrees) trees.")
            
            Button("Plant one more") {
                city.plantTree()
            }
        }
    }
}

CodePudding user response:

mutating func is the fix but I thought I'd include some other info below:

We don't use MVVM with SwiftUI because we don't use classes for transient view state and we don't control the View in the MVVM/MVC sense. SwiftUI creates and updates the Views on screen, i.e. UILabels, UITableView etc. automatically for us. The SwiftUI View structs are essentially the view model already, so if you were to recreate that as an object not only will you be needlessly make your code more complex but also would introduce object reference bugs SwiftUI is trying to eliminate by using structs. With property wrappers like @State and @Binding SwiftUI is doing some magic to make the struct behave like an object it is not a good idea to ignore that. To make your View structs more testable you can extract related vars into a struct and use mutating funcs like this:

// View Model
struct ParkConfig {
  var numberOfTrees = 0
  mutating func plantTree() {
    numberOfTrees  = 1
  }
}

struct ContentView {
    @State var parkConfig = ParkConfig()

    var body: some View {
        ParkView(config: $parkConfig)
    }
}

// View
struct ParkView: View {
  @Binding var config: ParkConfig
  var body: some View {
      Button("Click Me") {
          config.plantTree()
      }
  }
}

You can see Apple demonstrate this pattern in Data Essentials in SwiftUI WWDC 2020 at 4:18 where he says "EditorConfig can maintain invariants on its properties and be tested independently. And because EditorConfig is a value type, any change to a property of EditorConfig, like its progress, is visible as a change to EditorConfig itself."

  • Related