alt text

MagicCloud

License CocoaPods Platform Language CocoaPods Tag iOS App Store Jazzy Docs

Magic Cloud is a Swift (iOS) Framework that makes using CloudKit simple and easy.

For any data types that need to be saved as database records, just conform them to the MCRecordable protocol. Then the generic MCMirror classes can maintain a local array of that type, and mirror it to CloudKit’s databases in the background.

Default setup covers error handling, subscriptions, account changes and more. Can be configured / customized for optimized performance (for more details on that, the Magic Cloud Blog is coming to our site), or just use as is.

Check out the Quick Start Guide and see an app add working cloud functionality with less than 20 lines of code!

Requirements

Meet the requirements for CloudKit, which includes a paid developer account.

An iOS project (min 10.3), that requires a relational database. (Why wouldn’t you use Swift for that?)

Does NOT directly support shared databases (upcoming version).

Getting Started

In order to use Magic Cloud, a project has to be configured for CloudKit and the MagicCloud framework will need to be linked to its workspace.

Preparing App for CloudKit

Magic Cloud is meant to work on top of Apple’s CloudKit technology, not replace it. The developer does not maintain any actual databases and is not responsible for data integrity, security or loss.

Before installing Magic Cloud be sure CloudKit and Push Notification are enabled in your project’s capabilities.

Installations

If you’re comfortable using CocoaPods to manage your dependencies (recommended), add the following in the podfile.

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.3'   # <- 10.3 minimum requirement, can be more recent...
use_frameworks!         # <- MagicCloud is a swift framework, ensure this is present.

target '<Your Target Name>' do
    pod 'MagicCloud', '~> 3.0.1'  # <- Be sure to use the current version.
end

Then, from your project’s directory…

pod install

Alternatively, clone from github, then add the framework to your project manually (not recommended).

Quick Start Guide

Check out the Quick Start Guide, a how-to video at Escape Chaos, to see a test app get fully configured in less than 20 lines of code.

Examples

For basic projects, these examples should be all that is necessary.

MCNotificationConverter

Once you have CloudKit enabled and the cocoapod installed, there’s one last piece of configuration that has to happen in the app delegate.

First make your app delegate conform to MCNotificationConverter.

class AppDelegate: UIResponder, UIApplicationDelegate, MCNotificationConverter {    // <-- Add it here...

Next, insert the following two lines into the didFinishLaunchingWithOptions method already in the app delegate.

        MCUserRecord.verifyAccountAuthentication()      // <-- More information about this below @ MCUserRecord
        application.registerForRemoteNotifications()

Finally, scroll down and insert ONE of the FIRST TWO methods into the same class.

    // This is the current way ...
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        convertToLocal(from: userInfo)
    }
    // This version is deprecated, but works for pre-iOS 10 apps...
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
        convertToLocal(from: userInfo)
    }

    // This version DOES NOT work for silent push notifications, so it will miss any pushes from Magic Cloud...
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

        let userInfo = notification.request.content.userInfo
        convertToLocal(from: userInfo)          // <-- DOES NOT WORK !!
    }

With that in place, any notifications from the CloudKit databases will be converted to a local notification and handled by any MCMirrors that are setup.

If you’ll need to disable any of your features during a subscription failure (e.g. airplane mode, bad network connection, etc…), add this method to the app delegate and do so here (or more likely, post a notification here).

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        // This is only one of the ways you can 'gracefully disable' features that require cloud. 
    }

With all the setup / configuration out of the way, now you can get started…

MCRecordable

Any data type that needs to have it’s model stored as records (and it’s properties saved as those records’ fields) will need to conform to the MCRecordable protocol. Currently, Magic Cloud doesn’t handle CKRecords directly.

extension MockType: MCRecordable {

    public var recordType: String { return "MockType" }            // <-- This string will serve as a CKRecordType.Name

    public var recordFields: Dictionary<String, CKRecordValue> {   // <-- This is where the properties that should be CKRecord   
        get {                                                      //     fields are updated / recovered. 
            return [Mock.key: created as CKRecordValue] 
        }

        set {
            if let date = newValue[Mock.key] as? Date { created = date }
        }
    }

