SwiftUI小组件每分钟更新及重启/次日不刷新问题求助
Hey there! Let's tackle those two widget issues you're dealing with—they're both related to how WidgetKit handles data loading and timeline updates. Here's a step-by-step fix:
Issue 1: Widget fails to reload data after restarting the phone
The root problem here is that you're relying on an @ObservedObject in your main Widget struct to manage quotes, but WidgetKit doesn't preserve this state across app/widget restarts. Additionally, your SimpleEntry is a value type (struct) and can't properly use @ObservedObject—that won't trigger updates as expected.
Instead, we need to fetch data directly within the TimelineProvider and pass the quote data into the entry itself, so every timeline refresh (including post-restart) pulls fresh data.
Issue 2: Quote doesn't refresh in the first 5 minutes of the next day
Your current timeline only adds 1 minute to the current date for the next update, which means it might take up to a minute after midnight to check for new data—but even then, it's not explicitly targeting the start of a new day. We'll adjust the timeline to prioritize updating exactly at midnight, ensuring the new day's quote loads immediately.
Modified Code Fixes
1. Update QuoteManager with a completion handler
First, let's add a completion handler to the load method so we can get the quote result directly in the timeline provider:
public class QuoteManager: ObservableObject { @Published var endQuote = String() func load(completion: @escaping (String?) -> Void) { guard let url = URL(string: "https://zenquotes.io/api/today") else { completion(nil) return } URLSession.shared.dataTask(with: url) { [weak self] data, response, error in guard let data = data, error == nil else { print("No data or error occurred: \(error?.localizedDescription ?? "Unknown error")") completion(nil) return } do { let quotes = try JSONDecoder().decode([Contents].self, from: data) guard let firstQuote = quotes.first else { completion(nil) return } let finalizedQuote = "\"\(firstQuote.q)\" --\(firstQuote.a)" DispatchQueue.main.async { self?.endQuote = finalizedQuote completion(finalizedQuote) } } catch { print("Error decoding quote: \(error)") completion(nil) } }.resume() } } struct Contents: Codable { let q: String let a: String let h: String }
(Note: I removed the unused parseJSON method and added safer optional handling.)
2. Fix the SimpleEntry to hold quote data
Since entries are value types, we'll store the quote directly instead of using an @ObservedObject:
struct SimpleEntry: TimelineEntry { let date: Date let quote: String }
3. Update the TimelineProvider to fetch data and calculate correct update times
This is where the magic happens—we'll fetch the quote in getTimeline, and set the next update time to either 1 minute from now or midnight (whichever comes first):
struct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), quote: "Loading...") } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), quote: "Loading...") completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let quoteManager = QuoteManager() quoteManager.load { fetchedQuote in let currentDate = Date() let calendar = Calendar.current // Calculate the start of the next day (midnight) let nextDayStart = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: currentDate)!) // Determine next update time: if we're within 1 minute of midnight, update at midnight; else, update in 1 minute let nextUpdateDate: Date let secondsUntilMidnight = nextDayStart.timeIntervalSince(currentDate) if secondsUntilMidnight < 60 { nextUpdateDate = nextDayStart } else { nextUpdateDate = calendar.date(byAdding: .minute, value: 1, to: currentDate)! } // Create entry with fetched quote (or fallback text) let entry = SimpleEntry( date: currentDate, quote: fetchedQuote ?? "Failed to load quote" ) // Create timeline with the entry and correct update policy let timeline = Timeline(entries: [entry], policy: .after(nextUpdateDate)) completion(timeline) } } }
4. Update the Widget and Entry View
Remove the unused @ObservedObject from the main Widget struct, and pass the entry's quote directly to the view:
struct Quote_WidgetEntryView : View { let quote: String var body: some View { Text(quote) .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: .infinity) .background( Image("WidgetBackground") .resizable() .scaledToFill() ) .clipped() // Prevent image overflow } } @main struct Quote_Widget: Widget { let kind: String = "Quote_Widget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in Quote_WidgetEntryView(quote: entry.quote) } .configurationDisplayName("Daily Quotes") .description("A new quote everyday for inspiration and motivation!") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } }
Why This Works
- Restart fix: Every time WidgetKit requests a timeline (including after a restart),
getTimelinecallsQuoteManager.load()to fetch fresh data, so the widget never relies on preserved state. - Midnight refresh fix: We explicitly calculate midnight of the next day. If we're close to midnight (less than a minute away), the next update will happen exactly at midnight, ensuring the new day's quote loads immediately. Otherwise, we keep the 1-minute refresh interval as you wanted.
Just a quick reminder: Make sure your widget has network access enabled (check your app's Info.plist for App Transport Security settings if you haven't already).
内容的提问来源于stack exchange,提问作者ntm52

