Adding iCloud support to a Xamarin app

One of the facilities in my Mobile DMX app allows the user to back up all of their workspaces (light shows) into a zip file, which can later be restored. In the currently released Android version the backup can be written to a folder on the tablet, or saved on a PC by using the Windows Companion app.

The next Android version will be able to save directly to the user’s Dropbox account, and also restore from there. Unfortunately, I am not currently able to get Dropbox support working in iOS because it uses a System.Net.Http.WebRequestHandler object, which is in the System.Net.Http.WebRequest library. That library is not yet available in Xamarin.iOS, only Xamarin.Android.

Obviously, a similar facility can be implemented on iOS by using iCloud Drive, and in the long run I hope to give both options on iPads (as well as, perhaps, adding support for Google Drive, One Drive etc.)

It seemed like it should be easy to add iCloud Drive support. After all, the hardest part of adding Dropbox support was handling logging in to Dropbox, and there isn’t any need for an iOS app to have to manage that itself for iCloud Drive. The problem is that iCloud started out as more of a distributed database system, iCloud Drive is a relatively recent addition, and the documentation is very complex.

I managed it eventually, but wasted a lot of time wading through documentation that turned out to have nothing to do with what I was trying to achieve.

The first thing that is required is to set up a provisioning profile that gives the app access to an iCloud container. There’s a general explanation in the Xamarin documentation of how to set up a provisioning profile. I did that mainly on the Apple developer website, and just downloaded the profile into XCode when I had finished.

To set up a provisioning profile that contains an iCloud container, you create an iCloud container (in the Identifers list on the website), then go back to the App ID and edit it, enabling iCloud (with Include CloudKit support), and adding the container to the Enabled iCloud Containers list. Note that you can’t use a Wildcard App ID for this – it has to be an app specific ID. If you had already created a provisioning profile, you need to refresh it, by editing and saving it, and download it into XCode on the Mac again.

You now need to set up some information in Info.plist and Entitlements.plist. Some of this can be added in Visual Studio by using its plist editor, but much of it can’t, so it is easier to just edit the files directly outside of VS.

Info.plist needs this key adding

<key>NSUbiquitousContainers</key>
<dict>
    <key>iCloud.com.yourcompany.yourapp</key>
    <dict>
        <key>NSUbiquitousContainerIsDocumentScopePublic</key>
        <true/>
        <key>NSUbiquitousContainerSupportedFolderLevels</key>
        <string>One</string>
    </dict>
</dict>

yourcompany and yourapp should obviously be replaced. That key is the identifier of the iCloud container created above.

I’m not entirely sure what the best value for NSUbiquitousContainerSupportedFolderLevels really is. It could be Any or One, but One works for me, so I have left it at that.

Entitlements.plist needs this adding

<dict>
    <key>com.apple.developer.icloud-services</key>
    <array>
        <string>CloudDocuments</string>
    </array>
    <key>com.apple.developer.icloud-container-identifiers</key>
    <array>
        <string>iCloud.com.yourcompany.yourapp</string>
    </array>
    <key>com.apple.developer.ubiquity-container-identifiers</key>
    <array/>
</dict>

You may also find that you need to bump the Bundle Version (this can be done from the Visual Studio Info.plist editor).

The Bundle Signing should work automatically, but just in case you might want to go into iOS Bundle Signing in the project properties and select the identity and provisioning profile explicitly. If you can’t see them, make sure Visual Studio is connected to the Mac build machine.

The next job was to write the code to access iCloud Drive. The requirements I had were

  1. Write a zip file to an iCloud folder.
  2. Get a list of the zip files in the folder.
  3. Read a zip file from the folder.

There may be other ways of doing this, but the way I ended up doing it was to create a new UIDocument class to read and write the zip file. All I needed to provide is a way to get a byte[] of data in and out of the document class, and the UIDocument itself can manage opening and saving the data given a suitable iCloud url. To get a listing of the files I used NSFileManager and passed it a path derived from the url of the iCloud folder.

This is the UIDocument class

class ZipDocument : UIDocument {
    private byte[] _data;

    public ZipDocument(NSUrl url, MemoryStream data = null) : base(url) {
        _data = data == null ? new byte[0] : data.ToArray();
    }

    public override bool LoadFromContents(NSObject contents, string typeName, out NSError outError) {
        outError = null;
        if (contents != null) {
            _data = ((NSData)contents).ToArray();
        }
        return true;
    }

    public override NSObject ContentsForType(string typeName, out NSError outError) {
        outError = null;
        return NSData.FromArray(_data);
    }

    public ZipFile GetZip() {
        MemoryStream mem = new MemoryStream(_data);
        return new ZipFile(mem);
    }
}

To save, the document is created with the MemoryStream containing the data, and then the UIDocument base class’s Save() method is called. To load from iCloud, the document is created without a MemoryStream, and the Open() method called, followed by GetZip().

The NSUrl object is obtained by calling

NSUrl _baseUrl;
...
_baseUrl = NSFileManager.DefaultManager.GetUrlForUbiquityContainer(null);

This gets a url for the first (usually only) iCloud container in the provisioning profile. If it returns null, the user is probably not logged in to iCloud, or you haven’t got the provisioning profile set up right. This call should only be made once, because it can take a second or two. Some people recommend getting it on a background thread when the app starts up.

You need to add “Documents” to this url, together with the filename. I wrote a routine to do it (don’t try to use Path.Combine to do it!)

private NSUrl makeUrl(string fname = null) {
    var url = _baseUrl.Append("Documents", true);
    if (fname != null) url = url.Append(fname, false);
    return url;
}

This is the code I used to save the file

try {
    using (var mem = await GetBackupStreamAsync())
    using (var zip = new ZipDocument(makeUrl(fname), mem)) {
        zip.Save(zip.FileUrl, UIDocumentSaveOperation.ForOverwriting, async (success) => {
            if (!success) {
                await page.DisplayAlert("Backup Failed", "Failed to save the backup to iCloud", "OK");
            } else {
                await page.DisplayAlert("Backup Complete", $"The Workspaces have been saved to iCloud in\r\n{fname}", "OK");
            }
        });
    }
} catch (Exception ex) {
    await page.DisplayAlert("Backup Failed", $"Unable to save to iCloud at present\r\n{ex.Message}", "OK");
}

GetBackupStreamAsync returns a MemoryStream containing the contents of the zip file. page is the page this code is being called from.

This is the code to read the file

try {
    var zip = new ZipDocument(makeUrl(zipname));
    var completion = new TaskCompletionSource<ZipFile>();
    zip.Open(async (success) => {
        if (!success) {
            await page.DisplayAlert("iCloud", "Failed to load the zip file", "OK");
            completion.SetResult(null);
        }
        completion.SetResult(zip.GetZip());
    });
    return await completion.Task;
} catch (Exception ex) {
    await page.DisplayAlert("iCloud", $"Unable to access iCloud at present\r\n{ex.Message}", "OK");
    return null;
}

Note the use of the TaskCompletionSource. Open() will return immediately (as does Save), but I didn’t want this routine to return until it had finished loading the zip file.

To get the list of files in the iCloud folder I use

NSError error;
string[] files = NSFileManager.DefaultManager.GetDirectoryContent(makeUrl().Path, out error);

This returns just the filenames without any folder info.

If anything has gone right, saving a zip file like this should cause a folder to appear in iCloud Drive. Be patient, the first time it can take a while (20-30 seconds, maybe) for the folder to appear.

Leave a Reply

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.