Chapter 21: MyHood App

*A fun app to record images and details to be displayed in a table view. We will focus on saving and retrieving data using **UserDefaults**.*


What you will learn

  • Use Table Views
  • Store data and images with UserDefaults
  • Style images
  • Encode and decode data

Key Terms

  • UserDefaults
  • TableView
  • Table View Cell
  • Protocol and Method
  • Singleton
  • Encode and Decode

Resources

Download here: https://github.com/devslopes/book-assets/wiki

In this chapter, we are going to build an app called MyHood. This is a fun little app that you can use to document your neighborhood (or with different branding, anything really!) by taking pictures and adding descriptions. The images and descriptions will be saved to your apps **UserDefaults**, which is a way to permanently save data to your device, for as long as the app is installed. So lets get started!

Here is a sneak peek at the finished product!

Figure 3.1.0 (A & B)

Figure 3.1.0 A.png Figure 3.1.0 B.png

Creating an Xcode project

Open Xcode and, from the File menu, select New and then New Project. A new workspace window will appear, and a sheet will slide from its toolbar with several application templates to choose from. On the top, make sure to select iOS, and then look in the Application section.

From the choices that appear, select Single View Application and press the Next button (Figure   3.1.1). (Note that Apple changes these templates and their names often. But it should be very similar to what is shown here.)

Figure 3.1.1

Figure 3.1.1 Project Create.png

Then fill out the product name. I am calling mine MyHood. You can select a Team and Organization Name. The organization identifier is usually in reverse DNS format. Make sure the language is Swift and select which devices you want your app to run on. We'll keep it simple for this app and just do iPhone. We do not need Core Data, Unit or UI Tests for this chapter, so you can leave those unchecked (Figure 3.1.2).

Figure 3.1.2

Figure 3.1.2 Project Naming.png

Finally choose a location for your app to be saved. I recommend checking the Create Git repository so that you can push your project to your Github (or Bitbucket) account and show off your awesome app to friends and potential employers!

Once the project loads, you will be looking at a brand new project that is just waiting to be turned into something great!

Getting started with the Data Model

We are going to start building our app now. And you can start with the data model or the user interface. Sometimes it makes more sense to do it one way or the other, but often its best to start with the data model. So that is what we are going to do here!

Now if you go back and reference the final product in Figure 3.1.0 (A & B) you see that we have a number of posts in a TableView, and each post has a Title, Description, and an Image.

Now, you would not normally save a bunch of images to your app using UserDefaults. UserDefaults is best used for a small amount of data, like setting your username and password. But because the purpose of this app is to learn about UserDefaults, we will be using it extensively to teach the principles involved with this class.

Also keep in mind, that what we are saving to UserDefaults for the image, is actually the path to the saved image on disc. So do keep in mind, that in the future when working with images and large amounts of data, you would want to use something like an online database or CoreData.

So, lets start on the data model. Right-click on your project and select ‘New Group’ and name that new group ‘Model’. (Figure 3.1.4)

Figure 3.1.4

Figure 3.1.4 NewGroup.png

Then right-click on the Model folder and select New File This will open a new window as seen in Figure 3.1.5. Make sure iOS is selected at the top, and we want a Swift File. Select Next, then name the file Post and click Create. (Figure 3.1.6)

Figure 3.1.5

Figure 3.1.5 NewFileCreate.png

Figure 3.1.6

Figure 3.1.6 NewFileSave.png

We just created the file that will become the custom class that will hold all the information displayed in each post in the table view.

Inside the Post.swift file, it should be empty save a lonely import Foundation line, so let’s give it some company. Remember, a custom class is like a blue print. So what do we want each post to be able to have? An image path, a description, and a title.

We will add those as private variables then create an initializer as follows. We want every post to be required to have a title, description and an image, so we will include all three in the initializer. Note that you should not use the reserved keyword description when naming properties, that is why we went with postDesc.

swift
class Post {

    fileprivate var imagePath: String
    fileprivate var title: String
    fileprivate var postDesc: String

    init(imagePath: String, title: String, description: String) {
        self.imagePath = imagePath
        self.title = title
        self.postDesc = description
    }

}

Now when a new Post is initialized, the properties will be assigned the values that are passed into the initializer.

Now we can start on the UI, so hop on over to your Main.storyboard file.

User Interface

Referring back to Figure 3.1.0 A, we can see we will need a TableView to display the posts, and a View to contain the banner and navigation controls. So go ahead and search for uiview in the Object Library and drag one to the top of your View Controller. In the Attributes Inspector in the Utilities pane on the right, change the background color to any color you choose, but I will be using #2E87C3. Then in the Size Inspector in the Utilities pane, change the height to 65.

Figure 3.1.7 Figure 3.1.7 AddUIVIew.png

Now we will add constraints. With the UIView selected, click on the Pin button at the bottom right of the Storyboard. Make sure Constrain to margins is not checked and set the left, top, and right constraints to 0. Then set the height constraint to 65 by checking the Height box. Click Add Constraints.

