swiftswiftuiswiftui-picker

Experiencing trouble with Picker in SwiftUI


Running into an issue using a Picker against an enum. Trying to create a workout app, where each Day (type, 7) of the ExercisePlan needs assigned to a DayType (enum). Then I'd build out the ability to add Workouts (type).

I'm currently just going the lazy route and defining an array of Days in the view. The issue comes when I use a Picker. I can successfully make one change, but then immediately all of the Pickers stop working.

Day

struct Day: Identifiable, Codable, Hashable, Equatable {
    var id: Int // 1 - 7
    var type: DayType
    var workout: [Workout]?
    
    init(id: Int) {
        self.id = id
        self.type = .rest
        self.workout = [Workout]()
    }
    
    static func ==(lhs: Day, rhs: Day) -> Bool {
        return lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    enum CodingKeys: String, CodingKey {
        case id
        case type
        case workout
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        type = try values.decode(DayType.self, forKey: .type)
        
        if values.contains(.workout) {
            workout = try values.decode([Workout]?.self, forKey: .workout)
        } else {
            workout = nil
        }
    }
}

DayType

enum DayType: Hashable, Codable, CaseIterable, Identifiable, CustomStringConvertible {
    case upper
    case lower
    case core
    case lowerAndCore
    case rest
    
    var id: Self { self }
    
    var description: String {
        switch self {
        case .upper:
            return "Upper"
        case .lower:
            return "Lower"
        case .core:
            return "Core"
        case .lowerAndCore:
            return "Lower and Core"
        case .rest:
            return "Rest"
        }
    }
}

ViewCode, including Picker

import SwiftUI

struct ExercisePlanBuilderView: View {
    @ObservedObject var model: MainViewModel
    @State var selectedUser: User
    @State var days = [Day(id: 1), Day(id: 2), Day(id: 3), Day(id: 4), Day(id: 5), Day(id: 6), Day(id: 7)]
    
    var body: some View {
        VStack {
            HStack {
                Button(action: {
                    withAnimation {
                        model.showExerciseBuilder = false
                    }
                }, label: {
                    HStack {
                        Image(systemName: "arrow.left")
                            .foregroundStyle(.red)
                        
                        Text("Discard")
                            .foregroundStyle(.red)
                            .bold()
                    }
                })
                
                Spacer()
                
                Text("Exercise Plan Builder")
                    .bold()
            }
            .padding(.horizontal)
            
            ScrollView {
                ForEach($days, id: \.id) { day in
                    HStack {
                        Text("Day " + day.id.description + " - ")
                            .font(.title)
                        
                        Picker("Select a day focus", selection: day.type) {
                            ForEach(DayType.allCases, id: \.self) {
                                Text($0.description).tag($0)
                            }
                        }
                        .pickerStyle(.segmented)

                    }
                }
            }
        }
    }
}

#Preview {
    ExercisePlanBuilderView(model: MainViewModel(), selectedUser: User())
}

Clicking them still shows a list to choose from, but anything beyond that first selection doesn't take affect. I've tried a few different ways to write this with no luck.


Solution

  • Your Equatable and Hashable implementations only compares id:

    static func ==(lhs: Day, rhs: Day) -> Bool {
        return lhs.id == rhs.id
    }
    

    This means that after selecting a different DayType with the picker, nothing changes, as far as SwiftUI can see. The @State of days is still equal to what it originally was. That causes SwiftUI to not update anything in the UI.

    You should also take type into account:

    static func ==(lhs: Day, rhs: Day) -> Bool {
        return lhs.id == rhs.id && lhs.type == rhs.type
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(type)
    }
    

    Or use the automatically generated implementations if possible.

    Note that equality and identity are different concepts. It's the difference between "something changed about this Day" and "this Day has magically disappeared and a new Day appears".

    Identifiable is already satisfied by the id property, which I recommend declaring as a let instead of a var. You don't need the id: parameter for ForEach - just do ForEach($days) { day in ...

    You shouldn't make the equality of your struct based solely on its identity. Properties of a Day can change (equality), without changing what Day it is (identity).