© Bruce Wade 2016

Bruce Wade, OS X App Development with CloudKit and Swift, 10.1007/978-1-4842-1880-8_6

6. Refining Our Prototype

Bruce Wade

(1)Suite No. 1408, North Vancouver, British Columbia, Canada

In this chapter we will load the test data we created in CloudKit into our app. The app will show the list of parks, and when a user clicks on a park the app will display the park information inside the details plane.

Creating the Park Model

With CloudKit being only a transport layer, we need a way to store and work with our information on the OS X client app. We need to create a simple Swift class Park that will store the information for an individual park. We will use the Swift class in a Swift list to store a list of parks. Create a new Swift file, name it Park, and update its contents as shown in Listing 6-1.

Listing 6-1. Park Model Class
import Cocoa
import CloudKit


class Park: NSObject {
    var recordID: CKRecordID
    var name: String
    var overview: String
    var location: String
    var isFenced: Bool
    var hasFreshWater: Bool
    var allowsOffleash: Bool
    var thumbnail: NSImage?


    init(recordID: CKRecordID, name: String, overview: String, location: String, isFenced: Bool, hasFreshWater: Bool, allowsOffleash: Bool, thumbnailUrl: NSURL?) {

self.recordID = recordID
        self.name = name
        self.overview = overview
        self.location = location
        self.isFenced = isFenced
        self.hasFreshWater = hasFreshWater
        self.allowsOffleash = allowsOffleash


        if thumbnailUrl == nil {
            self.thumbnail = NSImage(named: "DefaultParkIcon")!
        } else {
            // TODO: Download image
        }


    }
}

We created a class Park that inherits from NSObject and contains the same properties we defined in CloudKit for our Parks record type, with the exception of the thumbnail, which is an NSImage type instead of an Asset type. For the thumbnail, we are going to include a default image sized to 48 x 48 pixels that will be used when a park does not have a thumbnail image provided. Everything else in this class is straightforward properties and an initializer. We are using a CKRecordID to enable us to query for the park images and also to make any updates we need to a park in a later chapter.

CloudKit API

Ideally, when working with a backend web service such as CloudKit you want to abstract it away from the client, which will enable you to completely replace the backend servicesay, from CloudKit to a custom Django API . It also enables you to use both CloudKit and an additional API without having to update any other part of the app.

Create a new Swift file named API; all of your CloudKit interaction will take place inside this file. Add imports for CloudKit and Cocoa to the top of the file. Then create a class called API that has no parent. Next, create two properties, one a constant to store the publicDB and the other an array to store our parks.

let publicDB = CKContainer.defaultContainer().publicCloudDatabase

var parks: [Park] = []

Now create a function to get a list of parks from CloudKit. Enter the code from Listing 6-2 inside your API class.

Listing 6-2. Fetching the List of Parks
func fetchParks(completionHandler: [Park] -> Void) {
   let parksPredicate = NSPredicate(value: true)
   let query = CKQuery(recordType: "Parks", predicate: parksPredicate)


   publicDB.performQuery(query, inZoneWithID: nil) { [unowned self] (results, error) -> Void in
      if error != nil {
         print("Error: (error)")
      } else {
         for result in results! {
            let park = Park(
               recordID: result["recordID"] as! CKRecordID,
               name: result["name"] as! String,
               overview: result["overview"] as! String,
               location: result["location"] as! String,
               isFenced: result["isFenced"] as! Bool,
               hasFreshWater: result["hasFreshWater"] as! Bool,
               allowsOffleash: result["allowsOffleash"] as! Bool,
               thumbnailUrl: nil
            )
            self.parks.append(park)
         }
         completionHandler(self.parks)
      }
   }
}

Our function takes in a closure that accepts an array of Park objects and returns nothing. We are using this completionHandler to pass back the list of parks to our calling code. We need this, because a query to CloudKit is asynchronous and so we don’t know when we will get results returned.

We are using a predicate with a value of true, which simply means it will return all the results. This predicate could be updated to instead perform a geolocation search. Next, create a CKQuery telling it you want to query the Parks record type using the predicate just created. Finally, use the publicDB to perform the query, using the query you just created as a parameter. Since we are using the Default Zone, pass nil as the inZoneWithID parameter. This query will return us a closure that contains either a list of parks or an error. We are also using [unowned self] to ensure there are no circular references when we use the Parks array.

You should always check for errors by testing any error that occurs for a non-nil value. Here you can use the errors info dictionary to determine what error has occurred. If there are no errors you can loop over the results, creating a new Park object for each object returned. You create a new Park instance with the values that were returned from CloudKit, and then you append the new park to the Parks array. Finally, when the for in loop is finished, call the completionHandler passing in our Parks array. We have left the thumbnail as nil for the moment, as it requires additional code changes that we will address later.

That’s it for the API for now. We will be updating it with more methods later.

Populating ParkListViewController

Let’s move over to our ParkListViewControllerand put our API to the test. We need to create two new properties, one a constant for the API and the other a variable for a dynamic array of Park objects.

let api = API()
dynamic var parks: [Park] = []

We initialize the API class so we can use it right away in code. It is best practice to have your API classes as singletons, so we will update our API class in a little while.

At the end of the viewDidLoad function, we add the call in Listing 6-3 to fetchParks.

Listing 6-3. API Call to Get the List of Parks
api.fetchParks { [unowned self] (parks) -> Void in
   dispatch_async(dispatch_get_main_queue()) {
      self.parks = parks
   }
}

We have to use dispatch_async to update our local parks variable on the main queue because we are going to use data bindings to populate our interface.

Setting Up Bindings

We are now going to set up bindings that will use our Parks array to populate the Table View with parks pulled from CloudKit.

Open Main.storyboard, then select the Table View inside the Document Outliner. Then, open the Bindings Inspector, and under Table Content check the “Bind to” checkbox and ensure “Park List View Controller” is selected. Finally, set the Model Key Path to self.parks (Figure 6-1).

A385012_1_En_6_Fig1_HTML.jpg
Figure 6-1. Selected Table View

In the Document Outliner, select Park Name under Table Cell View. Then, in the Bindings Inspector under Value, check “Bind to” and select “Table Cell View.” Now set the Model Key Path field to objectValue.name (Figure 6-2).

A385012_1_En_6_Fig2_HTML.jpg
Figure 6-2. Park name selection and data binding

In the Document Outliner, select Image View under Table Cell View, then in the Bindings Inspector under Value check “Bind to” and select “Table Cell View.” Now set the Model Key Path to objectValue.thumbnail (Figure 6-3).

A385012_1_En_6_Fig3_HTML.jpg
Figure 6-3. Image View selected and data binding

In the Document Outliner, select Park Location under Table Cell View, then in the Bindings Inspector under Value check “Bind to” and select “Table Cell View.” Now set the Model Key Path to objectValue.location (Figure 6-4).

A385012_1_En_6_Fig4_HTML.jpg
Figure 6-4. Park Location selected and data bindings

If you run the app now you should see the parks you created in the CloudKit Dashboard. The images you added still will not be displayed, and we will correct that next. Your app should now look like Figure 6-5.

A385012_1_En_6_Fig5_HTML.jpg
Figure 6-5. Running app with populated parks from CloudKit

Downloading the Thumbnail Asset

Let’s update our code so as to download the thumbnail assets from CloudKit. Inside the API Swift file, at the beginning of the for loop, add the code in Listing 6-4.

Listing 6-4. Get the Image fileURL from a CKAsset
var thumbnailUrl: NSURL? = nil
if let thumbnail = result["thumbnail"] as? CKAsset {
   thumbnailUrl = thumbnail.fileURL
}

This code first creates an optional nil variable to hold the URL of the thumbnail. If there is no thumbnail asset for the target park, the default image will be used. We next check for a thumbnail asset. If one exists we get the fileURL. Next, we need to replace nil with the thumbnail when creating the park, so we use the new thumbnailUrl variable. Listing 6-5 shows the updated for in loop:

Listing 6-5. Building the Park Objects from the Data Returned from CloudKit
for result in results! {
   var thumbnailUrl: NSURL? = nil
   if let thumbnail = result["thumbnail"] as? CKAsset {
      thumbnailUrl = thumbnail.fileURL
   }
   let park = Park(
      recordID: result["recordID"] as! CKRecordID,
      name: result["name"] as! String,
      overview: result["overview"] as! String,
      location: result["location"] as! String,
      isFenced: result["isFenced"] as! Bool,
      hasFreshWater: result["hasFreshWater"] as! Bool,
      allowsOffleash: result["allowsOffleash"] as! Bool,
      thumbnailUrl: thumbnailUrl
    )
    self.parks.append(park)
}

