TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: View not refreshing on State change

Forums > SwiftUI

I'm using SwiftData with related models. And I have a screen that displays all the related items with one specific parent. This detail view uses both add and edit sheets for all the related items.

The problem is when I add an item, the sheet dismisses (good), the information is inserted (good) but the screen doesn't refresh so the user can't see that it worked.

If you go back to the list of parent items and come into the detail view, the information is there (screen is refreshed). And the edit sheets use @Bindable so the view updates as the user types.

But I can't figure out how to get the details screen to refresh after the add action.

Here's the relevant code:

MODEL

@Model
class Trip: Identifiable {
    var id: String = ""
    var name: String = ""
    var dateStart: Date = Date()
    var dateEnd: Date = Date()

    @Relationship()
    var flights: [Flight]?
    var hotels: [Hotel]?

    init(name: String, dateStart: Date, dateEnd: Date) {
        self.id = UUID().uuidString
        self.name = name
        self.dateStart = dateStart
        self.dateEnd = dateEnd
    }
}

@Model
class Flight: Identifiable {
    var id: String = ""
    var cityStart: String = ""
    var cityEnd: String = ""
    var dateStart: Date = Date()
    var dateEnd: Date = Date()

    @Relationship(inverse: \Trip.flights)
    var trip: Trip?

    init(cityStart: String, cityEnd: String, dateStart: Date, dateEnd: Date, trip: Trip? = nil) {
        self.id = UUID().uuidString
        self.cityStart = cityStart
        self.cityEnd = cityEnd
        self.dateStart = dateStart
        self.dateEnd = dateEnd
        self.trip = trip
    }
}

@Model
class Hotel: Identifiable {
    var id: String = ""
    var name: String = ""
    var dateStart: Date = Date()
    var dateEnd: Date = Date()

    @Relationship(inverse: \Trip.hotels)
    var trip: Trip?

    init(name: String, dateStart: Date, dateEnd: Date, trip: Trip? = nil) {
        self.id = UUID().uuidString
        self.name = name
        self.dateStart = dateStart
        self.dateEnd = dateEnd
        self.trip = trip
    }

}

DETAIL VIEW

import SwiftData

struct TripDetailView: View {
    @Environment(\.dismiss) var dismiss

    @State var trip: Trip

    //Add variables
    @State private var showFlightAdd = false
    @State private var showHotelAdd = false

    //Edit variables
    @State var showFlightEdit: Flight?
    @State var showHotelEdit: Hotel?

