Support Low Data Mode Networking in iOS app (iOS 13)
Apple introduced a low data mode feature in iOS 13. Using this mode, (On iOS 13), users can specify their preference to minimize the data usage when the network is really bad or they want to save on their data while being on the cellular network.
When the low data mode is on, the system can make certain decisions to reduce the high data usage and defer discretionary tasks which demand the high network load. When the low data mode is on, the background app refresh is also disabled. This helps iOS to avoid refreshing apps which are in the background and have nothing to do with what the user is interested in.
Every time you can save the network data without impacting the user experience, you should do it
How to Turn On the Low Data Mode on iOS
As I mentioned in the introduction, Low data mode is only available on iOS 13+. It can be enabled from settings. If you are on iOS 15.3.1+, please follow the below steps to enable low data mode on iPhone,
Settings -> Cellular -> Cellular Data options -> Data Mode -> Low Data Mode
Adapting App for a Low Data Mode
There is a reason why users enable the low data mode. Maybe the network is spotty, the data plan is too expensive and limited or the phone battery is low. Irrespective of the reason, we should design our app so that it can easily adapt to the low data mode.
Here are some techniques you can use when you detect the low data mode is on for the device,
- Reduce image quality
- Provide alternate low-resolution media sources
- Avoid prefetching to avoid loading unneeded resources when they are not needed
- Synchronize less often with the server
- If your app supports videos, disable auto-play in the low data mode
Provide Low-resolution Media in Network Constrained Environment
There will be times when users will turn on the low power mode. If you're attempting a large data download in such an environment, the download will fail since iOS will put constraints on high data download tasks.
For example, consider the following function which downloads the large image from the URL.
struct ImageRequest {
static let highResolutionImage = "https://gcdnb.pbrd.co/images/NrAeiemLg6Ik.jpg?o=1"
static let lowResolutionImage = "https://gcdnb.pbrd.co/images/6xNf9bg7QBL6.jpg?o=1"
}
func downloadImage() -> AnyPublisher<UIImage?, Error> {
guard let url = URL(string: ImageRequest.highResolutionImage) else {
return Just<UIImage?>(UIImage(named: "profile")).setFailureType(to: Error.self).eraseToAnyPublisher()
}
let urlRequest = URLRequest(url: url)
return urlSession.dataTaskPublisher(for: urlRequest).tryMap {
UIImage(data: $0.data)
}.mapError { error in
return error
}.eraseToAnyPublisher()
}
In a regular case, you can call it as follows and get an image back no matter the size of the image or the quality of the underlying network.
var anyCancellables = Set<AnyCancellable>()
NetworkService()
.downloadImage()
.sink { completion in
} receiveValue: { image in
print("Received an image")
}
.store(in: &anyCancellables)
However, when the low data mode is on and an attempt is made to download a large file, the API may throw an error saying the network is unavailable due to resource constraints.
Let's turn on the low data mode on the real device,
Unfortunately, even when the low data mode is on, the default behavior is to still download the large data. In order to constrain the network download during low data mode, we need to edit the allowsConstrainedNetworkAccess
property on URLRequest
object before sending the request,
var urlRequest = URLRequest(url: url)
urlRequest.allowsConstrainedNetworkAccess = false
Please note that, setting this property to false is completely optional, but is strongly recommended to respect user's choice of low data mode. If the low data mode is on and user detects that your app is still downloading the lagre data, it will provide the poor experience and user may be strongly incliced to remove your app from the system. So my suggestion is to set this property to false as much as possible in order to keep app in sync with the system-wide low data mode state
There are exceptions to this though. If there are situations where you're downloadin large amount of data for a good reason, you can add an exception and set allowsConstrainedNetworkAccess
to true only for those cases
When an attempt is made to trigger the heavy download, it will trigger an error due to network constraints and execute the catch
block. We can detect that this is an error due to a constrained network by checking if the networkUnavailableReason
enum property on error is equal to constrained
.
However, we don't have to stop here. There is a workaround we can try if we get this error. Let's add one more chaining operator with tryCatch
to catch this error.
If the error originated due to constrained network resources, we will return another publisher with the same type, but instead of loading a high-res image, it will now return the low-res image upon completion.
struct ImageRequest {
static let highResolutionImage = "https://gcdnb.pbrd.co/images/NrAeiemLg6Ik.jpg?o=1"
static let lowResolutionImage = "https://gcdnb.pbrd.co/images/6xNf9bg7QBL6.jpg?o=1"
}
func downloadImage() -> AnyPublisher<UIImage?, Error> {
guard let url = URL(string: ImageRequest.highResolutionImage) else {
return Just<UIImage?>(UIImage(named: "profile")).setFailureType(to: Error.self).eraseToAnyPublisher()
}
var urlRequest = URLRequest(url: url)
urlRequest.allowsConstrainedNetworkAccess = false
return urlSession.dataTaskPublisher(for: urlRequest)
.tryCatch { error -> URLSession.DataTaskPublisher in
guard error.networkUnavailableReason == .constrained else {
throw error
}
return self.urlSession.dataTaskPublisher(for: URL(string: ImageRequest.lowResolutionImage)!)
}.tryMap {
UIImage(data: $0.data)
}.eraseToAnyPublisher()
}
There are two changes we did in the above code sample,
- Set
allowsConstrainedNetworkAccess
property onURLRequest
totrue
to account for the low data mode - Switch to low-resolution low-data mode API to download the low-res image when low data is enabled by the user
In the example above, the high-res image URL downloads the image of size 5MB while the low-res version is only 19KB in size
Running the App Adopted for Low Data Mode
Now that our app is set up to handle low data mode, let's run the app again and see the difference. First, we will run the app in regular mode and see what the full-screen image looks like,
var anyCancellables = Set<AnyCancellable>()
NetworkService()
.downloadImage()
.receive(on: DispatchQueue.main)
.replaceError(with: UIImage(named: "profile"))
.assign(to: \.image, on: imageView)
.store(in: &anyCancellables)
Now, let's turn on the low data mode again, run the app and see how what image is downloaded.
As you can see above, a low-res blurry image is downloaded when the low data mode is on. The first call to get a high-res image resulted in an error, but we detected that the low data mode is on, and we sent a similar request to another URL to get the low-res image and show it in the app.
And here's the side-by-side comparison of downloaded images in high-res and low-res mode
Summary
The cost of data is expensive. Especially in the region where there is not enough internet penetration. Being cognizant of the low-data mode and conforming to it is not just to gain the user's trust and continue doing what is intended, but it can also save the precious bandwidth and help them avoid the extra costs associated with the internet data on the cellular network.
When we develop an app with low data mode, we truly make it inclusive for all the users irrespective of their data plan. Work with your team to make a list of which resources you can download in a regular environment and which resources should replace them while being in the low data mode. By making these adjustments you not only give users the best possible experience but also are flexible enough to change the app's behavior depending on the underlying settings.
Have you used a low data mode in the app yet? Did you get any benefits out of it? If you haven't explored this topic yet, after reading this article, are you excited about it? Let me know what you think. If you have any other thoughts, comments, or feedback, please reach out to me on Twitter @jayeshkawli.