Now let’s open the Park Swift file and replace the TODO comment with code to either download the thumbnail image if there is one or assign the default image if not. Replace the TODO comment with Listing 6-6.

Listing 6-6. Set the Thumbnail on a Background Queue
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
   if let imageData = NSData(contentsOfFile: (thumbnailUrl?.path)!) {
     self.thumbnail = NSImage(data: imageData)!
   }
}

Here, we are downloading the image to the background queue if we have a valid thumbnail URL. We use the path to create an NSData object, which we then use to set/download our thumbnail for the park.

Handling Selecting a Park in the List

Let’s now add an @IBAction that will be used to update the detail pane with information about the selected park.

Open ParkListViewController and add the function in Listing 6-7 after the awakeFromNib function.

Listing 6-7. Stub Action for Selecting a Park
@IBAction func selectPark(sender: AnyObject) {
   print("Selected park")
}

Now let’s open Main.storyboard to connect the action. In the Document Outliner click the Table View within the Park List View Controller, and in the popup choose selectPark. Run the App; you should see “Selected Park” printed in the console when you click on a park (Figure 6-6).

A385012_1_En_6_Fig6_HTML.jpg
Figure 6-6. Connecting the “select park” action

In order to pass the selected park to the detail view controller, we are going to need to create a custom split view controller class, as well as a protocol that will be used by our custom controller to pass the park to our detail view controller.

Insert the protocol in Listing 6-8 at the top of the ParkListViewController file.

Listing 6-8. Protocol for Handling Selected Parks
protocol ParkListViewControllerDelegate: class {
    func parkListViewController(viewController: ParkListViewController, selectedPark: Park?) -> Void
}

Now create a new weak variable in the ParkListViewController for our delegate. We will use this delegate inside the selectPark action.

weak var delegate: ParkListViewControllerDelegate? = nil

Create a new Cocoa class with a subclass of NSSplitViewController and a name of MainSplitViewController (Figure 6-7).

A385012_1_En_6_Fig7_HTML.jpg
Figure 6-7. Creating the MainSplitViewController class

We are going to create two computed properties that will give us easy access to each of our view controllers, and also we will set our delegate property we just created to our MainSplitViewController instance. Finally, we will implement our custom protocol and load the new park into the detail controller. (At this point there will be an error, but we will fix that next.)

Update the MainSplitViewController contents to match Listing 6-9.

Listing 6-9. Initial MainSplitViewController Class
import Cocoa

class MainSplitViewController: NSSplitViewController {

    var masterViewController: ParkListViewController {
        let masterItem = splitViewItems[0]
        return masterItem.viewController as! ParkListViewController
    }


    var detailViewController: DetailViewController {
        let masterItem = splitViewItems[1]
        return masterItem.viewController as! DetailViewController
    }


    override func viewDidLoad() {
        super.viewDidLoad()


        masterViewController.delegate = self
    }
}


extension MainSplitViewController: ParkListViewControllerDelegate {

    func parkListViewController(viewController: ParkListViewController, selectedPark: Park?) {
        detailViewController.loadPark(selectedPark)
    }
}

Before we fix the error, let’s open the Main.storyboard and set the custom class for the SplitViewController to MainSplitViewController, as shown in Figure 6-8.

A385012_1_En_6_Fig8_HTML.jpg
Figure 6-8. Setting up the split view controller to use our custom class
Let’s fix the error in our  MainSplitViewController. Open the  DetailViewController and add the following function to the  DetailViewController class:

func loadPark(park: Park?) {
  print(park?.name)
}

In the ParkListViewController we are going to get the selected park and pass it through to our delegate, which in turn will pass it to our detail view controller.

Update the selectPark function to that seen in Listing 6-10.

Listing 6-10. Completed Action for Selecting a Park That Calls Our Delegate Method
@IBAction func selectPark(sender: AnyObject) {
   let selectedPark = parks[sender.selectedRow]
   delegate?.parkListViewController(self, selectedPark: selectedPark)
}