    var body: some View {
        NavigationStack{

            List{
                Section{
                    ForEach(trip.flights!){ flight in
                        FlightRowView(flight: flight)
                            .onTapGesture {
                                showFlightEdit = flight
                            }
                    }
                }

                Section{
                    ForEach(trip.hotels!){ hotel in
                        HotelRowView(hotel: hotel)
                            .onTapGesture {
                                showHotelEdit = hotel
                            }
                    }
                }

            }
            .navigationTitle(trip.name)
            .toolbar {
                ToolbarItem(placement: .confirmationAction, content: {

                    Menu {
                        Button(action: {
                            showFlightAdd.toggle()
                        } , label: {
                            Label("Add Flight", systemImage: "airplane")
                        })
                        Button(action: {
                            showHotelAdd.toggle()
                        } , label: {
                            Label("Add Hotel", systemImage: "house")
                        })
                            }
            //Add Sheets
            .sheet(isPresented: $showFlightAdd) {
                    FlightAddSheet(trip: trip)
                .presentationDetents([.fraction(0.6)])
                .presentationCornerRadius(30)
            }
            .sheet(isPresented: $showHotelAdd) {
                    HotelAddSheet(trip: trip)
                    .presentationDetents([.fraction(0.5)])
                .presentationCornerRadius(30)
            }
            //Edit Sheets
            .sheet(item: $showFlightEdit,
             content: { flight in FlightEditSheet(flight: flight)
                    .presentationDetents([.medium])
                    .presentationCornerRadius(30)
            })
            .sheet(item: $showHotelEdit,
             content: { hotel in HotelEditSheet(hotel: hotel)
                    .presentationDetents([.medium])
                    .presentationCornerRadius(30)
            })
        }
    }
}

ADD VIEW

import SwiftData

struct FlightAddSheet: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss

    @State private var flight = Flight(cityStart: "", cityEnd: "", dateStart: Date(), dateEnd: Date())
    @State var trip: Trip

    var body: some View {
        NavigationStack{
            Form{
                Section{
                    TextField("Origin City", text: $flight.cityStart)
                    TextField("Destination City", text: $flight.cityEnd)
                    DatePicker("Start Date", selection: $flight.dateStart)
                    DatePicker("End Date", selection: $flight.dateEnd)
                }
            }
            .toolbar{
                ToolbarItem(placement: .cancellationAction, content: {
                    Button {
                        withAnimation{
                            dismiss()
                        }
                    }  label: {
                        Label("", systemImage: "xmark.circle")
                    }
                    .tint(.gray)
                })
                ToolbarItem(placement: .confirmationAction, content: {
                    Button("Done") {
                        withAnimation{
                            let flight = Flight(cityStart: flight.cityStart, cityEnd: flight.cityEnd, dateStart: flight.dateStart, dateEnd: flight.dateEnd)
                            modelContext.insert(flight)

                            flight.trip = trip
                            dismiss()
                        }
                    }
                })
            }
        }
    }
}

2      

How do you fetch data from db? Using @Query or another way? Where is the "main" screen in your workflow?

1      

My main screen is a list of Trips using @Query:

//
//  ContentView.swift
//  Travelly
//
//  Created by Annie Jackson on 2/10/24.
//

import SwiftUI
import SwiftData

struct TripListView: View {
    @Environment(\.modelContext) private var modelContext

    @State var showTripEdit: Trip?
    @Query(sort: \Trip.dateStart, order: .reverse) private var trips: [Trip]
    @State private var showTripAdd = false

    var body: some View {
        NavigationStack{

                List{
                    ForEach(trips){ trip in
                        ZStack{
                        TripRowView(trip: trip)

                            VStack{
                                HStack{
                                    Spacer()
                                    Image(systemName: "ellipsis.circle").foregroundColor(.white)
                                        .padding()
                                }
                                .onTapGesture {
                                    showTripEdit = trip
                                }
                                Spacer()
                            }
                    }

                    }
                    .listRowSeparator(.hidden)
                }
                .scrollContentBackground(.hidden)
                .toolbar {
                    ToolbarItem(placement: .confirmationAction, content: {
                        Button(action: {
                            showTripAdd.toggle()
                        } , label: {
                            Label("Add Trip", systemImage: "plus")
                        })
                    })
                }
                .sheet(isPresented: $showTripAdd) {
                        TripAddSheet()
                    .presentationDetents([.medium])
                    .presentationCornerRadius(30)
                }
                .sheet(item: $showTripEdit,
                    content: { trip in TripEditSheet(trip: trip)
                        .presentationDetents([.medium])
                        .presentationCornerRadius(30)
                })

        }
        }
    }

#Preview {
    TripListView()
}

But then the TripRowView which has the link to the details screen just uses a let variable. Is that where it's going wrong?

  let trip: Trip

