Seeing some behaviour in Swift that I don't understand...

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:

  1. tap New Trigger enough times that there are multiple of at least one of the sunday/monday triggers
  2. tap one of the matching trigger rows
  3. 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)
                                }
            }
        }
    }
}
Answered by Claude31 in 855317022

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())
Accepted Answer

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())
Seeing some behaviour in Swift that I don't understand...
 
 
Q