Monday, 27 April 2020

Utilizing the iOS Background with Xamarin

The Background Transfer Service in iOS is a service that allows your app to download or upload large amounts of data in the background with no time limits. Whether your app is in the foreground, suspended, or sitting in the background, it can download as much data as it wants without the time limits that iOS enforces with other mechanisms.

When using the Background Transfer Service, you are essentially allowing iOS to take control of your download. Depending on external factors, such as available resources, interruptions, network connectivity, battery life, etc., iOS will decide when to give your application time to process network content. While the download/upload is in progress, iOS will periodically ping your application to inform current status or errors. 

By relinquishing control of the network transfer to the operation system, you lose some predictability and speed, but you gain a lot of extra flexibility and power with regard to your web traffic.

The backbone of the Background Transfer Service is implemented using the NSUrlSession API. To use this API, you must create a download/upload session along with a set of background tasks (or just a single task) to actually do the work. These tasks are instances of NSURLSessionTask objects. The session also requires a delegate—an instance of NSUrlSessionTaskDelegate, which is how iOS communicates with your application.

The Basics of Background Transfer Service

Let’s update the example from my previous post, using the Background Transfer Service, rather than HttpClient, to fetch the image.

1. Create the background session object.

In order to use the NSUrlSession API, we need to first create and configure an NSUrlSession object. As you can see below in the ConfigureBackgroundSession method, we use BackgroundSessionConfiguration to check if there is an already-configured session. If so, we can just use that. Otherwise, we create a new background session configuration with CreateBackgroundSessionConfiguration and pass in a session ID, which is just a unique string. If you try to create a session with a session identifier that already exists, iOS will not create a new session configuration.
Once we have our NSUrlSessionConfiguration, we can use this configuration to create the session object. Creating the session is quite simple—just use the NSUrlSession.FromConfiguration static method. This method requires the instance of your configuration object as well as a delegate, which will be used as the interface between iOS and your application. This UrlSessionDelegate will be defined later.
C#
public partial class AppDelegate
{
  const string SessionId = "com.example.backgroundSession";

  private NSUrlSession ConfigureBackgroundSession()
  {
    var configuration = NSUrlSessionConfiguration.BackgroundSessionConfiguration(SessionId);
    if (configuration == null) {
      configuration = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration (SessionId);
    }

    using (configuration) {
      return NSUrlSession.FromConfiguration (configuration, new UrlSessionDelegate(), null);
    }
  }
}

2. Kick off the download task(s).

Once you have the session configured, the next step is to kick off some download tasks. In our case, we only need one task, but if we were to be downloading multiple files, it would be best to utilize multiple download tasks. This FetchImage is the same method used in my previous post, but I have removed the call to HttpClient. Instead, we are creating the NSUrlSession and initializing a download task.
The CreateDownloadTask method is used to create a download task on a background session. It is important that you call Resume on the task to start the asynchronous download. You can also cancel and resume the download at any point.
C#
public UIBackgroundFetchResult FetchImage (string url)
{
  var session = ConfigureBackgroundSession();

  var request = new NSMutableUrlRequest(NSURL.FromString(url));
  request.HttpMethod = "GET";
  using (request) {
    var task = session.CreateDownloadTask(request);
    task.Resume();
  }

  return UIBackgroundFetchResult.NewData;
}

3. Create the NSUrlSessionDelegate.

Now that we have the session configured and the download tasks running, we need to implement the NSUrlSessionDelegate class to allow iOS to communicate status back to our app and for our app to inform iOS when it is all done. Since we are using an NSUrlSessionDownloadTask, we will implement an NSUrlSessionDownloadDelegate delegate.
Below is a basic implementation of a UrlSessionDelegate. In the constructor, we just set the file path where we want the image stored. This is the same location as in the earlier example. Then, we implement the DidWriteDataDidFinishDownloading and DidFinishEventsForBackgroundSession delegate methods. Depending on the needs of the application, there are several other optional methods that could be implemented.
C#
public class UrlSessionDelegate : NSUrlSessionDownloadDelegate
{
  private string destinationFilepath;
  
  public UrlSessionDelegate()
  {
    destinationFilepath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "xamarin.png");
  }

  public override void DidWriteData (NSUrlSession session, NSUrlSessionDownloadTask task, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite)
  {
    float progress = totalBytesWritten / (float)totalBytesExpectedToWrite;
    Console.WriteLine ($"Task: {task.TaskIdentifier}  progress: {progress}");
    // perform any UI updates here
  }

  public override void DidFinishDownloading (NSUrlSession session, NSUrlSessionDownloadTask task, NSUrl location)
  {
    Console.WriteLine($"Download task {task.TaskIdentifier} completed!");
    // Copy the file to a permanent location
    if (File.Exists(destinationUrl)) {
      File.Delete(destinationUrl);
    }
    File.Copy (location.Path, destinationUrl);
  }

  public override void DidFinishEventsForBackgroundSession (NSUrlSession session)
  {
    // Perform any processing on the data

    using (var appDelegate = UIApplication.SharedApplication.Delegate as AppDelegate) {
      var handler = appDelegate.BackgroundUrlCompletionHandler;
      if (handler != null) {
        handler.Invoke();
      }
    }
  }
}
The DidWriteData method is called periodically throughout the download process. It reports the number of bytes written in this chunk, the total number written by this task, and the total expected bytes to be received. This is a really useful method for reporting download progress. We are just calculating the progress and printing the percentage to the console.
The DidFinishDownloading method is called once the task has finished downloading all data. The task will write its data to a temporary file in the file system. It is up to you to copy that file to a more permanent location, if required. Once control leaves this method, the temporary file will be deleted. If you have multiple download tasks running concurrently, be sure to check that the task is the expected task before processing the data.
The DidFinishEventsForBackgroundSession method is a very important one, and it is the final step in a Background Transfer. This is where you can perform any last-minute processing or UI updates before iOS puts you in the background (assuming your app is in the background when this runs). Once you have performed all necessary processing of the downloaded content, you must call the completion handler. Which brings us to…

4. Hook into the AppDelegate.

Before iOS actually starts downloading your content, it will call the HandleEventsForBackgroundUrl delegate method and pass in a completion handler.
C#
public Action BackgroundUrlCompletionHandler { get; private set; }

public override void HandleEventsForBackgroundUrl (UIApplication application, string sessionIdentifier, Action completionHandler)
{
  BackgroundUrlCompletionHandler = completionHandler;
}
This completion handler is not to be confused with the completion handler passed into the PerformFetch delegate method. This completion handler is used to tell iOS when the background transfer is complete, and you are ready to go back to the background. All we need to do in the HandleEventsForBackgroundUrl method is save off the completion handler. It must be called from DidFinishEventsForBackgroundSession (see above).

Looking Down the Road with Xamarin

The Background Transfer Service is a powerful feature of iOS which, if used correctly, can greatly enhance the user experience of your application. It allows developers to be flexible with our timing requirements and lets large and unpredictably sized downloads or uploads happen in the background.
This was a very basic example to demonstrate and explain the iOS Background Transfer Service, especially as it relates to Xamarin’s implementation. In my experience with Xamarin so far, it has proven a very painless exercise to implement background services, and I am excited to see what the future has in store for Xamarin.

No comments:

Post a Comment

All About .NET MAUI

  What’s .NET MAUI? .NET MAUI (.NET Multi-platform App UI) is a framework for building modern, multi-platform, natively compiled iOS, Androi...

Ads2