Search code examples
swiftmvvmswiftuiviewviewmodel

MVVM model in SwiftUI


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.


Solution

  • 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()
                }
            }
        }
    }