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,

  1. Reduce image quality
  2. Provide alternate low-resolution media sources
  3. Avoid prefetching to avoid loading unneeded resources when they are not needed
  4. Synchronize less often with the server
  5. 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,

  1. Set allowsConstrainedNetworkAccess property on URLRequest to true to account for the low data mode
  2. 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.

References

Advances in Networking, Part 1