Quantcast
Channel: Envato Tuts+ Code - Mobile Development
Viewing all articles
Browse latest Browse all 1836

iOS From Scratch With Swift: Building a Shopping List Application 1

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-25515

In the next two lessons, we'll put what we learned in this series into practice by creating a shopping list application. Along the way, you'll also learn a number of new concepts and patterns, such as creating a custom model class and implementing a custom delegate pattern. We've got a lot of ground to cover so let's get started.

Outline

The shopping list application we're about to create has two features, managing a list of items and creating a shopping list by selecting items from the list.

We'll build the application with a tab bar controller to make switching between the two views fast and straightforward. In this lesson, we focus on the first feature. In the next lesson, we the finishing touches on this feature and zoom in on the shopping list, the application's second feature.

Even though the shopping list application isn't complicated from a user's perspective, there are several decisions that need to be made during its development. What type of store do we use to store the list of items? Can the user add, edit, and delete items? These are questions that we address in the next two lessons.

In this lesson, I also show you how to seed the shopping list application with dummy data to give new users something to start with. Seeding an application with data is often a good idea to help new users get up to speed quickly.

1. Creating the Project

Launch Xcode and create a new project based on the Single View Application template in the iOS > Application section.

Choosing the Single View Application Template

Name the project Shopping List and enter an organization name and identifier. Set Language to Swift and Devices to iPhone. Make sure the checkboxes at the bottom are unchecked. Tell Xcode where to save the project and click Create.

Configuring the Project

2. Creating the List View Controller

As you might have expected, the list view controller is going to be a subclass of UITableViewController. Create a new class by selecting New > File... from the File menu. Select Cocoa Touch Class from the iOS > Source section.

Creating a Cocoa Touch Class

Name the class ListViewController and make it a subclass of UITableViewController. Leave the checkbox Also create XIB file unchecked and make sure Languages is set to Swift. Tell Xcode where you want to save the class and click Create.

Configuring the List View Controller Class

Open Main.storyboard, select the view controller that's already present, and delete it. Drag a UITabBarController instance from the Object Library and delete the two view controllers that are linked to the tab bar controller. Drag a UITableViewController from the Object Library, set its class to ListViewController in the Identity Inspector, and create a relationship segue from the tab bar controller to the list view controller.

Select the tab bar controller, open the Attributes Inspector, and make it the storyboard's initial view controller by checking the checkbox Is Initial View Controller.

Adding a Tab Bar Controller

The list view controller needs to be the root view controller of a navigation controller. Select the list view controller and choose Embed In > Navigation Controller from the Editor menu.

Embedding the List View Controller in a Navigation Controller

Select the table view of the list view controller and set Prototype Cells in the Attributes Inspector to 0.

Running the Application for the First Time

Run the application in the simulator to see if everything is set up correctly. You should see an empty table view with a navigation bar at the top and a tab bar at the bottom.

3. Creating the Item Model Class

How are we going to work with items in the shopping list application? In other words, what type of object do we use to store the properties of an item, such as its name, price, and a string that uniquely identifies each item?

The most obvious choice is to store the item's properties in a dictionary. Even though this would work just fine, it would severely limit and slow us down as the application gains in complexity.

For the shopping list application, we're going to create a custom model class. It requires a bit more work to set up, but it'll make development much easier down the road.

Create a new class, Item, and make it a subclass of NSObject. Tell Xcode where to save the class and click Create.

Properties

Open Item.swift and declare four properties:

  • uuid of type String to uniquely identify each item
  • name of type String
  • price of type Float
  • inShoppingList of type Bool to indicate if the item is present in the shopping list

It's essential that the Item class conforms to the NSCoding protocol. The reason for this will become clear in a few moments. Take a look at what we've got so far. Comments are omitted.

Each property needs to have an initial value. We set name to an empty string, price to 0.0, and inShoppingList to false. To set the initial value of uuid, we make use a class we haven't seen before, NSUUID. This class helps us create a unique string or UUID. We initialize an instance of the class and ask it for the UUID as a string by invoking UUIDString().

