Flourish - Viewing Stored Entries

Login to Access Code

Login to access video

Introduction

Alright now that we’ve successfully saved our journal entry, we need to a be able to view our saved entries. In this chapter we are going to set up our Journal view, where we’ll see a list of our entries.

New Concepts This Chapter

  • UITableView
  • Responder Chain
  • Property Observers
  • UIAlertController
  • Core Animations

Improving Entry Form UX

We’ll start off by designing a transition from our Entry view to our soon-to-exist journal view. What do I mean by designing a transition? A well-designed app guides a user through an experience. One of the biggest considerations for any experience is navigation. When do you take a user somewhere and when do you allow the user to freely navigate? Those choices severely impact your UX and can make your app feel better or worse to the user. Going through our choices in Flourish will show you illustrate some of these considerations. We’re going to transition to a new view, but for now we need to clean up our UX in EntryFormController.swift.

We left the last chapter by simply logging a success message, but we haven’t shown the user anything. As a rule of thumb, always show the user when an action he/she has taken fails or succeeds. This all relates to the larger concept of user feedback: signaling to the user that an action has consequences by altering the UI somehow. Let’s start of with a simple example.

Dismissing Keyboard

Let’s dismiss the keyboard when a user taps outside an input element. We’ll do this by using the touchesBegan:withEvent method of the UIResponderClass.

In EntryFormController.swift, add the following code:

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
    view.endEditing(true)
}

UIResponder is a class that respond to and handle touch and motion events. touchesBegan:withEvent is fired when one or more fingers touch down in a view to start a multi-touch sequence. A multi-touch sequence contains four phases. During each phase touches on the screen, the app sends a series of event messages to the responder and the responder can handle the messages by implementing UIResponder class methods.

The first of these event messages is handled by the touchesBegan class we’re going to implement. The first parameter is a set of touches that are new or have changed in this phase of the touch sequence. This set of type Set. For those that missed, Swift now has a native Set class so no more NSSet in these UIResponder class methods. The second parameter is a UIEvent class object containing all of the UITouch objects in the entire sequence, not just in this phase.

Inside touchesBegan:withEvent we could do all sorts of interesting things, but all we care about is dismissing our keyboard, which can be accomplished by using the endEditing() method of UIView. When we tap a text field or some other input, it becomes the first responder. That means any touch events are handled by that input field before anything else. When an input element is a first responder, our keyboard shows up. By passing YES into this method, we force our text field to resign first responder status, which hides the keyboard.

Finally, be sure to use the override prefix before you declare your function because there is always a default implementation of touchesBegan:withEvent that doesn’t do anything, so we need to override that with our method.

Build and run to make sure the keyboard is dismissed when you tap outside of an input element.

Showing Success/Failure Alerts

Our next UX improvement is to show success failure alert messages to the user. Before iOS 8, alerts were of class UIAlertView, but as of iOS 8.0 we have the simpler UIAlertController class that we can use to show alerts. UIAlertView also has an addAction() method that we use to configure buttons in the alert and tie those buttons to actions.

In the success completion block of our saveForm() method, add present an alert with the following code:

let statusAlert = UIAlertController(title:"Entry Saved", message: "Your entry has been saved!", preferredStyle: .Alert)
statusAlert.addAction(UIAlertAction(title:"OK", style: .Default, handler: nil))
self.presentViewController(statusAlert, animated: true, completion: nil)

The first line of our code declares a constant called statusAlert and we set it equal to an instance of the UIAlertController class. Our instance is initialized with three parameters: title, message, and preferred style. Title is a string that will be the heading in the alert. Message is the body copy of the alert, while prefferedStyle is a really a choice between alert and action. In this case we want our alert to look like, well, an alert.

The second line of our code adds an action button to our alert by using the addAction() method of UIAlertController. In that method we pass in a title, which sets the button’s label, a style, which we just set to default, and a completion handler, which we don’t need right now. By default pressing an alert button will dismiss the alert, and that’s all we need for our success alert.

The third line of our code uses the presentViewController method of UIViewController to add our alert to our view hierarchy. We set the animated parameter to true and we don’t need a completion block.

