Sunday, September 13, 2015

A Simple Swift iOS App from Start to Finish - Implementing the Edit View's Code

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 will be implementing the code for the Edit View. The Edit View will be used for two purposes within my app. Firstly, when a new thing is added to the List View the Edit View will be opened automatically to enable the user to edit the characteristics of the new thing. Secondly, the Edit View will be opened when the user selects an existing thing within the List View.


The Edit View will enable the user to change the name of any of their things to anything they want. However it will enforce two limitations. If the name is left blank the Edit View will show a warning and ignore the edit. If the new name is a duplicate of another existing thing the Edit View will also issue a warning and ignore the edit.

I am going to implement the code for the Edit View in six stages:

1. Create a new UIViewController subclass for the edit view.
2. Link the controls in the storyboard view to the new class.
3. Add a property to hold the current thing.
4. Populate the view from the current thing.
5. Add code to open the Edit View from the List View.
6. Add code to validate and save the edits the user makes.


Create the Edit View's Class

I am going to start by right clicking on my projects folder and selecting New File. 
Then iOS - Source - Swift File.




I will call my new file EditViewController.



I will then define my new class within the new file.


import Foundation
import UIKit

class EditViewController: UIViewController {
    

}

The final step in creating the new class is to associate it with the Edit View in the storyboard.




Link the Storyboard Controls to the new Class

At this point I am going to hide the Project Navigator and the storyboard's object tree and open the assistant editor. This lets me see my Edit View in Interface Builder with my new class next to it.


I need to be able to set the value of all of my controls except the reset button through code.  To do this I need to create reference outlets for each of them. This is done by right clicking on the control and then dragging from the empty circle next to New referencing outlet into the body of my class definition. When I release the drag I am prompted to set the name and type of the outlet variable.



Once I enter the name and click 'Connect' the new outlet is created in my class code with a filled circle in the margin next to it to show that it is connected.


I am now going to repeat this process for the other controls.

class EditViewController: UIViewController {
    
    @IBOutlet var nameField: UITextField!
    @IBOutlet var withSwitch: UISegmentedControl!
    @IBOutlet var currentDays: UILabel!
    @IBOutlet var recordDays: UILabel!
}

Finally I need to create two IBAction methods and link them to the reset button and with/without segmented control. To do this I start by defining my methods in the class code.

    @IBAction func didTapResetButton(sender : AnyObject) {
    }
    
    @IBAction func didChangeWithSwitch(sender : AnyObject) {
    }

To link these methods to their respective controls I just need to drag from the empty circles in the margin next to the method definitions onto the controls.


Add a property to hold the current thing

The Edit View is going to be shown in response to the users actions in the List View. When it is shown the List View's code needs to control which Thing the Edit View displays. To enable this I am going to add a currentThing property to my EditViewController class. The class will also need a reference to the global AppDelegate class to enable it to save any changes it makes to the Thing being edited.

class EditViewController: UIViewController {
    
    @IBOutlet var nameField: UITextField!
    @IBOutlet var withSwitch: UISegmentedControl!
    @IBOutlet var currentDays: UILabel!
    @IBOutlet var recordDays: UILabel!
    var currentThing:Thing?
    
    let appDelegate:AppDelegate
    
    required init(coder aDecoder: NSCoder) {
        appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        super.init(coder: aDecoder)
    }

Populate the view from the current thing

The Edit View will need to populate its controls with the properties of the current Thing object whenever it is shown. To achieve this I am going to override its viewWillAppear method and call a new method I will implement to update its controls.

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        updateControls()
    }

    func updateControls() {
        if let thing = currentThing {
            nameField.text = thing.name
            withSwitch.selectedSegmentIndex = thing.withSwitch == false ? 1 : 0
            currentDays.text = "Current Days: \(thing.currentDayCount)"
            recordDays.text = "Record Days: \(thing.recordDayCount)"
        } else {
            nameField.text = "Undefined"
            withSwitch.selectedSegmentIndex = 1
            currentDays.text = "Current Days:"
            recordDays.text = "Record Days:"
        }
    }

This method first tests if it has a valid Thing object. If it has it sets the controls to match that object's properties. If it has not it sets the controls to defaults. This is just a precaution.The Edit View should never be shown without having a valid current thing set.


Add code to open the Edit View from the List View

There are two places in the List View code where it needs to show the Edit View. Firstly immediately after a new Thing is added and secondly when an existing Thing is selected in the table. To facilitate this I am going to add a method that shows takes a Thing as its parameter and opens the Edit View to display that thing. I am going to put the code for this method in my original class definition for ListTableViewController.
Because I am going to be showing the Edit View from code the first thing I need to do is to give it an identifier in the storyboard.


Now I can go ahead and implement my method to show the Edit View.