First, we get the selectedRow from the sender and use that as an index into our Parks array to get the correct park. Then, we pass the selected park to our delegate method, which is set to our MainSplitViewController.

If you run the app now and select a row, you will see that the park’s name is printed in the console.

Update DetailViewController

Now that we are passing the selected park to the detail view controller, it is time to populate our interface with the park’s information.

First, let’s create a private dynamic park variable and then update the loadPark function to update that variable. See Listing 6-11.

Listing 6-11. Assign the Selected Park to the Detail View Controller
private dynamic var park: Park? = nil
func loadPark(park: Park?) {
  self.park = park
}

Now we are going to use bindings to update the UI whenever the selected park changes. Select the Park Name textfield, and in the Bindings Inspector under Value, check “Bind to” and select “Detail View Controller,” then set the Model Key Path to self.park.name (Figure 6-9).

A385012_1_En_6_Fig9_HTML.jpg
Figure 6-9. Data-binding setup for the park name

Repeat the same process for the Park Location field, changing the Model Key Path to self.park.location.

For the park overview, use the Document Outliner to select the Text View, and then set the binding for the Attributed String under Value to bind to “Detail View Controller,” with the Model Key Path set to self.park.overview (Figure 6-10).

A385012_1_En_6_Fig10_HTML.jpg
Figure 6-10. Data binding for the park overview Text View

Downloading Park Images for the Selected Park

Our final tasks for this chapter are downloading any park images related to this park and then adding them to the parkImagesCollection of the specific park selected by the user.

First, we need to create a new class that will store our image data. Create a new Swift file called ParkImage and update its contents with the code in Listing 6-12.

Listing 6-12. Code for the ParkImage Class
import Cocoa
import CloudKit


class ParkImage: NSObject {
    var recordID: CKRecordID
    var thumbnail: NSImage?
    var imageURL: NSURL?


 var image: NSImage?

    init(recordID: CKRecordID, thumbnailUrl: NSURL?, imageURL: NSURL?) {
        self.recordID = recordID
        self.imageURL = imageURL


        super.init()

        if thumbnailUrl != nil {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
                if let imageData = NSData(contentsOfFile: (thumbnailUrl?.path)!) {
                    self.thumbnail = NSImage(data: imageData)!
                }
            }
        }


    }
}

We are importing CloudKit only to use CKRecordID. I’m sure there are other ways to do this without relying on CKRecordID; however, we will just keep it simple. The fields are the same as we have in our ParkImages record type, with the addition of imageURL. There is no need to create the actual full-size image at this point, so we are just storing the path. We do, however, load the thumbnail if there is one inside the init method.

Next, let’s update our API to handle fetching park images related to the selected park. Open API.swift and add the function from Listing 6-13.

Listing 6-13. Method to Select All the Related Park Images
func fetchParkImages(parkRecordID: CKRecordID, completionHandler: [ParkImage] -> Void) {
   let reference = CKReference(recordID: parkRecordID, action: CKReferenceAction.DeleteSelf)
   let pred = NSPredicate(format: "park == %@", reference)
   let sort = NSSortDescriptor(key: "creationDate", ascending: true)
   let query = CKQuery(recordType: "ParkImages", predicate: pred)
   query.sortDescriptors = [sort]


   parkImages = []

   publicDB.performQuery(query, inZoneWithID: nil) { [unowned self] (results, error) -> Void in
     if error != nil {
        print(error!.localizedDescription)
     } else {
        for result in results! {
           var thumbnailUrl: NSURL? = nil
           if let thumbnail = result["thumbnail"] as? CKAsset {
              thumbnailUrl = thumbnail.fileURL
           }
           var imageUrl: NSURL? = nil
           if let image = result["image"] as? CKAsset {
              imageUrl = image.fileURL
           }
           let parkImage = ParkImage(
               recordID: result["recordID"] as! CKRecordID,


  thumbnailUrl: thumbnailUrl,
               imageURL: imageUrl
           )
           self.parkImages.append(parkImage)
           }
           completionHandler(self.parkImages)
     }
  }    
}

