Hello Friends, have you ever wanted to download a file from an API to your user’s phone in the background? You might have faced a situation where you want to let your users download very large files from your server. And you want to download these sorts of files in the background. You might have also been in a situation where you just want to get data from an API, and you want to stream this data, either downloading or uploading the data, and you need full control of the streaming input/output and writing process. This post got you covered.
Download Data Streams to a File in the Background with Xamarin iOS
We will look at two methods to download files in the background with Xamarin iOS. The first will be the most reliable way, using NSURLSessionDownloadDelegate, where we let iOS control file location, and read/write processes to the file. The second way will let us control the full download process, from determining where the file is saved, to writing into it ourselves using streams we control entirely This will be done with NSUrlSessionDataDelegate.
Easy Background Download With NSUrlSessionDownloadDelegate
If you don’t care about uploading data or you want to download large files, and you don’t need to have full control of the stream read/write process of your data, then this is the way to go.
First, we create a delegate that inherits from NSUrlSessionDownloadDelegate. We then create an NSURLSession. This will be done by configuring it with an id and telling it to be in the background. NOTE: Only one NSURLSession instance should be created for a given Id.
1 2 3 | var config = NSUrlSessionConfiguration.BackgroundSessionConfiguration("SessionId"); _session = NSUrlSession.FromConfiguration(config, this, new NSOperationQueue()); |
You can then set the appropriate headers you want, the request method “Get, Post, etc…”. Next, you create a download task and start the download by resuming it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var nsUrl = NSUrl.FromString(downloadUrl); var request = new NSMutableUrlRequest(nsUrl, cachePolicy: NSUrlRequestCachePolicy.ReloadIgnoringLocalCacheData, timeoutInterval: 20); _logger.LogInformation($"Getting ys client headers"); request.HttpMethod = "GET"; NSMutableDictionary dic = new NSMutableDictionary(); var ysHeaders = //You can add a dictionary of headers here foreach (var header in ysHeaders) { dic.Add(new NSString(header.Key), new NSString(header.Value)); } request.Headers = dic; _logger.LogInformation($"Creating download task"); _streamingTask = _session.CreateDownloadTask(request: request); _streamingTask.Resume(); |
You will surely need to update the UI as the download progresses, to keep your dear users updated. You do so by implementing the “DidWriteData” method of the delegate and updating the user as bytes are written.
1 2 3 4 5 6 7 8 | public override async void DidWriteData(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite) { //We calculate the percentage downloaded _progress = ((float)(new decimal(totalBytesWritten) / new decimal(totalBytesExpectedToWrite))) * 100; ReportProgress(_progress); } |
You might ask yourself; how do I know where my downloaded file is stored? It is easy. When download terminates, the method “DidFinishDownloading” is called, and it passes to you a path to the downloaded file. You can now get this file, rename it or move it to its final destination.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public override async void DidFinishDownloading(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, NSUrl location) { var filePath = location.ToString().Replace("file:///", "/"); if (System.IO.File.Exists(filePath)) { System.IO.File.Move(filePath, _finalPath); var args = new DownloadCompletedSuccessfullyEventArg { ProductId = _productId }; Device.BeginInvokeOnMainThread(() => MessagingCenter.Instance.Send<object, DownloadCompletedSuccessfullyEventArg>(this, args.MessageName, args)); } } |
Here is the full code of this delegate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | public class SessionDownloadDelegate : NSUrlSessionDownloadDelegate { private NSUrlSession _session; private NSUrlSessionDownloadTask _streamingTask; private IProductDownloadProcessCacheService _cacheService; private int _productId; private ILogger<SessionDownloadDelegate> _logger; private float _progress; private string _finalPath; private const string SessionId = "MobileApp.BackgroundDownload"; public SessionDownloadDelegate() { _cacheService = MobileApp.Setup.ServiceProvider.GetRequiredService<IProductDownloadProcessCacheService>(); var config = NSUrlSessionConfiguration.BackgroundSessionConfiguration(SessionId); _session = NSUrlSession.FromConfiguration(config, this, new NSOperationQueue()); } public async Task StartStreaming(string downloadUrl, string destinationFilePath, int productId) { _logger = MobileApp.Setup.ServiceProvider.GetRequiredService<ILogger<SessionDownloadDelegate>>(); try { _logger.LogInformation($"Beginning download"); _finalPath = destinationFilePath; _productId = productId; var nsUrl = NSUrl.FromString(downloadUrl); var request = new NSMutableUrlRequest(nsUrl, cachePolicy: NSUrlRequestCachePolicy.ReloadIgnoringLocalCacheData, timeoutInterval: 20); _logger.LogInformation($"Getting ys client headers"); request.HttpMethod = "GET"; NSMutableDictionary dic = new NSMutableDictionary(); var ysHeaders = //You can add a dictionary of headers here foreach (var header in ysHeaders) { dic.Add(new NSString(header.Key), new NSString(header.Value)); } request.Headers = dic; _logger.LogInformation($"Creating download task"); _streamingTask = _session.CreateDownloadTask(request: request); _streamingTask.Resume(); } catch (Exception e) { _logger.LogError(e, $"Download failed before starting completely for product: {_productId}."); } } public override async void DidFinishDownloading(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, NSUrl location) { var filePath = location.ToString().Replace("file:///", "/"); if (System.IO.File.Exists(filePath)) { System.IO.File.Move(filePath, _finalPath); var args = new DownloadCompletedSuccessfullyEventArg { ProductId = _productId }; Device.BeginInvokeOnMainThread(() => MessagingCenter.Instance.Send<object, DownloadCompletedSuccessfullyEventArg>(this, args.MessageName, args)); } } public override async void DidWriteData(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite) { //We calculate the percentage downloaded _progress = ((float)(new decimal(totalBytesWritten) / new decimal(totalBytesExpectedToWrite))) * 100; ReportProgress(_progress); } void ReportProgress(float percentage) { var args = new DownloadProgressEventArg() { ProductId = _productId, Percentage = percentage }; //TODO: show download progress to your users Device.BeginInvokeOnMainThread(() => MessagingCenter.Instance.Send<object, DownloadProgressEventArg>(this, args.MessageName, args)); } public override void DidCompleteWithError(NSUrlSession session, NSUrlSessionTask task, NSError error) { if (error != null) { //Error occured while downloading TODO: Report the error to the users } } } |
Fully Control Data stream Download/Upload with NSUrlSessionDataDelegate
On iOS, the API provided to handle downloads is NSUrlSession. This API contains a set of helpers to facilitate not only to download files, but to download and upload data of any sort while the user is using the app or when the app is asleep. Since we want to literally stream data bytes from our API to the user’s mobile phone, we will need to manipulate raw input data streams from the API and output the data to a file stream on the user’s device. This use case is very similar to uploading raw data from our user’s devices to an API.
To accomplish this, we will create a class that inherits from “NSUrlSessionDataDelegate” this delegate will manage the download of the raw data stream. Then in its constructor, we configure the session. You can learn more about the NSURL API here
1 2 3 | var config = NSUrlSessionConfiguration.DefaultSessionConfiguration; _session = NSUrlSession.FromConfiguration(config, this, new NSOperationQueue()); |
We want to precise the size of the buffer received from the server when data is read.
1 2 3 4 5 6 | public override void NeedNewBodyStream(NSUrlSession session, NSUrlSessionTask task, Action<NSInputStream> completionHandler) { NSStream.GetBoundStreams(BufferSize, out _inputStream, out _outputStream); completionHandler(_inputStream); } |
Before we start receiving data from our server, we need to configure an “NSMutableUrlRequest” by passing it authentication headers, the server’s URL then with this request, we create a “DataTask” that will start the download process as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public void StartStreaming(string downloadUrl, string filePath) { try { var nsUrl = NSUrl.FromString(downloadUrl); var request = new NSMutableUrlRequest(nsUrl, cachePolicy: NSUrlRequestCachePolicy.ReloadIgnoringLocalCacheData, timeoutInterval: 20); request.HttpMethod = "GET"; NSMutableDictionary dic = new NSMutableDictionary(); dic.Add(new NSString("Authentication "), new NSString("Bearer TokenGoesHere")); request.Headers = dic; CreateFile(filePath); _streamingTask = _session.CreateDataTask(request: request); _streamingTask.Resume(); } catch (Exception ex) { ;//TODO: send download error event } } |
Before the download starts, we want to know important information as; the total size of data we will get from the server, if the initial request was successful or if it failed. To do so, we override the delegate’s method “DidReceiveResponse” which will be called to provide us with all of this information.
1 2 3 4 5 6 7 8 9 | public override void DidReceiveResponse(Foundation.NSUrlSession session, Foundation.NSUrlSessionDataTask dataTask, Foundation.NSUrlResponse response, Action<Foundation.NSUrlSessionResponseDisposition> completionHandler) { _errorOccuredInResponse = response.ExpectedContentLength == 0; //If the content is zero, then an error such as 404 or unauthorized occured _totalBytesExpected = response.ExpectedContentLength; completionHandler(NSUrlSessionResponseDisposition.Allow); } |
To calculate the download progress, and eventually notify our Users of the percentage of data downloaded and written locally to our file, we need to override another method of our delegate “DidReceiveData”. This method permits us to get the data downloaded bytes by bytes, and do whatever we want with this data.
1 2 3 4 5 6 7 8 9 10 11 12 | public override void DidReceiveData(NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data) { _progress = (float)(new decimal(data.Length) / new decimal(_totalBytesExpected)); var bytes = ConvertFromNSDataToBytes(data); _fileStream.Write(bytes); Device.BeginInvokeOnMainThread(() => { //Notify the UI of the download progress }); } |
Then, once the download completes, we want to know if it did so successfully, or an error occurred. We also want to close the file stream and dispose of it once we finish writing data to it. The method “DidCompleteWithError” is called when the download completes. No matter if the download succeeded with or without errors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public override void DidCompleteWithError(NSUrlSession session, NSUrlSessionTask task, NSError error) { if (error != null || _errorOccuredInResponse) { //Notify the app that download error occured. } else { //TODO: handle cancellations and errors //No error occured _progress = 1; _fileStream.Close(); _fileStream.Dispose(); Device.BeginInvokeOnMainThread(() => { //Notify the UI that download completed successfully }); } //Called when download is terminated. Note: if error is null, then no error occured } |
Once we are done coding this delegate, we can call it as follows;
1 2 3 4 | var sd = new SessionDelegate(); sd.StartStreaming(“Download URL goes here”, “file path goes here”); |
Conclusion
We have seen how to download data streams to a file in the background with Xamarin iOS. This method is very similar to what is done when you want to upload data to a server as described in this Apple documentation. I really hope this will be helpful to you, please leave a like and subscribe to the notifications if you liked, and don’t forget to follow me on social media to stay updated. You might be interested in this article describing an advanced guide to sending push notifications from your backend to mobile devices with Firebase.
Follow me on social media and stay updated