Figure 3.1.8

Figure 3.1.8 UIViewConstraints.png

Now is as good a time as any to add our assets. Click here to download them now: URL** Once you have the assets unzipped and in a folder, drag them into the Assets.xcassets folder.

Figure 3.1.9 Figure 3.1.9 AddAssets.png

Now head back to the Main.storyboard file, and add an Image View to the blue View we added previously. In the Attributes Inspector in the Utilities pan on the right, set the Image to bannerlogo and set the Content Mode to Aspect Fit. Resize it to your liking. Then we will add our constraints.

Select the Pin button at the bottom right, and set the Width and Height constraints. Click Add Constraints. Then select the Align tool, to the left of the Pin tool, and check Horizontally in container and Vertically in container. This will align the banner image smack dab in the center of the View it is contained within. Figures 3.1.10 and 3.1.11

Figure 3.1.10 Figure 3.1.10 AddBanner.png

Figure 3.1.11 Figure 3.1.11 AlignBanner.png

Next lets add the camera button. In the Object Library, search for button. Then select and drag a button and place it in the top view on the right. In the Attributes Inspector remove the default Button text, and change the Image to camera. You will need to resize the button. I found 30 tall and 40 wide to be good. Lastly lets add some constraints. Pin it 8 from the right, 8 from the bottom, and set the width and height.

Figure 3.1.12 Figure 3.1.12 AddCameraButton.png

Next we need to add the TableView. In the object library search for TableView, and drag it into your view controller below your menu bar (which is what I will refer to the top blue view from now on).

Be sure not to grab a Table View Controller. Add constraints and (with Constrain to Margins checked) pin it 0 from the left, 20 from the top, 0 from the right, and 20 from the bottom. Then press Add Constraints.

Figure 3.1.13 Figure 3.1.13 Add**TableView**.png

If, after you have added constraints, the element has orange dotted lines as seen below, this means that the constraints you added are different from what were previously displayed in the Storyboard. So you need to update frames. This can be done by clicking the Resolve Auto Layout Issues button, and selecting update frames, or the keyboard shortcut command + alt (option) + = .

Figure 3.1.14 Figure 3.1.14 UpdateFrame.png

Now, we need to add a Table View Cell. In the Object Library, drag a Table View Cell into the table view. It will snap to the top of the TableView with the words ‘Prototype Cells’ above it. Select the Content View under TableView cell in the View Controller Scene hierarchy and change the Background to blue. This is just so that we can see the contents of the cell we are working with more easily, once we have all the elements inside it, we will change it back. At this point it should look like the contents of Figure 3.1.15.

Figure 3.1.15 Figure 3.1.15 Add**TableView**Cell.png

We need the cell to be a little bigger, so select the Table View Cell and in the size inspector make it 100.

Figure 3.1.16 Figure 3.1.16 **TableView**CellHeight.png

Now we can start adding our necessary elements. In each post, there is an image to the left and two labels to the right. So go ahead and add an Image View and two Labels as shown in Figure 3.1.17

Figure 3.1.17

Figure 3.1.17 AddLabelandImage.png

Select the Image View, change the Image to one of the assets we added earlier which is ‘barrel-water-bridge’. This is just a place holder image for now. Set the Content Mode to Aspect Fill and check the box Clip To Bounds. Then add constraints as follows, with Constrain to margins checked, pin it 0 from the top, left, and bottom and set the width to 83.

Figure 3.1.18 Figure 3.1.18 AddImageConstraints.png

Now lets work with the labels. Select the top label and change the color to a Dark Gray Color, then select the Font and choose Custom and the Family of Helvetica Neue is good. Do the same thing with the second label, except make the style Light.

Design tip: Black text color is usually too black. It is better to go with a dark gray. And, when you have a header and text below it, you may think, "make the header bold and the text below regular". But it is actually better to make the header regular, and the text below light. That is why we made the second labels style = light.

Now add constraints. For the top one give the constraints of 8 from the left, 0 from the top, and 0 from the right, with Constrain to margins checked and set height = 20.

Figure 3.1.19 Figure 1.19 AddLabelColor.png

Figure 3.1.20 Figure 1.20 AddLabelConstraints.png

For the bottom label set the constraint with Constrain to margins checked 8 from the left, 8 from the top, 0 from the right, and 0 from the bottom.

Then set the number of "lines" to 3, and "Autoshrink" to Minimum Font Size set to 9.

This makes it so that if there is a long description, it wont truncate at first. It will shrink the font trying to fit the whole text until it gets down to font size 9, at which point it will finally truncate it.

Figure 3.1.21 Figure 1.21 AddBottomLabelConstraints.png

Lastly, remove the blue color from the Content View and change it back to Default.

Figure 3.1.22 Figure 1.22 RemoveBlueBG.png

Working in the View Controller

Now what we need to do, is head into our ViewController.swift and start adding IBOutlets and Delegates so that we can work with the elements we just added to our Storyboard.

First lets add the IBOutlet for the TableView by adding to the View Controller the following above viewDidLoad.

IBOutlet weak var tableView: UI**TableView**!

You can also delete the didReceiveMemoryWarning function.

Then in your Storyboard, hook up the TableView to the IBOutlet by right-clicking View Controller and dragging from the tableView outlet to the Table View in the Storyboard. as shown in Figure 3.1.23

Figure 3.1.23 Figure 1.23 AddIBOutlet.png

Now to work with the Table View in the View Controller, we need to add some Protocols and Methods. A Protocol is used to declare a set of methods that a class adopts. It is a way of saying, "here is a set of behavior that is expected of an object in a given situation." The Methods we implement then carries out the expected behavior.

So for a Table View we need to add two Protocols to the class of View Controller like this:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

Once you have added those, you will have an error that says, “Type ViewController does not conform to protocol UITableViewDataSource”. So, we need to add some methods and other information. Go ahead and add to your viewDidLoad the following:

  override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
    }

Then we need to add the methods. Depending on the protocol, there are required and optional methods. For Table Views we are required to provide information on how many rows there will be and a function to create the cells.

These methods look like the following:

    func tableView(_ tableView: UI**TableView**, cellForRowAt indexPath: IndexPath) -> UI**TableView**Cell {

        return UI**TableView**Cell()
    }

    func tableView(_ tableView: UI**TableView**, numberOfRowsInSection section: Int) -> Int {

        return 10
    }

So lets digest these two functions real quick. The first one you can get to autocomplete by typing “cellForRowAt” and the rest will pop up. The second function you can get to autocomplete by typing “numberOfRowsInSection”.

Now there are a number of optional methods that you can use besides these ones which implement further functionality. For example - what happens when a cell is clicked on, or how many sections you want, or define cell size dynamically. But these two will be sufficient for us.

The first function is where we will initialize and display our custom Posts. For now I have a simple return UITableViewCell() to complete the function. Later on we will be returning a custom cell that is initialized based on posts that are created and saved by the user.

The second function, manages how many rows there will be in the table view. I currently have it hard-coded at 10. But what we really need is an array of type Post, and return the size of that array. So lets do that. Below the tableView IBOutlet, declare a variable posts as follows:

var posts = [Post]()

and change the numberOfRowsInSection method to

   func tableView(_ tableView: UI**TableView**, numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }

Now that you have the required Methods for the implemented Protocols, you will see that any errors have gone away.

Custom Cell

What we need to do now is create a custom class for the cell. If you look at the Storyboard, and the cell that we have our images and labels in, we need a way to communicate with those elements and have them update based on the data saved to our posts variable. So to do that, we create a new group called View. (just like we did with Model in Figure 1.4). And then inside that group create a new file by right clicking on the group and select New File

Now this time, select Cocoa Touch Class and press next. Then name it PostCell and make sure the subclass is UITableViewCell. Then press create.

Figure 3.1.24 Figure 1.24 CreateCellFile.png

Figure 3.1.25 Figure 1.25 NameCellFile.png

The PostCell file should open and look like this:

import UIKit

class PostCell: UI**TableView**Cell {

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

You can go ahead and delete the setSelected function.

Now, this custom class is meant to communicate with the cell in Storyboard, so we need to add some IBOutlets as follows.

class PostCell: UI**TableView**Cell {

    @IBOutlet weak var postImg: UIImageView!
    @IBOutlet weak var titleLbl: UILabel!
    @IBOutlet weak var descLbl: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
}

As you can see we have an outlet for the image, title, and description found in the Storyboard cell. Now lets go into Storyboard and hook those up the same way we did for the button.

But first we have to do something very important. And that is to change the Class of the Table View Cell to our newly created PostCell. Do that by selecting the Cell in the hierarchy, then in the Identity Inspector click on the Class drop down and select our custom PostCell class. Below that you will see Identifier, type in PostCell as seen in Figure 3.1.26. This identifier will be used later to identify which cell to use when creating and displaying cells in the Table View.

Figure 3.1.26 Figure 1.26 ChangeCustomClass.png

Right-click on PostCell and drag the outlet to its corresponding UI element as shown in Figure 3.1.27. Do this for each outlet we created in PostCell.

Figure 3.1.27 Figure 1.27 AddOutlets.png

While we are in the Storyboard, lets go ahead and remove the TableView separators. Select the TableView, go to the Attributes Inspector, and go to Separator, select None.

Figure 3.1.28 Figure 1.28 RemoveSeparators.png

Now, we want to be able to configure a cell in the PostCell file. To do that, we first need to modify the Post file. So head over to the Post file and make the following changes:

fileprivate var _imagePath: String!
fileprivate var _title: String!
fileprivate var _postDesc: String!

var imagePath: String {
    return _imagePath
}

var title: String {
    return _title
}

var postDesc: String {
    return _postDesc
}

init(imagePath: String, title: String, description: String) {
    self._imagePath = imagePath
    self._title = title
    self._postDesc = description
}

Lets talk about these changes real quick. The reason we made these changes is that when we first made this class, we made the properties private. Which is good practice because you don't want other files to be able to change these without special permissions.

However, we need to be able to have some way of accessing these properties, so we needed to create getters. So we modified the declaration of the private variable by adding a _ to the front, then created getters for each. This practice is called "data encapsulation".

Now that we have our getters, or sometimes called accessors, we are able to go on to the next step. Open your PostCell file, and add the following below the awakeFromNib function:

 func configureCell(_ post: Post) {
        titleLbl.text = post.title
        descLbl.text = post.postDesc
    }

This function takes in a post as a parameter. Then it sets the title and description labels that we set in the Storyboard, to the values of that specific post. We aren’t going to worry about the image for a while, it will just display our placeholder image we put in the Storyboard.

So where do we use this function? Lets go back to the ViewController file and update the cellForRowAt function to the following:

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let post = posts[indexPath.row]
        if let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell") as? PostCell {
            cell.configureCell(post)
            return cell
        }
        return PostCell()
    }

Now lets run through this.

First we declare a constant post that is created from the posts array. Next we grabbed the specific post that corresponds to the row that we are looking at in the TableView. (That's what IndexPath.row refers too). For example, if there are 10 entries in our posts array, there will be 10 rows in our table view, and each row corresponds exactly with its IndexPath.row property.

Then we are creating an implicitly unwrapped variable called cell, and setting it equal to tableView.dequeueReusableCell(withIdentifier: "PostCell") as? PostCell.

What is happening here, is with TableViews, they don’t load all the data into cells. If you had thousands of posts in your posts array, that would crash the app. So what it does is only load into memory as many as need to be shown on the screen at a time.

Then it will dequeue the cells as they go off the screen and push the new data into new cells as they come onto the screen. We are also telling the table view which cell to use with the 'identifier' of PostCell that we added in Storyboard. And finally we cast it as a PostCell class.

We take that newly created cell, and we call the function we created in the PostCell configureCell() and pass in the post from the posts array that corresponds to that row. This will update the title, description, and eventually the image information that we created in the Storyboard.

Then we return the cell. Lastly, in the unlikely event that there is no dequeued cell available, we return an empty PostCell().

Whew! I know that is a lot to take in and there are a lot of moving pieces here, so take some time and follow the bread crumbs to understand everything.

Now, we got everything set up, and we can run it, but we don't even have any data in our posts array, so lets just add some test data. In viewDidLoad, add the following:

        let post = Post(imagePath: "", title: "Post 1", description: "Post 1 Description")
        let post2 = Post(imagePath: "", title: "Post 2", description: "I am the second post. Yipeee!")
        let post3 = Post(imagePath: "", title: "Post 3", description: "I am the most important post.")

        posts.append(post)
        posts.append(post2)
        posts.append(post3)

        tableView.reloadData()

All we are doing here is creating three test entries of type Post, adding them to the posts array, and then reloading the Table View data. You want to use reloadData() any time you make a change to your data, this notifies your table view that changes have been made so it will call your cellForRow and numberOfRows and any other methods related to table view and reload the Table View.

Run that now and make sure it is working. You should see the following:

Figure 3.1.29

Figure 1.29 PostsScreenshot.png

Second View Controller

So we can see that it is working! Congratulations! You have done a lot so far, but now we need a way to add posts. Lets create a second View Controller for that. Create a new group in your project called Controller, by right clicking on the MyHood folder in the left pane and selecting New Group. Then right click on the new group Controller and select New File select Cocoa Touch Class from iOS Source, then click Next. Name it AddPostVC and set the subclass to UIViewController.

Figure 3.1.30 Figure 1.30 NewVCFile.png

Figure 3.1.31 Figure 1.31 NameNewVC.png

Delete the comments and didReceiveMemoryWarning function.

Before we begin building our second VC in Storyboard, lets revisit our finished product and remember what it looks like. We have our menu bar at the top, an image, then two text fields, and a button.

Figure 3.1.0 B

Figure 1.0 B.png

Go to Main.storyboard and add a new View Controller by searching for view controller in the object library. Drag it into the Storyboard next to our existing view controller. Then set the Class to AddPostVC in the Identity Inspector.

Figure 3.1.32 Figure 1.32 AddVCStoryboard.png

We can also add our segue from the first screen to the second screen by control dragging from our camera button to the new View Controller. Select show then select the segue as it appears as an arrow connecting the two View Controllers, and name the segue AddPostVC.

Figure 3.1.33 Figure 1.33 AddSegue.png

Figure 3.1.34 Figure 1.34 NameSegue.png

Now lets go ahead and add our menu bar to the AddPostVC. This can be done by selecting the MenuBar view in the original VC, copying it with cmd + c , then selecting the new VC and pasting it with cmd + v. Then drag it to the top, and pin it to the left, top, and right with the value of 0.

Figure 3.1.35 Figure 1.35 CopyMenuBar.png

You can delete the Camera button and the banner image.
Add a label to the menu bar and set the text to “Make New Post”. Change the color to white, and change the font to Helvetica Neue. Pin it 8 from the bottom, set the width and height, and center it.

Figure 3.1.36 Figure 1.36 AddLabel.png

Figure 3.1.37

Figure 1.37 CenterLabl.png

Next we need a button to cancel and go back to the original screen if we so choose. So drag a button to the left of the menu bar, and change the text to "Cancel", the color to white, and the font to Helvetica Neue. Add constraints of 8 from the left, 8 from the bottom, and set width and height.

Figure 3.1.38 Figure 1.38 AddCancelButton.png

Next we need to add an image to the AddPostVC. Drag an Image View onto the screen and change the size to 240 x 240 in the size inspector. Give it constraints of 35 from the top, set width and height, and center it horizontally in the container.

Figure 3.1.39 Figure 1.39 AddImage.png

Figure 3.1.40 Figure 1.37 CenterLabl.png

Go ahead and select the image, and set the image to our test image "barrel-water-bridge". Set the Content Mode to Aspect Fill, and make sure the Clip To Bounds is checked in the Attributes Inspector. This image is what we will click to add the images to be displayed in the Table View, so we need a way to click on it. One way to do that is by taking a shortcut, and just add a button over the top of it that is the same size as the image. Drag a new button onto the AddPostVC and make it the same size as the image, 240 x 240. Change the font to white, make the text say “+ Add Pic”. Next, select both the image and the button and in the Pin toolbar, select Equal Widths and Equal Heights. Then in the Alignment tool bar select Horizontal Center and Vertical Centers.

Figure 3.1.41 Figure 1.41 ButtonImageConstraints.png

Figure 3.1.42 Figure 1.42 CenterEach.png

Now we need to add a couple text fields below the image so we can enter the Title and Description of our Post. Add two text fields below the image as seen in the following figure. Change the color to Dark Gray Color and change the font to Helvetica Neue. Add a Placeholder text “Enter Title” for the top label and “Enter Description” for the bottom label.

Figure 3.1.43 Figure 1.43 TextFields.png

For the top one, add constraints, pinning it 0 from the left, 20 from the top, and 0 from the right. Set height.

Figure 3.1.44 Figure 1.44 TitleConstraints.png

For the Description text field, add constraints, pinning it 0 from the left, 8 from the top, and 0 from the right. Set height.

Figure 3.1.45 Figure 1.45 DescConstraints.png

Now we need a button that will make the post once the information is added. So drag in a new button below the text fields. Change the button text to ‘Make Post’, change the Text Color to White. Set the background to Blue. Add constraints as seen below, 8 from the top, set height, and width. Center horizontally.

Figure 3.1.46 Figure 1.46 Button Constraints.png

Figure 3.1.47

Figure 1.37 CenterLabl.png

Now we are ready to get into some code and hook up these elements. Open the AddPostVC file and above the viewDidLoad() function add the following outlets.

    @IBOutlet weak var titleField: UITextField!
    @IBOutlet weak var postImg: UIImageView!
    @IBOutlet weak var descField: UITextField!

then below the viewDidLoad() add the IBAction for the Make Post, AddPic, and Cancel buttons:

    @IBAction func addPicBtnPressed(_ sender: UIButton) {

    }

@IBAction func makePostBtnPressed(_ sender: UIButton) {

    }

@IBAction func cancelBtnPressed(_ sender: UIButton) {

    }

Now switch back to the Main.storyboard, right-click on the AddPostVC and drag from the outlets to the corresponding UI elements as seen in the below figures. When we hook up the IBActions for the Cancel, Make Post, and AddPic buttons, we also need to select the type of action, which is ‘Touch Up Inside’.

Figure 3.1.48 Figure 1.48 Hookup.png

Figure 3.1.49 Figure 1.49 HookupAction.png

Figure 3.1.50 Figure 1.50 SetActionType.png

So at this point, you should have hooked up the Outlets for the postImg, titleField, and descField, and hooked up the IBActions for the addPic, Cancel, and Make Post buttons.

Now, real quick lets make it so that when we tap the + Add Pic button, it looks like the button goes away. We will do this by just removing the button text. So in the addPicBtnPressed function modify it to the following.

 @IBAction func addPicBtnPressed(_ sender: UIButton) {
        sender.setTitle("", for: .normal)
    }

Next lets make it so when the cancel button is pressed, it takes us back to the initial screen. This is easy enough to do, simply modify that IBAction to the following:

swift
 @IBAction func cancelBtnPressed(_ sender: UIButton) {
        dismiss(animated: true, completion: nil)
    }

At this point, lets run it and make sure that we are able to click on the camera button in the first screen and segue to the second screen. Make sure when we click on the + Add Pic button in the second screen the button title disappears, and lastly make sure clicking on the Cancel button takes you back to the initial screen.

All right, hopefully that is working out just peachy for you!

Lets add a little styling to the main image on the second screen, and make it a circle. So in viewDidLoad() add the following:

        postImg.layer.cornerRadius = 120

All we are doing here is setting the cornerRadius of the image to 120 which is one half the width of the image, effectively turning it into a circle.

At this point we can remove the test image from the Post Img Image View and instead set the Background to Light Gray Color.

Figure 3.1.51 Figure 1.51 RemoveTestImage.png

And when you run it, it should look like the following:

Figure 3.1.52

Figure 1.52 RoundedTestRun.png

Now lets go ahead and add a little styling to the images in the table View. In the PostCell.swift file, in the awakFromNib() function modify it to this. This gives the image in each post a nice rounded edge which looks nice.

override func awakeFromNib() {
        super.awakeFromNib()

        postImg.layer.cornerRadius = 15
    }

Adding UIImagePicker

Now we are going to write the code that will allow us to click on the AddPic button and select an image from our camera roll. This is done by means of a UIImagePickerController, so under your IBOutlets, declare the following variable

    var imagePicker: UIImagePickerController!

Then in viewDidLoad() initialize it below everything else in that function:

        imagePicker = UIImagePickerController()

We also need to add a couple protocols that are required to work with the imagePicker, so modify your class as follows:

class AddPostVC: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

and just like we did with the Table View earlier, we need to add the delegate for the imagePicker in viewDidLoad() which should look like this at this point:

 override func viewDidLoad() {
        super.viewDidLoad()

        postImg.layer.cornerRadius = 120
        imagePicker = UIImagePickerController()
        imagePicker.delegate = self

    }

Now we need to add a method as follows. You can add this to the bottom of the AddPostVC below the IBActions:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        let selectedImage = info[UIImagePickerControllerOriginalImage] as! UIImage
        imagePicker.dismiss(animated: true, completion: nil)
        postImg.image = selectedImage

    }

