Thoughts while looking into upgrading from SCNetworkReachabilityGetFlags to NWPathMonitor

I have been using the SCNetworkReachabilityGetFlags for 10+ years to inform users that their request won't work. In my experience this works pretty well although i am aware of the limitations.

Now, i am looking into the NWPathMonitor, and i have one situation that i'm trying to. get my head around - it's asynchronous.

Specifically, i am wondering what to do when my geofences trigger and i want to check network connectivity - i want to tell the user why the operation i'll perform because of the trigger couldn't be done.

SO. say i start a NWPathMonitor in didFinishLaunchingWithOptions. When the app is booted up because of a geofence trigger, might i not end up in a case where my didEnterRegion / didExitRegion gets called before the NWPathMonitor has gotten its first status?

The advantage here with SCNetworkReachabilityGetFlags, as i understand it, would be that it's synchronous?

If i want to upgrade to nwpathmonitor, i guess i have to do a method that creates a nwpathmonitor, uses a semaphore to wait for the first callback, then contunues?

Thoughts appreciated

Answered by DTS Engineer in 868951022
just fire off the request blindly and wait to see what happens?

Yes. And use an expiration handler to deal with the case where the network is super slow.

That last bit is critical, because it crops up in practice. In most cases an immediate network request — that is, one without waitsForConnectivity set — will finish promptly. It’ll either succeed or it’ll fail immediately. But there are networks out there in the real world where such requests take a long time. Sometimes that’s caused by bad network conditions. The network is moving packets just fast enough to convince iOS to use it, but not fast enough to get anything useful done. But in other cases it’s caused by weird network configurations. For example, a Wi-Fi accessory might publish a network that goes out of its way to convince iOS that it has a path to the wider Internet when it actually doesn’t.

Now, unless you have access to such a network then testing your expiration handler can be tricky. My advice is that you add a mode to your networking layer to specifically simulate this. That is, you pass it a request and in this mode the network layer simply never completes the request.

Oh, and make sure to add logging so that you can investigate problems that shows up in the field. I have advice about this in Testing Background Session Code (you’re not using a background session but a lot of the advice still applies), and general advice about logging in Your Friend the System Log.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Check out this thread (and the tech talk video it links to) for more general guidance on how best to use reachability checks in your app. You can also find similar threads with a search term such as user:eskimo preflight.

Basically, don't rely on preflighting network operations. Either of the reachability APIs is still asynchronous to your actual network request and can't possibly guarantee that any given request either will or won't succeed. So first make the app solid around network requests that fail for any reason. Then use reachability to add user-facing polish where you can.

Yeah, i've seen that tech talk, and i know it's deprecated. I know the limitations and agree, but i mean, it wasn't deprecated before :)

None of the texts refer to situations where you only have a short time to do your thing, such as a network request in response to user interaction from a live activity or a geofence trigger like we do a lot.

For our geofence triggers for example: It's crucial for us that if a network request fails in the trigger, we can end it by posting a local notification to the user "the thing failed because of X". This won't happen if the system kills the trigger in the middle of the request waiting for network connectivity. In that case, it's better with a false positive for us, than nothing at all, because then we can be certain that the user is informed.

The advantage here with SCNetworkReachabilityGetFlags … would be that it's synchronous?

Yes and no. The API itself is synchronous, but it can block the calling thread waiting for the resolution to complete. That’s not exactly ideal.

Coming back to your top-level issue, you wrote:

For our geofence triggers for example

I think I’m missing something here. If you make the request without waiting for connectivity, it’ll fail promptly in most cases when the network is down. Do you need the network state to preflight the request? Or only to generate a better user-facing message if it’s already failed?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

When we made this, like back in 2013, we (thought we) had situations where the system killed the geofence trigger before it had a chance to complete its network request. The thought was that if we checked the network status before we do the actual http request, we reduced the risk of the trigger being killed before we had a chance to put up a notification explaining to the user that we failed and why.

