Sunday, June 14, 2015

A Simple Swift iOS App from Start to Finish - Creating the Collection Class

Introduction

This is part of a series of articles describing the creation of a simple iOS app. A complete list of the articles is included in the first part A Simple Swift iOS App from Start to Finish - Introduction.

I have set up a web site for the finished app at http://DaysWithoutThings.com and you can download it free directly from the app store:





In this article I am going to create a class to encapsulate a collection of my Thing objects. My app will use this to hold all the things the user defines. The class will contain the high level interface to the data which will be stored in a local core data store. I will leave the low level core data functions in the AppDelegate class.    


ThingCollection

My class will be called ThingCollection and contain method to do the following:
  1. Add a new Thing.
  2. Check if a Thing already exists in the data store.
  3. Find a Thing if it already exists in the data store.
  4. Delete a Thing from the data store.
  5. Update the record number of days for a Thing.
  6. Reset the current number of days for a thing.
  7. Reload all the Things from the data store.

My class will also include a property called ‘things’ that will return an array of all the things in the data store sorted into alphabetical order. 

To create the class I right-click on my project folder and select New File then Swift File.




I will call the new file ThingCollection.swift. 






Once I have clicked 'Create'  new swift file is added to my project. The swift file is empty apart from the copyright banner and the import for the foundation library. I will start by adding a declaration for my new class.



import Foundation


class ThingCollection {
}

The things property

The first thing I’m going to add is the things property. This will hold a sorted list of Thing objects.


class ThingCollection {
    var things:Array<Thing> = []
}

    

The property will be initialized in the init method of the class by calling the reloadData method.



class ThingCollection {
    var things:Array<Thing> = []
    
    init() {
        self.reloadData()
    }
    
    func reloadData() {
        
    }

}




The reloadData method

The reloadData method will just call a private method to create a Core Data fetch request result which it will then assign to the things property. Before I can do this I need to give my class a reference to the AppDelegate class so that it can access its  core data methods. 
 To do thisI am going to add a constant property  to hold a reference to the AppDelegate object and assign it using the global reference in the init method. The global reference is a property of the UIApplication class and to use this I need to import the UIKit library.


import Foundation
import UIKit

class ThingCollection {
    let appDelegate:AppDelegate
    var things:Array<Thing> = []
    
    init() {
        appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        reloadData()

    }

I can now implement my reloadData method.


    func reloadData() {
        things = fetchThings()

    }

In Swift the self prefix is optional for accessing class properties and methods unless it is needed  to distinguish between a class property and a local or global variable or a class method and a global function.

The fetchThings method

The fetchThings method creates a Core Data fetch request for all the Thing records in the data store and sets a sort descriptor to sort the result alphabetically based on the ‘name’ attribute. It returns an Array of Thing objects containing the results of the fetch request or nil if the request fails with an error. 

    func fetchThings() -> Array<Thing> {
        var error: NSError? = nil
        var request: NSFetchRequest = NSFetchRequest(entityName: "Thing")
        var sort: NSSortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        var resultItems = Array<Thing>()
        
        request.sortDescriptors = [sort]
        var results = appDelegate.managedObjectContext?.executeFetchRequest(
            request, error: &error)
        
        if let myError = error
        {
            // Handle error
        } else {
            var saveFlag = false
            if let resultArray = results {
                
                
                for result in resultArray {
                    if var thing = result as? Thing {
                        if updateRecord(&thing) {
                            saveFlag = true
                        }
                        resultItems.append(thing)
                    }
                }
                if saveFlag {
                    appDelegate.saveContext()
                }
            }
        }
        return resultItems

    }


I use the Swift 'if let' construct to ensure I have a valid set of results before attempting to process them. I have to use 'if var' instead to check for a valid Thing object. This is because my updateRecord method may make changes to the object.

The updateRecord method

The updateRecord method takes a reference to a Thing object as a parameter and checks if its currentDayCount is larger than its recordDayCount. If it is it sets the recordDayCount to the currentDayCount and then calls the AppDelegate’s saveContext method to commit the change to the data store.


