Continuum is a simple photo sharing service. Students will bring in many concepts that they have learned, and add more complex data modeling, Image Picker, CloudKit, and protocol-oriented programming to make a Capstone Level project spanning multiple days and concepts.
Most concepts will be covered during class, others are introduced during the project. Not every instruction will outline each line of code to write, but lead the student to the solution.
Students who complete this project independently are able to:
- follow a project planning framework to build a development plan
- follow a project planning framework to prioritize and manage project progress
- implement basic data model
- use staged data to prototype features
- implement search using the system search controller
- use the image picker controller and activity controller
- use container views to abstract shared functionality into a single view controller
- Check CloudKit availability
- Save data to CloudKit
- Fetch data from CloudKit
- Query data from CloudKit
- use subscriptions to generate push notifications
- use push notifications to run a push based sync engine
If you were in an interview and a developer asked you why you chose to use CloudKit, what would your answer be? "Because my mentors taught me", would be a lazy answer. Be confident with your decision to show you know what you're talking about. My reasons below would be as follows.
- CloudKit is native to Xcode and iOS development. You don't have to download anything. It forces you to become a better Apple programmer by following their conventions and design principles.
- Its free!
- It provides free iCloud user authentication.
- Apple-level privacy protection.
- It comes with a wealth of good resources, from Apple Programming Guides, WWDC videos on cloudKit best practices, and documentation.
All of these apps use cloudKit. Millions of users make use of these app every day. Beyond this, the concepts you'll learn when working with CloudKit will apply to almost any backend you use throughout your career.
- follow a project planning framework to build a development plan
- follow a project planning framework to prioritize and manage project progress
- implement basic data model
- use staged data to prototype features
Follow the development plan included with the project to build out the basic view hierarchy, basic implementation of local model objects, model object controllers, and helper classes. Build staged data to lay a strong foundation for the rest of the app.
- Fork and Clone the starter project from the Devmountain Github Project
- Create your own new local “starter” branch and pull the remote starter branch
git checkout -b startergit pull origin starter - Branch from starter to your own local develop branch you can begin to code on
git checkout -b develop
Implement the view hierarchy in Storyboards. The app will have a tab bar controller as the initial controller. The tab bar controller will have two tabs.
The first is a navigation controller that has a PostListTableViewController that will display the list of posts, and will also use a UISearchBar to display search results. The PostListTableViewController will display a list of Post objects and segue to a Post detail view.
The second tab is a separate navigation controller that will hold a view controller to add new posts.
- Add a
UITabBarControlleras the initial viewController of the storyboard. Delete both of the view controller that are attached to the Tab Bar Controller when you drag it out. - Add a
UITableViewControllerTimeline scene, embed it in aUINavigationController, Make the navigation controller your first tab in the tab bar controller. (hint: control + drag from the tab bar controller to the navigation controller and select "view controllers" under the "Relationship Segue" section in the contextual menu) - Make the
UITableViewControllerfrom step 1 aPostListTableViewControllerCocoa Touch file subclass ofUITableViewControllerand assign the subclass to the storyboard scene - Add a
UITableViewControllerPost Detail scene, add a segue to it from the prototype cell ofPostListTableViewControllerscene - Create a
PostDetailTableViewControllerCocoa Touch subclass ofUITableViewControllerand assign it to the Post Detail scene - Add a
UITableViewControllerAdd Post scene, embed it into aUINavigationController. Make this navigation controller your second tab in the tab bar controller. - Add a
AddPostTableViewControllerCocoa Touch subclass ofUITableViewControllerand assign it to the Add Post scene
Your Storyboard should be simple skeleton resembling the set up below:

Continuum will use a simple, non-persistent data model to locally represent data stored on CloudKit.
Start by creating model objects. You will want to save Post objects that hold the image data, and Comment objects that hold text. A Post should own an array of Comment objects.
Create a Post model object that will hold image data and comments.
- Add a new
Postclass to your project. - Add a
photoDataproperty of typeData?, atimestampDateproperty, acaptionof typeString, and acommentsproperty of type[Comment]. You will get a “undeclared type” error as we have not cerated theCommentmodel object. Ignore this for now - Add a computed property,
photowith a getter which returns aUIImageinitialized using the data inphotoDataand a setter which adjusts the value of thephotoDataproperty to match that of thenewValuefor UIImage. Notice, the initalizer forUIImage(data: )is failable and will return an optional UIImage and thatnewValue.jpegData(compressionQuality: )optional data. You will need to handle these optionals by makingphotoDataandphotooptional properties.
Computed Photo Property
var photo: UIImage?{
get{
guard let photoData = photoData else {return nil}
return UIImage(data: photoData)
}
set{
photoData = newValue?.jpegData(compressionQuality: 0.5)
}
}- Add an initializer that accepts a photo, caption, timestamp, and comments array. Provide a default values for the
timestampargument equal to the current date i.e.Date()and a default value for thecommentsof an empty array. Becausephotois a computed property, you may get this error:
You can solve this by moving the initialization of photoi.e.self.photo = phototo the last line of the initalizer.
Create a Comment model object that will hold user-submitted text comments for a specific Post.
- Add a new
Commentclass to your project. - Add a
textproperty of typeString, atimestampDateproperty, and a weakpostproperty of typePost?.
- The comment objects reference to the post object should be weak in order to avoid retain cycles later on.
weak var post: Post?
- Add an initializer that accepts text, timestamp, and a post. Provide a default values for the
timestampargument equal to the current date, so it can be omitted if desired.
Add and implement the PostController class that will be used for CRUD operations.
- Add a new
PostControllerclass file. - Add a
sharedsingleton property. - Add a
postsproperty initialized as an empty array. - Add an
addCommentfunction that takes atextparameter as aString, aPostparameter, and a closure which takes in aCommentand returns Void.
- For now this function will only initialize a new comment and append it to the given post's comments array. The completion will be used when CloudKit is implemented
- Add a
createPostWithfunction that takes an image parameter as aUIImage, a caption as aString, and a closure which takes in aPost?and returnsVoid. - The function will need to initialize a post from the image and new comment and append the post to the
PostControllerspostsproperty (think source of truth). The completion handler will be utilized with CloudKit integration
Note: These CRUD functions will only work locally right now. We will integrate Cloudkit further along in the project
Implement the Post List Table View Controller. You will use a similar cell to display posts in multiple scenes in your application. Create a custom PostTableViewCell that can be reused in different scenes.
- Implement the scene in Interface Builder by creating a custom cell with an image view that fills most of the cell, a label for the posts caption, and another label for displaying the number of comments a post has. With the caption label selected, turn the number of lines down to 0 to enable this label to spread to the necessary number of text lines. Constrain the UI elements appropriately. Your
PostTableViewCellshould look similar to the one below.
- Create a
PostTableViewCellclass, subclass the table view cell from the previous step in your storyboard and add the appropriate IBOutlets. - In your
PostTableViewCelladd apostvariable, and implement anupdateViewsfunction to thePostTableViewCellto update the image view with the post’sphoto, and each of the labels with the relevant information from the post. Call the function in the didSet of thepostvariable - Keeping with the aesthetic of our favorite original photo sharing application, give the imageView an aspect ratio of 1:1. You will want to do this for all Post Image Views within the app to maintain consistency. Place a sample photo in your storyboard and explore the options of
Aspect Fill,Aspect FitandScale to Fill. The master project will be usingAspect FillwithClips to BoundsOn. - Implement the
UITableViewDataSourcefunctions for thePostListTableVIewController. Use the source of truth from thePostControllerto populate the tableView. - Implement the
prepare(for segue: ...)function to check the segue identifier, capture the detail view controller, index path, selected post, and assign the selected post to the detail view controller.- note: You will need to add an optional
postproperty to thePostDetailTableViewController.
- note: You will need to add an optional
Implement the Post Detail View Controller. This scene will be used for viewing post images and comments. Users will also have the option to add a comment, share the image, or follow the user that created the post.
Use the table view's header view to display the photo and a toolbar that allows the user to comment, share, or follow. Use the table view cells to display comments.
- Add a UIView to the header of the
PostDetailTableViewController - Add a vertical
UIStackViewto the Header of the table view. Add aUIImageViewand a horizontalUIStackViewto the stack view. Add 'Comment', 'Share', and 'Follow Post'UIButtonss to the horizontal stack view. Set the horizontal stack view to have a center alignment and Fill Equally distribution. Set the Vertical Stack View to have Fill alignment and Fill distribution. - Constrain the image view to an aspect ratio of 1:1
- Constrain the vertical Stack View to be centered horizontally and vertically in the header view and equal to 80% of the height of the header view (i.e. the users screen width).
- In
PostDetailTableViewController.swiftcreate an IBOutlet from the Image View namedphotoImageViewand connect IBActions from each button. - The cells of this tableView should support comments that span multiple lines without truncating them and a timestamp for each comment. Set the
UITableViewCellto the subtitle style. Set the number of lines for the cells title label to zero. - Add an
updateViewsfunction that will update the scene with the details of the post. Implement the function by setting thephotoImageView.imageand reloading the table view. Use adidSeton thepostvariable to callupdateViews. - Implement the
UITableViewDataSourcefunctions to populate the tableView with the post’s array of comments. - In the IBAction for the 'Comment' button. Implement the IBAction by presenting a
UIAlertControllerwith a text field, a Cancel action, and an 'OK' action. Implement the 'OK' action to initialize a newCommentvia thePostControllerand reload the table view to display it. Leave the completion closure in theaddCommentfunction blank for now.- note: Do not create a new
Commentif the user has not added text. Leave the Share and Follow button IBActions empty for now. You will fill in implementations later in the project.
- note: Do not create a new
Implement the Add Post Table View Controller. You will use a static table view to create a simple form for adding a new post. Use three sections for the form:
Section 1: Large button to select an image, and a UIImageView to display the selected image
Section 2: Caption text field
Section 3: Add Post button
Until you implement the UIImagePickerController, you will use a staged static image to add new posts.
- In the the attributes inspector of the
AddPostTableViewController, assign the table view to use static cells. Adopt the 'Grouped' cell style. Add three sections. - Build the first section by creating a tall image selection/preview cell. Add a 'Select Image'
UIButtonthat fills the cell. Add an emptyUIImageViewthat also fills the cell. Make sure that the button is on top of the image view so it can properly recognize tap events. - Build the second section by adding a
UITextFieldthat fills the cell. Assign placeholder text so the user recognizes what the text field is for. - Build the third section by adding a 'Add Post'
UIButtonthat fills the cell. - Add an IBAction and IBOutlet to the 'Select Image'
UIButtonthat assigns a static image to the image view (use the empty state space drawing in Assets.xcassets from the starter project for prototyping this feature), and removes the title text from the button.- note: It is important to remove the title text so that the user no longer sees that a button is there, but do not remove the entire button, that way the user can tap again to select a different image (i.e. do not hide the button).
- Add an IBAction to the 'Add Post'
UIButtonthat checks for animageandcaption. If there is animageand acaption, use thePostControllerto create a newPost. Guard against either the image or a caption is missing. Leave the completion closure in thecreatePostWithfunction empty for now. - After creating the post, you will want to navigate the user back to
PostListTableViewControllerof the application. You will need to edit the Selected View Controller for your apps tab bar controller. You can achieve this by setting theselectedIndexproperty on the tab bar controller.
self.tabBarController?.selectedIndex = 0
- Add a 'Cancel'
UIBarButtonItemas the left bar button item. Implement the IBAction to bring the user back to thePostListTableViewControllerusing the same line of code from the previous step. - Override
ViewDidDisappearto reset the Select Image Button's title back to "Select Image”, reset the imageView's image to nil, and remove the any text from the caption textField. - Navigate back to the
PostListTableViewController. OverrideviewWillAppearto reload the tableView.
Consider that this Photo Selection functionality could be useful in different views and in different applications. New developers will be tempted to copy and paste the functionality wherever it is needed. That amount of repetition should give you pause. Don't repeat yourself (DRY) is a shared value among skilled software developers.
Avoiding repetition is an important way to become a better developer and maintain sanity when building larger applications.
Imagine a scenario where you have three classes with similar functionality. Each time you fix a bug or add a feature to any of those classes, you must go and repeat that in all three places. This commonly leads to differences, which leads to bugs.
You will refactor the Photo Selection functionality (selecting and assigning an image) into a reusable child view controller in Part 2.
At this point you should be able view added post images in the Timeline Post List scene, add new Post objects from the Add Post Scene, add new Comment objects from the Post Detail Scene. Your app will not persist or share data yet.
Use the app and polish any rough edges. Check table view cell selection. Check text fields. Check proper view hierarchy and navigation models. You’re app should look similar to the screenshots below:
- Use a UIAlertController to present an error message to the User if they do not insert a photo, or caption when trying to create a post.
- Refactor the code from the first black diamond to present a similar alert if a user tries to create a comment without any text. Do not repeat the code for creating a UIAlertController.
- implement search using UISearchBarDelegate
- use the image picker controller and activity controller
Add and implement search functionality to the search view. Implement the Image Picker Controller on the Add Post scene.
Build functionality that will allow the user to search for posts with comments that have specific text in them. For example, if a user creates a Post with a photo of a waterfall, and there are comments that mention the waterfall, the user should be able to search the Timeline view for the term 'water' and filter down to that post (and any others with water in the comments).
Add a SearchableRecord protocol that requires a matchesSearchTerm function. Update the Post and Comment objects to conform to the protocol.
- Add a new
SearchableRecord.swiftfile. - Define a
SearchableRecordprotocol with a requiredmatches(searchTerm: String)function that takes asearchTermparameter as aStringand returns aBool.
Consider how each model object will match to a specific search term. What searchable text is there on a Comment? What searchable text is there on a Post?
- Update the
Commentclass to conform to theSearchableRecordprotocol. Returntrueiftextcontains the search term, otherwise returnfalse. - Update the
Postclass to conform to theSearchableRecordprotocol. Returntrueif any of thePostcommentsor itscaptionmatch the search term , otherwise returnfalse.
You can use a Playground to test your SearchableRecord and matches(searchTerm: String) functionality and understand what you are implementing.
Use a UISearchbar to allow a user to search through different posts for the given search text. This will require the use of the of the SearchableRecord protocol and the each models implentation of the matches(searchTerm: String) function. The PostListTableViewController will need to conform to the UISearchBarDelegate and implement the appropriate delegate method.
- Add a
UISearchBarto the headerView of thePostListTableViewControllerscene in the main storyboard. Check theShows Cancel Buttonin the attributes inspector. Create an IBOutlet from the search bar to thePostListTableViewControllerclass. - Add a
resultsArrayproperty in thePostListTableViewControllerclass that contains an array ofPost - Add an
isSearchingproperty at the top of the class which stores aBoolvalue set tofalseby default - Created a computed property called
dataSourceas an array ofPostWhich will return theresultsArrayifisSearchingistrueand thePostController.shared.postsifisSearchingisfalse.
var dataSource: [SearchableRecord]
var dataSource: [SearchableRecord] {
return isSearching ? resultsArray : PostController.shared.posts
}- Refactor the
UITableViewDataSourcemethods to populate the tableView with the newdataSourceproperty. - In
ViewWillAppearset the results array equal to thePostController.shared.posts. - Adopt the UISearchBarDelegate protocol in an extension on
PostListTableViewController, and implement thesearchBar(_:textDidChange:)function. Within the function filterPostController.shared.postsusing thePostobject'smatches(searchTerm: String)function and setting theresultsArrayequal to the results of the filter. CalltableView.reloadData()at the end of this function. - Implement the
searchBarCancelButtonClicked(_ searchBar:)function, using it to set the results array equal toPostController.shared.poststhen reload the table view. You should also set the searchBar's text equal to an empty String and resign its first responder. This will return the feed back to its normal state of displaying all posts when the user cancels a search. - Implement the
searchBarTextDidBeginEditingand setisSearchingtotrue. - Implement the
searchBarTextDidEndEditingand setisSearchingtofalse. - In
ViewDidLoadset the Search Bar's delegate property equal toself
Add Several Posts with a variety of captions and comments. Test whether you can successfully search the posts using the search bar.
Implement the Image Picker Controller in place of the prototype functionality you built previously.
- In the
AddPostTableViewController, update the 'Select Image' IBAction to present anUIAlertControllerwith anactionSheetstyle which will allow the user to select from picking an image in their photo library or directly from their camera. - Implement
UIImagePickerControllerto access the phones photo library or camera. Check to make sure eachUIImagePickerController.SourceTypeis available, and for each that is add the appropriate action to theUIAlertControllerabove. - Implement the
UIImagePickerControllerDelegatefunction to capture the selected image and assign it to the image view. Please read through the documentation for UIImagePickerController and its delegate
- note: Be sure to add a
NSCameraUsageDescriptionandNSPhotoLibraryUsageDescriptionto your appsInfo.plist. These strings will be displayed in the Alert Controller apple presents to ask users for specific permissions.
You should now be able to select and initialize posts with the photos from your camera or photo library. You will need to test the camera feature on an actual iPhone as the simulator does not support a camera.
Refactor the photo selection functionality from the Add Post scene into a child view controller.
Child view controllers control views that are a subview of another view controller. It is a great way to encapsulate functionality into one class that can be reused in multiple places. This is a great tool for any time you want a similar view to be present in multiple places.
In this instance, you will put 'Select Photo' button, the image view, and the code that presents and handles the UIImagePickerController into a PhotoSelectorViewController class. You will also define a protocol for the PhotoSelectorViewController class to communicate with it's parent view controller.
Use a container view to embed a child view controller into the Add Post scene.
A Container View defines a region within a view controller's view subgraph that can include a child view controller. Create an embed segue from the container view to the child view controller in the storyboard.
- Open
Main.storyboardto your Add Post scene. - Add a new section to the static table view to build the Container View to embed the child view controller.
- Search for Container View in the Object Library and add it to the newly created table view cell.
- note: The Container View object will come with a view controller scene. You can use the included scene, or replace it with another scene. For now, use the included scene.
- Set up constraints so that the Container View fills the entire cell.
- Move or copy the Image View and 'Select Photo' button to the container view controller.
- Create a new
PhotoSelectorViewControllerfile as a subclass ofUIViewControllerand assign the class to the new embedded scene in Interface Builder. - Create the necessary IBOutlets and IBActions, and migrate your Photo Picker code from the Add Post view controller class. Delete the old code from the Add Post view controller class. Check for any broken or duplicate outlets in your Interface Builder scenes.
You now have a container view which can be referenced and reused throughout your app. In this version of the app, we will only use the scene once, but the principle remains.
Your child view controller needs a way to communicate events to it's parent view controller. This is most commonly done through delegation. Define a child view controller delegate, adopt it in the parent view controller, and set up the relationship via the embed segue.
- Define a new
PhotoSelectorViewControllerDelegateprotocol in thePhotoSelectorViewControllerfile with a requiredphotoSelectorViewControllerSelected(image: UIImage)function that takes aUIImageparameter to pass the image that was selected.- note: This function will tell the assigned delegate (the parent view controller, in this example) what image the user selected.
- Add a weak optional delegate property to the
PhotoSelectorViewController. - Call the delegate function in the
didFinishPickingMediaWithInfofunction, passing the selected media to the delegate. - Adopt the
PhotoSelectViewControllerDelegateprotocol in theAddPostTableViewController, implement thephotoSelectViewControllerSelectedImagefunction to capture a reference to the selected image.- note: In the
AddPostTableViewControllerscene, you will use that captured reference to create a new post.
- note: In the
Note the use of the delegate pattern. You have encapsulated the Photo Selection workflow in one class, but by implementing the delegate pattern, each parent view controller can implement its own response to when a photo was selected.
You have declared a protocol, adopted the protocol, but you now must assign the delegate property on the instance of the child view controller so that the PhotoSelectViewController can communicate with its parent view controller. This is done by using the embed segue, which is called when the Container View is initialized from the Storyboard, which occurs when the view loads.
- Assign a segue identifier to the embed segue in the Storyboard file
- Implement the
prepare(forSegue: ...)function in theAddPostTableViewControllerto check for the segue identifier, capture thedestinationViewControlleras aPhotoSelectorViewController, and assignselfas the child view controller's delegate.
Use the UIActivityController class to present a share sheet from the Post Detail view. Share the image and the text of the first comment.
- Add an IBAction from the Share button in your
PostDetailTableViewControllerif you have not already. - Initialize a
UIActivityViewControllerwith thePost's image and the caption as the shareable objects. - Present the
UIActivityViewController.
- Some apps will save photos taken or processed in their app in a custom Album in the user's Camera Roll. Add this feature to the
AddPostTableViewControllerso that when a user adds a photo to the app, it saves to a “Continuum” album in their photo library.
- Check CloudKit availability
- Save data to CloudKit
- Fetch data from CloudKit
Following some of the best practices in the CloudKit documentation, add CloudKit to your project as a backend syncing engine for posts and comments. Check for CloudKit availability, save new posts and comments to CloudKit, and fetch posts and comments from CloudKit.
When you finish this part, the app will support syncing photos, posts, and comments from the device to CloudKit, and pulling new photos, posts, and comments from CloudKit. You will implement push notifications, subscriptions, and basic automatic sync functionality in Part Four.
- Import CloudKit in the
Post.swiftfile - Add a recordID property to your
Postclass of typeCKRecord.ID. Update the Post initializer to take in aCKRecord.IDwith a default value ofCKRecord.ID(recordName: UUID().uuidString) - To save your photo to CloudKit, it must be stored as a
CKAsset.CKAssets must be initialized with a file path URL. In order to accomplish this, you need to create a temporary directory that copies the contents of thephotoData: Data?property to a file in a temporary directory and returns the URL to the file. This is going to be a 2 step process.
- 3.1. Save the image temporarily to disk
- 3.2. Create the CKAsset
var imageAsset: CKAsset?
var imageAsset: CKAsset? {
get {
let tempDirectory = NSTemporaryDirectory()
let tempDirecotryURL = URL(fileURLWithPath: tempDirectory)
let fileURL = tempDirecotryURL.appendingPathComponent(UUID().uuidString).appendingPathExtension("jpg")
do {
try photoData?.write(to: fileURL)
} catch let error {
print("Error writing to temp url \(error) \(error.localizedDescription)")
}
return CKAsset(fileURL: fileURL)
}
}The whole point of the above computed property is to read and write for our photo property. Look up CKAsset, it can only take a fileURL.
- We will need a way of converting local
Postobjects into a type which can be saved to CloudKit (i.e. CKRecords). To achieve this, we will extend CloudKit’sCKRecordclass and add a convenience initializer which takes in a singlePostinstance.- Initialize a
CKRecordwith recordType of “Post” and recordID of the post’s recordID property. - Set the values of the CKRecord with the post’s properties. CloudKit only supports saving Foundational Types (save dictionaries) and will not allow saving
UIImageorCommentinstances. We will therefore need to save aCKAssetinstead of an image. We will ignore comments for now, and come back to them using a process called back referencing. - Note: Setting the values of this glorified dictionary will require many hardcoded string which can lead to typo errors especially in larger projects. Consider creating a constants struct to hold each of these string values.
- Initialize a
CKRecord Extension
extension CKRecord {
convenience init?(post: Post) {
self.init(recordType: PostConstants.typeKey, recordID: post.recordID)
self.setValue(post.caption, forKey: PostConstants.captionKey)
self.setValue(post.timestamp, forKey: PostConstants.timestampKey)
self.setValue(post.imageAsset, forKey: PostConstants.photoKey)
}
}struct PostConstants {
static let typeKey = "Post"
static let captionKey = "caption"
static let timestampKey = "timestamp"
static let commentsKey = "comments"
static let photoKey = "photo"
}- Add a failable initializer to
Postwhich takes in a CKRecord.- Remember, a CKRecord is little more than a glorified dictionary. Pull all of the necessary values out of the CKRecord, casting and unwrapping them as necessary, then call the original designated initializer you edited in the previous step
- You will need to first get the CKAsset back from the CKRecord then use its
fileURLproperty to initializeData - Remember to initialize the posts recordID property with the ckRecord’s recordID.
- We will initialize comments with an empty array for now.
- Note: The strings you use to pull values out of the CKRecord will need to exactly match their respective strings in your CKRecord convenience initializer. If you failed to implement in constant struct in the previous step, please reconsider your decision, and use that same constant struct here
Make the same structural adjustments for you Comment model object to integrate it with CloudKit.
Extend CKRecord to add a convenience initializer which takes in a Comment. Write a failable initializer for your Comment which takes in a CKRecord.
- Add A CKRecord.ID property to the ‘Comment’ model.
- Adjust your designated initializer to take in a CKRecord.ID with a default initializer value of a new CKRecord.ID initialized with the name of a new, unique uuid.
CKRecord.ID(recordName: UUID().uuidString) - Extend CKRecord to add a convenience initializer which takes in a comment. Initialize a new CKRecord using a recordType of “Comment” and the comment object’s recordID. Set the values of the CKRecord to each of the comments properties.
- Note: You should follow the same pattern of using a constants struct for referencing hard coded strings for CloudKit keys.
You will likely run into some issues as you try to save the comment’s post property to CloudKit. You will not be able to set the value of a CKRecord as Post (CloudKit does not support saving custom types). Rather we will need to use a strategy called back referencing to coordinate the relationship between posts and comments. Rather than a comment, directly containing a post, a comment in CloudKit is going to maintain a reference to a Post. You can think about this distinction as the difference between one webpage containing the entire contents of another web page vs containing a url link to that other webpage. We will use the posts recordID property as our analogous “url” in this example.
CloudKit contains a special class for creating references like this called CKRecord.Reference. Please read through the documentation for CKRecord.Reference here . Using a CKRecord.Reference is preferable to just saving the post’s recordID as a string because CloudKit will then handle writing operations for us on the relationship. For example, if I delete a post, a CKRecord.Reference may allow CloudKit to automatically delete all of its associated comments.
- Add a computed property of types
CKRecord.Reference?to the comment class. This should return a newCKRecord.Referenceusing the comment’s post object
var postReference: CKRecord.Reference
var postReference: CKRecord.Reference? {
guard let post = post else { return nil }
return CKRecord.Reference(recordID: post.recordID, action: .deleteSelf)
}- Revisit your convenience initializer on
CKRecordwhich takes in a comment and add the postReference to the record being created. - Add a failable convenience initializer on the
Commentclass which takes in aCKRecordand aPost. Unwrap the necessary properties for a comment from the ckRecord and call the designated initializer we wrote earlier.
If the user isn't signed into their iCloud account, they will not be able to save or fetch data using CloudKit. Most of the features wouldn't fully work. If they are not signed in, we want to let the user know immediately. If they are signed into iCloud, we want the app to continue as usual. Take a moment and think about this, if the user is signed in 'do something' if the user isn't signed in 'do something else'. What would our function signature look like? You will want to do this when the app first launches.
- In the
AppDelegate, write a function to check the iCloud Account Status of a iPhone user.
- The
default()Singleton of theCKContainerclass has anaccountStatusfunction that can check the users status. There are 4 options, forCKAccountStatuswhich you can read about here.
-
In the completion of
CKContainer.default().accountStatus, write a switch statement based on the users status inside the closure where you can handle each case as necessary. You will need a@escapingcompletion closure to handle the events if the user is signed in or not. If the users account status is anything other than.availablewe’ll need to call the completion passing infalseand present an alert to notify the user that they are not signed in. Note: In this case you will not use the completion of this function for anything more than a print statement; however, it is good practice to include an escaping completion for any function which makes asynchronous calls in order to give yourself or other developer using your code the opportunity to run code when the call has completed.- Create an extension on UIView controller and add a function
presentSimpleAlertWith(title: String, message: String?). You'll call this function within yourcheckAccountStatus(completion: @escaping (Bool) -> Void)Based on the users status you'll provide the proper Error Message to inform the user. If you already completed the Black Diamond from Part 1 you will already have the code for this.
If you attempt to present an alert in this class, you'll notice an error. That's because
AppDelegateisn't a subclass ofUIViewControllernor should it be. We don't have access to anyUIViewControlleryet.- Access the
window.rootViewControllerproperty of your AppDelegate. This will give you access to your applications initial ViewController. In our case that will be the TabBarController. Since UITabBarController is a subclass of UIViewController, this object will have access to thepresentSimpleAlertWith(title: String, message: String?)function we wrote on the extension ofUIViewController.
- Create an extension on UIView controller and add a function
-
Call this newly minted function in the
application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Booldelegate method to check the user’s account status when the app is launched.
Take a moment and try this on your own if you get stuck here is the code for the AppDelegate and Extension of UIViewController.
I swear I spent 10 minutes on my own before clicking this button
import UIKit
import CloudKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
checkAccountStatus { (success) in
let fetchedUserStatment = success ? "Successfully retrieved a logged in user" : "Failed to retrieve a logged in user"
print(fetchedUserStatment)
}
return true
}
func checkAccountStatus(completion: **@escaping** (Bool) -> Void) {
CKContainer.default().accountStatus { (status, error) in
if let error = error {
print("Error checking accountStatus \(error) \(error.localizedDescription)")
completion(false); return
} else {
DispatchQueue.main.async {
let tabBarController = self.window?.rootViewController
let errrorText = "Sign into iCloud in Settings"
switch status {
case .available:
completion(true);
case .noAccount:
tabBarController?.presentSimpleAlertWith(title: errrorText, message: "No account found")
completion(false)
case .couldNotDetermine:
tabBarController?.presentSimpleAlertWith(title: errrorText, message: "There was an unknown error fetching your iCloud Account")
completion(false)
case .restricted:
tabBarController?.presentSimpleAlertWith(title: errrorText, message: "Your iCloud account is restricted")
completion(false)
}
}
}
}
}
}*(In a Separate File)*
```swift extension UIViewController { func presentSimpleAlertWith(title: String, message: String?) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let okayAction = UIAlertAction(title: "Okay", style: .cancel, handler: nil) alertController.addAction(okayAction) present(alertController, animated: true) } } ```Update the PostController to support pushing and pulling data from CloudKit.
In order to enable the sharing functionality of this application, we will need to save Post and Comment Records to CloudKit’s public database.
let publicDB = CKContainer.default().publicCloudDatabase
-
Update the
createPostfunction, using the convenience initializer we wrote onCKRecordto turn the post into a ckRecord. -
Use CloudKit’s
save(_:completionHandler:)function which you can read more about here. You will need to handle any errors and call the completion on yourcreatePostWith(photo:caption:completion:)function inside the completionHandler of the save function.
At this point you should be able to save a post record and see it in your CloudKit dashboard. You dashboard should look similar to this.