Give it a try by adding the following code snippet to the viewDidLoad() method of the ListViewController class.

Run the application and take a look at the output in Xcode's console to see what the resulting UUID looks like. You should see something like this:

Archiving

One strategy to save custom objects to disk, such as instances of the Item class, is through a process known as archiving. We'll use NSKeyedArchiver and NSKeyedUnarchiver to archive and unarchive instances of the Item class.

The class prefix, NS, indicates that both classes are defined in the Foundation framework. The NSKeyedArchiver class takes a set of objects and stores them to disk as binary data. An added benefit of this approach is that binary files are generally smaller than plain text files containing the same information.

If we want to use NSKeyedArchiver and NSKeyedUnarchiver to archive and unarchive instances of the Item class, Item needs to adopt the NSCoding protocol. Let's start by updating the Item class to tell the compiler Item adopts the NSCoding protocol.

Remember from the lesson about the Foundation framework, the NSCoding protocol declares two methods that a class must implement to allow instances of the class to be encoded and decoded. Let's see how this works.

Encoding

If you create custom classes, then you are responsible for specifying how instances of that class should be encoded, converted to binary data. In encodeWithCoder(_:), the class conforming to the NSCoding protocol specifies how instances of the class should be encoded. Take a look at the implementation below. The keys that we use are not that important, but you generally want to use the property names for clarity.

Decoding

Whenever an encoded object needs to be converted back to an instance of the respective class, init(coder:) is invoked. The same keys that we used in encodeWithCoder(_:) are used in init(coder:). This is very important.

Note that we use the required keyword. Remember that we covered the required keyword earlier in this series. Because decodeObjectForKey(_:) returns an object of type AnyObject?, we cast it to a String object.

You should never directly call init(coder:) and encodeWithCoder(_:). They are only called by the operating system. By conforming the Item class to the NSCoding protocol, we only tell the operating system how to encode and decode instances of the class.

Creating Instances

To make the creation of new instances of the Item class easier, we create a custom initializer that accepts a name and a price. This is optional, but it will make development easier as you'll see later in this lesson.

Open Item.swift and add the following initializer. Remember that initializers don't have the func keyword in front of their name. We first invoke the initializer of the superclass, NSObject. We then set the name and price properties of the Item instance.

You may be wondering why we use self.name in init(name:price:) and name in init(coder:) to set the name property. In both contexts, self references the Item instance we're interacting with. In Swift, you can access a property without using the self keyword. However, in init(name:price:) one of the parameters has a name that is identical to one of the properties of the Item class. To avoid confusion, we use the self keyword. In short, you can omit the self keyword to access a property unless there's cause for confusion.

4. Loading and Saving Items

Data persistence is going to be key in our shopping list application so let's take a look at how we're going to implement data persistence. Open ListViewController.swift and declare a variable stored property, items, of type [Item]. Note that the initial value of items is an empty array.

The items displayed in the view controller's table view will be stored in items. It is important that items is a mutable array hence the var keyword. Why? We will add the ability to add new items a bit later in this lesson.

In the initializer of the class, we load the list of items from disk and store it in the items property that we declared a few moments ago.

The view controller's loadItems() method is nothing more than a helper method to keep the init?(coder:) method concise and readable. Let's take a look at the implementation of loadItems().

Loading Items

The loadItems() method starts with fetching the path of the file in which the list of items is stored. We do this by calling pathForItems(), another helper method that we'll look at in a few moments. Because pathForItems() returns an optional of type String?, we bind the result to a constant, filePath. We discussed optional binding earlier in this series.

What we didn't cover yet is the where keyword in an if statement. By using the where keyword, we add an additional constraint to the condition of the if statement. In loadItems(), we make sure pathForItems() returns a String. Using a where clause, we also verify that the value of filePath corresponds to a file on disk. We use the NSFileManager class for this.

