My issue is that when I edit a template, the UI in BeginWorkoutView does not update correctly. I believe I am somehow misusing @ObservableObject/@Published/@Mainactor, but cannot for the life of me figure out how. Every tutorial seems to use the pattern I am currently using, and it works for them.
I have a main page, BeginWorkoutView. From here, you can create new workout or create workout templates. You can also see all created templates, and click to edit them.
here is a simplified version of that:
struct BeginWorkoutView: View {
@ObservedObject var templateManager = TemplateManager.shared
var body: some View {
NavigationView{
ScrollView{
Section("Start from scratch"){
HStack{
NavigationLink("New Workout"){
//new workout page
}
NavigationLink("New Template"){
CreateTemplateView( viewModel: CreateTemplateViewModel(), editMode: true)
}
}
}
Section("Start from template") {
ForEach(templateManager.templates, id: \.id.self) { template in
TemplateRowView(template: template)
}
}
}
}
}
}
I manage my data in a template manager. Im able to add new workouts and they show up on my BeginWorkoutView page no issue. As I said, when I edit the title, it does not update in BeginWorkoutView. From what I can tell, the templateManager list of templates IS being updated, but it doesnt seem to trigger the UI. I am currently using the @mainactor/@Observable object structure because i want to support pre-IOS 17.
@MainActor
final class TemplateManager: ObservableObject {
@Published private(set) var templates: [WorkoutTemplate] = []
static let shared = TemplateManager()
init(){
getTemplates()
}
func getTemplates() {
//gets templates from firebase
}
func updateTemplates(template: WorkoutTemplate) {
Task{
do {
//updates templates in firebase
//HERE is where the UI should be getting the signal to update. Obviously spot replacing the template didnt work, but replacing the entire thing wont work either.
var updated = templates
if let index = updated.firstIndex(where: { $0.id == template.id }) {
updated[index] = template
} else {
updated.append(template)
}
self.templates = updated
} catch {
print("unable to update templates")
}
}
}
}
whether i am creating a new template or editing one, i use createtemplateview/createtemplateviewmodel. here are simplified versions with mostly just the function calls. I dont believe the issue is here but im adding them in case they are helpful.
@MainActor
final class CreateTemplateViewModel: ObservableObject {
@Published var template: WorkoutTemplate
var templateManager = TemplateManager.shared
init(template: WorkoutTemplate) {
self.template = template
}
init(){
self.template = WorkoutTemplate(name: "Workout Template", workoutModules: [])
}
func addTemplateList() {
templateManager.updateTemplates(template: template)
}
func removeTemplateList() {
templateManager.removeTemplateList(templateId: template.id)
}
}
struct CreateTemplateView: View {
@StateObject var viewModel: CreateTemplateViewModel
var body: some View {
ScrollView{
StringTextField(preview: "Template Name", $viewModel.template.name )
.padding()
//shows editable modules for the template
Button("save"){
viewModel.addTemplateList()
}
}
}
}
there are "hacks" out there like using notifications or combine to notify the UI to update, but I believe this should work if done correctly. I am mostly writing this to learn these data structures and i'm having a difficult time grasping this one so I'd definitely like to get to the bottom of it rather than slap a bandaid fix on it.
You're close, replace @StateObject
with @EnvironmentObject
, e.g.
struct CreateTemplateView: View {
@EnvironmentObject var manager: TemplateManager
@State var template = Template() // must be struct
var body: some View {
ScrollView{
StringTextField(preview: "Template Name", $template.name)
.padding()
//shows editable modules for the template
Button("save"){
manager.addTemplate(template)
}
}
}
}
And remove CreateTemplateViewModel
since the @State
already does that job.
struct BeginWorkoutView: View {
@EnvironmentObject var manager: TemplateManager
var body: some View {
NavigationView{
ScrollView{
Section("Start from scratch"){
HStack{
NavigationLink("New Workout"){
//new workout page
}
NavigationLink("New Template"){
CreateTemplateView()
}
}
}
Section("Start from template") {
ForEach(manager.templates) { template in // Template struct should be Identifiable
TemplateRowView(template: template)
}
}
}
}
}
}
In your App
do:
ContentView()
.environmentObject(TemplateManager.shared)
In your Previews do:
.environmentObject(TemplateManager.preview) // where this singleton has some sample templates and doesn't do any network requests.