- Update the
addCommentToPostfunction to to create aCKRecordusing the convenience initializer which takes in a comment onCKRecord. - Again use CloudKit’s
save(_:completionHandler:)function to save the comment to the database. If you are wondering where the documentation for that function went, it’s still here (: Handle any error thrown in the save function completion and call your own completion accordingly.
At this point, each new Post or Comment should be pushed to CloudKit when new instances are created from the Add Post or Post Detail scenes.
Note: The safest practice for calling your own completions here is to unwrap the record passed back by CloudKit’s save completion, then initialize a Post or Comment respectively, and complete with that object.
There are a number of approaches you could take to fetching new records. For Continuum, we will simply be fetching (or re-fetching, after the initial fetch) all the posts at once. Note that while we are doing it in this project, it is not an optimal solution. We are doing it here so you can master the basics of CloudKit first.
- Add a
fetchPostsfunction that has a completion closure which takes in an array of optional[Post]?'s and returnsVoid. - Use the
publicDBproperty to perform a query. - We will need to make a
CKQueryand aNSPredicate. The predicate value will be set to true which means it will fetch every post. - Handle any errors that may have been passed back, unwrap the records, and
compactMapacross the array of records calling your failable initializerinit?(record: CKRecord)on each one. This will return a new array of posts fetched from our publicDB. - Don't forget to set your local array to the new array of posts. This is how the TVC will populate all our posts. And call completion.
We're going to create a function that will allow us to fetch all the comments for a specific post we give it.
- Add a
fetchComments(for post: Post, ...)function that has a completion closure which takes in an optional array of comments[Comment]?and returnsVoid. - Call your
publicDBto perform a query for all the comments for the given post.
- Because we don't want to fetch every comment ever created, we must use a different
NSPredicatethen the default one. Create a predicate that checks the value of the correct field that corresponds to the postCKReferenceon the Comment record against theCKReferenceyou created in the previous step.
- Add a second predicate to includes all of the commentID's that have NOT been fetched.
let postRefence = post.recordID
let predicate = NSPredicate(format: "%K == %@", CommentConstants.postReferenceKey, postRefence)
let commentIDs = post.comments.compactMap({$0.recordID})
let predicate2 = NSPredicate(format: "NOT(recordID IN %@)", commentIDs)
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, predicate2])
let query = CKQuery(recordType: "Comment", predicate: compoundPredicate) - In the completion closure of the perform(query) , follow the common pattern of checking for errors, making sure the records exist, then create an array of comments using the array of records.
- Append the contents of the newly created array of comments to the posts comments array.
- Call your completion and pass in the comments which were fetched.
- In the
PostListTableViewControlleradd a new function to request a full sync operation that takes in an optional completion closure. Implement the function by turning on the network activity indicator, calling thefetchPostsfunction on thePostController. Reload the tableView and turn the network activity indicator off in the completion. - Call the function in the
viewDidLoadto initiate a full sync when the user first opens the application.
You may have noticed that it takes a long time to fetch the results from CloudKit. Moreover, there is a major bug. Post objects, when they are initially fetched from CloudKit will display a comment count of 0 in the PostListTableViewController. In order to display these with our current app structure, we would need to fetch all of the post, then for each post, go fetch all of its comments. This is a heavy ask for CloudKit and could hang out UI fairly quickly. Meanwhile we don’t even need the data for those comments until a user clicks into the detail view for post. We will need to refactor our Post model to keep track of how many comments it has, and delay the fetching of comments until a user click on the detail page for a post.
- Add a commentCount variable to the
Postclass - Adjust the
Postinitializers and the convenience initializer on CKRecord which takes in a post. - Add functionality to the
PostController’saddCommentfunction to increment the post’s commentCount by 1. Make sure you update the value of this integer in CloudKit. You will need to use the CKModifyRecordsOperation class to do this. - Adjust the
PostTableViewCellto populate the comment count label with this new property.
Note: Changing our model in this way will make any old Post objects saved in the database incompatible with our new setup. Clear your database to avoid any stagnant old data.
- In
viewDidLoad()of thePostDetailTableViewControllercall your fetch comments function and reload the tableView
At this point the app should support basic push and fetch syncing from CloudKit. Use your Simulator and your Device to create new Post and Comment objects. Check for and fix any bugs.
When you tap on a post cell it should bring you to the detailVC. The comments that belong to that post should be fetched.
- Use subscriptions to generate push notifications
Implement Subscriptions and push notifications to create a simple automatic sync engine. Add support for subscribing to new Post records and for subscribing to new Comment records on followed Postss. Request permission for remote notifications. Respond to remote notifications by initializing the new Post or Comment with the new data.
When you finish this part, the app will support sending push notification when new records are created in CloudKit.
Update the PostController class to manage subscriptions for new posts and new comments on followed posts. Add functions for following and unfollowing individual posts.
When a user follows a Post, he or she will receive a push notification and automatic sync for new Comment records added to the followed Post.
Create and save a subscription for all new Post records.
- Add a function
subscribeToNewPoststhat takes an optional completion closure withBoolandError?parameters.- note: Use an identifier that describes that this subscription is for all posts.
- Initialize a new CKQuerySubscription for the
recordTypeof 'Post'. Pass in a predicate object with it value set totrue. - Save the subscription to the public database. Handle any error which may be passed out of the completion handler and complete with true or false based on whether or not an error occurred while saving.
- Call the
subscribeToNewPostsin the initializer for thePostControllerso that each user is subscribed to newPostrecords saved to CloudKit.
Create and save a subscription for all new Comment records that point to a given Post
- Add a function
addSubscriptionTo(commentsForPost post: ...)that takes aPostparameter and an optional completion closure which takes in aBoolandErrorparameters. - Initialize a new NSPredicate formatted to search for all post references equal to the
recordIDproperty on thePostparameter from the function. - Initialize a new
CKQuerySubscriptionwith a record type ofComment, the predicate from above, asubscriptionIDequal to the posts record name which can be accessed usingpost.recordID.recordName, with theoptionsset toCKQuerySubscription.Options.firesOnRecordCreation - Initialize a new
CKSubscription.NotificationInfowith an empty initializer. You can then set the properties ofalertBody,shouldSendContentAvailable, anddesiredKeys. Once you have adjusted these settings, set thenotificationInfoproperty on the instance ofCKQuerySubscriptionyou initialized above. - Save the subscription you initialized and modified in the public database. Check for an error in the ensuing completion handler.
- Please see the CloudKit Programming Guide and CKQuerySubscription Documentation for more detail.
The Post Detail scene allows users to follow and unfollow new Comments on a given Post. Add a function for removing a subscription, and another function that will toggle a subscription for a given Post.
-
Add a function
removeSubscriptionTo(commentsForPost post: ...)that takes aPostparameter and an optional completion closure withsuccessanderrorparameters. -
Implement the function by calling
delete(withSubscriptionID: ...)on the public data base. Handle the error which may be returned by the completion handler. If there is no error complete withtrue.- note: Use the unique identifier you used to save the subscription above. Most likely this will be your unique
recordNamefor thePost.
- note: Use the unique identifier you used to save the subscription above. Most likely this will be your unique
-
Add a function
checkSubscription(to post: ...)that takes aPostparameter and an optional completion closure with aBoolparameter. -
Implement the function by fetching the subscription by calling
fetch(withSubscriptionID: ...)passing in the uniquerecordNamefor thePost. Handle any errors which may be generated in the completion handler. If theCKSubscriptionis not equal to nil complete withtrue, else complete withfalse. -
Add a function
toggleSubscriptionTo(commentsForPost post: ...)that takes aPostparameter and an optional completion closure withBool, andErrorparameters. -
Implement the function by calling the
checkForSubscription(to post:...)function above. If a subscription does not exist, subscribe the user to comments for a given post by calling theaddSubscriptionTo(commentsForPost post: ...); if one does, cancel the subscription by callingremoveSubscriptionTo(commentsForPost post: ...).
Update the Post Detail scene's Follow Post button to display the correct text based on the current user's subscription. Update the IBAction to toggle subscriptions for new comments on a Post.
- Update the
updateViewsfunction to call thecheckSubscriptionTo(commentsForPost: ...)on thePostControllerand set appropriate text for the button based on the response. You will need to add an IBOutlet for the button if you have not already. - Implement the
Follow Postbutton's IBAction to call thetoggleSubscriptionTo(commentsForPost: ...)function on thePostControllerand update theFollow Postbutton's text based on the new subscription state.
Update the Info.plist to declare backgrounding support for responding to remote notifications. Request the user's permission to display remote notifications.
- Go to the Project File. In the "capabilities" tab, turn on Push Notifications and Background Modes. Under Background Modes, check Remote Notifications.
- Request the user's permission to display notifications in the
AppDelegatedidFinishLaunchingWithOptionsfunction.- note: Use the
requestAuthorizationfunction that is a part ofUNUserNotificationCenter.
- note: Use the
- Register the App to receive push notifications
application.registerForRemoteNotifications()