This method takes in a park record ID, which it uses to create a reference that we will utilize to find all park images in the app related to this park. It also takes in a completionHandler that we use to pass back all the park images returned by the reference.

The first line creates a CKReference using the parkRecordID and tells it to delete the image if the parent park is deleted. This is only really important if we are creating a new park image; however, we are not at this point and are only using the reference to query.

Next, we create an NSPredicate that finds all the park images with a park reference that matches the one we just created. In other words, it says “find all park images that belong to this park.”

Then, we create an NSSortDescriptor to order our images into ascending order based on the creationDate field. CreationDate is a field that is automatically created by CloudKit.

The next line ensures that our local parkImages array is empty. If we don’t do this, every call to this API function will just keep appending new images for each park we click on, which isn’t what we want.

We now have the information we require to create a CKQuery. We tell CKQuery we want to search the ParkImages record type using the NSPredicate we just created. Then, we assign our sort descriptor to the query.

Finally, we use the public database to perform the query, passing in the query we just created and a nil zone ID, as we are not using zones.

If there are no errors, we loop through the results, getting the image URLs as we did in the other API function. Then, we create a park image and add it to our local parkImages array. Once we are finished with the loop, we pass in the array of images to the completionHandler.

Now we need to update our detail view controller. First, create a new IBOutlet for an NSArrayController. We will use this to work with the content of our parkImagesCollection we created earlier:

@IBOutlet var imagesArrayController: NSArrayController!

Next, we need to create a constant for our API, as well as another private dynamic variable to store our park images:

let api = API()
private dynamic var parkImages: [ParkImage] = []

Now we need to update our loadPark function to handle fetching and updating the imagesArrayController, which in turn will update the parkImagesCollection. Update the loadPark function to look like Listing 6-14.

Listing 6-14. Updated Load Park Method to Handle Fetching Park Images
func loadPark(park: Park?) {
   self.park = park


   parkImages = []

   if self.park != nil {
      api.fetchParkImages((self.park?.recordID)!, completionHandler: { [unowned self] (parkImages) -> Void in
         if parkImages.count > 0 {
            dispatch_async(dispatch_get_main_queue()) {
                        self.imagesArrayController.addObjects(parkImages)


            }
         }
      })
  }
}

In the first new line we ensure the reset of the parkImages array. Next, we make sure there is an existing park. Then, we call our new API method, passing in our park recordID. Finally, we check to make sure there were some images returned, and if there were we call addObjects on the array controller. This method allows us to add multiple images at once instead of looping over all the returned images and adding them one by one.

Lastly, we must update our storyboard to connect everything together. Open the Main.storyboard file. In the Object Library find the array Controller. We need to drag this onto the detail view controller. Figure 6-11 shows the array controller in object list. The top of the detail view controller shows an array controller has been attached.

A385012_1_En_6_Fig11_HTML.jpg
Figure 6-11. Array controller attached in object list

In the Document Outliner, press Control and drag from the Detail View Controller down to the Array Controller, and in the Outlets popup select the option for imagesArrayController (Figure 6-12).

A385012_1_En_6_Fig12_HTML.jpg
Figure 6-12. Connecting the imagesArrayController outlet to the new array controller

Still in the Document Outliner, find the Park Images Collection. In the Bindings Inspector under Content, check “Bind to” and select “Images Array Controller,” with the Controller Key set to arrangedObjects (Figure 6-13). If you forget this binding, the array controller and the collection will not be connected, meaning no images will show up.

A385012_1_En_6_Fig13_HTML.jpg
Figure 6-13. Park images collection binding to the images array controller

In the Collection View item, select the Image View, and under Value check “Bind to” and select “Collection View Item.” Set Model Key Path to representedObject.thumbnail.

Finally, select the Images Array Controller and update its binding for the controller content to bind to the detail view controller with a model key path of self.parkImages.

Now if you run the app and select a park with images, everything should work as expected (Figure 6-14).

A385012_1_En_6_Fig14_HTML.jpg
Figure 6-14. Running app with a selected park and its data displayed in the detail view controller

Conclusion

This chapter covered taking our app, which had no data, and having it load data from CloudKit. We covered how to load all parks or a selected park, and how to then query and load references for a selected park. In the next chapter we will cover how to update the data we pulled from CloudKit from inside our app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset