1

I have a minimal working example of something I'm still not sure about:

import SwiftUI

struct Car {
    var name: String
}

class DataModel: ObservableObject {
    @Published var cars: [Car]

    init(_ cars: [Car]) {
        self.cars = cars
    }
}

struct TestList: View {
    @EnvironmentObject var dataModel: DataModel

    var body: some View {
        NavigationView {
            List(dataModel.cars, id: \.name) { car in
                NavigationLink(destination: TestDetail(car: car).environmentObject(self.dataModel)) {
                    Text("\(car.name)")
                }
            }
        }
    }
}

struct TestDetail: View {
    @EnvironmentObject var dataModel: DataModel
    var car: Car

    var carIndex: Int {
        dataModel.cars.firstIndex(where: {$0.name == self.car.name})!
    }

    var body: some View {
        Text(car.name)
            .onTapGesture {
                self.dataModel.cars[self.carIndex].name = "Changed Name"
        }
    }
}

struct TestList_Previews: PreviewProvider {
    static var previews: some View {
        TestList().environmentObject(DataModel([.init(name: "A"), .init(name: "B")]))
    }
}

It's about the usage of structs as data models. The example is similar to the official SwiftUI tutorial by Apple (https://developer.apple.com/tutorials/swiftui/handling-user-input).

Basically, we have a DataModel class that is passed down the tree as EnvironmentObject. The class wraps the basic data types of our model. In this case, it's an array of the struct Car:

class DataModel: ObservableObject {
    @Published var cars: [Car]
    ...
}

The example consists of a simple list that shows the names of all cars. When you tap on one, you get to a detail view. The detail view is passed the car as property (while the dataModel is passed as EnvironmentObject):

NavigationLink(destination: TestDetail(car: car).environmentObject(self.dataModel)) {
    Text("\(car.name)")
}

The property car of the detail view is used to populate it. However, if you want to e.g. change the name of the car from within the detail view you have to go through the dataModel because car is just a copy of the original instance found in dataModel. Thus, you first have to find the index of the car in the dataModel's cars array and then update it:

struct TestDetail: View {
    ...
    var carIndex: Int {
        dataModel.cars.firstIndex(where: {$0.name == self.car.name})!
    }
    ...
    self.dataModel.cars[self.carIndex].name = "Changed Name"

This doesn't feel like a great solution. Searching for the index is a linear operation you have to do whenever you want to change something (the array could change at any time, so you have to constantly repeat the index search).

Also, this means that you have duplicate data. The car property of the detail view exactly mirrors the car of the viewModel. This separates the data. It doesn't feel right.

If car was a class instead of a struct, this would no be a problem because you pass the instance as reference. It would be much simpler and cleaner.

However, it seems that everyone wants to use structs for these things. Sure, they are safer, there can't be reference cycles with them but it creates redundant data and causes more expensive operations. At least, that's what it looks like to me.

I would love to understand why this might not be a problem at all and why it's actually superior to classes. I'm sure I'm just having trouble understanding this as a new concept.

SwiftedMind
  • 3,701
  • 3
  • 29
  • 63
  • 1
    I had a mind to start from copy-on-write, but I'm not a very good explainer, so just use `@Binding` as in [example approach here](https://stackoverflow.com/a/59915549/12299030), which had similar model as you described, and no copies/searches, at all. But... [this WWDC session](https://developer.apple.com/videos/play/wwdc2016/416/) will be helpful. – Asperi Mar 29 '20 at 15:30
  • Thanks! I will check it out. I did not know that bindings on arrays also create bindings for their elements when accessed. That's neat – SwiftedMind Mar 29 '20 at 15:36
  • 1
    I'm not sure what the best architectural solution for this is, but at the minimum, you could improve this situation by using a dict instead of an array, and keying it by car names, or other identifying data. I had a related question, unfortunately it never got answered: https://stackoverflow.com/questions/58809724/how-to-apply-structs-to-modeling-the-data-a-view-controller-operates-on – Alexander Mar 29 '20 at 15:51
  • @Asperi Your answer to that question works pretty well. However, I want to put all the data inside a view model class instead of the view itself (to follow MVVM architecture). This created a follow up question: How to do bindings when using view models? I can't find a way to do it conveniently in `swiftui`: https://stackoverflow.com/questions/60946578/how-to-work-with-bindings-when-using-a-view-model-vs-using-binding-in-the-view. Any ideas? Thanks! – SwiftedMind Mar 31 '20 at 08:49

0 Answers0