Code composition of SwiftUI when the model is passed from parent and dependend on parents setup - swift

How to structure the #ObservedObject, #State, #StateObject for a situation like this
i have a box which displays next and previous appointment (NextPreviousAppointmentBoxUI)
this NextPreviousAppointmentBoxUI uses a generic appointment box (SingleAppointmentBoxUI), which loads and appointment by the configuration of the model (FAppointmentsStore) provided by the parent
this model is however dependent on the appointment given to NextPreviousAppointmentBoxUI from above
i restructred the code several times, but still not sure how the SwiftUI composition should look like in such case
how to avoid setupAndLoadModels be called .onChange and .onAppear and be called just once? Is there a more suitabl composition?
class FAppointmentsStore: ObservableObject {
#Published state: State = .loading
...
}
class FAppointment: RealmObject {
...
}
struct SingleAppointmentBoxUI: View {
#ObservedObject var model: FAppointmentsStore
var body: some View {
Group {
switch model.loadingState {
case .loading:
Text("Loading")
case .loaded:
if let appointment = model.appointment {
Text("Display appointment \(appointment.title)")
}
case .error:
Text("Error")
}
}
}
}
struct NextPreviousAppointmentBoxUI: View {
var appointment: FAppointment
#State var nextModel: FAppointmentsStore?
#State var previousModel: FAppointmentsStore?
var body: some View {
VStack {
if let nextModel = nextModel, let previousModel = previousModel {
Text("Previous")
SingleAppointmentBoxUI(model: previousModel)
Divider()
Text("Next")
SingleAppointmentBoxUI(model: nextModel)
}
}
.onAppear {
setupAndLoadModels(appointment)
}
.onChange(of: appointment, perform: { newValue in
setupAndLoadModels(newValue)
})
}
func setupAndLoadModels(_ appointment: FAppointment?) {
guard let user = appointment.user, let start = appointment.start, let end = appointment.end else {
return
}
nextModel = FAppointmentsStore(user: user, startFrom: end, limit: 1, sort: .startAsc)
previousModel = FAppointmentsStore(user: user, startTo: start, limit: 1, sort: .startDesc)
nextModel?.loadContent()
previousModel?.loadContent()
}
}

Related

SwiftUI enum binding not refreshing view