What this function is doing is listening for when the imagePicker is presented, then when the user selects a picture, it takes that picture and assigns it to the constant selectedImage and casts it as a UIImage, then assigns that to the postImg.image so that it can be displayed and used later. Then it dismisses itself.

Then we need to present the imagePicker View Controller when the addPicButton is pressed:

  @IBAction func addPicBtnPressed(_ sender: UIButton) {
        sender.setTitle("", for: .normal)
        present(imagePicker, animated: true, completion: nil)
    }

And lastly, before we can test this, we need to add a permissions to the info.plist. Open the info.plist from the left hand pane and in the last entry, when you hover over there should be a + sign that pops up. Click on it and type Privacy and you should get some auto completed entries, and we are looking for Privacy - Photo Library Usage Description then on the right there is space available to enter a message to the user why you would like to access their photos. Say something like "MyHood needs to access your photos."

Figure 3.1.53 Figure 1.53 CameraPersmissions.png

Go ahead and run it, and verify that when you click the Add Pic button, you are asked to allow access to photos, then when you click a photo, it returns to the AddPostVC and the image you selected is now displayed as seen in following figure:

Figure 3.1.54

Figure 1.54 CameraWorking.png

DataService

In our original View Controller file, we have our variable of posts array. But that is not globally accessible. So what we want to do is introduce a new data model called a Singleton, which is a single instance of data that is globally accessible.