Build and run and save an entry. Your alert should look like the image below.

success_modal

Now we need a similar alert for when our saving fails.

In the error completion block of our saveForm() method, add present an alert with the following code:

let statusAlert = UIAlertController(title:"Error", message: error!.localizedDescription, preferredStyle: .Alert)
statusAlert.addAction(UIAlertAction(title:"OK", style: .Default, handler: nil))
self.presentViewController(statusAlert, animated: true, completion: nil);

The only difference between our success and error alerts is that we don’t want to hard code a message, rather we want to pass the error message we return into the message parameter of the UIAlertController init method. Our error is an object of type NSError, yet the message parameter needs to be a string. Luckily, NSError has a handy property called localizedDescription, which is a string containing the localized description of the error. So all we need to do is unwrap the error optional and pass the localizedDescription property as our message parameter.

Build and run to test your error alert. The easiest way to get an error thrown is to log out of iCloud in the emulator and try to save an entry. Your alert view should look like the image below

error_modal

Failure Handler

Now that we’ve given our user some success/error feedback in the form of alert views, we can finish up by giving the user an option to retry the save from the alert view. When building an app for mobile, retry is an important function because wifi/4G/LTE signals can be unreliable. It could be that a user simply lost service for a split second as they went to submit the entry, so we want to make it as easy as possible to attempt it again.

We already tied our saveForm method to the touchUpInside event of our submit button, so all we need to do is programmatically call the touchUpInside event of the submit button from our retry method. First step is accessing the submitButton as an IBOutlet.

Write the following below your other variable and constant declarations:

  @IBOutlet weak var submitButton: UIButton!

With both our main.storyboard file and our EntryFormController.swift files open, Ctrl-click on the save entry button in the storyboard and drag it onto the submitButton variable declaration.

Create a retrySave() function with the following code:

  func retrySaving(alert: UIAlertAction!) {
        submitButton.sendActionsForControlEvents(UIControlEvents.TouchUpInside)
    }

Our retrySaving function takes an alert parameter of type UIAlertAction. UIAlertAction is simply an object representing an action that can be taken when tapping a button in an alert. Since we are going to be calling this method from an alert, we need this parameter. Inside the function we are using the sendActionsForControlEvents() method of UIControl to fire the touchUpInside event on our submitButton. This will then call whatever IBAction is assigned to the touchUpInside event of our submitButton, which is our saveForm function.

Now that we have our function and IBOutlet, we need to add an action button to our alert view that calls our retrySaving() function it is handler parameter:

statusAlert.addAction(UIAlertAction(title:"Retry", style: .Default, handler: self.retrySaving))

Your saveForm method should now look like this:

@IBAction func saveForm(sender: AnyObject) {
    let entry = Entry(title:titleInput.text, body:bodyInput.text, mood: feelingButton.tag, location: currentLocation)

    entry.create() {
        success, message, error in
        if success {
            println("success!", message)
            self.titleInput = nil
            self.feelingButton.setTitle("select", forState: .Normal)
            self.feelingButton.setTitleColor(UIColor(rgba:"#3687FF"), forState: .Normal)
            self.bodyInput.text = nil
            let statusAlert = UIAlertController(title:"Entry Saved", message: "Your entry has been saved!", preferredStyle: .Alert)
            statusAlert.addAction(UIAlertAction(title:"OK", style: .Default, handler: nil))
            self.presentViewController(statusAlert, animated: true, completion: nil);
        }
        else {
            let statusAlert = UIAlertController(title:"Error", message: error!.localizedDescription, preferredStyle: .Alert)
            statusAlert.addAction(UIAlertAction(title:"OK", style: .Default, handler: nil))
            statusAlert.addAction(UIAlertAction(title:"Retry", style: .Default, handler: self.retrySaving))
            self.presentViewController(statusAlert, animated: true, completion: nil)
        }
    }
}

Adding Journal View

Now that we’ve cleaned up the UX in our Entry view, we can turn our attention to our Journal view, which will display our saved entries.

Add a new swift file to your project. Go to file > new > file and select “swift file.” Name the file JournalController.swift