NSFileManager is a class we haven't worked with yet. It provides an easy-to-use API for working with the file system. We obtain a reference to an instance of the class by asking it for the default manager.

We then invoke fileExistsAtPath(_:) on the default manager, passing in the file path we obtained in the first line of loadItems(). If a file exists at the location specified by the file path, we load the contents of the file into the items property. If no file exists at that location, the items property retains its initial value, an empty array.

Loading the contents of the file is done through the NSKeyedUnarchiver class. It can read the binary data contained in the file and convert it to an object graph, an array of Item instances. This process will become clearer when we look at the saveItems() method in a minute.

Let's now take a look at pathForItems(), the helper method we invoked earlier. We first fetch the path of the Documents directory in the application's sandbox. This step should be familiar by now.

The method returns the path to the file containing the application's list of items. We do this by fetching the path for the Documents directory of the application's sandbox and appending "items" to it.

The beauty of using URLByAppendingPathComponent(_:) is that the insertion of path separators is done for us wherever necessary. In other words, the system makes sure that we receive a valid file URL. Note that we invoke path() on the resulting NSURL instance to make sure we return a String object.

Saving Items

Even though we won't be saving items until later in this lesson, it's a good idea to implement it while we're at it. The implementation of saveItems() is very concise thanks to the pathForItems() helper method.

We first fetch the path to the file that contains the application's list of items and then write the contents of the items property to that location. Easy. Right?

The process of writing an object graph to disk is known as archiving. We use the NSKeyedArchiver class to accomplish this by calling archiveRootObject(_:toFile:) on NSKeyedArchiver.

During this process, every object in the object graph is sent a message of encodeWithCoder(_:) to convert it to binary data. Remember that there's rarely a need to directly call encodeWithCoder(_:).

To verify that loading the list of items from disk works, add a print statement to the viewDidLoad() method of the ListViewController class. Run the application in the simulator and check if everything's working.

If you take a look at the output in Xcode's console, you'll notice that the items property is equal to en empty array. That's what we expect at this point. What's important is that items isn't equal to nil. In the next step, we'll give the user a few items to work with, a process known as seeding.

5. Seeding the Data Store

Seeding an application with data can often mean the difference between an engaged user and a user quitting the application after using it for less than a minute. Seeding an application with dummy data not only helps users get up to speed, it also shows new users how the application looks and feels with data in it.

Seeding the shopping list application with an initial list of items isn't difficult. Because we don't want to create duplicate items, we check during application launch if the data store has already been seeded with data. If the data store hasn't been seeded yet, we load a list with seed data and use that list to create the data store of the application.

The logic for seeding the data store can be called from a number of locations in an application, but it's important to think ahead. We could put the logic for seeding the data store in the ListViewController class, but what if, in a future version of the application, other view controllers also have access to the list of items. A better place to seed the data store is in the AppDelegate class. Let's see how this works.

Open AppDelegate.swift and amend the implementation of application(_:didFinishLaunchingWithOptions:) to look like the one shown below.

The only difference with the previous implementation is that we first invoke seedItems(). It's important that seeding the data store takes place before the initialization of any of the view controllers, because the data store needs to be seeded before any of the view controllers loads the list of items.

The implementation of seedItems() isn't complicated. We start by storing a reference to the shared user defaults object and we then check if the user defaults database has an entry for a key with name "UserDefaultsSeedItems" and whether this entry is a boolean with a value of true.

The key can be whatever you like as long as you are consistent in naming the keys that you use. The key in the user defaults database tells us whether or not the application has already been seeded with data. This is important since we only want to seed the application once.

If the application hasn't been seeded yet, we load a property list from the application bundle, seed.plist. This file contains an array of dictionaries with each dictionary representing an item with a name and a price.

Before iterating through the seedItems array, we create a mutable array to store the Item instances that we're about to create. For each dictionary in the seedItems array, we create an Item instance by invoking the initializer we declared earlier in this lesson. Each item is added to the items array.