    public var recordID: CKRecordID {                              // <-- This ID needs to be unique for each instance.
        get { return _recordID ?? CKRecordID(recordName: "EmptyRecord") }
        set { _recordID = newValue }                               // <-- This value needs to be saved when instances are
    }                                                              //     created from downloaded database records. 

    // MARK: - Functions: Recordable

    public required init() { }                                     // <-- This empty init is used to generate empty instances
}                                                                  //     that can then be overwritten from database records.

MCMirror

Once there are recordables to work with, use MCMirror(s) to save and recover these types in the CloudKit databases.

let mocksInPublicDatabase = MCMirror<MockType>(db: .publicDB)
let mocksInPrivateDatabase = MCMirror<MockType>(db: .privateDB)

Shortly after they’re initialized, the receivers should finish downloading and transforming any existing records. These can be accessed from the dataModel array.

let publicMocks = mocksInPublicDatabase.dataModel

Voila! Any changes to records in the cloud database (add / edit / remove) will automatically be reflected in the receiver’s recordables array until it deinits. When elements are added, modified or deleted from the cloudRecordables array, the MCMirror will ensure those changes are mirrored to the respective database in the background.

let new = MockType(created: Date())

mocksInPublicDatabase.cloudRecordables.append(new)                      // <-- This will add a new record to the database.

mocksInPublicDatabase.cloudRecordables[0].created = Date.distantFuture  // <-- This will modify an existing database record.

mocksInPublicDatabase.cloudRecordables.removeLast                       // <-- This will remove a record from the database.

Note: While multiple mirrors for the same data type in the same app reduces stability, it is supported. Any change should be reflected in all mirrors, both in the local app and in other users’ apps.

MCUserRecord

If you’re dealing with private databases or need a unique token for each cloud account, use MCUserRecord to retrieve the user’s unique iCloud identifier.

if let userRecord = MCUserRecord().singleton {          // <-- Returns nil if not logged in OR if not connected to network.
    print("User Record: \(userRecord.recordName)") 
}

To test if a user is logged in to their iCloud account, and have them receive a warning with a link to the Settings app if not, simply call the following static method.

MCUserRecord.verifyAccountAuthentication()

If needed, you’ll probably want to get this out of the way in the app delegate (didFinishLaunchingWithOptions method is recommended).

Considerations

While the aforementioned code is all that is needed for most projects, there are still a few design considerations and common issues to keep in mind.

Concurrency, Grand Central Dispatch & the Main Thread

If this project is your first attempt at working with asynchronous operations, Apple has several great resources out there that will ultimately save you a lot of time and trouble…

CloudKit QuickStart

CloudKit Documentation

Concurrency Programming Guide

CloudKit Design Guide

Apple and Magic Cloud have done most of the heavy lifting, but you will still have to understand the order your processes will execute and that varying amounts of time will be needed for cloud interactions to occur. Dispatch Groups (and XCTExpectations for unit testing) can be very helpful, in this regard.

Do NOT lock up the main thread with cloud activity; every app needs to have separate threads for updating views and waiting for data. If you’re not sure what that means, then you may want to review the documentation mentioned above.

Error Notifications

Error Handling is a big part of cloud development, but in most cases Magic Cloud can deal with them sufficiently. For developers that need to perform additional handling, every time an issue is encountered a Notification is posted that includes the original CKError.

To listen for these notifications, use MCErrorNotification.

let name = Notification.Name(MCErrorNotification)
NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil) { notification in

     // Error notifications from MagicCloud should always include the actual CKError as Notification.object.
     if let error = notification.object as? CKError { print("CKError: \(error.localizedDescription)") }
}

CAUTION: In cases where there’s a batch issue, a single error may generate multiple notifications.

CloudKit Dashboard

Each CloudKit container can be directly accessed at the CloudKit Dashboard, where developers can modify the database schema, query / modify records, manage subscriptions, etc…

DON’T FORGET to make all record names queryable. MCMirrors use those names to find and fetch records.

Reporting Bugs

If you’ve had any issues, first please review the existing documentation thoroughly. After being certain that you’re dealing with a replicable bug, the best way to submit the issue is through GitHub.

@ github.com/jalingo/MagicCloud > "Issues" tab > "New Issue" button

You can also email dev@escapechaos.com, or for a more immediate response try Stack Overflow.