Erase any code in the JournalController.swift file and replace it with the following:

import UIKit

class JournalController: UITableViewController
{

}

Our class declaration should be familiar to you by now, with the only difference being our JournalController class of type UITableViewController, not UIViewController. UITableViewController inherits from UIViewController and as you can probably guess, creates a controller object that manages a table view. Our Journal view is essentially a table with each cell representing a saved entry.

We currently have a view controller associated with the journal tab. We need to replace that with a UITableViewController.

Go to main.storyboard and delete the view controller associated with the journal tab item.

Drag a table view controller object from the object library and drop it onto the storyboard and ctrl-drag an outlet from the navigation controller scene to the new view scene. When you drop the outlet, select “root view controller” from the Relationship Segue menu.

link_table_view

Go to main.storyboard and click on the table view controller object we just added to the Journal scene. Now go to that view controller’s identity inspector and in the Custom Class section’s class field, type “JournalController” or select it from the dropdown options.

link_table_view

Now we’ve associated our Journal Controller file with our UITableViewController in our storyboard. Our next step is to retrieve our entries from iCloud.

Add the following code to the JournalController class to retrieve entries from iCloud:

import UIKit

class JournalController: UITableViewController
{
    let entries = Entry()

    override func viewDidLoad()
    {
        entries.load()
    }

}

We first declare a constant and assign it to an instance of our Entry class. Then when the viewDidLoad() function gets called, we use the load() method of our Entry class to retrieve our saved entries. That’s actually all we need to do to get our saved entries. Let’s add a bit more code so we can actually see some of the data we’ve gotten back.

Modify JournalController to conform to the ModelDelegate protocol:

import UIKit

class JournalController: UITableViewController, ModelDelegate
{
    let entries = Entry()

    override func viewDidLoad()
    {
        entries.model.delegate = self
        entries.load()

    }

    func errorUpdating(error:NSError) {}

    func modelUpdated() {}
}

Our Entry class calls methods on the ModelDelegate in the completion blocks of its asynchronous methods. We’ll use those delegate methods to set our local variables. In order to conform to the ModelDelegate protocol, we need to implement two functions: errorUpdating() and modelUpdated().

Here we’ve also added a line in viewDidLoad() to set the model’s delegate property to self.

Add one line to the modelUpdated() function to log the records property of our entries object.

func modelUpdated() {
    println("our saved entries are \(entries.records)")
}

If you build and run at this point, you’ll see a log message of CKRecord objects we retrieve in our Entry class’ load() method when you tab over to the Journal view.

Save some entries and make sure you get some entries logged. You should see a log message that looks like the the following:

logged_entries

Getting our entries

Now that we have gotten some data, let’s populate the table cells in our UITableView with that data. In order to populate our cells with our data, we need to implement two delegate methods. One tells the app how many rows we need in our table and the other tells the app what data to put in the table cell. It is important to note now that anytime we use the UITableViewController class, we automatically conform to the UITableViewDelegate and UITableVIewDataSource protocols.

Set the number of table rows to the number of entries we have by adding the following code to our JournalController class:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
    return entries.records.count
}

The tableView:numberOfRowsInSection method takes two parameters: the table view we’re referring to, and an index number identifying a section in the tableView. The method must return an integer. We return the length of our entries array to have only as many table rows as entries.

Now that we’ve implemented our nuberOfRowsInSection, we are only missing a data source. There are two ways to do this: the fast, simple way with default styles and the more involved way with custom styles. Want to venture a guess as to which one we are going to implement? If you guessed “custom”, you get a PRIZE!

Our table view data source must return a cell from the method tableView:cellForRowAtIndexPath method. The cell is of type UITableViewCell, so we can subclass the UITableViewCell class to create a custom cell.

Create a new swift file for your custom cell class. Go to file > new > file and select “swift file.” Name the file JournalEntryCell.swift.

In JournalEntryCell.swift, add the class declaration for the new journal class with the following code:

import UIKit

class JournalEntryCell: UITableViewCell
{
    var entry: Entry? {
        didSet {
            updateUI()
        }
    }

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var bodyLabel: UILabel!

    func updateUI()
    {
        // Reset properties
        titleLabel?.text = nil
        bodyLabel?.text = nil

        if let entry = self.entry
        {
            titleLabel?.text = entry.title
            bodyLabel?.text = entry.body
        }
    }
}

The variable declaration in the first line declares a variable entry, which is of optional type Entry. The next line we use a method we haven’t used before: didSet. The didSet method is a property observer. Property observers observe and respond to changes in a property’s value. didSet is called immediately after the new value is stored. So whenever a value for our entry variable is set, we call an updateUI method that reloads the table cell with the latest data. An astute person reading a draft of this chapter asked: “Where in the world are you assigning any values to the entry variable?” That’s a question I’m sure many of you will have. To answer, recall we implemented a singleton pattern in writing our Entry class. That means each time we call an instance of the Entry class, we are calling the same exact instance. So whenever any variable of type Entry sets values for any of its properties, we setting values for our entry variable. Therefore, when we update the entries variable of JournalController, which is of type Entry, we are going to be calling the didSet property observer of the entry variable in JournalEntryCell. If you’re still confused, add a some log messages to see when things are getting called and set. This stuff can get really confusing.

The block of code contains two IBOutlet declarations: one for the title of our entry and one for the body of our entry. That’s all we’re going to display in our table cell.

Lastly, we need to write our updateUI() function. This function sets our IBOutlet variables to nil before reassigning those variables to values in our entry variable. Notice the familiar use of if let to check if our optional entry variable has a value. If it does have a value, we update the text property of our IBOutlet variables to match the title and body properties of our entry variable.

In main.storyboard, select the table cell inside the table view of your journal controller. In its identity inspector’s custom class section, change the class to JournalEntryCell.

TableViews render by establishing a prototype cell with certain properties and repeating that cell as many times as specified by the code. Each repetition allows us to set new values for those properties in the prototype cell. What we’ve just done is set the prototype cell (and therefore all of our cells) as being of class JournalEntryCell.

Hop back to JournalController.swift and implement the tableView(cellForRowAtIndexPath) method of the UITableViewDataSource protocol by adding the following code:

 override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
  {
    let cell : JournalEntryCell = tableView.dequeueReusableCellWithIdentifier("entryCell") as! JournalEntryCell

    cell.entry = entries.records[indexPath.row]

    return cell
  }

In order to understand the tableView(cellForRowAtIndexPath) method, we need to start with the last line: return cell. This method returns a table cell. The rest of the code in our method tells the method which cell we are returning. Our parameters are a tableView to which the cell we return will belong to and a cellForRowAtIndexPath, which is of type NSIndexPath. The NSIndexPath is hard to understand and it is rarely used so for our purposes think of it this way: NSIndexPath is a vector for referencing arrays within arrays. It’s kind of like a coordinate system. In our method this indexPath determines which row and section the cell is returned for.

hat tip to this stack overflow question http://stackoverflow.com/questions/8079471/how-does-cellforrowatindexpath-work upvote if you can*

Inside our method our first line declares a constant called cell of type JournalEntryCell. On the right side of the declaration, we use the dequeResuableCellWithIndentifier method of UITableView, which returns a UITableViewCell, and cast the return cell as a JournalEntryCell. dequeueReusableCellWithIdentifier returns a reusable table-view object that we locate by its identifier. A table view keeps a queue of of UITableViewCell cells for reuse. What we are doing is removing any cells that are currently queued up from the queue and replacing it with a new one using a class or nib file.

Next we set the entry property of our cell constant to the specific CKRecord object in our indexPath row. If you’ve never seen the object lookup notation, you can look up an object value inside an array of objects using this syntax: arrayName[key].

Finally we end up where we started our explanation: returning the cell.

In main.storyboard, drag two UILabel object from the object library into the prototype cell.

Using the assistant editor to open Main.storyboard and JournalEntryCell.swift side by side, ctrl-click and drag from each label in the prototype cell to link to each of the IBOutlet variables in JournalCellController.swift. The top label should be linked to the titleLabel variable and the lower label should be linked to the bodyLabel variable.

