0

I have created a class to perform a network request and parse the data using Combine. I'm not entirely certain the code is correct, but it's working as of now (still learning the basics of Swift and basic networking tasks). My Widget has the correct data and is works until the data becomes nil. Unsure how to check if the data from my first publisher in my SwiftUI View is nil, the data seems to be valid even when there's no games showing.

My SwiftUI View

struct SimpleEntry: TimelineEntry {
    let date: Date
    public var model: CombineData?
    let configuration: ConfigurationIntent
}

struct Some_WidgetEntryView : View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var widgetFamily
    
    var body: some View {
        VStack (spacing: 0){
            if entry.model?.schedule?.dates.first?.games == nil {
                Text("No games Scheduled")
            } else {
                Text("Game is scheduled")
            }
        }
    }
}

Combine

import Foundation
import WidgetKit
import Combine

// MARK: - Combine Attempt
class CombineData {
    var schedule: Schedule?
    var live: Live?
    
    private var cancellables = Set<AnyCancellable>()
    
    func fetchSchedule(_ teamID: Int, _ completion: @escaping (Live) -> Void) {
        let url = URL(string: "https://statsapi.web.nhl.com/api/v1/schedule?teamId=\(teamID)")!
        let publisher = URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: Schedule.self, decoder: JSONDecoder())
            //.catch { _ in Empty<Schedule, Error>() }
            //.replaceError(with: Schedule(dates: []))
        let publisher2 = publisher
            .flatMap {
                return self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
            }
        Publishers.Zip(publisher, publisher2)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: {_ in
            }, receiveValue: { schedule, live in
                self.schedule = schedule
                self.live = live
                completion(self.live!)
                WidgetCenter.shared.reloadTimelines(ofKind: "NHL_Widget")
            }).store(in: &cancellables)
    }

    func fetchLiveFeed(_ link: String) -> AnyPublisher<Live, Error /*Never if .catch error */> {
        let url = URL(string: "https://statsapi.web.nhl.com\(link)")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: Live.self, decoder: JSONDecoder())
            //.catch { _ in Empty<Live, Never>() }
            .eraseToAnyPublisher()
    }
}
cole
  • 1,039
  • 1
  • 8
  • 35
  • What data is `nil` - you have plenty of optionals here, so difficult to know what specifically you're referring to? What do you want to have happened when the data is `nil`? Do you want to ignore it, or error out? This is too broad and has less to do with Combine and more to do with your data model. Also, unrelated, but if you're already using combine, return the publisher, instead of creating a subscription (`sink`) for each request to invoke the callback. – New Dev Mar 22 '21 at 13:25
  • Are optionals a bad thing? I assumed I could just use the variables I assigned in `.sink` in my Widget since you can't use `@Published`? Also unsure how I would return the publisher then getting the data in my `View` in SwiftUI. That's where the confusion is. – cole Mar 22 '21 at 19:01
  • Meant can't use `ObservedObject` not `Published`, like you'd normally use in `WidgetKit`. – cole Mar 22 '21 at 19:03
  • Optionals aren't bad. That wasn't my point/question. Why of these optionals is a `nil` that you are trying to take care of? And what do you want to have happened if it was `nil` – New Dev Mar 22 '21 at 20:15
  • @NewDev I'd like to present a placeholder when my variable `schedule` returns it's `Codable` element `let dates: [DateElement]` as empty or nil. When checking the API it returns an array of data from `dates` if there's no games it's `"dates" : [ ]`. So in my widget when no dates are returned I'd like to have just the team logo and as of right now it's showing the correct data because it's showing the correct teams playing but when there's no games the widget remains white. – cole Mar 22 '21 at 20:27
  • It's white because probably `fetchLiveFeed` makes a request to `"https://statsapi.web.nhl.com/"` (without the `link`), which doesn't return anything that you could decode as `Live.self` - so the decode probably returns an error, and the entire pipeline completes. Either handle the `nil` inside the `flatMap` by returning another publisher, or handle the error inside sink – New Dev Mar 22 '21 at 22:03
  • Okay, I'll try something like that. Currently I can't get any data now at all. Looking into the issue more. – cole Mar 22 '21 at 22:06
  • @NewDev I added a check but still the same issue `if schedule.dates.isEmpty` in `.sink` then return nil for both schedule and live. – cole Mar 22 '21 at 22:37