func showEditViewForThing(thing:Thing!) {
        let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil);
        let editController : EditViewController = storyboard.instantiateViewControllerWithIdentifier("EditViewController") as EditViewController;
        editController.currentThing = thing
        navigationController?.pushViewController(editController, animated: true)
    }

The first line gets a reference to the storyboard using its file name. The second line calls the storyboard method to create an instance of my EditViewController class. The third line sets the Thing object to be displayed and the final line calls the navigationController's pushViewController method to actually show the Edit View.
I am going to call my new method from two places within the ListTableViewController class. Firstly I will call it immediately after adding a new Thing in the didTapAdd method.

    @IBAction func didTapAdd(sender: AnyObject) {
        var thing = createNextNewThing()
        reloadData()
        showEditViewForThing(thing)
    }
Now when I tap add the newly created Thing is immediately shown in the Edit View.



 I will also call my showEditViewForThing  when the user taps on an existing thing in the list. To do this I need to override another tableview method.


    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        var thing = appDelegate.thingCollection?.things[indexPath.row]
        showEditViewForThing(thing)

    }

This method is called whenever a row is tapped in the table view. The first line retrieves a reference to a Thing based on the row number in the table.
The second line calls my shows that Thing in the Edit View.


Before moving back to the EditView there is one more change I need to make to the List View. Because the name of Thing's will soon be changeable I am going to add a viewWillAppear override to update the table whenever the user switches back from the Edit View.


    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        reloadData()

    }

Add code to validate and save the edits the user makes

There are three things the user can do to edit a Thing. 
1. Change the name
2. Change between With and Without.
3. Reset the current number of days to zero.

The last two are the simplest to implement. I have already connected the IBAction methods for the segmented control and button so I just have to fill in their implementation.

    @IBAction func didTapResetButton(sender : AnyObject) {
        appDelegate.thingCollection?.resetDaysForThing(&currentThing!)
        updateControls()
    }
    
    @IBAction func didChangeWithSwitch(sender : AnyObject) {
        currentThing?.withSwitch = withSwitch.selectedSegmentIndex == 0 ? true : false
        appDelegate.saveContext()
    }

The didTapResetButton calls the ThingCollection method to reset the Thing's day count and then updates the Edit View's controls to show the new day count.
The didChangeWithSwitch changes the Thing's withSwitch property and the uses the appDelegate to commit the change to the store.There is no need to update the Edit View's controls after this because the segmented control automatically changes its display and no other controls are affected by this change.


Validate the Name Text Field

Validating a UITextField requires the creation of a delegate that implements UITextFieldDelegate protocol. I am going to add an extension to my EditViewController class to do this.


extension EditViewController: UITextFieldDelegate {
    

}

Within this extension I will then implement the protocol's textFieldDidEndEditing method. This is called when the text field stops being the first responder. 


    func textFieldDidEndEditing(textField: UITextField) {
        if textField == nameField {
            if textField.text == currentThing?.name {
                return
            } else if textField.text.isEmpty {
                textField.text = currentThing?.name
                showIsBlankWarning()
            } else if appDelegate.thingCollection!.thingExists(textField.text) {
                textField.text = self.currentThing?.name
                showIsDuplicateWarning()
            } else {
                saveNewName(textField.text)
            }
        }

    }

The first line checks that the method has been called for the correct Text Field. The second line checks if the name has actually changed. If it has not the method just returns. The next if statement checks if the name is blank. If it is it calls a method to show a warning message and resets the name to the one currently stored in the Thing object.
The next if statement checks if there is already a Thing in the collection with the same name. If there is it will reset the current Thing's name back to whatever it was before it was edited and then show a warning message.
The final else clause calls a method to save the new name. Obviously this clues is only reached if the new name passes all the validation steps.

My app will use the UIAlertView class to show its warning messages.


    func showAlert(message: String) {
        var theAlert = UIAlertView(title: nil, message: message, delegate: nil, cancelButtonTitle: "OK")
        theAlert.show()
    }
    
    func showIsBlankWarning() {
        showAlert("Could not save blank name.")
    }
    
    func showIsDuplicateWarning() {
        showAlert("Could not save duplicate name.")

    }




The saveNewName method will set the current Thing object's name to the new name and use the appDelegate to save it to the store.


    func saveNewName(name:String) {
        currentThing?.name = name
        appDelegate.saveContext()
    }

 The final step is to set the UITextField's delegate. I will override the viewDidLoad method and set the delegate within it.


    override func viewDidLoad() {
        super.viewDidLoad()
        nameField.delegate = self

    }


Wrapping up

I have now completed the code for my List and Edit Views and can now edit the things I track in my app. 




In my next article I will finish the Main View's code.






No comments:

Post a Comment