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.
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).
Try this approach using struct Day: Identifiable, Codable {..}
without the Hashable
and Equatable
code (use the defaults for those).
Also using ForEach($days) { $day in ...
and
Picker("Select a day focus", selection: $day.type) {...
as shown in the example code.
struct Day: Identifiable, Codable { // <--- here
var id: Int // 1 - 7
var type: DayType
var workout: [Workout]?
init(id: Int) {
self.id = id
self.type = .rest
self.workout = [Workout]()
}
enum CodingKeys: String, CodingKey {
case id, type, 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
}
}
}
struct ExercisePlanBuilderView: View {
// ...
@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 {
// ...
ScrollView {
ForEach($days) { $day in // <--- here
HStack {
Text("Day " + day.id.description + " - ").font(.title)
Picker("Select a day focus", selection: $day.type) { // <--- here
ForEach(DayType.allCases, id: \.self) {
Text($0.description).tag($0)
}
}
.pickerStyle(.segmented)
}
}
}
}
}
}
The tag type should match the selection type. A string isn’t a day type
Thank you. I had attempted this. It still only allows me to change one of the 7 pickers, and only one time, then the Pickers all stop responding while the rest of the UI remains responsive. I’ll edit the post to reflect this change.
Unless you have an exceptional reason never override Equatable and Hashable