How to refresh Widget data?
My Code fetches two JSON
variables and should show them on my Widget.
The Widget stays blank. Without widget it shows me everything correct in my application.
What am I doing wrong? The API in the code is only for testing so you can also check that. Is there something I need to change to make it show in the widget?
My Struct:
import Foundation
struct Results: Decodable {
let data: [Post]
}
struct Post: Decodable, Identifiable {
let id: String
var objectID: String {
return id
}
let home_name: String
let away_name: String
}
Fetching JSON:
import Foundation
class NetworkManager: ObservableObject {
@Published var posts = [Post]()
@Published var test = ""
@Published var test2 = ""
func fetchData() {
if let url = URL(string: "https://livescore-api.com/api-client/teams/matches.json?number=10&team_id=19&key=I2zBIRH3S01Kf0At&secret=6kLvfRivnqeNKUzsW84F0LISMJC1KdvQ&number=7&team_id=46") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = gettingInfo {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.data
self.test = results.data[0].away_name
self.test2 = results.data[0].home_name
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
Showing Widget:
import WidgetKit
import SwiftUI
import Intents
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct WidgetNeuEntryView : View {
@ObservedObject var networkManager = NetworkManager()
var entry: Provider.Entry
var body: some View {
Text(networkManager.test)
}
}
@main
struct WidgetNeu: Widget {
let kind: String = "WidgetNeu"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetNeuEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct WidgetNeu_Previews: PreviewProvider {
static var previews: some View {
WidgetNeuEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
networkManager.test
should be shown as text but as I said it is blank.
Solution 1:
You can't use the ObservedObject
like you'd normally use in your App.
In Widgets you use a TimelineProvider
which creates an Entry
for your view.
- Add another property to your
TimelineEntry
, let's call itclubName
:
struct SimpleEntry: TimelineEntry {
let date: Date
let clubName: String
}
- Update the
NetworkManager
and return results in thecompletion
:
class NetworkManager {
func fetchData(completion: @escaping ([Post]) -> Void) {
...
URLSession(configuration: .default).dataTask(with: url) { data, _, error in
...
let result = try JSONDecoder().decode(Results.self, from: data)
completion(result.data)
...
}
.resume()
}
}
- Use the
NetworkManager
in theTimelineProvider
and create timelines entries when thefetchData
completes:
struct Provider: TimelineProvider {
var networkManager = NetworkManager()
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), clubName: "Club name")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
let entry = SimpleEntry(date: Date(), clubName: "Club name")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
networkManager.fetchData { posts in
let entries = [
SimpleEntry(date: Date(), clubName: posts[0].home_name)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}
}
- Use
entry.clubName
in the view body:
struct WidgetNeuEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text("Club: \(entry.clubName)")
}
}
}
Note that in the above example the reload policy is set to never
to only load the data once.
You can easily change it to atEnd
or after(date:)
if you want to reload the timeline automatically.
If you need to reload the timeline manually at any point you can just call:
WidgetCenter.shared.reloadAllTimelines()
This will work in both App and Widget.
Here is a GitHub repository with different Widget examples including the Network Widget.