Discuss EVCloudKitDao :
With Apple CloudKit, you can focus on your client-side app development and let iCloud eliminate the need to write server-side application logic. CloudKit provides you with Authentication, private and public database, structured and asset storage services - all for free with very high limits. For more information see Apple CloudKit documentation
This is a library to simplify the access to Apple's CloudKit data and notifications (see a more detailed description below)
There is a dependency with EVReflection This will automatically be setup if you are using cocoapods.
See the Quick Help info for method descriptions or the documentation at cocoadocs.org
The AppMessage demo is a complete functional messaging app based on CloudKit:
I'm looking for feedback. Please let me know if you want something changed or added to the library or the demo.
Here are screenshots of the included demo app chat functionality:
Documentation is now available at cocoadocs.org
If you add a property to your object of type CKReference, then also add a property of type String for the RecordID.recordName. You could add a setter for populating both properties. Then if you query this using a NSPredicate, then query the string field and not the CKReference field. You have to do this because a NSPredicate works difrently for NSCloudkit than for an object. The EVCloudData class needs them to function in the same way. For a sample, see the Message class.
Optional objects properties can now be used. Optional type properties not. Swift is not able to do a .setValue forKey on an optional like Int? or Double? As a workaround for this you could use a NSNumber? This limitation is part of EVReflection
The AppMessage demo is using the following components which can be installed using CocoaPods. See instructions below. Because of dependency compatibility the AppMessage demo requires Xcode 6.2 or later.
Besides these the dependency to EVCloudKitDao has been skipped by just using the classes directly
'EVCloudKitDao' is now available through the dependency manager CocoaPods. You do have to use cocoapods version 0.36. At this moment this can be installed by executing:
[sudo] gem install cocoapods
If you have installed cocoapods version 0.36 or later, then you can just add EVCloudKitDao to your workspace by adding the folowing 2 lines to your Podfile:
use_frameworks!
pod "EVCloudKitDao"
I have now moved on to Swift 2. If you want to use EVCloudKitDao with Swift 1.2, then get that version by using the podfile command:
use_frameworks!
pod 'EVReflection', :git => 'https://github.com/evermeer/EVReflection.git', :branch => 'Swift1.2'
pod 'SwiftTryCatch'
pod 'EVCloudKitDao', '~> 2.6'
Version 0.36 of cocoapods will make a dynamic framework of all the pods that you use. Because of that it's only supported in iOS 8.0 or later. When using a framework, you also have to add an import at the top of your swift file like this:
import EVCloudKitDao
If you want support for older versions than iOS 8.0, then you can also just copy the Cloudkit folder containing the 5 classes EVCloudKitDao, EVCloudData, EVReflection, EVCloudData and EVglobal to your app.
When you have added EVCloudKitDao to your project, then have a look at the AppMessage code for how to implement push notifications and how to connect to CloudKit data (see AppDelegate.swift and LeftMenuViewController.swift) For contacts see the RightMenuViewController.swift and for other usage see the TestsViewController.swift
Clone the repo to a working directory
CocoaPods is used to manage dependencies. Pods are setup easily and are distributed via a ruby gem. Follow the simple instructions on the website to setup. After setup, run the following command from the toplevel directory of AppMessage to download the dependencies for AppMessage:
pod install
Open the AppMessage.xcworkspace
in Xcode.
Go to AppMessage target settings and update the:
Build and Run the app. In the AppDelegate there is a call to initiate all objects (createRecordTypes). All required CloudKit objects will be created.
Open the CloudKit dashboard, select all recordtypes and enable all 'Metadata Indexes'
Disable the call to .createRecordTypes in AppDelegate and run the app again.
Make sure you run the app on 2 devices, each using a diverent iCloud account and each device having the other account in it's contact list.
and you are ready to go!
Below is all the code you need to setup a news feed including push notification handling for any changes.
// Just enherit from CKDataObject so that you have access to the CloudKit metadata
class News : CKDataObject {
var Subject : String = ""
var Text : String = ""
}
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
// Make sure we receive subscription notifications
application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: .Alert | .Badge | .Sound, categories: nil))
application.registerForRemoteNotifications()
return true
}
func application(application: UIApplication!, didReceiveRemoteNotification userInfo: [NSObject : NSObject]!) {
EVLog("Push received")
EVCloudData.publicDB.didReceiveRemoteNotification(userInfo, {
EVLog("Not a CloudKit Query notification.")
})
}
func applicationDidEnterBackground(application: UIApplication) {
// If you do a backup then this backup will be reloaded after app restart.
EVCloudData.publicDB.backupData()
}
}
class LeftMenuViewController: UIViewController {
var newsController: NewsViewController!
override func viewDidLoad() {
super.viewDidLoad()
connectToNews()
// Only already setup CloudKit connect's will receive these notifications (like the News above)
EVCloudData.publicDB.fetchChangeNotifications()
}
deinit {
EVCloudData.publicDB.disconnect("News_All")
}
func connectToNews() {
EVCloudData.publicDB.connect(News()
, predicate: NSPredicate(value: true)
, filterId: "News_All"
, configureNotificationInfo: { notificationInfo in
notificationInfo.alertBody = "New news item"
notificationInfo.shouldSendContentAvailable = true }
, completionHandler: { results in
EVLog("There are \(results.count) existing news items")
self.newsController.tableView.reloadData()
return results.count < 200 // Continue reading if we have less than 200 records and if there are more.
}, insertedHandler: {item in
Helper.showStatus("New News item: '\(item.Subject)'")
self.newsController.tableView.reloadData()
}, updatedHandler: {item in
Helper.showStatus("Updated News item:'\(item.Subject)'")
self.newsController.tableView.reloadData()
}, deletedHandler: {recordId in
Helper.showStatus("News item was removed")
self.newsController.tableView.reloadData()
}, dataChangedHandler : {
EVLog("Some News data was changed")
}, errorHandler: {error in
Helper.showError("Could not load news: \(error.description)")
})
}
}
class NewsViewController : UIViewController, UITableViewDataSource, UITableViewDelegate, RESideMenuDelegate {
...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? UITableViewCell
...
//This line all you need to get the correct data for the cell
var news:News = EVCloudData.publicDB.data["News_All"]![indexPath.row] as News
cell.textLabel?.text = news.Subject
cell.detailTextLabel?.text = news.Body
return cell;
}
}
// Just enherit from CKDataObject so that you have access to the CloudKit metadata
class Message : CKDataObject {
var From : String = ""
var To : String = ""
var Text : String = ""
}
let dao: EVCloudKitDao = EVCloudKitDao.publicDB
let dao2 = EVCloudKitDao.publicDBForContainer("iCloud.nl.evict.myapp")
var message = Message()
message.From = "[email protected]"
message.To = "[email protected]"
message.Text = "This is the message text"
dao.saveItem(message, completionHandler: {record in
createdId = record.recordID.recordName;
EVLog("saveItem : \(createdId)");
}, errorHandler: {error in
EVLog("<--- ERROR saveItem");
})
dao.query(Message()
, completionHandler: { results in
EVLog("query : result count = \(results.count)")
}, errorHandler: { error in
EVLog("<--- ERROR query Message")
})
var queryRunning:Int = 0
var data:[Message] = []
func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchString searchString: String!) -> Bool {
self.filterContentForSearchText(searchString)
return false
}
func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchScope searchOption: Int) -> Bool {
self.filterContentForSearchText(self.searchDisplayController!.searchBar.text)
return false
}
func filterContentForSearchText(searchText: String) {
EVLog("Filter for \(searchText)")
networkSpinner(1)
EVCloudKitDao.publicDB.query(Message(), tokens: searchText, completionHandler: { results in
EVLog("query for tokens '\(searchText)' result count = \(results.count)")
self.data = results
NSOperationQueue.mainQueue().addOperationWithBlock {
self.searchDisplayController!.searchResultsTableView.reloadData()
self.tableView.reloadData()
self.networkSpinner(-1)
}
}, errorHandler: { error in
EVLog("ERROR: query Message for words \(searchText)")
self.networkSpinner(-1)
})
}
func networkSpinner(adjust: Int) {
self.queryRunning = self.queryRunning + adjust
UIApplication.sharedApplication().networkActivityIndicatorVisible = self.queryRunning > 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Folowin_Search_Cell";
var cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: .Subtitle, reuseIdentifier: cellIdentifier)
}
var item:Message = data[indexPath.row]
cell.textLabel?.text = item.Text
return cell;
}
All cloudkit function have an errorHandler codeblock. You should handle the error appropriate. There is a helper function for getting a functional error status. In most cases you would get something like the code below. When you are doing data manupilations you should also handle the .RecoverableError
func initializeCommunication(retryCount: Double = 1) {
...
}, errorHandler: { error in
switch EVCloudKitDao.handleCloudKitErrorAs(error, retryAttempt: retryCount) {
case .Retry(let timeToWait):
Async.background(after: timeToWait) {
self.initializeCommunication(retryCount: retryCount + 1)
}
case .Fail:
if error.code == .limitExceeded {
//TODO: try again with a smaller load?
}
Helper.showError("Could not load messages: \(error.localizedDescription)")
default: // For here there is no need to handle the .Success, and .RecoverableError
break
}
})
}
EVCloudKitDao is available under the MIT license. See the LICENSE file for more info.
Also see my other open source iOS libraries: