core-dataswiftuifetchrequest

Updating a `SectionedFetchRequest` when `scenePhase` changes


I’ve run into an issue with my app. It’s a simple Core Data app using a @SectionedFetchRequest to grab and format my data into a list.

The sectionIdentifier I’ve written checks the entry date against Calendar and formats the date accordingly (“Today”, “Yesterday” and then just dates for days before that).

The issue is that when I bring the app out of the background in the mornings, my data is still bucketed as if it was yesterday (ie things that were marked as “today” yesterday are still marked as “today” even though they should now be “yesterday”. Adding a new entry doesn’t force the view to fully redraw and correct the sections… only quitting and restarting the app does it. This is the root view of the app.

I’ve tried to use a @State object combined with scenePhase to try force the list to redraw, but it doesn’t appear to be working (I can only test once a day when the day changes).

Ask: How can I make sure my list of entries refreshes when the app is brought out of the background so I don’t have to quit and reopen it?

struct ListView: View {    
    @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.scenePhase) var scenePhase
    @State private var formID: UUID = UUID()
    
    @SectionedFetchRequest<String, Entry> (
        sectionIdentifier: \. daySection,
        sortDescriptors: [
            SortDescriptor(\.date, order: .reverse)
        ]
    ) var entrySection: SectionedFetchResults<String, Entry>

    var body: some View {
        NavigationView {
            List {
                ForEach(entrySection) { section in
                    Text(“\(section.id)”)

                    ForEach(section) { entry in 
                        Text(“/(entry.name)”)
                    }
                } 
            }
        }
        .onChange(of: scenePhase) { _ in
            formID = UUID()
        }
    }
}

extension Entry {
    @objc var daySection: String {
        var formattedDate = ""
        
        if Calendar.current.isDateInToday(date) {
            formattedDate = “Today”
        } else if  Calendar.current.isDateInYesterday(date) {
            formattedDate = “Yesterday”
        } else {
            formattedDate = date.formatted(.dateTime.weekday(.wide).day().month())
        }
        return formattedDate
    }
}


Solution

  • I think the issue here is that your underlying data isn't changing, so Core Data updates aren't happening, and that means that your calculated property doesn't get called.

    On the other hand, your presentation requirements are changing: e.g., what was showing as Today should now be displayed as Yesterday, etc.

    So I wonder if it might be better to move that conditional logic into your presentation layer.

    In your Core Data model, I'd change your section identifier to something that differentiates the days, but will never change unless the underlying data does:

    @objc var daySection: Date {
      Calendar.current.startOfDay(for: date)
    }
    

    So the sections remain the same as you currently have, but the section ID values don't change until the data within the fetch request does.

    Then, in your view layer, you present each section header by converting section.id - which is now a Date - into the appropriate format:

    ForEach(entrySection) { section in
      Section {
        ForEach(section) { entry in 
          // entry details
        }
      } header: {
        Text(formattedHeader(section.id))
      }
    }
    
    // ...
    func formattedHeader(_ date: Date) -> String {
      let formattedDate: String
            
      if Calendar.current.isDateInToday(date) {
        formattedDate = "Today"
      } else if  Calendar.current.isDateInYesterday(date) {
        formattedDate = "Yesterday"
      } else {
        formattedDate = date.formatted(.dateTime.weekday(.wide).day().month())
      }
    
      return formattedDate
    }
    

    This might still need a bit of nudging when it wakes up, but this way you keep your fetch requests purely data-focused and your presentation choices purely in the SwiftUI layer, so maintaining the latter shouldn't need a round trip to the database.