Finally, we create the path to the file in which we will store the list of items and we write the contents of the items array to disk like we saw in the saveItems() method of ListViewController.

The method archiveRootObject(_:toFile:) returns true if the operation ended successfully and it's only then that we update the user defaults database by setting the boolean value for the key "UserDefaultsSeedItems" to true. The next time the application launches, the data store won't be seeded again.

You've probably noticed that we used another helper method in seedItems()pathForItems(). Its implementation is identical to that of the ListViewController class.

Before you run the application, make sure to copy the property list, seed.plist, to your project. It doesn't matter where you store it as long as it's included in the application's bundle.

Run the application and inspect the output in the console to see if the data store was successfully seeded with the contents of seed.plist. Note that seeding a data store with data or updating a database takes time. If the operation takes too long, the system may kill your application before it has had a chance to finish launching. Apple refers to this event as the watchdog killing your application.

Your application is given a limited amount of time to launch. If it fails to launch within that timeframe, the operating system kills your application. This means that you have to carefully consider when and where you perform certain operations, such as seeding your application's data store.

6. Displaying the List of Items

We now have a list of items to work with. Displaying the items in the table view of the list view controller isn't difficult. Take a look at the implementation of the three methods of the UITableViewDataSource protocol shown below. The implementations should look familiar if you've read the tutorial about table views.

There are two details we need to take care of before running the application, declaring the constant CellIdentifier and telling the table view which class to use for creating table view cells.

While you're at it, set the title property of the list view controller to "Items".

Run the application in the simulator. This is what you should see in the simulator.

Populating the List View Controller With Items

7. Adding Items - Part 1

No matter how well we craft the list of seed items, the user will certainly want to add additional items to the list. On iOS, a common approach to add new items to a list is by presenting the user with a modal view controller in which new data can be entered. This means that we'll need to:

  • add a button to the user interface to add new items
  • create a view controller that manages the view that accepts user input
  • create a new item based on the user's input
  • add the newly created item to the table view

Step 1: Adding a Button

Adding a button to the navigation bar requires one line of code. Revisit the viewDidLoad() method of the ListViewController class and update it to reflect the implementation below.

In the lesson about tab bar controllers, you learned that every view controller has a tabBarItem property. Similarly, every view controller has a navigationItem property, a unique instance of UINavigationItem representing the view controller in the navigation bar of the parent view controller—the navigation controller.

The navigationItem property has a leftBarButtonItem property, an instance of UIBarButtonItem, that references the bar button item displayed on the left side of the navigation bar. The navigationItem property also has a titleView and a rightBarButtonItem property.

In viewDidLoad(), we set the leftBarButtonItem property of the view controller's navigationItem to an instance of UIBarButtonItem by invoking init(barButtonSystemItem:target:action:), passing in .Add as the first argument. The first argument is of type UIBarButtonSystemItem, an enumeration. The result is a system-provided instance of UIBarButtonItem.

Even though we've already encountered the target-action pattern, the second and third parameter of init(barButtonSystemItem:target:action:) needs an explanation. Whenever the button in the navigation bar is tapped, a message of addItem(_:) is sent to target, that is, self or the ListViewController instance.

As I said, we've already encountered the target-action pattern when we connected the touch event of a button to an action in the storyboard. This is very similar, the only difference being that the connection is made programmatically.

The target-action pattern is a common pattern in Cocoa. The idea is simple. An object keeps a reference to the message that needs to be sent and the target, an object that acts as the receiver of that message.

The message is stored as a selector. Wait a minute. What is a selector? A selector is the name or the unique identifier that is used to select a method that an object is expected to execute. You can read more about selectors in Apple's Cocoa Core Competencies guide.

Before running the application in the simulator, we need to create the corresponding addItem(_:) method in the list view controller. If we don't do this, the view controller isn't able to respond to the message it receives when the button is tapped and an exception is thrown, crashing the application.

