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.