Now go to the attributes inspector of the UITableCell and in the identifier field add “entryCell.” This sets our identifier in our storyboard to match what our cellForRowAtIndexPath method is looking for. Next in the indentity inspector, change the custom class field to JournalEntryCell.

Finally, one line to the modelUpdated() function to reload table data when our model is updated. For that we’ll use UITableView’s relaodData() method. .

func modelUpdated() {
   tableView.reloadData()
    println("our saved entries are \(entries.records)")
}

Build and run. When you tab over to the journal view, you should see all of your saved entries in a table. It’s not pretty but it’s there.

build_table_entries

Improving look and feel of table cells

Now let’s do some storyboard work to get our table cells to look better.

In main.storyboard, select the titleLabel label and apply a Leading Space (via the pin menu or the drag method) and Trailing space constraint of 20. Then set a Top Space constraint to 10. Those three constraints should be relative to the superview. Go ahead and set a height constraint of 100 as well.

In main.storyboard, select the bodyLabel and apply a Leading Space (via the pin menu or the drag method) and Trailing space constraint of 20. Then set a Bottom Space constraint to 10. Those three constraints should be relative to the superview.

Control-drag from the titleLabel to the bodyLabel and apply vertical spacing constraint. In the title label’s size inspector, edit the bottom space to label constraint to equal 0.

By default, our labels are constrained to one line and any overflow is truncated. We want our labels to be as many lines as they need for now, so we need to change that setting.

In the titleLabel’s attributes inspector, change the line property of the Label menu to 0. Do the same for bodyLabel.

By setting the line property to 0, we are telling iOS not to truncate anything. Labels will take as many lines as they need. As you can probably anticipate, we might have label heights that overflow our cell height. In fact, we haven’t done anything to specify a cell height.

As of iOS 8, we have this wonderful concept of dynamic cell heights. We can resize our table cells to accommodate the height of its subviews which just two lines of code. Trust me when I say this is so much easier now than it used to be.

In the viewDidLoad() method of JournalController.swift, add the following code:

tableView.estimatedRowHeight = 40.0
tableView.rowHeight = UITableViewAutomaticDimension

The first line we set the estimatedRowHeight property of UITableView class to 40. It’s odd to set an estimated height when the whole point of what we’re trying is to dynamically calculate height. However, doing so can improve the user experience of our app. Sometimes there is a noticeable delay between the app calculating the appropriate cell height and actually refreshing the view. That means uses can see your cells “grow.” That’s not sexy. Setting a reasonable estimate for row height can defer some of the cost of geometry calculation from load time to scrolling time.

The second line we set the rowHeight property of our UITableView class to UITableviewAutomaticDimension, which tells our app to calculate the right cell height.

Here’s what we’d have if we build and run:

dynamic_table_cells

Not bad, huh? Let’s do some typeography changes to establish a visual distinction between titleLabels and bodyLabels.

The title is going to be the focal point, so let’s set it in a larger font size. In the attributes inspector of titleLabel, change the color property to a brownish color with RGB values (R:170 G:151 B:131). Next change the font to “Avenir Next Condensed Medium 22.0.” I can’t recall if this is a system font that comes with all macs or not, so don’t be surprised if you don’t have the font available.

In the attributes inspector of bodyLabel, change the color property to a brownish color with RGB values (R:170 G:151 B:131). Next change the font property to “Avenir Roman 15.0” if you have it.

Our cells are already looking better. Let’s go ahead and add a left and bottom border to our cells to make them look even better and learn a bit about drawing graphics on screen in iOS. We’ve already used image assets to show custom graphics in our app. Another way to create custom graphics is to use a graphics rendering framework to pragmatically define shapes, attributes, and animations. To add borders to our table cells we will be using the Core Animation framework included in iOS.

Every app in iOS uses Core Animation whether or not you explicitly call any Core Animation methods in your code. To quote the documentation “Core Animation is not a drawing system itself. It is an infrastructure for compositing and manipulating your app’s content in hardware. At the heart of this infrastructure are layer objects, which you use to manage and manipulate your content.” Most of our hierarchy so far has been discussed in terms of views, not layers. Layers actually work very similarly to views. Each view contains layer objects that can have a size, position, and pretty much everything we come to expect from views. You can stack layers, transform layers, and define layer properties. Core Animation then talks to the graphics hardware to achieve the results you define in the code. I could say more, but let’s see some code!

