It's related to the passByValue nature of structs. In the sample code below, I'm displaying a list of structs (and I can add instances to my list using Int.random(1..<3) to pick one of two possible predefined versions of the struct).
I also have a detail view that can modify the details of a single struct. However when I run this code, it will instead modify all the instances (ie either Sunday or Monday) in my list. To see this behaviour, run the following code and:
- tap New Trigger enough times that there are multiple of at least one of the sunday/monday triggers
- tap one of the matching trigger rows
- modify either the day, or the int
expected: only one of the rows will reflect the edit
actual: all the matching instances will be updated.
This suggests to me that my Sunday and Monday static instances are being passed by reference when they get added to the array. But I had thought structs were strictly pass by value. What am I missing?
thanks in advance for any wisdom, Mike
struct ContentView: View {
@State var fetchTriggers: [FetchTrigger] = []
var body: some View {
NavigationView {
VStack {
Button("New Trigger") {
fetchTriggers.append(Int.random(in: 1..<3) == 1 ? .sunMorning : .monEvening)
}
List($fetchTriggers) { fetchTrigger in
NavigationLink(destination: FetchTriggerDetailView(fetchTrigger: fetchTrigger)
.navigationBarTitle("Back", displayMode: .inline))
{
Text(fetchTrigger.wrappedValue.description)
.padding()
}
}
}
}
}
}
struct FetchTrigger: Identifiable {
static let monEvening: FetchTrigger = .init(dayOfWeek: .monday, hour: 6)
static let sunMorning: FetchTrigger = .init(dayOfWeek: .sunday, hour: 3)
let id = UUID()
enum DayOfWeek: Int, Codable, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case sunday = 1
case monday
case tuesday
var description: String {
switch self {
case .sunday: return "Sunday"
case .monday: return "Monday"
case .tuesday: return "Tuesday"
}
}
}
var dayOfWeek: DayOfWeek
var hour: Int
var description: String {
"\(dayOfWeek.description), \(hour):00"
}
}
struct FetchTriggerDetailView: View {
@Binding var fetchTrigger: FetchTrigger
var body: some View {
HStack {
Picker("", selection: $fetchTrigger.dayOfWeek) {
ForEach(FetchTrigger.DayOfWeek.allCases) { dayOfWeek in
Text(dayOfWeek.description)
.tag(dayOfWeek)
}
}
Picker("", selection: $fetchTrigger.hour) {
ForEach(1...12, id: \.self) { number in
Text("\(number)")
.tag(number)
}
}
}
}
}
This suggests to me that my Sunday and Monday static instances are being passed by reference
No, that's not the problem.
But this way of adding a new trigger is causing the problem.
fetchTriggers.append(Int.random(in: 1..<3) == 1 ? .sunMorning : .monEvening)
This adds a new item. By it adds the same Sunday or Monday, with its existing ID.
So, you reuse the same struct.
Change to this to solve:
Button("New Trigger") {
let newTrigger = Int.random(in: 1..<3) == 1 ? FetchTrigger(dayOfWeek: .sunday, hour: 3) : FetchTrigger(dayOfWeek: .monday, hour: 6)
fetchTriggers.append(newTrigger)
// fetchTriggers.append(Int.random(in: 1..<3) == 1 ? .sunMorning : .monEvening)
}
You can also create a new ID:
struct FetchTrigger: Identifiable {
static var monEvening: FetchTrigger = .init(dayOfWeek: .monday, hour: 6)
static var sunMorning: FetchTrigger = .init(dayOfWeek: .sunday, hour: 3)
var id = UUID() // all need to be var
mutating func changedId() -> FetchTrigger {
self.id = UUID()
return self
}
and call:
fetchTriggers.append(Int.random(in: 1..<3) == 1 ? .sunMorning.changedId() : .monEvening.changedId())