Take a look at the format of the method definition in the next code snippet. As we saw earlier in this series, the action accepts one argument, the object that sends the message to the view controller (target). In this example, the sender is the button in the navigation bar.

I've added a print statement to the method's implementation to test if everything works correctly. Build the project and run the application to test the button in the navigation bar.

Step 2: Creating a View Controller

Create a new UIViewController subclass and name it AddItemViewController. In AddItemViewController.swift, we declare two outlets for two text fields, which we'll create in a few moments.

We also need to declare two actions in AddItemViewController.swift. The first action, cancel(_:), cancels the creation of a new item. The second action, save(_:), uses the user's input to create and save a new item.

Open Main.storyboard, drag a UIViewController instance from the Object Library to the workspace, and set its class to AddItemViewController in the Identity Inspector.

Creating the Add Item View Controller Class

Create a manual segue by pressing Control and dragging from the List View Controller object to the Add Item View Controller object. Select Present Modally from the menu that pops up.

Creating a Segue to the Add Item View Controller
Creating a Segue to the Add Item View Controller

Select the segue you just created, open the Attributes Inspector, and set its Identifier to AddItemViewController.

Configuring the Segue to the Add Item View Controller

Before we add the text fields, select the add item view controller and embed it in a navigation controller by selecting Embed In > Navigation Controller from the Editor menu.

Earlier in this lesson, we programmatically added a UIBarButtonItem to the list view controller's navigation item. Let's find out how this works in a storyboard. Zoom in on the add item view controller and add two UIBarButtonItem instances to its navigation bar, positioning one on each side. Select the left bar button item, open the Attributes Inspector, and set Identifier to Cancel. Do the same for the right bar button item, setting its Identifier to Save.

Configuring the Navigation Bar of the Add Item View Controller

Select the Add Item View Controller object, open the Connections Inspector on the right, and connect the cancel(_:) action with the left bar button item and the save(_:) action with the right bar button item.

Connecting the Actions in the Connections Inspector

Drag two UITextField instances from the Object Library to the view of the add item view controller. Position the text fields as shown below. Don't forget to add the necessary constraints to the text fields.

Adding Text Fields to the Add Item View Controller

Select the top text field, open the Attributes Inspector, and enter Name in the Placeholder field. Select the bottom text field and, in the Attributes Inspector, set its placeholder text to Price and Keyboard to Number Pad. This ensures that users can only enter numbers in the bottom text field. Select the Add Item View Controller object, open the Connections Inspector, and connect the nameTextField and priceTextField outlets with the corresponding text field in the view controller's view.

That was quite a bit of work. Everything we've done in the storyboard can also be accomplished programmatically. Some developers don't even use storyboards and create the entire application's user interface programmatically. That's exactly what happens under the hood anyway.

Step 3: Implementing addItem(_:)

With AddItemViewController ready to use, let's revisit the addItem(_:) action in ListViewController. The implementation of addItem(_:) is short as you can see below. We invoke performSegueWithIdentifier(_:sender:), passing in the AddItemViewController identifier we set in the storyboard and self, the view controller.

Step 4: Dismissing the View Controller

The user should also be able to dismiss the view controller by tapping the cancel or save button of the add item view controller. Revisit the cancel(_:) and save(_:) actions in AddItemViewController and update their implementations as shown below. We will revisit the save(_:) action a bit later in this tutorial.

When we call dismissViewControllerAnimated(_:completion:) on the view controller whose view is presented modally, the modal view controller forwards that message to the view controller that presented the view controller. In our example, this means that the add item view controller forwards the message to the navigation controller, which, on its turn, forwards it to the list view controller. The second argument of dismissViewControllerAnimated(_:completion:) is a closure that's executed when the animation is completed.

Run the application in the simulator to see the AddItemViewController class in action. When you tap the name or price text field, the keyboard should automatically pop up from the bottom.

Adding an Item in the Add Item View Controller

7. Adding Items - Part 2

