Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can you use URLSession in a background refresh task #4

Open
fruitcoder opened this issue Jun 3, 2022 · 17 comments
Open

How can you use URLSession in a background refresh task #4

fruitcoder opened this issue Jun 3, 2022 · 17 comments

Comments

@fruitcoder
Copy link

I am writing a weather application which should provide some complications and after watching the WWDC video the slide at 20:51 states that you cannot use URLSession in a Background App Refresh task. If I understand your code, this is exactly what you do though and I would like to know if you had any problems with this?

AFAIK, the documented way to do this is schedule a url session background task to download the json, then decode it and store it (in your case in user defaults) and in the extension delegate's handle(:) schedule the next background task and reload the complications.

@mackuba
Copy link
Owner

mackuba commented Jun 4, 2022

Hey!

I haven't watched the newer watchOS WWDC talks yet (beyond watchOS ~5)… I've looked at the video you've linked now, and I'd say the slide doesn't explicitly say that you can't use URLSession in Background App Refresh, however,
there is an earlier part around ~9:55 that says in fairly strong words "no networking is allowed, if you do try to use URLSession, it will fail with an error".

This is absolutely not how it worked for me back when I tried it a couple of years ago, and also in another project in 2020 that I've helped a bit with. There was always a recommendation that you should use a background session, but it wasn't said you can't use a normal one. I left the background session as a "second phase" to improve the app into at some point in the next step, to "first make it work, then make it work right", but I just never got to that step.

So the proper way as I understand it is:

  • create a download task on a background URL session
  • when the task completes, you get a session handling bg task
  • in that task, update the UI and schedule another background URL session task

How I did it instead:

  • schedule a Background App Refresh task
  • while handling that task, run a normal URL session request
  • receive a response in the callback closure below
  • update the UI and schedule another Background App Refresh task

And it definitely did work this way, for a long time. I ran the refresh once per hour (I think it was less reliable with more frequent refreshes, though I think still possible), and I made sure to set the timeouts so that the request doesn't use up the whole available time slot, getting the app killed.

So I'm not sure what's going on here - either something was changed in the SDK at some point, or this is still really just a guideline. But he says pretty explicitly about getting errors… Maybe I'm misunderstanding something ¯\_(ツ)_/¯

The sample app doesn't work at the moment, because the air monitoring service has changed their API and I didn't have the time to update it yet. But I guess you can just build some kind of quick prototype that sends the request directly like in my code, and run it for a bit and see if it works or not.

@mackuba
Copy link
Owner

mackuba commented Jun 4, 2022

@trevorturk Hey ;) Could you comment here - did you have any problems with this kind of inline downloads lately, or does it still work for you? Or do you only use background url sessions now?

@trevorturk
Copy link

trevorturk commented Jun 5, 2022

So, big caveat that we (https://helloweather.com) still get occasional complaints about our Complication not refreshing reliably, but it seems fine to me, and to the vast majority of our users. We use the pattern from this demo app. We're currently doing a rewrite and moving into async/await, but basically it's the same thing (no background tasks.) Perhaps we're doing it wrong and that's the problem with our reliability, but it seems mostly fine?

@fruitcoder
Copy link
Author

Thank you for the detailed answer! I'll try to use url background tasks just to be safe. Something else I noticed is that the handle(_:) function is only called when the app is not running in the foreground, right? So the scheduling of the next refresh and complication refresh should actually be also done in the url session's task completion. It's weird that Apple's samples only reschedule background refreshes in the handle(_:) function if it's not called in every situation.

Anyways, congrats that you have a watch app in the store, it's an incredibly annoying development environment (for me at least :D)

@trevorturk
Copy link

OMG yeah, Apple Watch development can be demoralizing sometimes, and even if things work fairly well, IRL use can cause unexpected issues that are difficult to debug. See https://twitter.com/trevorturk/status/1480566496539594763?s=20&t=B27pqZflEBxLF8-kd7lTdQ for a couple things you might consider handling, especially the weird lat/lon issue.

@fruitcoder
Copy link
Author

Ah interesting so you use startUpdatingLocation until you get a valid location instead of the one-off requestLocation? I've just been hitting requestLocation and used the last coordinate of the first locationManager(_:didUpdateLocations:) function :)

@fruitcoder
Copy link
Author

fruitcoder commented Jun 7, 2022

Now that I watched the Platform State of the Union I'm even more confused since the Complications are now driven by WidgetKit so the entire way the timeline is created is via the timeline provider APIs. I have a 1on1 scheduled with an Apple Engineer today and will ask how this fits with the entire "the watch has to do everything with background tasks" approach since our iOS widgets simply fetch location/data in the timeline provider functions

@trevorturk
Copy link

That would be great to hear. Please let us know.

I use the function that keeps returning locations since the single one has errors too frequently. I stop updating when I have a location and also unset the delegate to avoid getting locations multiple times. It's a weird pattern but seems to be the norm.

@mackuba
Copy link
Owner

mackuba commented Jun 7, 2022

@fruitcoder ask them what's the deal with those URL sessions, if it is OK to use non-background URL session requests since it seems to work and doesn't throw an error like they said in the talk

@fruitcoder
Copy link
Author

I tried running a normal data task in getTimeline(for:in:completion) in the updated sample code and it seems to work in the first Beta.

func getTimeline(for configuration: DynamicCharacterSelectionIntent, in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
    /* ... */   
    URLSession.shared.dataTask(with: URL(string: "https://placeholder.com")!) { data,
response, error in
        completion(timeline)
    }.resume()
}

I'll also try to ask for "legacy" ClockKit Complications but I only have a 15min timeslot so let's see how it goes :)

@trevorturk
Copy link

I use the same code for Widgets and Complications currently. (Sounds like we'll switch to Widgetkit for everything now, but currently they're different systems.)

@fruitcoder
Copy link
Author

You mean the same view code right? But the way the supported complications/families are declared will also move to WidgetKit. It's interesting that they reduce the families to just 4, I thought they would just add 3 for the Lock Screen widgets

@trevorturk
Copy link

No, I use the same location+weather fetching code between all platforms now, not just the UI.

@fruitcoder
Copy link
Author

So the standard data tasks are more like a grey area and with the new widget kit it's gotten even "blurrier" (that's how they put it). The getTimelineEntries function is extremely budgeted so if your code to fetch the current location + reverse geocoding already takes some time (depending on device state) it can happen that your data task runs out of time. But as long as it works it's ok (no premature optimization).

I also asked about whether we also need the NSWidgetWantsLocation for the watchOS Widget Extension (we do) and whether it's enough to have the When In Use permission instead of the Always permission (not 100% sure but he said we should still have to request Always).

@trevorturk
Copy link

Thanks for the update. Another thing about the budget and time/power is that I'm using kCLLocationAccuracyHundredMeters -- since weather data APIs really can't get more than that much fidelity anyway. I was told a while back by a watchOS engineer that this setting means the GPS usually doesn't need to fire up (I guess it triangulates using wifi or something?) and I've had good luck with that.

@trevorturk
Copy link

Just watching this session Efficiency awaits: Background tasks in SwiftUI and it seems super relevant here. I'm going to consider switching over to this method. I'm not 100% clear on it, but seems like you can use the URLSessionConfiguration.background without too much fuss? We'll see how it operates IRL, but this looks promising!

@fruitcoder
Copy link
Author

Oh wow didn't have that session on my list. It really looks like it answers some of the conceptual questions I had 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants