'In SwiftUI, what's the best way to dynamically update rows based on underlying data when underlying data has not changed?
I have an app that lists Events from Core Data. Each event has a date.
When I list the Events, I want show the date, unless the date was today or yesterday, and in that case I want to show Today or Yesterday instead of the date.
As of now, I have a function that handles generating the String to show in the row. However, I've noticed that if a day passes and I re-open the app, it shows outdated information. For example, if there is an event from the previous day that said Today when I had the app open the previous day, it will still say Today instead of Yesterday when I re-open the app. Obviously this function is not being called every time I open the app, but I am wondering what the best approach is for making this more dynamic.
These are the avenues I am considering, but not sure what would be best, so I wanted to post here to get recommendations and see if I'm overlooking anything important:
- Somehow do something with
.onAppearon the row to re-calculate it every time the app is opened (I'm not sure how expensive this date calculation stuff is for each event, but even if it's not expensive I'm not sure how I would tell the rows to re-run the function when the app comes to the foreground) - Switch to a computed property (I don't know if this would be any different than putting a function in there, like I have now. This could be bad to have it called every time if it's an expensive call, but assuming it's not how would I get this to refresh every time the app comes to the foreground?)
- Come up with a solution to only re-calculate each row if the day has changed (this is probably what I'd try to do if I knew the row calculation was very expensive, but seems like it might be overkill here, and I'm also not sure how I would go about telling each row to re-run the function)
Here is my code (I left out my date formatter code, but it's pretty standard and shouldn't matter for this):
struct ContentView: View {
@FetchRequest(fetchRequest: Event.eventsNewestFirst)
private var events: FetchedResults<Event>
var body: some View {
NavigationView {
ForEach(events){ event in
EventRow(event: event)
}
}
}
}
struct EventRow: View {
@ObservedObject var event: Event
var body: some View {
Text(event.dateAndTimeString())
}
}
extension Event {
func dateAndTimeString() -> String {
guard let date = self.date else { return "Error" }
let timeString = DateAndNumberFormatters.simpleTimeDisplay.string(from: date)
let dateString: String
if let todayOrYesterday = date.asTodayOrYesterday() {
dateString = todayOrYesterday
} else {
dateString = DateAndNumberFormatters.simpleShortDateDisplay.string(from: date)
}
return "\(dateString) at \(timeString)"
}
}
extension Date {
func asTodayOrYesterday() -> String? {
let calendar = Calendar.current
let dayComponents = calendar.dateComponents([.year, .month, .day], from: self)
let todayDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
var yesterdayDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
yesterdayDateComponents.day = yesterdayDateComponents.day! - 1
let dayDate: Date! = calendar.date(from: dayComponents)
let todayDayDate: Date! = calendar.date(from: todayDateComponents)
let yesterdayDayDate: Date! = calendar.date(from: yesterdayDateComponents)
switch dayDate {
case todayDayDate:
return "Today"
case yesterdayDayDate:
return "Yesterday"
default:
return nil
}
}
}
Solution 1:[1]
The possible approach is to observe scene phase and force refresh observed core data object as needed, like
struct EventRow: View {
@ObservedObject var event: Event
@Environment(\.scenePhase) var scenePhase
var body: some View {
Text(event.dateAndTimeString())
.onChange(of: scenePhase) {
if $0 == .active {
event.objectWillChange.send()
}
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Asperi |