We do the whole UIBackgroundTaskIdentifier but back then the documentation (we thought at the time) made it a bit uncertain if we could be sure that we'd always get a chance to finish and clean up properly.

Thanks for your explanation of how you got to where you are.

My natural inclination here is to use a UIApplication background task with an expiration handler. It sounds like you’re already doing most of that. If you simply delete the preflight code and rely on your expiration handler, does that work in practice?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Yeah, my process is basically when i get a trigger:

  1. check network status (the old, lame way :) )
  2. if network down or other issue, create local notification failure user explanation and exit
  3. if all good, get a background task id and start working

It was a long time since we initially evaluated this, but back then we had issues with being killed off mid-flight sometimes. As you can perhaps guess, i didn't document everything, and it's totally possible that we did something wrong or that whatever wasn't working 100% right now.

So, what you are saying is that i should not bother with NWPathMonitor (or the one i'm using right now) at all? just fire off the request blindly and wait to see what happens?

just fire off the request blindly and wait to see what happens?

Yes. And use an expiration handler to deal with the case where the network is super slow.

That last bit is critical, because it crops up in practice. In most cases an immediate network request — that is, one without waitsForConnectivity set — will finish promptly. It’ll either succeed or it’ll fail immediately. But there are networks out there in the real world where such requests take a long time. Sometimes that’s caused by bad network conditions. The network is moving packets just fast enough to convince iOS to use it, but not fast enough to get anything useful done. But in other cases it’s caused by weird network configurations. For example, a Wi-Fi accessory might publish a network that goes out of its way to convince iOS that it has a path to the wider Internet when it actually doesn’t.

Now, unless you have access to such a network then testing your expiration handler can be tricky. My advice is that you add a mode to your networking layer to specifically simulate this. That is, you pass it a request and in this mode the network layer simply never completes the request.

Oh, and make sure to add logging so that you can investigate problems that shows up in the field. I have advice about this in Testing Background Session Code (you’re not using a background session but a lot of the advice still applies), and general advice about logging in Your Friend the System Log.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Mr. Eskimo,

huge thanks for a great response. Not 100% about "mode to the networking layer", do you mean basically add Thread.sleep on the server when testing?

I will take your suggestions to heart. It's great since i'm also rewriting my objc networkhandler in Swift.

Just a final belt/suspenders-question: If i understand you correctly, most applications should just perform the request regardless, wait for the API response/timeout and not bother at all with NWPathMonitor ?

most applications should just perform the request regardless … ?

Yes.

When using URLSession, our general advice is to:

  1. Set up your constraints in advance, that is, things like allowsConstrainedNetworkAccess.
  2. Set waitsForConnectivity.
  3. Issue the request.
  4. Implement urlSession(_:taskIsWaitingForConnectivity:) if you need to know that the request is waiting. This is useful, for example, if you want to update your UI.
  5. Apply your own timeout if you feel that’s absolutely necessary, or use timeoutIntervalForResource [1].

Note In cases like yours, where it’s a now-or-never kinda thing, it’s fine to skip steps 2 and 4.

We specifically recommend against preflighting your network requests because:

  • TOC/TOU makes it impossible to do correctly.
  • Which means you’re required to handle errors anyway.
  • So you might as well not implement the preflight.
  • An incorrect preflight can prevent you from making a network request that might’ve worked if you didn’t have the preflight, for example, when on-demand interfaces are in play.

NWPathMonitor is useful when there’s no specific network operation that you can wait for. Even in that case, however, it’s critical that you not prevent the user from issuing a network request just because NWPathMonitor says it’s not going to work. Don’t disable your Retry button based on the state returned by NWPathMonitor.

In Networking Resources there’s a link to a tech talk where we explain these ideas in more detail.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] It’s generally best to leave timeoutIntervalForRequest set to the default.

Thoughts while looking into upgrading from SCNetworkReachabilityGetFlags to NWPathMonitor
 
 
Q