How will the list view controller know when a new item has been added by the add item view controller? Should we keep a reference to the list view controller that presented the add item view controller? This would introduce tight coupling, which isn't a good idea, because it makes our code less independent and less reusable.

The problem that we're faced with can be solved by implementing a custom delegate protocol. Let's see how this works.

Delegation

The idea is simple. Whenever the user taps the save button, the add item view controller collects the information from the text fields and notifies its delegate that a new item was saved.

The delegate object should be an object conforming to a custom delegate protocol that we define. It's up to the delegate object to decide what needs to be done with the information the add item view controller sends. The add item view controller is only responsible for capturing the input of the user and notifying its delegate.

Open AddItemViewController.swift and declare the AddItemViewControllerDelegate protocol at the top. The protocol defines one method, to notify the delegate that an item was saved. It passes along the name and the price of the item.

As a reminder, a protocol declaration defines or declares the methods and properties that objects conforming to the protocol should implement. Every method and property in a Swift protocol is required.

We also need to declare a property for the delegate. The delegate is of type AddItemViewControllerDelegate?. Note the question mark, indicating it is an optional type.

As I mentioned in the lesson about table views, it's good practice to pass along the sender of the message, the object notifying the delegate object, as the first argument of each delegate method. This makes it easy for the delegate object to communicate with the sender without the strict requirement to keep a reference to the delegate.

Notifying the Delegate

It's time to use the delegate protocol that we declared a moment ago. Revisit the save(_:) method in the AddItemViewController class and update its implementation as shown below.

We use optional binding to safely extract the values of the name and price text fields. We notify the delegate by invoking the delegate method we declared earlier. At the end, we dismiss the view controller.

Two details are worth pointing out. Did you spot the question mark after the delegate property? In Swift, this construct is known as optional chaining. Because the delegate property is an optional, it is not guaranteed to have a value. By appending a question mark to the delegate property when invoking the delegate method, the method is only invoked if the delegate property has a value. Optional chaining makes your code much safer.

Also notice that we create a Float from the value stored in priceAsString. This is necessary, because the delegate method expects a float as its third parameter, not a string.

Responding to Save Events

The final piece of the puzzle is to make ListViewController conform to the AddItemViewControllerDelegate protocol. Open ListViewController.swift and update the interface declaration of ListViewController to make the class conform to the new protocol.

We now need to implement the methods defined in the AddItemViewControllerDelegate protocol. In ListViewController.swift, add the following implementation of controller(_:didSaveItemWithName:andPrice:).

We create a new Item instance by invoking init(name:price:), passing in the name and price that we receive from the add item view controller. In the next step, the items property is updated by adding the newly created item. Of course, the table view doesn't automagically reflect the addition of a new item. We manually insert a new row into the table view. To save the changes to disk, we call saveItems() on the view controller, which we implemented earlier in this tutorial.

Setting the Delegate

The final piece of this somewhat complex puzzle is to set the delegate of the add item view controller when presenting it to the user. We do this in prepareForSegue(_:sender:) as we saw earlier in this series.

If the segue's identifier is equal to AddItemViewController, we ask the segue for its destinationViewController. You may think the destination view controller is the add item view controller, but remember that the add item view controller is embedded in a navigation controller.

This means that we need to fetch the first item in the navigation controller's navigation stack, which gives us the root view controller or the add item view controller object we're looking for. We then set the delegate property of the add item view controller to self, the list view controller.

Run the application one more time to see how everything works together—as if by magic.

Conclusion

That was a lot to take in, but we've accomplished quite a bit already. In the next lesson, we make some changes to the list view controller to edit and remove items from the list. In that lesson, we also add the ability to create a shopping list from the list of items.

If you have any questions or comments, you can leave them in the comments below or reach out to me on Twitter.

2015-12-28T17:45:16.000Z2015-12-28T17:45:16.000ZBart Jacobs

Viewing all articles
Browse latest Browse all 1836

Trending Articles