© Bruce Wade 2016

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

7. Updating CloudKit Data from Our App

Bruce Wade

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

In the last chapter we got our app working with dynamic data pulled from the CloudKit servers. In this chapter we will update our app to allow us to change existing CloudKit data, create new data for our record types, and finally to delete data from CloudKit servers.

Updating Existing Data

Let’s take a look at how to update a park when it is changed within our app. First, we need to update our API with a helper method to prevent code duplication. This helper method’s job is to convert the park CKRecord into a Park object. Copy the code inside the fetchParks for-in statement up to but not including the line that adds the new park to the array. With that code, create the new function in Listing 7-1.

Listing 7-1. Code to Convert CKRecord to a Park Object
private func convertCKRecordToPark(parkRecord: CKRecord) -> Park {
   var thumbnailUrl: NSURL? = nil
   if let thumbnail = parkRecord["thumbnail"] as? CKAsset {
      thumbnailUrl = thumbnail.fileURL
   }
   let savedPark = Park(
      recordID: parkRecord["recordID"] as! CKRecordID,
      name: parkRecord["name"] as? String ?? "",
      overview: parkRecord["overview"] as? String ?? "",
      location: parkRecord["location"] as? String ?? "",
      isFenced: parkRecord["isFenced"] as? Bool ?? false,


hasFreshWater: parkRecord["hasFreshWater"] as? Bool ?? false,
      allowsOffleash: parkRecord["allowsOffleash"] as? Bool ?? false,
      thumbnailUrl: thumbnailUrl)
   return savedPark
}

Now replace the code you just copied with a call to the method in the fetchParks function (Listing 7-2).

Listing 7-2. Updated for-loop to Use Our New Convert Method
for result in results! {
   let park = self.convertCKRecordToPark(result)
   self.parks.append(park)
}

With that in place, let’s create a new function that will update our park. We are going to take advantage of the CloudKit convenience API, which requires us to first fetch our park record from the server, then update the fetched record, and finally save it back to the server. Add the following function to our API.swift file (Listing 7-3).

Listing 7-3. Updating and Saving the Park to CloudKit Using Our New Convert Method
func updatePark(park: Park, completionHandler: Park -> Void) {

   // Get any changes for the park from the server
   publicDB.fetchRecordWithID(park.recordID) { (parkRecord, error) -> Void in
      if error != nil {
         print(error!.localizedDescription)
      } else {
         // Currently just overriding as we are the only ones using this app.
         parkRecord!["name"] = park.name
         parkRecord!["location"] = park.location
         parkRecord!["overview"] = park.overview
         parkRecord!["isFenced"] = park.isFenced
         parkRecord!["hasFreshWater"] = park.hasFreshWater
         parkRecord!["allowsOffleash"] = park.allowsOffleash


         // Save update back to the server
         self.publicDB.saveRecord(parkRecord!, completionHandler: { (parkRecord, error) -> Void in
           if error != nil {
              print(error!.localizedDescription)
           } else {
                        completionHandler(self.convertCKRecordToPark(parkRecord!))
           }
         })
      }
   }     
}

This method first queries the CloudKit servers using the park recordID, then if there are no errors the returned record is updated with the new park data from our app, and finally we save the record back to the server. If there are no errors we pass the completionHandler a call to our helper method, which will convert the CKRecord into a Park object our app can use.

Now let’s open the DetailViewController and create a new IBAction that will save the record for us. There are also other ways to handle this that save the record whenever the bound park changes; however, that is out of the scope of this book.

Add an action to the bottom of the detail view controller (Listing 7-4).

Listing 7-4. Code That Uses the API to Save a Park
@IBAction func savePark(sender: AnyObject) {

   if let park = self.park {
      api.updatePark(park) { [unowned self] (updatedPark) -> Void in
         dispatch_async(dispatch_get_main_queue()) {
            self.park = updatedPark
         }
      }
   }   
}

Next, let’s open the storyboard, where we need to create a new button for saving the record and also connect it to our action. Drag a button next to the Delete Park button and set its title to Save Park. Pin it to the right and top. Finally, press Control and drag from the Save Park button to the detail view controller icon (blue with a white square); in the popup under Received Actions choose savePark (Figure 7-1).

A385012_1_En_7_Fig1_HTML.jpg
Figure 7-1. Save Park button connected to the savePark action

Now if you run the app you will see an error about write permissions. This is where our custom permissions we created earlier come into play. We could have allowed anyone to update records; however, we are going to limit it to people with the correct permissions. Go back to the CloudKit Dashboard and under Public Data chose User Records, then select your user record. Set the security roles to ParkManager and save the record. If you run the app again you should have no issues saving. It is important to note that if you update a text field you will have to click outside of it before clicking Save if you also want the park list to update. Make some changes and save the park, then stop the app and start it again to make sure your park is updated.

Creating New Data

We need to create two different types of objects in our app: parks and park images for each park. First, we need to update our API to handle creating new parks. Create a new function in your API.swift file (Listing 7-5).

Listing 7-5. New API Method to Handle Saving a New Park Record
func createPark(completionHandler: Park -> Void) {
   let newParkRecord = CKRecord(recordType: "Parks")
   newParkRecord["name"] = "New Park"


   publicDB.saveRecord(newParkRecord) { (newPark, error) -> Void in
     if error != nil {
        print(error!.localizedDescription)
     } else {
                completionHandler(self.convertCKRecordToPark(newPark!))
     }
   }
}

In this function we simply create a new CKRecord of type Parks, set the name property to New Park, then use the convenience API to save the record. If everything saves as expected, we call our completion handler and pass it the New Park object.

In ParkListViewController we need to make two updates. First, we need an IBOutlet for accessing our Table View, and second, we need a new IBAction for creating new parks. Add the IBOutlet under the newParkButton outlet.

@IBOutlet weak var tableView: NSTableView!

Next, at the end of the controller class, add a new action with the code in Listing 7-6.

Listing 7-6. New Action That Uses Our API to Save a Park
@IBAction func createPark(sender: AnyObject) {
    api.createPark { [unowned self] (newPark) -> Void in
       dispatch_async(dispatch_get_main_queue()) {
          self.parks.append(newPark)
                self.tableView.selectRowIndexes(NSIndexSet(index: self.parks.endIndex-1), byExtendingSelection: false)
                self.tableView.scrollRowToVisible(self.parks.endIndex-1)
          self.delegate?.parkListViewController(self, selectedPark: self.parks.last)
       }
   }
}

Inside this action we call our createPark API method, and then on the main queue we add the new park to our array. We next tell the Table View to select our new park and to delete anything that was already selected. Next, we tell the Table View to scroll down to our new park. Finally, we tell our detail view controller to use the data from our new park.

If you run the app you should now be able to create a new park. You can verify in the CloudKit Dashboard that the park is saved, and you can also update and save any changes.

Let’s move on to adding park images before we look at how to delete objects from CloudKit.

We are first going to create an extension for NSImage that will enable us to create thumbnails and also save them to a file. Create a new Swift file named NSImageExtension and replace its contents with the code in Listing 7-7.

Listing 7-7. Helper Code for Creating a Thumbnail
import Cocoa

extension NSImage {

    func makeThumbnail(width: CGFloat, _ height: CGFloat) -> NSImage {
        let thumbnail = NSImage(size: CGSizeMake(width, height))


        thumbnail.lockFocus()
        let context = NSGraphicsContext.currentContext()
        context?.imageInterpolation = .High
        self.drawInRect(NSMakeRect(0, 0, width, height), fromRect: NSMakeRect(0, 0, size.width, size.height), operation: .CompositeCopy, fraction: 1)
        thumbnail.unlockFocus()


        return thumbnail
    }


    func saveTo(filePath: String) {
        let bmpImageRep: NSBitmapImageRep = NSBitmapImageRep(data: TIFFRepresentation!)!
        addRepresentation(bmpImageRep)


        let data: NSData = bmpImageRep.representationUsingType(.NSJPEGFileType, properties: [:])!

        data.writeToFile(filePath, atomically: false)
    }
}

The first function, makeThumbnail, allows us to pass in a width and height that will be used to scale or crop an image to the specified size. The details of how exactly this function works are not in the scope of this book; just know that it returns a new thumbnail image that is the size you pass in using the data from the original image.

The second function, saveTo(filePath: String), takes in the complete file path of where we want to write the temporary thumbnail to on disk. We need to create an NSBitmapImageRep and add it to our current NSImage instance. We use this to create the NSData object that is finally used to write our file to disk.

With these helper methods in place, let’s move over to our API.swift file. We will first create a new function for converting ParkImages record types into ParkImage objects like we did for Parks. At the bottom of the class add the code from Listing 7-8.

Listing 7-8. Code to Convert a CKRecord into a ParkImage Object
private func convertCKRecordToParkImage(parkImageRecord: CKRecord) -> ParkImage {
   var thumbnailUrl: NSURL? = nil
   if let thumbnail = parkImageRecord["thumbnail"] as? CKAsset {
      thumbnailUrl = thumbnail.fileURL
   }
   var imageUrl: NSURL? = nil
   if let image = parkImageRecord["image"] as? CKAsset {
      imageUrl = image.fileURL
   }
   let parkImage = ParkImage(
      recordID: parkImageRecord["recordID"] as! CKRecordID,
      thumbnailUrl: thumbnailUrl,
      imageURL: imageUrl
   )
   return parkImage
}

There is nothing new here. This is the same code that was inside the for-in loop of the fetchParkImages API call. Replace the code in the for-in loop with the following:

self.parkImages.append(self.convertCKRecordToParkImage(result))

Now let’s create a new API method that will be used to save the park images that are added to a given park. This method is going to need to know what park these images belong to, so we will provide it the parkRecordID. It will also need a list of imageUrls, as well as a completionHandler where we can pass all the images that were uploaded successfully.

This method is more complicated than the rest we have seen because we need to use CKReferences, and we also need to make thumbnails from a file that has been selected so that we can upload both the thumbnail and the original image. Finally, we are using CKModifyRecordsOperation, which will enable us to save multiple records at once. We are doing this because we want to allow the user to select multiple records, or images, at once instead of a single file at a time. Create the new function with the code in Listing 7-9.

Listing 7-9. Saving Multiple Records to CloudKit at Once
func saveParkImages(parkRecordID: CKRecordID, imageUrls: [NSURL], completionHandler: [ParkImage] -> Void) {

   var tempThumbnailFiles = [String]()
   var imageRecordsToUpload = [CKRecord]()


   for imageUrl in imageUrls {
       // Get file name
       let originalFileName = imageUrl.URLByDeletingPathExtension!.lastPathComponent!
       let thumbnailFileName = "/tmp/(originalFileName)_90x90.jpg"
       tempThumbnailFiles.append(thumbnailFileName)


       // Create a thumbnail from the original image
       let originalImage = NSImage(contentsOfURL: imageUrl)
       let thumbnailImage = originalImage?.makeThumbnail(90, 90)


       // Save the thumbnail to a file
       thumbnailImage?.saveTo(thumbnailFileName)


       let record = CKRecord(recordType: "ParkImages")
       record["park"] = CKReference(recordID: parkRecordID, action: .DeleteSelf)
       record["image"] = CKAsset(fileURL: imageUrl)
       record["thumbnail"] = CKAsset(fileURL: NSURL(fileURLWithPath: thumbnailFileName))
       imageRecordsToUpload.append(record)
   }


   let uploadOperation = CKModifyRecordsOperation(recordsToSave: imageRecordsToUpload, recordIDsToDelete: nil)
   uploadOperation.atomic = false
   uploadOperation.database = publicDB


   uploadOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, deletedRecords: [CKRecordID]?, operationError: NSError?) -> Void in
     guard operationError == nil else {
        print(operationError!.localizedDescription)
        return
     }
     if let records = savedRecords {
        var imagesUploaded = [ParkImage]()
        for parkImageRecord in records {
            // Create a new ParkImage record and append it to array of imagesUploaded
                    imagesUploaded.append(self.convertCKRecordToParkImage(parkImageRecord))
        }


        // Now that we know our file was uploaded, delete temp local files.
        for tempThumbnailFile in tempThumbnailFiles {
            do {
               try NSFileManager.defaultManager().removeItemAtPath(tempThumbnailFile)
            } catch _ {
                 print("Couldn't delete file: (tempThumbnailFile)")
            }
        }


        completionHandler(imagesUploaded)
     }
   }


   NSOperationQueue().addOperation(uploadOperation)  
}

We are creating temporary thumbnail files, so we need to make sure we clean up after ourselves. Therefore, we create a variable, tempThumbnailFiles, to store the complete file paths as we create them. We are also going to be creating multiple CKRecords, which will be saved all together instead of one by one, so we create an imageRecordsToUpload variable to store the array of CKRecords.

Next, we loop over the list of imageUrls. First, we get the file name without the path extension from the imageUrl. We then use that name and add _90x90.jpg to it so it’s clear what image size we are creating, and then append it to the /tmp/ directory. We then append the full path to our thumbnailFiles array.

In the next step, we need to use the imageUrl to create an NSImage and then use that image to make a thumbnail. Finally, we save the thumbnail to the file path we just created.

In the final part of the loop, we create the CKRecord of ParkImages type. We use the parkRecordID we passed in to create a CKReference and set the action to .DeleteSelf, which means if the park is deleted this image and others linked to the park will be deleted as well. We use the imageUrl as a CKAsset for the full-size image, and the full path to the thumbnail we just created as the CKAsset for the thumbnail. We then add the record to our imageRecordsToUpload array that we will be using shortly.

Next, we create a CKModifyRecordsOperation and use it to pass our imageRecordsToUpload and nil as the records to delete. We tell the operation that we do not need every save to succeed. Finally, we tell the operation that we are using the public database; this is also the other reason atomic is set to false, as it is only supported in private record zones.

Now we must create a completion block that will be called when all the records have been saved. There is also another completion block that can be used for each record save; however, we are not using it here. This completion block is passed in the saved records, any deleted records, and any errors, if there were any.

We must first check if there were any errors, and if there were we print out the error and exit out of the completion block. If there were no errors we loop through the records returned and convert them to ParkImage objects, adding them to our imagesUploaded array.

Next, we will clean up all the temporary files we created when making the thumbnails. We do this using the NSFileManager and by looping through our tempThumbnailFiles array, deleting each file in the list.

At the end of the completion block we call our completionHandler, passing in our array of ParkImages.

Lastly, at the end of the function we add the operation we just created to the NSOperationQueue.

The last thing we need to do is create an action that will be used to allow users to upload images to the park pages. We will attach this action to our Add Image(s) button. We will be taking advantage of the standard NSOpenPanel to handle this for us. We tell the open panel to look for image files, that we can select multiple files (holding Shift or Command to select them), and that we can actually choose files. Finally, we call the beginWithCompletionHandler on the open panel to open the panel. This completion handler will provide us with a list of file URLs that we can pass to our API to upload. We also use our current park instance and pass the recordID to our API. See Listing 7-10.

Listing 7-10. Selecting Images from the File System and Using Our API to Save Them to CloudKit
@IBAction func addImages(sender: AnyObject) {
   let openPanel = NSOpenPanel()
   openPanel.allowsMultipleSelection = true
   openPanel.canChooseDirectories = false
   openPanel.canCreateDirectories = false
   openPanel.canChooseFiles = true
   openPanel.allowedFileTypes = NSImage.imageUnfilteredTypes()
   openPanel.beginWithCompletionHandler { (result) -> Void in


     if result == NSFileHandlingPanelOKButton {
       self.api.saveParkImages((self.park?.recordID)!, imageUrls: openPanel.URLs, completionHandler: { (parkImages) -> Void in
         dispatch_async(dispatch_get_main_queue()) {
                        self.imagesArrayController.addObjects(parkImages)
         }
       })
     }
  }
}

Now open the Main.storyboard and Control-drag from the Add Image(s) button to the detail view controller; in the popup select addImages. Run the app now and you should be able to add multiple images to a park. (Note there is a short delay, as currently we are uploading the images then downloading them again before we update the collection view).

Deleting a Park

In this section we are going to look at deleting an entire park, which will also delete any images related to it. Then we will look at how we can delete a park image(s).

Let’s update our API to support deleting parks. Open API.swift and add the code seen in Listing 7-11.

Listing 7-11. API Method to Handle Deleting a Park
func deletePark(parkRecordID: CKRecordID, completionHandler: NSError? -> Void) {
    publicDB.deleteRecordWithID(parkRecordID) { (deletedRecordID, error) -> Void in
      completionHandler(error)
    }
}

Here we use the deleteRecordWithID method from the CloudKit convenience API. We pass the record ID for the park and a completionHandler call into our deletePark method. We then pass the completionHandler the error, if there is one.

Now we need to make changes in a few different places. We will first update our ParkListViewControllerDelegate to the following. Notice we have removed the view controller parameter because we determined we don’t need it. Open ParkListViewController and update the delegate to look like Listing 7-12.

Listing 7-12. Updated Protocol to Handle Deleting Parks and Updating Park List
protocol ParkListViewControllerDelegate: class {
  func selectPark(selectedPark: Park?, index: Int) -> Void
  func deletePark(deletedParkIndex: Int) -> Void
}

Here we rename the original protocol method, remove the view controller parameter, and add a new index parameter. This will enable us to easily update the user interface after a park has been deleted. Then, we add a new protocol method for deleting a park where we pass in the current park’s method.

Next, update the selectPark action to look like Listing 7-13.

Listing 7-13. Show Selections of a Park Image
@IBAction func selectPark(sender: AnyObject) {
   let selectedRow = sender.selectedRow
   let selectedPark = parks[selectedRow]
   delegate?.selectPark(selectedPark, index: selectedRow)
}

Here we create a new constant in which to store the current selected row, update the delegate method call name, and pass in the new index parameter.

Finally, we also need to update the createPark action, and this time we only need to change the delegate call to the following:

self.delegate?.selectPark(self.parks.last, index: self.parks.endIndex-1)

Here we update the name and parameters for the index. We are just using the last index in the parks array, because we simply add new parks to the end of the array.

Now let’s update our MainSplitViewController to implement the current, updated protocol methods. Open the view controller and update the extension to look like Listing 7-14.

Listing 7-14. Implementation of Our Protocol Methods
extension MainSplitViewController: ParkListViewControllerDelegate {

    func selectPark(selectedPark: Park?, index: Int) {
        detailViewController.loadPark(selectedPark, index: index)
    }


    func deletePark(deletedParkIndex: Int) {
        masterViewController.deletePark(deletedParkIndex)
    }
}

Here we update the selectPark method to include the index when calling loadPark. This is important for when we need to delete the park and update the Table View of parks. Then we add the new protocol method. This time we are calling a method on the masterViewController that will delete the park from the Table View.

Finally, at the end of the viewDidLoad method, we set the detailViewController delegate to self:

detailViewController.delegate = self

At this point we will have some errors that we will be fixing as we go. Let’s move on to updating the detail view controller.

We need to add two new propertiesone for storing the park index and the other for storing the delegate (Listing 7-15).

Listing 7-15. Updated loadPark Method
var parkIndex: Int? = nil
weak var delegate: ParkListViewControllerDelegate? = nil

First we need to update the loadPark method with two minor changes. Add a new parameter for the index, and then set the local parkIndex to that value:

func loadPark(park: Park?, index: Int) {
        self.park = park
        self.parkIndex = index
. . . // Rest of the code left out
}

Now create a new IBAction called deletePark that will call our API, update the detail UI, and call our delegate to deletePark, which will remove the park from the list of parks. Create the action with the code in Listing 7-16.

Listing 7-16. New Action for Deleting a Park
@IBAction func deletePark(sender: AnyObject) {
   api.deletePark((park?.recordID)!) { [unowned self] (error) -> Void in
     if error != nil {
        print(error!.localizedDescription)
     } else {
       dispatch_async(dispatch_get_main_queue()) {
         self.parkImages.removeAll()
         self.park = nil
         self.delegate?.deletePark(self.parkIndex!)
         self.parkIndex = nil
       }
    }
  }
}

Here we call our API deletePark method, passing in the park record ID. If the delete was successful we will have emptied the parkI mages array, nil'ed out the park, called the delegate method to delete the park by passing in the park index, and set the parkIndex to nil. We do all this on the main thread. It is important to remember that deleting a Park record type will also delete all the Park Image records that have a reference to it.

Now let’s open the Main.storyboard to connect our new delete action. Control-drag from the Delete Park button to the detail view controllers icon, and from the popup select the deletePark action (Figure 7-2).

A385012_1_En_7_Fig2_HTML.jpg
Figure 7-2. Delete Park button connected to the deletePark action

Finally, let’s go back to our ParkListViewController and create the missing deletePark function with the code in Listing 7-17.

Listing 7-17. Method That Will Update the Table View to Remove the Deleted Park
func deletePark(index: Int) {
    dispatch_async(dispatch_get_main_queue()) {
       self.tableView.deselectRow(index)
       self.tableView.removeRowsAtIndexes(NSIndexSet(index: index), withAnimation: NSTableViewAnimationOptions.EffectFade)
       self.parks.removeAtIndex(index)
    }
}

Here on the main queue, we first deselect the park we just deleted, then we remove the row where the park was, and finally we remove the park from the parks list.

If you run your app now you should be able to delete an existing park, or create a new park and then delete it. You can also create a park with multiple images, view it in the CloudKit Dashboard, and delete it to ensure all the images related to the park are also deleted.

Now we are able to delete a park along with all the images that belong with it. However, wouldn’t it be nice to keep the park and just delete one or more pictures that belong with it? That is exactly what we will do next.

Deleting Park Images

There are a few minor updates we need to make in order for this to work. First, we need to enable the selection of images in our collection view. Open the Main.storyboard using the Document Outliner and find the Park Images Collection in the Detail View Controller scene. Then, in the Attributes Inspector, make sure the checkboxes for “Selectable,” “Allows Empty Selection,” and “Allows Multiple Selection” are checked. (To select multiple images, hold down the Command key as you select images.)

If we were to run our app and select one or more images we would not see the changes. To fix this we are going to create a subclass of NSCollectionViewItem and override the selected property. Create a new Cocoa Class, with subclass NSCollectionViewItem, and name it ParkImageViewItem. Update the contents of the file so it looks like Listing 7-18.

Listing 7-18. Custom NSCollectionViewItem to Allow Us to Visually Show When an Image Was Selected
import Cocoa

class ParkImageViewItem: NSCollectionViewItem {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do view setup here.
    }


    override var selected: Bool {
        didSet {
            if self.selected {
                self.view.layer?.borderColor = NSColor.orangeColor().CGColor
                self.view.layer?.borderWidth = 3
                self.view.layer?.cornerRadius = 10
            } else {
                self.view.layer?.borderColor = NSColor.clearColor().CGColor
                self.view.layer?.borderWidth = 0
                self.view.layer?.cornerRadius = 0
            }
        }
    }
}

Here we use the views layer and set the border color, width, and the corner radius for when the item is selected, and set it so when the item is not selected these changes are not implemented. The selected property is automatically set by Cocoa, so we don’t have to worry about manually setting this. We are using the didSet of the computed property so we leave the default behavior intact and only use the updated value for our own purposes.

Finally, we need to update the storyboard to take advantage of our new class. Select the scene that is being used to show an individual image, and then update the custom class in the Identity Inspector to be our new ParkImageViewItem class. Now if you run the app you will be able to see the selected images with an orange outline (Figure 7-3).

A385012_1_En_7_Fig3_HTML.jpg
Figure 7-3. Application running with two selected park images

We are ready to update our API to handle deleting images. Open the API.swift file and add a new function, as shown in Listing 7-19.

Listing 7-19. API Method That Handles Deleting Park Images
func deleteParkImages(imagesToDelete: [ParkImage], completionHandler: NSError? -> Void) {
    var imageRecordIDsToDelete = [CKRecordID]()


    for image in imagesToDelete {
        imageRecordIDsToDelete.append(image.recordID)
    }


    let deleteOperation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: imageRecordIDsToDelete)
        deleteOperation.atomic = false
        deleteOperation.database = publicDB


        deleteOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, deletedRecords: [CKRecordID]?, operationError: NSError?) -> Void in
            completionHandler(operationError)
     }


     NSOperationQueue().addOperation(deleteOperation)
}

We want to be able to delete one or more images at the same time, so instead of using the convenience API we use CKModifyRecordsOperation again, but this time instead of passing content to recordsToSave we pass the record IDs we want to delete.

In this function we are passing in an array of ParkImages and a completionHandler that accepts an error or nil. The first thing we need is an array to store the record IDs we want to delete. We then loop through all the image objects we passed into the method to update our IDs array with the image CKRecordID.

Next, we create a CKModifyRecordsOperationto pass nil as the records to save and our array of IDs for the delete parameter. We set atomic to false and tell the operation to use our public database.

Finally, we create a modifyRecordsCompletionBlock that simply calls our completionHandler, passing in any errors that may have occurred. Then we add our operation to the operation queue.

Now let’s move back to our DetailViewController to create a new action we will use for deleting images. Create the action in Listing 7-20.

Listing 7-20. Action to Get the Selected Images and Pass Them to deleteParkImages
@IBAction func deleteImages(sender: AnyObject) {
    let selectedParkImages = parkImagesCollection.selectionIndexes
    var imagesToDelete: [ParkImage]? = []
    selectedParkImages.enumerateIndexesUsingBlock { [unowned self] (index, _) -> Void in
       imagesToDelete?.append(self.parkImages[index])
    }
    api.deleteParkImages(imagesToDelete!) { (error) -> Void in
       if error != nil {
          print(error!.localizedDescription)
       } else {
          dispatch_async(dispatch_get_main_queue()) {
              self.imagesArrayController.removeObjectsAtArrangedObjectIndexes(selectedParkImages)
          }
       }
    }
}

First we need to know which images were selected, so we create a new constant that stores an NSIndexSet of selected images. Then we create an optional array of park images that we want to delete.

We enumerate over the NSIndexSet to get each index and then use that index to update our imagesToDelete array with the image from the parkImages array matching the index.

Once we have the array of images to delete we simply pass it into our api.deleteParkImages call. If there were an error we would print out the error message (in a real-world app you would want to notify the user in some way). If there were no problems deleting the images we would remove the selected indexes from our imagesArrayController on the main thread, which would in turn update our collection view, remove the images, and update the display.

Make the Search Feature Functional

In this last section we are going to enable the filtering of the results of our park list using the Search field . We are not going to be calling the server to do the filtering; instead, we are only going to be filtering the local copy of parks. It would be a good challenge for you to update the fetchParks API call to actually use the search parameter, then update the NSPredicate to take search terms into consideration and return only parks whose names match the search string.

In order to get the search to work we will need to make some changes to our code. We currently are not using an array controller to handle our parks, but we are going to change our code to use one. This will require a few alterations to the code for selecting and deleting parks.

First, open the Main.storyboard file and drag an array controller onto it. Next, open the Assistant Editor and Control-drag from the array controller to the ParkListViewController file; below the list of outlets create another one called arrayController. Close the Assistant Editor, as we have to make some more changes in the storyboard before moving on.

In the Document Outliner select the Table View, and in the Bindings Inspector update content and selection indexes to look like those in Figure 7-4.

A385012_1_En_7_Fig4_HTML.jpg
Figure 7-4. Bindings between the Table View and the Array Controller

Select the Search field. In the Binding Inspector, bind the search predicate to the array controller, and make sure its Controller Key is set to filterPredicate and the Predicate Format is set to name contains[cd] $value.

Let’s open the ParkListViewController to make some code changes. First, update the selectPark action to look like Listing 7-21.

Listing 7-21. Updated selectPark to Use the arrayController
@IBAction func selectPark(sender: AnyObject) {
        //let selectedRow = sender.selectedRow
        //let selectedPark = parks[selectedRow]
   let selectedRow = arrayController.selectionIndex
   let selectedPark = arrayController.selectedObjects.first as! Park
   delegate?.selectPark(selectedPark, index: selectedRow)
}

Now that we are using an arrayController we can use it to find the selected index and the selected object. Then we need to update the deletePark method to look like Listing 7-22.

Listing 7-22. Updated deletePark to Use the New arrayController
func deletePark(index: Int) {
    dispatch_async(dispatch_get_main_queue()) {
            self.arrayController.removeObjectAtArrangedObjectIndex(index)
//    self.tableView.deselectRow(index)
//            self.tableView.removeRowsAtIndexes(NSIndexSet(index: index), withAnimation: NSTableViewAnimationOptions.EffectFade)
//    self.parks.removeAtIndex(index)
   }
}

Here we replace the current lines that update the table list with a single arrayController call to removeObjectAtArrangedObjectIndex, which we must do since we updated the select code to use the array controller’s selected row. We don’t have to update anything else. When running the app now you should be able to filter the park list results by typing in the Search field, and be able to select or delete a park as before. However, if you are filtering parks the table is also updated as expected.

You may have noticed we have no way to update the park thumbnail that is shown in the list. I left this out on purpose as a challenge for you. You now have enough information to handle this on your own. Tip: check in saveParkImages to see if the park already has a thumbnail. If it doesn’t, create a new thumbnail of the first image and update the park’s thumbnail asset with it.

Conclusion

This chapter has covered a lot of ground. We have learned how to add parks, add park images, delete parks, and delete park images. We also learned how as code progresses you may be required to make changes to the original design. Finally, we learned how to add and delete multiple records at once.

In the next chapter we will look at some optimizations to improve performance.

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

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