I'm trying to show different views (with the same base) depending on an enum value but depending on how to "inspect" the enum the behavior changes. This is the code (I'm using a "useSwitch" variable to be able to alternate between both behaviors)
import SwiftUI
enum ViewType: CaseIterable {
case type1
case type2
var text: String {
switch self {
case .type1:
return "Type 1"
case .type2:
return "Type 2"
}
}
}
final class BaseVM: ObservableObject {
let type: ViewType
#Published var requestingData = false
init(type: ViewType) {
self.type = type
}
#MainActor func getData() async {
requestingData = true
try! await Task.sleep(nanoseconds: 1_000_000_000)
requestingData = false
}
}
struct BaseView: View {
#StateObject var vm: BaseVM
var body: some View {
Group {
if vm.requestingData {
ProgressView("Getting data for \(vm.type.text)")
} else {
Text("\(vm.type.text)")
}
}
.onAppear {
Task {
await vm.getData()
}
}
}
}
struct TestZStackView: View {
private let types = ViewType.allCases
#State var currentType: ViewType = .type1
private var useSwitch = true
var body: some View {
VStack {
if useSwitch {
Group {
switch currentType {
case .type1:
BaseView(vm: BaseVM(type: currentType))
case .type2:
BaseView(vm: BaseVM(type: currentType))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
BaseView(vm: BaseVM(type: currentType))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Spacer()
Picker("", selection: $currentType) {
ForEach(types, id: \.self) {
Text($0.text)
}
}
.pickerStyle(.segmented)
.padding(.top, 20)
}
.padding()
}
}
struct TestZStackView_Previews: PreviewProvider {
static var previews: some View {
TestZStackView()
}
}
I don't understand why using a switch (useSwitch == true) refreshes the view but using the constructor passing the enum as parameter (useSwitch = false) doesn't refresh the view... It can't detect that the currentType has changed if used as parameter instead of checking it using a switch?
This is all about identity. If you need more information I would recommend watching WWDC Demystify SwiftUI.
If your #State var triggers when changing the Picker the TestZStackView rebuilds itself. When hitting the if/else clause there are two possibilities:
private var useSwitch = true. So it checks the currentType and builds the appropriate BaseView. These differ from each other in their id, so a new View gets build and you get what you expect.
the second case is less intuitive. I really recommend watching that WWDC session mentioned earlier. If private var useSwitch = false there is no switch statement and SwiftUI tries to find out if your BaseView has changed and needs to rerender. For SwiftUI your BaseView hasn´t changed even if you provided a new BaseVM. It does notify only changes on depending properties or structs (or #Published in ObservableObject).
In your case #StateObject var vm: BaseVM is the culprit. But removing #StateObject will create the new View but you loose the ObservableObject functionality.
Solution here would be to restructure your code. Use only one BaseVm instance that holds your state and pass that on into the environment.
E.g.:
final class BaseVM: ObservableObject {
// create a published var here
#Published var type: ViewType = .type1
#Published var requestingData = false
#MainActor func getData() async {
requestingData = true
try! await Task.sleep(nanoseconds: 1_000_000_000)
requestingData = false
}
}
struct BaseView: View {
// receive the viewmodel from the environment
#EnvironmentObject private var vm: BaseVM
var body: some View {
Group {
if vm.requestingData {
ProgressView("Getting data for \(vm.type.text)")
} else {
Text("\(vm.type.text)")
}
}
// change this also because the view will not apear multiple times it
// will just change depending on the type value
.onChange(of: vm.type) { newValue in
Task{
await vm.getData()
}
}.onAppear{
Task{
await vm.getData()
}
}
}
}
struct TestZStackView: View {
private let types = ViewType.allCases
#StateObject private var viewmodel = BaseVM()
private var useSwitch = false
var body: some View {
VStack {
if useSwitch {
//this group doesn´t really make sense but just for demonstration
Group {
switch viewmodel.type {
case .type1:
BaseView()
.environmentObject(viewmodel)
case .type2:
BaseView()
.environmentObject(viewmodel)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
BaseView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.environmentObject(viewmodel)
}
Spacer()
Picker("", selection: $viewmodel.type) {
ForEach(types, id: \.self) {
Text($0.text)
}
}
.pickerStyle(.segmented)
.padding(.top, 20)
}
.padding()
}
}

SwiftUI: How to update element in ForEach without necessity to update all elements?

Imagine that you have some parent view that generate some number of child views:
struct CustomParent: View {
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index)
}
}
}
}
struct CustomChild: View {
#State var index: Int
#State private var text: String = ""
var body: some View {
Button(action: {
// Here should be some update of background/text/opacity or whatever.
// So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Question is included in the code as comment.
Thanks!
UPDATE:
First of all really thanks for all of your answers, but now imagine that you use mentioned advanced approach.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
If I use let _ = Self._printChanges() method in CustomChildView, to catch UI updates/changes, it'll print that every element in ForEach was updated/changed on button action.
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
let _ = Self._printChanges() // This have been added to code
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}
And now imagine that you have for example 1000 custom elements which have some background, opacity, shadow, texts, fonts and so on. Now I change text in any of the elements.
Based on log from let _ = Self._printChanges() method, it goes through all elements, and all elements are updated/changed what can cause delay.
Q1: Why did update/change all elements, if I change text in only one element?
Q2: How can I prevent update/change all elements, if I change only one?
Q3: How to update element in ForEach without necessity to update all elements?
Simpler Approach:
Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.
I strongly suggest not to do this though, as more elegant approaches exist.
struct CustomParent: View {
#State var childTexts = [String](repeating: "", count: 10)
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, childTexts: $childTexts)
}
}
}
}
struct CustomChild: View {
let index: Int
#Binding private var text: String
#Binding private var childTexts: [String]
init(index: Int, childTexts: Binding<[String]>) {
self.index = index
self._childTexts = childTexts
self._text = childTexts[index]
}
var body: some View {
Button(action: {
//button behaviors goes here
//for example
childTexts[index + 1] = "A"
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Advanced Approach:
By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the #Published variable in the ObservableObject will publish a change when it senses its own mutation, while the #StateObjectand the #ObservedObject will listen and recalculate the view for you.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}

WidgetKit #StateObject not updating View

I'm having trouble understanding how to make my SwiftUI data model example work with my Widget. It's working in my test app just fine, I observe changes immediately. When I attempt the Widget, I can see the data being printed in console but no changes are happening in my View in WidgetKit. I'm using a ObservableObject class and #Published variables. I've attempted to use a #StateObject, #ObservedObject and an #EnvironmentObject and the same results. Always results in No game today. My data model is Combine related, not sure if that has anything to do with why I can't see data changes in my WidgetKit's View.
SwiftUI App example working
struct ContentView: View {
#ObservedObject var data = CombineData()
#State private var showSortSheet: Bool = false
#State private var nhlTeams: TeamOrder = .NewYorkIslanders
var body: some View {
NavigationView {
VStack(alignment: .leading) {
if data.schedule?.dates.first != nil {
Text("Game today")
} else {
Text("No game today")
}
}
.listStyle(PlainListStyle())
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading: HStack {
Button(action: {
self.showSortSheet.toggle()
}) {
HStack {
Image(systemName: "arrow.up.arrow.down")
}
}
})
.actionSheet(isPresented: $showSortSheet) {
ActionSheet(title: Text("Teams:"), buttons: [TeamOrder.NewYorkIslanders, TeamOrder.MontrealCanadiens].map { method in
ActionSheet.Button.default(Text("\(method.rawValue)")) {
self.nhlTeams = method
data.fetchSchedule(self.nhlTeams.rawValue)
print("\(self.nhlTeams.rawValue)")
}
})
}
.onAppear {
data.fetchSchedule(self.nhlTeams.rawValue)
}
}
}
}
Example Widget #StateObject (No data changes)
struct ExampleEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily) var widgetFamily
#StateObject var model = CombineData()
#ObservedObject var model = CombineData()
/*#EnvironmentObject private var model: CombineData*/
var body: some View {
VStack(alignment: .leading) {
if model.schedule?.dates.first != nil {
Text("Game today")
} else {
Text("No game today")
}
}
.onAppear {
model.fetchSchedule(entry.configuration.teams.rawValue) {
WidgetCenter.shared.reloadTimelines(ofKind: "NHL_Widget")
}
}
}
}
Example Widget 2 TimelineEntry (No data changes)
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let model: CombineData
}
struct Provider: IntentTimelineProvider {
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
let model = CombineData()
let entryDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration, model: model)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
model.fetchSchedule(entry.configuration.teams.rawValue) {
completion(timeline)
}
}
}
struct ExampleEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily) var widgetFamily
var body: some View {
VStack(alignment: .leading) {
if entry.model.schedule?.dates.first != nil {
Text("Game today")
} else {
Text("No game today")
}
}
}
}
Widgets are static, the view you provide on getTimeline is what will be displayed as is and won't be updated, so the call to fetchSchedule on onAppear won't even be executed. Once you execute the completion handler in getTimeline, no more code is executed.
To display data on your widgets you need to fetch your data in getTimeline, create a view that displays the data from the start, and return that entry in your completion handler.
In your last example I would suggest that you create your entry in the completion handler of model.fetchSchedule and pass the schedule object to your view.

ObservableObject text not updating even with objectWillChange - multiple classes

I am currently writing a utility, and as part of this, a string is received (through WebSockets and the Starscream library) and the string value is then displayed in the SwiftUI view (named ReadingsView).
The structure of the code is as follows - there are two classes, the WSManager class which manages the WebSocket connections, and the GetReadings class which has the ObservableObject property, which manages and stores the readings.
When the string is received using the didReceive method in the WSManager class, it is decoded by the decodeText method in the WSManager class, which then calls the parseReceivedStrings method in the GetReadings class.
class WSManager : WebSocketDelegate {
func didReceive(event: WebSocketEvent, client: WebSocket) {
case .text(let string):
// Decode the text
DispatchQueue.main.async {
self.decodeText(recvText: string)
print("Received text: \(string)")
}
recvString = string
}
func decodeText(recvText: String) {
// If the message is allowed, then pass it to getReadings
print("Decoding")
if recvText.hasPrefix("A=") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .allreadings)
print("All readings received")
} else if recvText.hasPrefix("T = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .temperature)
} else if recvText.hasPrefix("P = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .pressure)
} else if recvText.hasPrefix("H = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .humidity)
} else {
print("Unrecognised string.")
}
}
}
enum ReadingType {
case allreadings
case temperature
case pressure
case humidity
}
class GetReadings: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var temp: Float = 0.0 {
willSet {
print("Temp new = " + String(temp))
objectWillChange.send()
}
}
#Published var pressure: Float = 0.0 {
willSet {
print("Pressure new = " + String(pressure))
objectWillChange.send()
}
}
#Published var humidity: Float = 0.0 {
willSet {
print("Humidity new = " + String(humidity))
objectWillChange.send()
}
}
func getAll() {
//print(readings.count)
//print(readings.count)
wsManager.socket.write(string: "get_all")
}
func parseReceivedStrings (recvText: String, readingType: ReadingType) {
if readingType == .allreadings {
// Drop first two characters
let tempText = recvText.dropFirst(2)
// Split the string into components
let recvTextArray = tempText.components(separatedBy: ",")
// Deal with the temperature
temp = (recvTextArray[0] as NSString).floatValue
// Pressure
pressure = (recvTextArray[1] as NSString).floatValue
// Humidity
humidity = (recvTextArray[2] as NSString).floatValue
}
}
}
When the values are parsed, I would expect the values in the ReadingsView to update instantly, as I have marked the variables as #Published, as well as using the objectWillChange property to manually push the changes. The print statements within the willSet parameters reflect the new values, but the text does not update. In the ReadingsView code, I have compensated for this by manually calling the parseReceivedString method when the refresh button is pressed (this is used as part of the WebSocket protocol to send the request), but this causes the readings to be one step behind where they should be. Ideally, I would want the readings to update instantly once they have been parsed in the method described in the previous paragraph.
struct ReadingsView: View {
#ObservedObject var getReadings: GetReadings
var body: some View {
VStack {
Text(String(self.getReadings.temp))
Text(String(self.getReadings.pressure))
Text(String(self.getReadings.humidity))
Button(action: {
print("Button clicked")
self.getReadings.getAll()
self.getReadings.parseReceivedStrings(recvText: wsManager.recvString, readingType: .allreadings)
}) {
Image(systemName: "arrow.clockwise.circle.fill")
.font(.system(size: 30))
}
.padding()
}
}
}
I am wondering whether I have used the right declarations or whether what I am trying to do is incompatible with using multiple classes - this is my first time using SwiftUI so I may be missing a few nuances. Thank you for your help in advance.
Edited - added code
ContentView
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
ReadingsView(getReadings: GetReadings())
.tabItem {
VStack {
Image(systemName: "thermometer")
Text("Readings")
}
} .tag(0)
SetupView()
.tabItem {
VStack {
Image(systemName: "slider.horizontal.3")
Text("Setup")
}
}
.tag(1)
}
}
}
If you are using ObservableObject you don't need to write objectWillChange.send() in willSet of your Published properties.
Which means you can as well remove:
let objectWillChange = ObservableObjectPublisher()
which is provided by default in ObservableObject classes.
Also make sure that if you're updating your #Published properties you do it in the main queue (DispatchQueue.main). Asynchronous requests are usually performed in background queues and you may try to update your properties in the background which will not work.
You don't need to wrap all your code in DispatchQueue.main - just the part which updates the #Published property:
DispatchQueue.main.async {
self.humidity = ...
}
And make sure you create only one GetReadings instance and share it across your views. For that you can use an #EnvironmentObject.
In the SceneDelegate where you create your ContentView:
// create GetReadings only once here
let getReadings = GetReadings()
// pass it to WSManager
// ...
// pass it to your views
let contentView = ContentView().environmentObject(getReadings)
Then in your ReadingsView you can access it like this:
#EnvironmentObject var getReadings: GetReadings
Note that you don't need to create it in the TabView anymore:
TabView(selection: $selection) {
ReadingsView()
...
}