So create a new group in your file tree, like we did with Model, View, and Controller, and inside that group, create a new file > select Swift File > and name it Data Service.

Once the file is opened modify it as follows:

import Foundation
import UIKit

class DataService {

    static let instance = DataService()

    private var _loadedPosts = [Post]()

    var loadedPosts: [Post] {
        return _loadedPosts
    }

    func savePosts() {

    }

    func loadPosts() {

    }

    func saveImageAndCreatePath(image: UIImage) {

    }

    func imageForPath(path: String) {

    }

    func addPost(post: Post) {

    }
}

And let’s talk about what we got here. What we have done is laid the groundwork for the functions we will need to make this all work together. We have created and instantiated an instance of the DataService.

We have created a private array of posts, and created the getter for that array. Then we have created empty functions for saving and loading posts, as well as saving images and creating the path for the image, a function to fetch that image given a path, and then finally a function to add posts that are created.

And as we move forward, each of these functions will get fleshed out. In fact we can start with the last addPost function. Since we know that once we add a post, we will be adding it to the _loadedPosts array, then saving the posts, then reloading them, we can modify that function to be:

    func addPost(post: Post) {
        _loadedPosts.append(post)
        savePosts()
        loadPosts()
    }

Now those that may not do anything quite yet, but again we are laying the foundation.

