0

I use WidgetCenter.shared.reloadAllTimelines() in my main app to refresh my widget.

The widget contains a picture and a String which gets fetched with a json request. If I use the code above the picture gets refreshed immediately. So thats how it should be.

But the string stays the same. It needs to do another json request. But it does not. It shows the old String. Why? The String gets imported with TimelineEntry. So I guess I also need to reload TimelineEntry?

How can I do that? For the String I use in my view entry.clubname

Here is some samplecode. I removed a bit of it so that it is not to much code.

My Networking:

class NetworkManager: ObservableObject {
@Published var clubNameHome = "..."
init() {
            fetchData() // fetch data must be called at least once
        }
func fetchData() {
if let url = URL(string: "...) {
            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.clubNameHome = results.data[0]....
                              
                                if #available(iOS 14.0, *) {
                                    WidgetCenter.shared.reloadAllTimelines()
                                } else {
                                    // Fallback on earlier versions
                                }
                            }
                        } catch {
                            print(error)
                        }
                    }
                }
            }
            task.resume()
        }
    }
}

And my TimelineProvider with View:

struct Provider: IntentTimelineProvider {
    let networkManager = NetworkManager()
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent(), clubnamehome: networkManager.clubNameHome)
    }
    
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration, clubnamehome: networkManager.clubNameHome)
        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()
        
        let entryDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
        let entry = SimpleEntry(date: entryDate, configuration: configuration, clubnamehome: networkManager.clubNameHome)
        entries.append(entry)
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
    let clubnamehome: String
    
}

struct MyTeamWidgetEntryView : View {
    var entry: Provider.Entry
    var body: some View {
        
        HStack {
            Spacer()
            VStack (alignment: .leading, spacing: 0) {
                Spacer().frame(height: 10)
                HStack {
                    Spacer()
                    switch logo {
                    case "arminia":
                        Image("bundesliga1/arminia").resizable().aspectRatio(contentMode: .fit).frame(width: 90, height: 90, alignment: .center)
                    case "augsburg":
                        Image("bundesliga1/augsburg").resizable().aspectRatio(contentMode: .fit).frame(width: 90, height: 90, alignment: .center)
                    default:
                        Image("bundesliga1/bayern").resizable().aspectRatio(contentMode: .fit).frame(width: 90, height: 90, alignment: .center)
                    }
                }
                HStack{
                    Text(entry.clubname)
                }
            }
        }}}
submariner
  • 308
  • 1
  • 5
  • 23

1 Answers1

0

The thing is that your network call should be in the getTimeline(for:in:completion:) -> Void). It will perform your data in the completion handler from the WidgetViewModel class. The getTimeline method will trigger automatically every 15 minutes as shown in your code.

So it must be looking to something like this, but note you must change the code according to your project. And because it is for widget only, I would advise you to use Combine and to create this network call just for your Widget.

So I would rewrite your NetworkManager this way:

import Combine

class NetworkManager<Resource> where Resource: Codable {

  func fetchData() -> AnyPublisher<Resource, Error> {

    URLSession.shared
      .dataTaskPublisher(for: URL(string: "....")!) // Don't forget your url
      .receive(on: DispatchQueue.main)
      .map(\.data)
      .decode(
        type: Resource.self,
        decoder: JSONDecoder())
      .mapError { error -> Error in
        return error }
      .eraseToAnyPublisher()
  }
}

Then, I would create a view model to handle the widgets logic and set a method to handle the NetworkManager call to fetch data:

import Combine

final class WidgetViewModel {

  private var subscriptions = Set<AnyCancellable>()

  // You can see here how to use a completion handler that is set to be
  // a String, the same as the value your expect for 'clubnamehome'.

  func downloadWidgetData(completionHandlers: @escaping (String) -> Void) {
    NetworkManager<String>().fetchData()
      .sink(
        receiveCompletion: { _ in },
        receiveValue: { data in
          completionHandlers(data) }) // Completion Handler set here
      .store(in: &subscriptions)
  }
}

And now, in your struct Provider: IntentTimelineProvider, change your getTimeline to use you NetworkManager from your viewModel. You might need to make some changes in your code because I don't have your url to test it and the full code of your Widget, but it should work closely with this setup.

let viewModel = WidgetViewModel()

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {

  let currentDate = Date()
  guard let refreshTimeline = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate) else { return }

  // You must declare your network call here and handle the data as a completionHandler.
  // Like this, the widget will refresh every 15 minutes.

  viewModel.downloadWidgetData { data in  // CompletionHandler is used like this.
    let entry = SimpleEntry(date: currentDate, configuration: configuration, clubnamehome: data)
    let timeline = Timeline(entries: [entry], policy: .after(refreshTimeline))
    completion(timeline)
  }
}
Roland Lariotte
  • 2,606
  • 1
  • 16
  • 40
  • Thank you for your suggestion. What exactly do I have to rewrite in my network call? I don't have much experience with completion handler. I tried your code above and it says: Argument passed to call that takes no arguments – submariner Oct 03 '20 at 14:33
  • @submariner I have made changes for you to be able to follow the processus. The thing is to make your NetworkManager call inside the getTimeline function. You can also used this viewModel.downloadWidgetData in getSnapshot(in:completion:) to show data in the Widget library. If you have any issue, let me know, we will work this out. – Roland Lariotte Oct 03 '20 at 17:46
  • Thanks for your answer. This looks more complicated then I thought. It is possible to contact you someway? I would even pay. I really can't fix this because I am too new to swift. – submariner Oct 05 '20 at 18:50
  • This is my real name that is shown, so feel free to contact me via LinkedIn, Twitter or Messenger if you want, like this, I can help you with your source code. But you should try step by step my answer and follow the explanation from this link https://developer.apple.com/videos/play/wwdc2020/10034/ – Roland Lariotte Oct 06 '20 at 08:16