'Fatal error: index out of range' when deleting bound object in view

I am having some trouble avoiding index out of range errors when modifying an array that a child view depends on a bound object from.
I have a parent view called WorkoutList. WorkoutList has an EnvironmentObject of ActiveWorkoutStore. ActiveWorkoutStore is an ObservableObject that has an array of Workout objects. I have a list of active workouts being retrieved from ActiveWorkoutStore. I'm using a ForEach loop to work with the indices of these active workouts and pass an object binding to a child view called EditWorkout as a destination for a NavigationLink. EditWorkout has a button to finish a workout, which removes it from ActiveWorkoutStore's array of workouts and adds it to WorkoutHistoryStore. I'm running into trouble when I remove this object from ActiveWorkoutStore's activeWorkouts array, immediately causing an index out of range error. I'm suspecting this is because the active view relies on a bound object that I've just deleted. I've tried a couple permutations of this, including passing a workout to EditWorkout, then using its id to reference a workout in ActiveWorkoutStore to perform my operations, but run into similar troubles. I've seen a lot of examples online that follow this pattern of leveraging ForEach to iterate over indices and I've mirrored it as best I can tell, but I suspect I may be missing a nuance to the approach.
I've attached code samples below. Let me know if you have any questions or if there's anything else I should include! Thanks in advance for your help!
WorkoutList (Parent View)
import SwiftUI
struct WorkoutList: View {
#EnvironmentObject var activeWorkoutsStore: ActiveWorkoutStore
#State private var addExercise = false
#State private var workoutInProgress = false
var newWorkoutButton: some View {
Button(action: {
self.activeWorkoutsStore.newActiveWorkout()
}) {
Text("New Workout")
Image(systemName: "plus.circle")
}
}
var body: some View {
NavigationView {
Group {
if activeWorkoutsStore.activeWorkouts.isEmpty {
Text("No active workouts")
} else {
List {
ForEach(activeWorkoutsStore.activeWorkouts.indices.reversed(), id: \.self) { activeWorkoutIndex in
NavigationLink(destination: EditWorkout(activeWorkout: self.$activeWorkoutsStore.activeWorkouts[activeWorkoutIndex])) {
Text(self.activeWorkoutsStore.activeWorkouts[activeWorkoutIndex].id.uuidString)
}
}
}
}
}
.navigationBarTitle(Text("Active Workouts"))
.navigationBarItems(trailing: newWorkoutButton)
}
}
}
EditWorkout (Child View)
//
// EditWorkout.swift
// workout-planner
//
// Created by Dominic Minischetti III on 11/2/19.
// Copyright © 2019 Dominic Minischetti. All rights reserved.
//
import SwiftUI
struct EditWorkout: View {
#EnvironmentObject var workoutHistoryStore: WorkoutHistoryStore
#EnvironmentObject var activeWorkoutStore: ActiveWorkoutStore
#EnvironmentObject var exerciseStore: ExerciseStore
#Environment(\.presentationMode) var presentationMode
#State private var addExercise = false
#Binding var activeWorkout: Workout
var currentDayOfWeek: String {
let weekdayIndex = Calendar.current.component(.weekday, from: Date())
return Calendar.current.weekdaySymbols[weekdayIndex - 1]
}
var chooseExercisesButton: some View {
Button (action: {
self.addExercise = true
}) {
HStack {
Image(systemName: "plus.square")
Text("Choose Exercises")
}
}
.sheet(isPresented: self.$addExercise) {
AddWorkoutExercise(exercises: self.$activeWorkout.exercises)
.environmentObject(self.exerciseStore)
}
}
var saveButton: some View {
Button(action: {
self.workoutHistoryStore.addWorkout(workout: self.$activeWorkout.wrappedValue)
self.activeWorkoutStore.removeActiveWorkout(workout: self.$activeWorkout.wrappedValue)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Finish Workout")
}
.disabled(self.$activeWorkout.wrappedValue.exercises.isEmpty)
}
var body: some View {
Form {
Section(footer: Text("Choose which exercises are part of this workout")) {
chooseExercisesButton
}
Section(header: Text("Exercises")) {
if $activeWorkout.wrappedValue.exercises.isEmpty {
Text("No exercises")
} else {
ForEach(activeWorkout.exercises.indices, id: \.self) { exerciseIndex in
NavigationLink(destination: EditWorkoutExercise(exercise: self.$activeWorkout.exercises[exerciseIndex])) {
VStack(alignment: .leading) {
Text(self.activeWorkout.exercises[exerciseIndex].name)
Text("\(self.activeWorkout.exercises[exerciseIndex].sets.count) Set\(self.activeWorkout.exercises[exerciseIndex].sets.count == 1 ? "" : "s")")
.font(.footnote)
.opacity(0.5)
}
}
}
saveButton
}
}
}
.navigationBarTitle(Text("Edit Workout"), displayMode: .inline )
}
}
ActiveWorkoutStore
import Foundation
import Combine
class ActiveWorkoutStore: ObservableObject {
#Published var activeWorkouts: [Workout] = []
func newActiveWorkout() {
activeWorkouts.append(Workout())
}
func saveActiveWorkout(workout: Workout) {
let workoutIndex = activeWorkouts.firstIndex(where: { $0.id == workout.id })!
activeWorkouts[workoutIndex] = workout
}
func removeActiveWorkout(workout: Workout) {
if let workoutIndex = activeWorkouts.firstIndex(where: { $0.id == workout.id }) {
activeWorkouts.remove(at: workoutIndex)
}
}
}
Workout
import SwiftUI
struct Workout: Hashable, Codable, Identifiable {
var id = UUID()
var date = Date()
var exercises: [WorkoutExercise] = []
}
ForEach<Range> is constant range container (pay attention on below description of constructor), it is not allowed to modify it after construction.
extension ForEach where Data == Range<Int>, ID == Int, Content : View {
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
}
If you want to modify container, you have to use ForEach(activeWorkout.exercises)