I have repetitive code for survey steps that I'm trying to make reusable:
private var workoutTypeSection: some View {
VStack(spacing: 20) {
header("What Is Your Preferred Workout?")
ForEach(WorkoutType.allCases) { workoutType in
Text(workoutType.rawValue)
.font(.headline)
.foregroundColor(.purple)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(selectedWorkoutTypes.contains(workoutType) ?
Color.blue : Color.white)
.cornerRadius(10)
.onTapGesture {
workoutButtonPressed(workout: workoutType)
}
}
}
.padding(30)
}
private var bodyPartSection: some View {
VStack(spacing: 20) {
header("Select a Body Part to Strengthen:")
ForEach(BodyPart.allCases) { bodyPart in
Text(bodyPart.rawValue)
.font(.headline)
.foregroundColor(.purple)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(selectedBodyParts.contains(bodyPart) ?
Color.blue : Color.white)
.cornerRadius(10)
.onTapGesture {
bodyPartButtonPressed(part: bodyPart)
}
}
}
.padding(30)
}
OnTapGesture selects or deselects survey options:
func workoutButtonPressed(workout: WorkoutType) {
if selectedWorkoutTypes.contains(workout) {
if let index = selectedWorkoutTypes.firstIndex(of: workout) {
selectedWorkoutTypes.remove(at: index)
}
} else {
selectedWorkoutTypes.append(workout)
}
}
func bodyPartButtonPressed(part: BodyPart) {
if selectedBodyParts.contains(part) {
if let index = selectedBodyParts.firstIndex(of: part) {
selectedBodyParts.remove(at: index)
}
} else {
selectedBodyParts.append(part)
}
}
where
@State var selectedWorkoutTypes: [WorkoutType] = []
@State var selectedBodyParts: [BodyPart] = []
and enum types are:
enum WorkoutType: String, CaseIterable, Codable, Identifiable {
case yoga = "Yoga"
case mobility = "Mobility"
case strength = "Strength"
case cardio = "Cardio"
var id: WorkoutType { self }}
enum BodyPart: String, CaseIterable, Codable, Identifiable {
case legs = "Legs"
case core = "Core/Abs"
case back = "Back"
case chest = "Chest/Arms"
case neck = "Neck/Shoulders"
case body = "Whole Body"
var id: BodyPart { self }}
I created a generic function that makes the code reusable:
func surveyStepOptions<T : Identifiable & Hashable & CaseIterable & RawRepresentable >(_ enumType: T.Type, selected: [T])
-> some View
where T.RawValue == String
{
ForEach(Array(enumType.allCases)) {option in
Text(option.rawValue).font(.headline)
.foregroundColor(.purple)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(selected.contains(option) ?
Color.blue : Color.white)
.cornerRadius(10)
.onTapGesture {
if selected.contains(option) {
if let index = selected.firstIndex(of: option) {
selected.remove(at: index)
}
} else {
selected.append(option)
}
}
}
}
I struggle with changing state variables in onTapGesture because it says "selected" param is unmutable, but when I make it mutable it says that I can't use inout in the escaping closure. How to fix it?
You are almost there, you just have to change the argument to a Binding
so you can alter the selection from within the ForEach
function
@ViewBuilder func surveyStepOptions<EnumType>(selected: Binding<[EnumType]>) -> some View where EnumType: CaseIterable & RawRepresentable<String> & Equatable{
ForEach(Array(EnumType.allCases), id:\.rawValue) {option in
Text(option.rawValue).font(.headline)
.foregroundColor(.purple)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(selected.wrappedValue.contains(option) ?
Color.blue : Color.white)
.cornerRadius(10)
.onTapGesture {
if let index = selected.wrappedValue.firstIndex(of: option) {
selected.wrappedValue.remove(at: index)
} else {
selected.wrappedValue.append(option)
}
}
}
}
Notice I also removed the explicit type argument, it isn't needed in Swift.
Here is the full code.
import SwiftUI
struct ReusableEnumParentView: View {
@State var selectedWorkoutTypes: [WorkoutType] = []
@State var selectedBodyParts: [BodyPart] = []
var body: some View {
ScrollView{
LazyVStack {
workoutTypeSection
bodyPartSection
}
}
}
@ViewBuilder var workoutTypeSection: some View {
VStack(spacing: 20) {
header("What Is Your Preferred Workout?")
surveyStepOptions(selected: $selectedWorkoutTypes)
}
.padding(30)
}
@ViewBuilder var bodyPartSection: some View {
VStack(spacing: 20) {
header("What Is Your Preferred Workout?")
surveyStepOptions(selected: $selectedBodyParts)
}
.padding(30)
}
@ViewBuilder func header(_ title: String) -> some View {
Text(title)
}
@ViewBuilder func surveyStepOptions<EnumType>(selected: Binding<[EnumType]>) -> some View where EnumType: CaseIterable & RawRepresentable<String> & Equatable{
ForEach(Array(EnumType.allCases), id:\.rawValue) {option in
Text(option.rawValue).font(.headline)
.foregroundColor(.purple)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(selected.wrappedValue.contains(option) ?
Color.blue : Color.white)
.cornerRadius(10)
.onTapGesture {
if let index = selected.wrappedValue.firstIndex(of: option) {
selected.wrappedValue.remove(at: index)
} else {
selected.wrappedValue.append(option)
}
}
}
}
}
#Preview {
ReusableEnumParentView()
}
enum WorkoutType: String, CaseIterable, Codable, Identifiable {
case yoga = "Yoga"
case mobility = "Mobility"
case strength = "Strength"
case cardio = "Cardio"
var id: WorkoutType { self }}
enum BodyPart: String, CaseIterable, Codable, Identifiable {
case legs = "Legs"
case core = "Core/Abs"
case back = "Back"
case chest = "Chest/Arms"
case neck = "Neck/Shoulders"
case body = "Whole Body"
var id: BodyPart { self }
}