The Actors app

The next example app I am going to show will display information about actors that comes from the IMDB web service. The app will interact with that web service to download information about movies and TV series and the actors that appear in them.

I will be using this example to demonstrate a number of small but useful ideas in iOS programming.

You can download the project code for this example by clicking this link.

Tab Bar controllers

So far in this course we have been using navigation controllers to handle multiple views in an app. An alternative to using a navigation controller is to use a tab bar controller. This type of controller can contain an array of multiple views. At the bottom of each of the views the controller will display a tab bar containing icons that users can click on to move from one view to another.

Chapter four covers the basics of setting up a tab bar controller. Also, in the recording above I give a short demonstation at the start of the lecture to show the basics of setting up a tab bar controller.

Sharing data across multiple tabs

An important issue you will need to think about when using a tab bar controller is sharing data across tabs. This is an issue in the Actors app, since one of the features I wanted to support is a favorites list. In the search tab of the app users can search for movies and TV series and then view the actors in those shows. In the view that shows information about an actor I placed a favorite button that allows the user to add this actor to their list of favorites. When the user clicks the favorites tab in the tab bar controller they can view the list of all of their favorites and pull up the actor view for any actor in their favorites list. To make this all work we need a way to share the list of favorites across several different views.

To handle this in a reasonable way I started by creating an AppData class that would store the list of Favorites:

class AppData {
    var favorites : [Favorite]
    static let instance : AppData = AppData()

    let itemArchiveURL: URL = {
        let documentsDirectories =
            FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentDirectory = documentsDirectories.first!
        return documentDirectory.appendingPathComponent("favorites.plist")
    }()

    init() {
        do {
            let data = try Data(contentsOf: itemArchiveURL)
            let unarchiver = PropertyListDecoder()
            let loc = try unarchiver.decode(Array<Favorite>.self, from: data)
            favorites =  loc
        } catch {
            favorites = []
        }

        let nc = NotificationCenter.default
        nc.addObserver(self,
                       selector: #selector(saveChanges),
                       name: UIApplication.didEnterBackgroundNotification,
                       object: nil)
    }

    @objc func saveChanges() -> Bool {
        do {
            let encoder = PropertyListEncoder()
            let data = try encoder.encode(favorites)
            try data.write(to: itemArchiveURL)
            return true
        } catch {
            return false
        }
    }
}

To ensure that the app uses exactly one instance of this class, I used a common programming pattern known as the singleton pattern. In the class I placed a static property that will store a single instance of the AppData class. This single instance will get created at app startup time, and all of the code in the app will use this single AppData object whenever it needs to work with the favorites list. To access the favorites list at any point in the app we will use the code

AppData.instance.favorites

The advantage of doing things this way is that we can now access the favorites list anywhere we want in the app without any further special setup. We don't have to worry about passing the favorites list around from view to view, since that list will always we accessible via the code above.

Note also that I put code in this class to load the favorites list from a file when we create the AppData object and also automatically save that data whenever the app enters the background. This will allow the favorites list to persist across runs of the app.

Putting a table view in another view

Up to this point when have needed to display a list of things in an app we have used a UITableViewController. With that style of controller the table view is the entire view. When we have needed to put additional things in the view along with the table view we have been able to put those things in the navigation bar at the top of the view.

In some cases we will need to display both a table view and other things in the same view. For example, the search view in the Actors app contains a text field where the user can enter a search term, a couple of buttons below that the user can click to launch the search, and a table view to display the results.

A table view is just like any other user interface element in that if we need to display a table view in a view along with other elements we can drag a table view from the library and drop it into the view along with the other elements.

A table view is also a little different than other elements in that a table view will want us to implement various tableView() methods in the view controller. Setting those up takes a little bit of extra work. Here is the first part of the code for the view controller for this search view:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet var searchField : UITextField!
    var reply : SearchReply = SearchReply()
    var selected : Int = 0

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

The first special thing we have to do here is to have our view implement the UITableViewDelegate and UITableViewDataSource protocols. We need to implement these protocols so we can set ourselves up as the delegate for the table view and also provide data to it. Implementing these protocols also makes it possible for us to override the appropriate tableView() methods.

Simply implementing these protocols is not enough. We also need to access the table view itself and tell it who is going to act as its delegate and its data source. To do this, we first set up an IBOutlet in the usual way and link it to the table view in the storyboard. Then, in the viewDidLoad() method we talk to the table view and tell it that we will be acting as its delegate and data source. From that point forward the table view will be calling our tableView() methods to answer questions about what it should be doing.

Working with images

In the actors app users can view information about actors. Part of this information includes a picture of the actor.

The images that the app displays are UIImageView objects. The pictures we display in the image object are downloaded from the IMDB web service. Here is some code to fetch one of these pictures and display it in the image view:

func fetchImage() {
    let url = URL(string: bio.image)

    let task = URLSession.shared.dataTask(with: url!) { data, response, error in
        DispatchQueue.main.async {
            self.actorImage.image = UIImage(data:data!)
        }
    }
    task.resume()
}

When we navigate to this view the code will start by downloading the actor's bio from IMDB. One of the properties in that bio object is a URL we can use to download the actor's picture. Once we have the URL we can set up a simple data task to fetch the image. The data that the query returns is image data that we can load directly into a UIImage object and place in the UIImageView.

You can read more about images in chapter 15 of the text.