Lets now work on the savePosts() function. We are going to be using the UserDefaults class to save and load data, in conjunction with the NSKeyedArchiver.
So modify the savePosts() function as follows:

   func savePosts() {
        let postsData = NSKeyedArchiver.archivedData(withRootObject: _loadedPosts)
        UserDefaults.standard.set(postsData, forKey: “posts")
        UserDefaults.standard.synchronize()
    }

What we are doing here is taking the _loaded posts array and using the NSKeyedArchiver class to transform that array into data. Then we are using UserDefaults to save that data to a key we are calling “posts”. And finally, using UserDefault method called synchronize() to save the data to disc.

Next we can work on the loadPosts() function, so modify it as follows:

    func loadPosts() {
        if let postsData = UserDefaults.standard.object(forKey: "posts") as? Data {

            if let postsArray = NSKeyedUnarchiver.unarchiveObject(with: postsData) as? [Post] {
                _loadedPosts = postsArray
            }
        }
    }

Here we are essentially reversing the process we took to save the posts. First we are using UserDefaults to load the archived and saved data in the savePosts() function, then un-archiving it and casting it to an array of type Post, then setting _loadedPosts equal to that newly restored postsArray.

Now, we have a challenge to overcome. We can add a post, which will then save, and in turn load the posts, however, there is currently no way of letting the Table View know that there has been any change. So lets fix that.

First lets go into the AddPostVC.swift file and make it so we can actually make posts. Modify the makePostBtnPressed action as follows:

    @IBAction func makePostBtnPressed(_ sender: UIButton) {
        if let title = titleField.text, let desc = descField.text, let img = postImg.image {
            let post = Post(imagePath: "", title: title, description: desc)
            DataService.instance.addPost(post: post)
    dismiss(animated: true, completion: nil)
        }
    }

Lets break this down. First we have a string of if let's to check that there is in fact something inside each of the text fields and Image View. Since we are requiring there to be an entry for each, the action will not continue if there is not. Then we create a post based on the input of the text fields (we are still not ready to test images, so it is an empty string). Then we call the DataService.instance.addPost function and pass into it the newly created post. And remember that when the addPost function is called, that post is then added to the loadedPosts array, and so are the savePosts and loadPosts functions. Lastly we dismiss the view and return to the initial screen to see the TableView.

So now lets continue solving the problem of the Table View not knowing when a new post has been added. We are going to solve this by using notifications.

Back in the DataService.swift file modify the loadPosts() function by adding:

    func loadPosts() {
        if let postsData = UserDefaults.standard.object(forKey: "posts") as? Data {

            if let postsArray = NSKeyedUnarchiver.unarchiveObject(with: postsData) as? [Post] {
                _loadedPosts = postsArray
            }
        }

        NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: "postsLoaded"), object: nil))
    }

What we are doing here is using Notification Center to signal whenever this function is called. So it sends a signal out that posts have been loaded, and now we need to implement the listener in the ViewController.swift file that contains the Table View.

We can remove all the test data we used before as well as deleting var posts = [Post]() since we will be using the data from the singleton now. Modify the viewDidLoad() function as follows:

   override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self

        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.onPostsLoaded(_:)), name: NSNotification.Name(rawValue: "postsLoaded"), object: nil)
    }

We have deleted the test data, and added the Notification Observer. This function is listening for the signal sent by the function we created just prior in the loadPosts() function. When it receives the signal, it will then call the function onPostsLoaded which we will create now. Below your other functions add:

    func onPostsLoaded(_ notif: AnyObject) {
        tableView.reloadData()
    }

This one is simple enough. Once the observer receives word that new posts have been loaded, it will call this function which will then reload the data.

We deleted the test data and the posts array we were using, so we need to change a couple functions as follows, swapping out posts for DataService.instance.loadedPosts:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let post = DataService.instance.loadedPosts[indexPath.row]
        if let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell") as? PostCell {
            cell.configureCell(post)
            return cell
        }
        return PostCell()
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return DataService.instance.loadedPosts.count
    }

The last thing we can do, before we test that our posts are being saved is prepare the Post class to be encoded and decoded by the Archiver and Un-archiver. Now, when using UserDefaults, you can save and retrieve simple objects very very easily. For example the following works right out of the box:

UserDefaults.standard.set(“Jonny B", forKey: “userNameKey”)

 if let name = defaults.string(forKey: "userNameKey") {
            print(name)
 }

No need to add any encoding or decoding. This works for Strings, integers, booleans, Double, Floats, and URLs.

   let defaults = UserDefaults.standard
        defaults.set("Jonny B", forKey: "userNameKey")
        defaults.set(30, forKey: "age")
        defaults.set(true, forKey: "isAwesome")
        defaults.set("This is a string", forKey: "string")


        if let name = defaults.string(forKey: "userNameKey") {
            print(name)
        }

But when you want to save more complex data such as custom classes you have to be very explicit and tell UserDefaults how to encode and decode each property of the class. So that is what we are going to do next in the Post.swift file.

First off we have to modify the class to inherit from NSObject and NScoding as follows:

class Post: NSObject, NSCoding {

then after the initializer add the following:

   override init() {

    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(self._imagePath, forKey: "imagePath")
        aCoder.encode(self._postDesc, forKey: "description")
        aCoder.encode(self._title, forKey: "title")
    }

    required convenience init?(coder aDecoder: NSCoder) {
        self.init()
        self._imagePath = aDecoder.decodeObject(forKey: "imagePath") as? String
        self._title = aDecoder.decodeObject(forKey: "title") as? String
        self._postDesc = aDecoder.decodeObject(forKey: "description") as? String
    }

We are required to add that override init. Then what we are doing is simply providing keys for each property to be encoded, then upon decoding explicitly stating what type of object they should be decoded to. It looks a little scary, but if you look at just one line at a time, its not too bad.

Now we are ready to test it out! Run it and make sure that you are able to add a picture, set title and description, then press Make Post and it should return to TableView and display the post you just made!

The image in the Table View isn't updated yet because we have not yet implemented that code, but we are getting there. And just to recap what is going on behind the scenes here, when we press Make Post, it is taking the information you input into the image, title, and description fields, creates a new post with that information, then calls the DataService function, addPost() which takes the new post, and adds it to the loadedPosts array in the DataService.

It then saves the entire postsArray which encodes the posts into data and saves it to a UserDefault key. Then loadPosts() is called which retirees the data that was just saved, un-archives it and turns it back into an array of usable posts, at which point we send a notification to the initial View Controller that the loadedPosts have been updated. So it should reload the Table View data. Then we see the newly added posts! Whew! That is quite the rabbit hole eh?

Figure 3.1.55 and Figure 3.1.56

Figure 1.55 SavePosts.png Figure 1.56 SavePostsView.png

The last thing we have to do is get those images working!

Saving and retrieving images

I said earlier, that when I say we are saving an image to UserDefaults, what we are actually saving is a reference to the location of that image we save. So we need a way to get the path to that image that we have saved. In the DataService.swift file, at the very bottom add this function:

    func documentsPathForFileName(_ name: String) -> String {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let fullPath = paths[0] as NSString
        return fullPath.appendingPathComponent(name)
    }

You don’t need to understand everything that is going on here, but basically we are passing in a file name and saying go into my file directory and return to me the path to that file. Then we are appending that path string to the file name we passed in. So for example say I passed in image001.png into this function. It goes and finds the path, then appends that path to my file name and returns user/jonnyb/images/image001.png (or whatever the path would look like).

Next we need to update the saveImageAndCreatePath function as follows:

    func saveImageAndCreatePath(_ image: UIImage) -> String {
        let imgData = UIImagePNGRepresentation(image)
        let imgPath = "image\(Date.timeIntervalSinceReferenceDate).png"
        let fullPath = documentsPathForFileName(imgPath)
        try? imgData?.write(to: URL(fileURLWithPath: fullPath), options: [.atomic])
        return imgPath
    }

Let’s break it down. We pass into this function an actual image of type UIImage. We the turn that image into data. We create an image path and use the Date.timeInterval function to ensure that each time we save an image it will have a unique path name. Then we pass that path into the documentsPathForFileName function we just created and use that path that is returned to write to disc the image data! Then we return the imgPath.

Now lets get ready to retrieve an image from storage by updating the imageForPath function as follows:

func imageForPath(_ path: String) -> UIImage? {
        let fullPath = documentsPathForFileName(path)
        let image = UIImage(named: fullPath)
        return image
    }

In this function we are passing in a path and returning an actual UIImage. We get the fullPath by way of our documentsPathForFileName function and then create an image from the path, then return the image. Not too bad!

Now we are ready to modify our makePostBtnPressed action to work with images, so head on over to the AddPostVC.swift file and modify it as follows:

    @IBAction func makePostBtnPressed(_ sender: UIButton) {

        if let title = titleField.text, let desc = descField.text, let img = postImg.image {
            let imgPath = DataService.instance.saveImageAndCreatePath(img)
            let post = Post(imagePath: imgPath, title: title, description: desc)
            DataService.instance.addPost(post: post)
            dismiss(animated: true, completion: nil)
        }
    }

What we did here was create the variable imgPath and use the saveImageAndCreatePath function in DataService which takes an image, turns that image into data, returns a String that contains the path to that file. Then we use that imgPath string in the initializer of our post to save the path.

Next we need to update the configureCell() function in the PostCell file so that when we reload the data, the image in the cells load the saved image corresponding with each cell. Modify the configureCell function as follows:

    func configureCell(_ post: Post) {
        titleLbl.text = post.title
        descLbl.text = post.postDesc
        postImg.image =          DataService.instance.imageForPath(post.imagePath)
    }

Here we are using the imageForPath function we created to take the imagePath we just saved, and retrieving the data and turning it into a UIImage that will be displayed in the Table View cell.

Finally, add one last thing to add to the viewDidLoad() function in ViewController.swift file. This is so that when we load the TableView we are loading the posts. Add this right above the NotificationCenter Observer.

        DataService.instance.loadPosts()
    }

Wrapping up

And that’s it! I know this was a bit of a journey, but look how much you have learned! You know how to use TableViews, how to encode and decode data using UserDefaults, how to use Notifications, and much more.

If things are still hazy I encourage you to go back over this and read through a few times to cement this knowledge. Way to go, give yourself a pat on the back and keep on learning.

Exercise

Now that you have learned about table views and segues, your exercise for this section is to make it so when you click on one of the table view entries, it takes you to a new view controller detail screen about that entry. Display the information and the picture in a larger format. Happy coding!