19

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.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
submariner
  • 308
  • 1
  • 5
  • 23

1 Answers1

36

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.


  1. Add another property to your TimelineEntry, let's call it clubName:
struct SimpleEntry: TimelineEntry {
    let date: Date
    let clubName: String
}
  1. Update the NetworkManager and return results in the completion:
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()
    }
}
  1. Use the NetworkManager in the TimelineProvider and create timelines entries when the fetchData 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)
        }
    }
}
  1. 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.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • Thanks it worked. I needed to add if #available(iOS 14.0, *), import WidgetKit and rename entry.clubName to entry.clubname. But there is a problem with PreviewProvider. I needed to change it too and now it says Instance member 'networkManager' cannot be used on type 'MyTeamWidget_Previews' – submariner Sep 20 '20 at 13:44
  • 2
    What I explained above is just an example - I don't know what's the actual name of your property (`clubname`, `clubName`, etc.). And Widgets are available in iOS 14+, so if you have a lower deployment target then yes, you need to add `if #available(iOS 14.0, *)`. – pawello2222 Sep 20 '20 at 14:04
  • Again thank you very much. But you now how I can solve the problem with the preview provider? – submariner Sep 20 '20 at 14:11
  • 3
    @submariner Note that previews are static - you can't use instance variables there. You can check [this answer](https://stackoverflow.com/a/63997127/8697793) for a solution. But in reality previews are better suited for simple tasks like creating a view layout etc. It's better to test the networking code in the simulator. – pawello2222 Sep 21 '20 at 20:58
  • Which Xcode + MacOS version did you stay at? @pawello2222 – Neo.Mxn0 Sep 24 '20 at 03:35