Refactoring to respect model-view-controller

We have already made some good progress on the core functionality of our app. However, before we move any further, we should reflect on the code we have written. Ultimately, we haven't actually written that many lines of code, but it can definitely be improved. The biggest shortcoming of our code is that we have put a lot of business logic inside our view controller. This is not a good separation of our different model, view, and controller layers. Let's take this opportunity to refactor this code into a separate type.

We will create a class called PhotoStore that will be responsible for storing our photos and that will implement the data source protocol. This will mean moving some of our code out of our view controller.

First, we will move the photo's property to the photo store class:

import UIKit

class PhotoStore: NSObject {
    var photos = [Photo]()
}

Note that this new photo store class inherits from NSObject. This is necessary for us to be able to fully satisfy the UICollectionViewDataSource protocol, which is our next task.

We could simply move the code from our view controller to this class, but we do not want our model to deal directly with our view layer. The current implementation creates and configures our collection view cell. Lets allow the view controller to still handle that by providing our own callback for when we need a cell for a given photo. To do that, we will first need to add a callback property:

class PhotoStore: NSObject {
    var photos = [Photo]()
    var cellForPhoto:
        (Photo, NSIndexPath) -> UICollectionViewCell
    
    init(
        cellForPhoto: (Photo,NSIndexPath) -> UICollectionViewCell
        )
    {
        self.cellForPhoto = cellForPhoto

        super.init()
    }
}

We need to provide an initializer now so that we can get the callback function. Next, we have to tweak our data source implementations and put them in this new class:

extension PhotoStore: UICollectionViewDataSource {
    func collectionView(
        collectionView: UICollectionView,
        numberOfItemsInSection section: Int
        ) -> Int
    {
        return self.photos.count
    }
    
    func collectionView(
        collectionView: UICollectionView,
        cellForItemAtIndexPath indexPath: NSIndexPath
        ) -> UICollectionViewCell
    {
        let photo = self.photos[indexPath.item]
        return self.cellForPhoto(photo, indexPath)
    }
}

The collectionView:numberOfItemsInSection: method can still just return the number of photos in our array, but collectionView:cellForItemAtIndexPath: is implemented to use the callback instead of creating a cell itself.

The second thing we need to add to this class is the ability to save a photo. Let's add a method to take a new image and label that returns the index path that should be added:

func saveNewPhotoWithImage(
    image: UIImage,
    labeled label: String
    ) -> NSIndexPath
{
    let photo = Photo(image: image, label: label)
    self.photos.append(photo)
    return NSIndexPath(
       forItem: self.photos.count - 1,
       inSection: 0
    )
}

This looks identical to the code we wrote in the view controller to do this, but it is better separated.

Now our photo store is complete and we just have to update our view controller to use it instead of our old implementation. First, lets add a photo store property that is an implicitly unwrapped optional in ViewController so we can create it after the view is loaded:

    var photoStore: PhotoStore!

To create our photo store in viewDidLoad, we will call the photo store initializer and pass it a closure that can create the cell. For clarity, we will define that closure as a separate method:

func createCellForPhoto(
    photo: Photo,
    indexPath: NSIndexPath
    ) -> UICollectionViewCell
{
    let cell = self.collectionView
        .dequeueReusableCellWithReuseIdentifier(
        "DefaultCell",
        forIndexPath: indexPath
        ) as! PhotoCollectionViewCell
    
    cell.imageView.image = photo.image
    cell.label.text = photo.label
    
    return cell
}

This method looks almost identical to our old collectionView:cellForItemAtIndexPath: implementation; the only difference is that we already have a reference to the correct photo.

This method allows our viewDidLoad implementation to be very simple. All we need to do is initialize the photo store with a reference to this method and make it the data source for the collection view:

override func viewDidLoad() {
    super.viewDidLoad()
    self.photoStore = PhotoStore(
        cellForPhoto: self.createCellForPhoto
    )
    self.collectionView.dataSource = self.photoStore
}

Lastly, we just have to update the save action to use the photo store:

let saveAction = UIAlertAction(
    title: "Save",
    style: .Default
    ) { action in
    let label = textField.text ?? ""
    let indexPath = self.photoStore.saveNewPhotoWithImage(
        image,
        labeled: label
    )
    self.collectionView.insertItemsAtIndexPaths([indexPath])
}

You can run the app again and it will operate as before, but now our code is modular, which will make any future changes much easier.

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

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