In JournalEntryCell.swift, add the following code:

override func drawRect(rect: CGRect)
    {
        if let mood = entry?.mood
        { 
        }
    }

What we are doing here is overriding the call to the drawRect method of UIView. Any view that contains UIKit or Core Graphics custom content will call drawRect, so for our custom content we need to override it. The only parameter is of class CGRect and it simply defines a portion of the view that needs to be updated. We are going to update our entire view so we don’t have to worry about the parameter. The only other thing we have here in our method now is an our trust if let statement to check for a mood value in our entry variable. This lays the groundwork for displaying the mood as a color in our table cell.

Draw a bottom border on the table cells. In the drawRect method, add the following code to now have the following:

override func drawRect(rect: CGRect)
    {
        if let mood = entry?.mood
        { 
            let bottomPath = UIBezierPath()
            bottomPath.moveToPoint(CGPoint(x: 0, y: contentView.bounds.height))
            bottomPath.addLineToPoint(CGPoint(x: contentView.bounds.width, y: contentView.bounds.height))         

            let borderBottom = CAShapeLayer()
            borderBottom.path = bottomPath.CGPath
            borderBottom.lineWidth = 1.0
            borderBottom.strokeColor = UIColor(rgba: "#dad5cd").CGColor
            borderBottom.zPosition = 2
            contentView.layer.addSublayer(borderBottom)

        }
    }

Each table cell has a content view property, which is the default superview for any content shown inside a UITableViewCell object. You can see the content view’s place in the view hierarchy in storyboards. Our bottom border is added by drawing a new layer in the shape of a rectangle and adding that as a sublayer to our table cell’s contentView’s layer property.

Core Animation layers are of type CALayer. A subclass of CALayer is CAShapeLayer, which basically allows us to draw shapes. In the code we just added we define a constant called border bottom as an instance of the CAShapeLayer class. Right after that declaration we set a value for borderBottom’s path property. The path property defines the shape to be rendered. The path property must be of type CGPath. So we needed to define a path and assign it to borderBottom’s path property. This is where we turn our attention to the first block of code:


let bottomPath = UIBezierPath()
bottomPath.moveToPoint(CGPoint(x: 0, y: contentView.bounds.height))
bottomPath.addLineToPoint(CGPoint(x: contentView.bounds.width, y: contentView.bounds.height))         

We first define a constant bottomPath as an instance of the UIBezierPath class. UIBezierPath allows us to define a path as a series of straight and curved line segments. It helps to picture yourself drawing line segments with a pencil when defining UIBezierPaths because the methods work the same way. The next line we use the moveToPoint method to move the starting point of our path from the origin to a point at 0 on the x axis and the height of the content view of the table cell on the y axis. Now that we have a new starting point, we can actually draw a line using the addLineToPoint method, which takes an end point and draws a line between the previous point and the end point. The end point has the same y coordinate as the origin but the x coordinate is set to the horizontal edge of the content view.

Now we have a UIBezierPath defined with a line stretching across the bottom of the table cell’s content view. UIBezierPath has a property called CGPath, which is a snapshot of the path we defined at a static time. Now let’s turn our attention back to the second block of code and pick up at

borderBottom.path = bottomPath.CGPath            

We are setting the path property of our borderBottom constant to the bezier path we defined by setting it equal to the CGPath property of bottomPath. Setting the path property is what defines the shape and is therefore the most important line we have. The rest of the code block simply alter the appearance of the shape we have defined. Let’s look at that:


// Specifies the line width of the shape’s path
borderBottom.lineWidth = 1.0
// The color used to stroke the shape’s path.
borderBottom.strokeColor = UIColor(rgba: "#dad5cd").CGColor
// stacking the layer in the z index, from CA layer
borderBottom.zPosition = 2
// add layer
contentView.layer.addSublayer(borderBottom)   