2 Answers2

0

Like I said in the comments, it's likely that the decode(type: Live.self, decoder: JSONDecoder()) returns an error because the URL that you're fetching from when link is nil doesn't return anything that can be decoded as Live.self.

So you need to handle that case somehow. For example, you can handle this by making the Live variable an optional, and returning nil when link is empty (or nil).

This is just to set you in the right direction - you'll need to work out the exact code yourself.

let publisher2 = publisher1
   .flatMap {
       self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
          .map { $0 as Live? } // convert to an optional
          .replaceError(with: nil)
   }

Then in the sink, handle the nil:

.sink(receiveCompletion: {_ in }, receiveValue: 
      { schedule, live in
          if let live = live {
              // normal treatment
             self.schedule = schedule 
             self.live = live
             //.. etc
          } else {
              // set a placeholder
          }
      })
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • `Live` variable is optional to begin with. Live can't take `nil`. The above code won't work for the `if let live = live`. – cole Mar 22 '21 at 23:54
  • Forgot to map to an optional (see edit). But, at a high level, it wasn't my intention to give you exactly the code that works, because I don't see the rest of your code. Use this as an idea of how to approach the problem. – New Dev Mar 22 '21 at 23:58
  • All the code is there for the request, no other relevant code. I initialize the code in my widget timeline with the completion handler and get the data with Entry. – cole Mar 23 '21 at 00:07
  • Fair enough.. I didn't notice that you had these properties defined. But should work now though? – New Dev Mar 23 '21 at 00:13
  • Thanks for all your help but unfortunately it's still presenting the same issue as before and now getting `Unexpectedly found nil while unwrapping an Optional value:` – cole Mar 23 '21 at 01:51
  • It's not easy to diagnose what's happening since it's unclear what the API returns. My suggestion is to insert `.print()` somewhere around your publishers to see what's happening and if you're getting the values you expect. "Unexpectedly found nil" happens when you're force-unwrapping with `!` - you should typically avoid those – New Dev Mar 23 '21 at 02:20
  • I'm thinking no values are emitted when the URL doesn't exist in `flatMap`. Works and prints data only when there is a valid URL provided if not it won't print. Narrowed it down to that, I'm thinking. Not sure of a fix though. – cole Mar 24 '21 at 09:21
  • Is the `.catch` commented out (as in your question?) inside `fetchLiveFeed`? If so, a request to `https://statsapi.web.nhl.com` would return some data that cannot be decoded as `Live.self`, which would set off an error. If, on the other hand, `catch` catches the error and returns an `Empty` publisher, then yes - you won't see any emitted values. – New Dev Mar 24 '21 at 13:17
  • Yes, .catch is commented out. But not seeing the value being emitted when there’s no link. – cole Mar 24 '21 at 17:35
  • The exact same code works fine in my `SwiftUI` app but no go in the `Widget` with the same data mode as always game scheduled. Example here https://pastebin.com/epJDZ8MM – cole Mar 25 '21 at 00:22
  • Ok... well, it seems to point to a problem being elsewhere, beyond the scope of this question, can't really help you there. All I can say, simplify your code. Try display a static data first. Then try a local, but asynchronously received data. Then request a well-know simple JSON from your own server, and so on... – New Dev Mar 25 '21 at 00:26
  • Well thanks for helping a ton. I think it’s relating to WidgetKit somehow. I attempted to use StateObject and still the same. Not too sure what else to try.. – cole Mar 25 '21 at 01:38
0

SwiftUI and WidgetKit work differently. I needed to fetch data in getTimeline for my IntentTimelineProvider then add a completion handler for my TimelineEntry. Heavily modified my Combine data model. All credit goes to @EmilioPelaez for pointing me in the right direction, answer here.

cole
  • 1,039
  • 1
  • 8
  • 35