    var body: some View {
        NavigationStack{
            ScrollView (showsIndicators: false) {

                NavigationLink{
                    TripDetailView(trip: trip)
                } label: {
                                    Text(trip.name).font(.largeTitle).bold().foregroundStyle(.black)                                    
                                    HStack{
                                        Text(trip.dateStart.formatted(
                                            Date.FormatStyle()
                                                .day(.twoDigits)
                                                .month(.abbreviated)
                                        ))
                                        Text(" - ")
                                        Text(trip.dateEnd.formatted(
                                            Date.FormatStyle()
                                                .day(.twoDigits)
                                                .month(.abbreviated)
                                        ))
                                    }
                                }
                            }
                        }

1      

This is something I only just realized today when pondering a non-updating view, perhaps this will be helpful: TripDetailView will update if it notices a change in an @State var that it either displays directly or uses to display something else, otherwise it doesn't know it should redraw. Alternatively, you could use .onChange(of: statevar, initial: true) { (oldValue, newValue) in } to make the view sensitive to a change in statevar.

1      

Without full code picture difficult to say. As a simple workaround try to add this mofiider to the view, that seems to be not updated propertly

.id(UUID())

basically whenever you change and come back to this view it makes it update. Very often it helps.

1      

I'm relatively new to SwiftData, but dare to suggest the following, because I've faced a similar issue the last days. At least I would ask you to give it a try:

Add an explicit modelContext.save() after your modelContext.insert() statements.

In my case, that was the solution.

1      

I appreciate the suggestions and tried each of them. Since they haven't quite worked, I feel terribly inept at this :(

  • the @State on TripDetailView is Trip, but it seems adding a new item to a trip doesn't qualify as updating it, possibly because the related items are optional. I feel like this is where I have a gap somewhere because @State should update the Trip when a new item is added to it, but it's not.
  • I've researched onChange and thought about using it but I have no idea what the proper context would be. I think I could sort out the change of @State Trip but I don't know what to put after the in -- I don't want it to print anything I just want the view to refresh and I can't find the context to tell it to do that or something else benign.
  • I tried putting .id(UUID()) on several of the views and it didn't trigger a refresh.
  • and I tried inserting modelContext.save() but I get an message that it can throw and error that I'm not handling but when I also use try with it i get a different set of errors.

I created a new much simpler app simply to troubleshoot this problem so the code is simpler and everything (almost) is on one screen.

DATA MODEL

@Model
class Project: Identifiable {
    var id: String
    var name: String
    var status: Bool

    @Relationship() var tasks: [Task]?

    init(name: String, status: Bool) {
        self.id = UUID().uuidString
        self.name = name
        self.status = status
        self.tasks = tasks
    }
}

@Model
class Task: Identifiable {
    var id: String
    var name: String
    var status: Bool

    @Relationship(inverse: \Project.tasks) var project: Project?

    init(name: String, status: Bool) {
        self.id = UUID().uuidString
        self.name = name
        self.status = status
        self.project = project
    }

}

LIST THAT LINKS TO DETAIL VIEW

struct ProjectList: View {
    @Environment(\.modelContext) private var modelContext

    @Query var projects: [Project]

    var body: some View {
        NavigationStack{
            List{
                ForEach(projects){ project in
                    NavigationLink{
                        ProjectDetail(project: project)
                    } label: {
                        Text(project.name)
                        }
                            }
                    }
                }
            }
        }

DETAIL VIEW WITH ALL THE CODE TOGETHER

struct ProjectDetail: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) var dismiss

    @State var project: Project
    @State private var showAddTask = false

    var body: some View {
        NavigationStack{

            List{
                ForEach(project.tasks!){ task in
                    Text(task.name)
                }
            }
            .toolbar{
                ToolbarItem(placement: .confirmationAction, content: {
                    Button(action: {
                        showAddTask.toggle()
                    }, label: {
                        Label("Add Task", systemImage: "plus")
                    })
                })
            }
            .sheet(isPresented: $showAddTask) {
                TaskProjectAddSheet(project: project)
            }

        }
    }
}

AND THEN THE ADD TASK SHEET -- If this were working properly once the task was inserted, it would update the Project (by inserting a new task into the project.tasks field and therefore change the @State Project on the Detail view. Again, I'm not sure if that's not happening because tasks is optional in the Project model.

struct TaskProjectAddSheet: View {

    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss

    @State var task = Task(name: "", status: false)
    @State var project: Project

    var body: some View {
        NavigationStack{
            Form{
                TextField("Task", text: $task.name)

                Section{
                    Button("Save"){
                        let task = Task(name: task.name, status: false)
                        task.project = project
                        modelContext.insert(task)

                        dismiss()
                    }
                }
            }
        }
    }
}

I also tried inserting let project = task.project (Xcode filled it in for me as soon as I typed let project) to see if that would force an update to the @State view on the project with no luck.

1      

I figured out a solution. Well, I didn't I started looking through example code and the iTour demo app has related data.

First I made the asks not optional on the Project model and that didn't fix it.

But then I inserted project.tasks.append(task) to the save function and that did the trick to update the @State Project variable and refresh the view.

Thank you everyone for the suggestions!

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.