    func updateRecord(inout thing:Thing) -> Bool {
        if Int(thing.recordDayCount) < Int(thing.currentDayCount) {
            thing.recordDayCount = thing.currentDayCount
            appDelegate.saveContext()
            return true
        }
        return false
    }


The addThing method

The next method I am going to create is the addThing method. This will take a name string as a parameter and check to make sure that a Thing with the same name does not already exist. If the name is not a duplicate it will then call the createThing method to create a new Thing record for it within the data store. It will return true if it creates a new record and false if one already exists with the same name.


    func addThing(name: String!) -> Bool
    {
        if !thingExists(name)
        {
            createThing(name)
            return true
        }
        return false

    }

The thingExists and findThing methods

The thingExists and findThing methods are both concerned with finding a record for a particular Thing.  They both use the Thing’s name to identify it so they can both use the same fetch request. Because of this I’m going to construct that fetch request in a separate method called findRequest.
All three methods take a single string parameter which is the name of the thing. findRequest creates an NSFetchRequest object for my Thing entity and attaches a predicate to match the name to the name parameter.    


    func findRequest(name: String) -> NSFetchRequest {
        var request: NSFetchRequest = NSFetchRequest(entityName: "Thing")
        request.predicate = NSPredicate(format:"name == '\(name)' ")
        request.includesSubentities = false
        
        return request
    }  

The thingExists method only needs to know if a matching thing exists so it will use the countForFetchRequest method which just return the number of matching record rather than the records themselves.
If the count is zero the method returns false and if the count is greater than zero it returns true. If the request fails with an error it will return false.


    func thingExists(name: String) -> Bool {
        var error: NSError? = nil
        var request: NSFetchRequest = findRequest(name)
        var count = appDelegate.managedObjectContext?.countForFetchRequest(
                request, error:&error)
        
        if let myError = error
        {
            return false
        } else {
            return count > 0
        }

    }

The findThing method will use the executeFetch request method and return an optional Thing object which is either the first matching record retuned or Nil if no records were found or if the request fails with an error.


    func findThing(name: String) -> Thing? {
        var error: NSError? = nil
        var request: NSFetchRequest = findRequest(name)
        
        if let results = appDelegate.managedObjectContext?.executeFetchRequest(
            request, error: &error) {
                
                if results.count < 1 {
                    return nil
                } else {
                    return results[0] as? Thing
                }
        } else {
            return nil
        }
    }

The createThing method


The createThing method uses the insertNewObjectForEntity method of the NSEntityDescription class to create a new record and assigned a new Thing object to map to it.
If the new Thing object is created successfully I assign the name that was given as a parameter and set the startDate to the current Date and Time. The ‘dateType’ attribute is set to its default value of 'when'.
Once the attributes are set I use the appDelegate's saveContext method to save the new record to the store.
Finally I call the ThingCollection's reloadData method to update the things property to include the new record.


    func createThing(name:String)
    {
        if let newThing =
                NSEntityDescription.insertNewObjectForEntityForName("Thing",inManagedObjectContext: appDelegate.managedObjectContext!)
                    as? Thing {
            
            newThing.name = name
            newThing.startDate = NSDate().beginningOfDay()
            newThing.dateType = .When
            appDelegate.saveContext()
            reloadData()
        }

    }

The deleteThing method

The deleteThing method takes an instance of the Thing class as its parameter and attempts to delete it from the store using the deleteObject method followed by the saveContext method and then calls my reloadData method to update the things property. 


    func deleteThing(thing:Thing)
    {
        appDelegate.managedObjectContext?.deleteObject(thing)
        appDelegate.saveContext()
        reloadData()
        

    }

The resetDaysForThing method

The resetDaysForThing method will call updateRecord in case the currentDayCount exceeds the record day count. It then sets the start date to the start of the current day which has the effect of reseting the currentDayCount to zero.


    func resetDaysForThing(inout thing:Thing) {
        updateRecord(&thing)
        
        thing.startDate = NSDate().beginningOfDay()
        appDelegate.saveContext()
        

    }

That completes the ThingCollection class.

Adding a global ThingCollection instance

I’m going to add a ThingCollection property to my AppDelegate class so that the rest of my app can share a single instance of it.


@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var thingCollection: ThingCollection?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        //sleep(10)
        thingCollection = ThingCollection()
        return true
    }

Wrapping up

My ThingCollection class is now complete. Before moving on I am going to run my app in the Simulator to make sure it still compiles and then commit it to source control.


No comments:

Post a Comment