I’ve added comments to the code which should do a good job of explaining each layer. As you’ll notice, the properties are often self explanatory. We define a line width and strokeColor for our shape and we give it a zPosition. Z position is simply a transformation on the z axis. Think of it as a position in a vertical stack. Something with a higher z position stacks on top of something with a lower z position. Finally we add the sublayer we defined to the contentView’s existing layer.

Build and run and you should have a journal view that looks like this

bottom_border_table

Next we want to add a left border to our cells using the same strategy we used for the bottom border.

Draw a left border on the table cells. In the drawRect method, add the following code:


 // Draw left border
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: contentView.bounds.height))

let borderLeft = CAShapeLayer()
borderLeft.path = path.CGPath
borderLeft.lineWidth = 8.0
borderLeft.strokeColor = UIColor(rgba: "#999999").CGColor
borderLeft.zPosition = 3
contentView.layer.addSublayer(borderLeft)

There are a few differences between the left border and bottom border worth mentioning. Our bezier path for the left border starts at the origin and extends to the height of the cell. We also set a z position for the shape layer of 3, which is greater than the one we set for the bottomBorder. This means our left border is going to cover up any part that bottom border that it overlaps with.

Build and run and you’ll have this:

left_border_table

add color to entry

Probably the most important part of each journal entry is the mood field, but our journal view doesn’t actually display our mood for any of our entries. If you log any of the entries we retrieve from our database, you’ll see that our mood field is stored as an integer. Going back to EntryForm.swift, you’ll recall we have an array of dictionaries that has key-value pairs for each mood. What we stored in our database is the index of the array that contains the key-value pair for the mood we’re storing. Naturally, when we retrieve that index from our database, we need to look up that key value pair in the same object.

Now we’ve gotten to a point in our app development where two controllers need to reference the same object. This is almost always a good indicator of needing to add a model to your MVC project. This model will take the shape of a .swift file with our feelings object in it. Any controller that needs access to that object will reference this model.

Add a new swift file to your project. Go to file > new > file and select “swift file.” Name the file AppHelper.swift.

In AppHelper.swift, add the following code:


struct AppHelper
{    
    struct properties
    {
        static let moods = [
            ["title" : "the best", "color" : "#8647b7"],
            ["title" : "really good", "color": "#4870b7"],
            ["title" : "okay", "color" : "#45a85a"],
            ["title" : "meh", "color" : "#a8a23f"],
            ["title" : "not so great", "color" : "#c6802e"],
            ["title" : "the worst", "color" : "#b05050"]
        ]

    }

}

Our code in AppHelper.swift boils down to two structs. The AppHelper struct will be a place for us to put static model content that more than one controller needs to access, but nothing we’re going to need to store in our database.

The only struct in AppHelper so far is our familiar moods array inside of a struct we call properties. Now we need to lookup the color that corresponds to the mood of our entry.

In JournalEntryCell.swift, replace this line:

leftBorder.strokeColor = UIColor(rgba: "#999999").CGColor

with

leftBorder.strokeColor = UIColor(rgba: AppHelper.properties.moods[mood]["color"]!).CGColor

Our new line sets the strokeColor property of our leftBorder constant to a CGColor value converted from a UIColor value. In that UIColor value we pass an rgba value for the color we want. That rgba value is actually a hex string that we access by passing the mood property of our entry variable as an index to look up an array in the properties struct in AppHelper. That hex string is converted to an RGBA value by our UIColorHelper extension to UIColor. Here’s how might read this lookup out loud: “In the AppHelper struct’s properties’s struct’s moods array, go to index [mood] and return the value for the key ‘color’.” Sheesh, maybe that isn’t making anything clearer. Oh well, reach out if this doesn’t make sense.

Build and run and you’ll see that the left border of our table cell is the color that corresponds to the mood you stored for that entry!

*** Challenge: refactor EntryFormController.swift to use the moods constant from AppHelper.swift as opposed to declaring its own feelings constant. We want to use the same array to reduce the points of failure in our app. If we ever decide to change our mood options, we want to only have to make that change in one place. ***