Building an iOS app using UIKit and storyboards
The focus of this book is ultimately on the Swift programming language itself, as opposed to the use of the language to produce apps for Apple platforms or to build server-side services. That being said, it can’t be ignored that the vast majority of the Swift code being written is for building, or building upon, iOS and iPadOS apps.
In this recipe, we will take a brief look at how we can interact with Apple’s Cocoa Touch frameworks using Swift and begin to build and create our very own iOS app.
Cocoa Touch is the name given to the collection of UI frameworks available as part of the iOS SDK. Its name derives from the Cocoa framework on macOS, which provides UI elements for macOS apps. While Cocoa on macOS is a framework in its own right, Cocoa Touch is a collection of frameworks that provide UI elements for iOS apps and handle the app’s life cycle; the core of these frameworks is UIKit.
Getting ready
First, we’ll need to create a new iOS app project:
- From the Xcode menu, choose File, then New.
- From the dialog box that opens, choose App from the iOS tab:
Figure 7.1 – Choosing a template
The next dialog box asks you to enter details about your app, pick a product name and organization name, and add an organization identifier in reverse DNS style.
Reverse DNS style means to take a website that you or your company owns and reverse the order of the domain name components. So, for example, http://maps.google.com becomes com.google.maps
:
Figure 7.2 – Options for a new project
Pay attention to the preceding choices as not all of them may be selected by default. For this recipe, the ones that are important to us are Interface and Include Tests, both of which we’ll cover later on in this chapter when we look at unit testing with XCTest and UI testing with XCUITest.
- Once you’ve chosen a save location on your Mac, you will be presented with the following Xcode layout:
Figure 7.3 – New project template
Here, we have the start of our project – it’s not much, but it’s where all new iOS apps begin.
From this menu, press Product | Run. Xcode will now compile and run your app in a simulator.
How to do it...
Continuing from a previous recipe, we’ll build our app based on data that is returned from the public GitHub API:
- In the file explorer, click on
Main.storyboard
; this view is a representation of what the app will look like and is called Interface Builder. At the moment, there is only one blank screen visible, which matches what the app looked like when we ran it earlier. This screen represents a view controller object; as the name suggests, this is an object that controls views. - We will display our list of repositories in a table. We actually want to create a view controller class that is a subclass of
UITableViewController
. So, from the menu, choose File, then New, and select a Cocoa Touch Class template:
Figure 7.4 – New file template
- We will be displaying repositories in this view controller, so let’s call it
ReposTableViewController
. Specify that it’s a subclass ofUITableViewController
and ensure that the language is Swift:
Figure 7.5 – New filename and subclass
Now that we have created our view controller class, let’s switch back to Main.storyboard
and delete the blank view controller that was created for us.
- From the object library, find the
Table View Controller
option and drag it into the Interface Builder editor:
Figure 7.6 – Object library
- Now that we have a table view controller, we want this controller to be part of our custom subclass. To do this, select the controller, go into the class inspector, enter
ReposTableViewController
as theClass
type, and press Enter:
Figure 7.7 – Custom class inspector
Although we have the view controller that will be displaying the repository names, when a user selects a repository, we want to present a new view controller that will show details about that particular repository. We will cover what type of view controller that is and how we present it shortly, but first, we need a mechanism for navigating between view controllers.
If you have ever used an iOS app, you will be familiar with the standard push and pop way of navigating between views. The following screenshot shows an app in the middle of that transition:
Figure 7.8 – Push and pop view controller
The management of these view controllers, as well as their presentation and dismissal transitions, is handled by a navigation controller, which is provided by Cocoa Touch in the form of UINavigationController
. Let’s take a look:
- To place our view controller inside a navigation controller, select ReposTableViewController in Interface Builder. Then, from the Xcode menu, go to Editor | Embed In | Navigation Controller.
This will add a navigation controller to the storyboard and set the selected view controller as its root view controller (if there is an existing view controller already inside the storyboard from the initial project we created, this can be highlighted and deleted).
- Next, we need to define which view controller is initially on the screen when the app starts. Select Navigation Controller on the left-hand side of the screen and within the property inspector, select Is Initial View Controller. You will see that an entry arrow will point toward the navigation controller on the left, indicating that it will be shown initially.
- With this set up, we can start working on our
ReposTableViewController
by selecting it from the File navigator menu.When we created our view controller, the template gave us a bunch of code, with some of it commented out. The first method that the template provides is
viewDidLoad
. This is part of a set of methods that cover the life cycle of the root view that the view controller is managing. Full details about the view life cycle and its relevant method calls can be found at https://developer.apple.com/documentation/uikit/view_controllers/displaying_and_managing_views_with_a_view_controller/#3370691.viewDidLoad
is fired quite early on in the view controller’s life cycle but before the view controller is visible to the user. Due to this, it is a good place to configure the view and retrieve any information that you want to present to the user. - Let’s give the view controller a title:
class ReposTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() self.title = "Repos" } //... }
Now, if you build and run the app, you’ll see a navigation bar with the title we just added programmatically.
- Next, we’ll fetch and display a list of GitHub repositories. Implement the following snippet of code in order to fetch a list of repositories for a specific user:
@discardableResult internal func fetchRepos(forUsername username: String, completionHandler: @escaping (FetchReposResult) -> Void)-> URLSessionDataTask? { let urlString = "https://api.github.com/users/\ (username)/repos" guard let url = URL(string: urlString) else { return nil } var request = URLRequest(url: url) request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept") let task = session.dataTask(with: request) { (data, response, error) in // First unwrap the optional data guard let data = data else { let error = ResponseError. requestUnsuccessful completionHandler(.failure(error)) return } do { let decoder = JSONDecoder() let responseObject = try decoder. decode([Repo].self, from: data) completionHandler(.success(responseObject)) } catch { completionHandler(.failure(error)) } } task.resume() return task }
- Let’s add the following highlighted code to the top of the file, before the start of the class definition. We will also add a session property to the view controller, which is needed for the network request:
import UIKit struct Repo: Codable { let name: String? let url: URL? enum CodingKeys: String, CodingKey { case name = "name" case url = "html_url" } } enum FetchReposResult { case success([Repo]) case failure(Error) } enum ResponseError: Error { case requestUnsuccessful case unexpectedResponseStructure } class ReposTableViewController: UITableViewController { internal var session = URLSession.shared //... }
You may notice something a little different about the preceding functions since we’re now making full use of Swift’s
Codable
protocol. WithCodable
, we can map the JSON response from our API straight to our struct models, without the need to convert this into a dictionary and then iterate each key-value pair to a property. - Next, in our table view, each row of the table view will display the name of one of the repositories that we retrieve from the GitHub API. We need a place to store the repositories that we retrieve from the API:
class ReposTableViewController: UITableViewController { internal var session = URLSession.shared internal var repos = [Repo]() //... }
The
repos
array has an initially empty array value, but we will use this property to hold the fetched results from the API.We don’t need to fetch the repository data right now. So, instead, we’ll learn how to provide data to be used in the table view. Let’s get started.
- Let’s create a couple of fake repositories so that we can temporarily populate our table view:
class ReposTableViewController: UITableViewController { let session = URLSession.shared var repos = [Repo]() override func viewDidLoad() { super.viewDidLoad() let repo1 = Repo(name: "Test repo 1", url: URL(string: "http://example.com/repo1")!) let repo2 = Repo(name: "Test repo 2", url: URL(string: "http://example.com/repo2")!) repos.append(contentsOf: [repo1, repo2]) } //... }
The information in a table view is populated from the table view’s data source, which can be any object that conforms to the
UITableViewDataSource
protocol.When the table view is displayed and the user interacts with it, the table view will ask the data source for the information it needs to populate the table view. For simple table view implementations, it is often the view controller that controls the table view that acts as the data source. In fact, when you create a subclass of
UITableViewController
, as we have, the view controller already conforms toUITableViewDataSource
and is assigned as the table view’s data source. - Some of the methods defined in
UITableViewDataSource
were created as part of theUITableViewController
template; the three we will take a look at are as follows:override func numberOfSections(in tableView: UITableView) -> Int { // #warning Incomplete implementation, return the number of sections return 0 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows return 0 } /* override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "RepoCell", for: indexPath) // Configure the cell... return cell } */
Data in a table view can be divided into sections, and information is presented in rows within those sections; information is referenced through
IndexPath
, which consists of a section integer value and a row integer value. - The first thing that the data source methods ask us to provide is the number of sections that the table view will have. Our app will only be displaying a simple list of repositories, and as such, we only need one section, so we will return
1
from this method:override func numberOfSections(in tableView: UITableView) -> Int { return 1 }
- The next thing we have to provide is the number of rows the table view should have for a given section. If we had multiple sections, we could examine the provided section index and return the right number of rows, but since we only have one section, we can return the same number in all scenarios.
We are displaying all the repositories we have retrieved, so the number of rows is simply the number of repositories in the
repos
array:override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return repos.count }
Tip
Notice that in the preceding two functions, we no longer use the return keyword. This is because, starting with Swift 5.1, you can now use implicit returns in functions. As long as your function doesn’t carry ambiguity about what should and should not be returned, the compiler can work this out for you. This allows for more streamlined syntax.
Now that we have told the table view how many pieces of information to display, we must be able to display that information. A table view displays information in a type of view called UITableViewCell
, and this cell is what we have to provide next.
For each index path within the section and row bounds that we have provided, we will be asked to provide a cell that will be displayed by the table view. A table view can be very large in size as it may need to represent a large amount of data. However, there are only a handful of cells that can be displayed to the user at any one time. This is because only a portion of the table view can be visible at any one time:
Figure 7.9 – Table view cell overview
In order to be efficient and prevent your app from slowing down as the user scrolls, the table view can reuse cells that have already been created but have since moved off-screen. Implementing cell reuse happens in two stages:
- Registering the cell’s type with the table view with a reuse identifier.
- Dequeuing a cell for a given reuse identifier. This will return a cell that has moved off-screen or create a new cell if none are available for reuse.
How a cell is registered will depend on how it has been created. If the cell has been created and its subviews have also been laid out in the code, then the cell’s class is registered with the table view through this method on UITableView
:
func register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String)
If the cell has been laid out in .xib
(usually called a “nib” for historical reasons), which is a visual layout file for views that’s similar to a storyboard, then the cell’s nib
is registered with the table view through this method on UITableView
:
func register(_ nib: UINib?, forCellReuseIdentifier identifier: String)
Lastly, cells can be defined and laid out within the table view in a storyboard. One advantage of this approach is that there is no need to manually register the cell, as with the previous two approaches; registering with the table view is free. However, one disadvantage of this approach is that the cell layout is tied to the table view, so it can’t be reused in other table views, unlike the previous two implementations.
Let’s lay out our cell in the storyboard since we will only be using it with one table view:
- Switch to our
Main.storyboard
file and select the table view in ourReposTableViewController
. - In the attributes inspector, change the number of prototype cells to
1
; this will add a cell to the table view in the main window. This cell will define the layout of all the cells that will be displayed in our table view. You should create a prototype cell for each type of cell layout you will need; we are only displaying one piece of information in our table view, so all our cells will be of the same type. - Select a cell in the storyboard. The attributes inspector will switch to showing the attributes for the cell. The cell style will be set to custom, and often, this will be what you want it to be. When you are displaying multiple pieces of information in a cell, you will usually want to create a subclass of
UITableViewCell
, set this to be the cell’s class in the class inspector, and then lay outsubviews
in this custom cell type. However, for this example, we just want to show the name of the repository. Due to this, we can use a basic cell style that just has one text label, without a custom subclass, so choose Basic from the Style dropdown. - We need to set the reuse identifier that we will use to dequeue the cell later, so type an appropriate string, such as
RepoCell
, into the reuse Identifier box of the attributes inspector:
Figure 7.10 – Table view cell identifier
- Now that we have a cell that is registered for reuse with the table view, we can go back to our view controller and complete our conformance with
UITableViewDataSource
. - Our
ReposTableViewController
contains some commented code that was created as part of the template:/* override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "RepoCell", for: indexPath) // Configure the cell... return cell } */
- At this point, you can remove the
/* */
comment signifiers as we are ready to implement this method.This data source method will be called every time the table view needs to place a cell on-screen; this will happen the first time the table is displayed as it needs cells to fill the visible part of the table view. It will also be called when the user scrolls the table view in a way that will reveal a new cell so that it becomes visible.
- Regarding the method’s definition, we can see that we are provided with the table view in question and the index path of the cell that is needed, and we are expected to return
UITableViewCell
. The code provided by the template actually does most of the work for us; we just need to provide the reuse identifier that we set in the storyboard and set the title label of the cell so that we have the name of the correct repository:override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "RepoCell", for: indexPath) // Configure the cell... let repo = repos[indexPath.row] cell.textLabel?.text = repo.name return cell }
The cell’s
textLabel
property is optional because it only exists when the cell’s style is not custom. - Since we’ve now provided everything the table view needs to display our repository information, let’s click on Build and Run and take a look:
Figure 7.11 – Our app’s first run
Great! Now that we have our two test repositories displayed in our table view, let’s replace our test data with real repositories from the GitHub API.
We added our fetchRepos
method earlier, so all we need to do is call this method, set the results to our repos
property, and tell our table view that it needs to reload since the data has changed:
class ReposTableViewController: UITableViewController { internal var session = URLSession.shared internal var repos = [Repo]() override func viewDidLoad() { super.viewDidLoad() title = "Repos" fetchRepos(forUsername: "SwiftProgrammingCookbook"){ [weak self] result in switch result { case .success(let repos): self?.repos = repos case .failure(let error): self?.repos = [] print("There was an error: \(error)") } self?.tableView.reloadData() } } //... }
As we did in previous recipes, we fetched the repositories from the GitHub API and received a result in enum
informing us of whether this was a success or a failure. If it was successful, we store the resulting repository
array in our repos
property. Once we have handled the response, we call the reloadData
method on UITableView
, which instructs the table view to requery its source for cells to display.
We also provided a weak reference to self
in our closure’s capture list to prevent a retain cycle. You can find out more about why this is important in the Passing around functionality with closures recipe of Chapter 1, Swift Fundamentals.
At this point, there is an important consideration that needs to be addressed. The iOS platform is a multithreaded environment, which means that it can do more than one thing at once. This is critical to being able to maintain a responsive UI, while also being able to process data and perform long-running tasks. The iOS system uses queues to manage this work and reserves the “main” queue for any work involving the UI. Therefore, any time you need to interact with the UI, it is important that this work is done from the main queue.
Our fetchRepos
method presents a situation where this might not be true. The fetchRepos
method performs networking, and we provide closure to URLSession
as part of creating a URLSessionDataTask
instance, but there is no guarantee that this closure will be executed on the main thread. Therefore, when we receive a response from fetchRepos
, we need to dispatch the work of handling that response to the main queue to ensure that our updates to the UI happen on the main queue. We can do this using the Dispatch
framework, so we need to import that at the top of the file:
class ReposTableViewController: UITableViewController { let session = URLSession.shared var repos = [Repo]() override func viewDidLoad() { super.viewDidLoad() title = "Repos" fetchRepos(forUsername: "SwiftProgrammingCookbook"){ [weak self] result in DispatchQueue.main.async { switch result { case .success(let repos): self?.repos = repos case .failure(let error): self?.repos = [] print("There was an error: \(error)") } self?.tableView.reloadData() } } } }
We discussed multithreading and the Dispatch
framework in greater depth in Chapter 6, Understanding Concurrency in Swift. So, let’s jump right in:
- Click on Build and Run. After a few seconds, the table view will be filled with the names of various repositories from the GitHub API.
Now that we have repositories being displayed to the user, the next piece of functionality we’ll implement for our app is the ability to tap on a cell and have it display the repository’s GitHub page in a WebView.
Actions triggered by the table view, such as when a user taps on a cell, are provided to the table view’s delegate, which can be anything that conforms to
UITableViewDelegate
. As was the case with the table view’s data source, ourReposTableViewController
already conforms toUITableViewDelegate
because it is a subclass ofUITableViewController
. - If you take a look at the documentation for the
UITableViewDelegate
protocol, you will see a lot of optional methods; this documentation can be found at https://developer.apple.com/reference/uikit/uitableviewdelegate. The one that’s relevant for our purposes is as follows:func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
- This will be called on the table view’s delegate whenever a cell is selected by the user, so let’s implement this in our view controller:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let repo = repos[indexPath.row] let repoURL = repo.url // TODO: Present the repo's URL in a webview }
- For the functionality it provides, we will use
SFSafariViewController
, passing it the repository’s URL. Then, we will pass that view controller to theshow
method, which will present the view controller in the most appropriate way:override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let repo = repos[indexPath.row] guard let repoURL = repo.url else { return } let webViewController = SFSafariViewController( url: repoURL) show(webViewController, sender: nil) }
- Make sure you import
SafariServices
at the top of the file. - Click on Build and Run, and once the repositories are loaded, tap on one of the cells. A new view controller will be pushed onto the screen, and the relevant repository web page will load.
Congratulations – you’ve just built your first app and it looks great!
How it works...
Currently, our app fetches repositories from a specific, hardcoded GitHub username. It would be great if, rather than hardcoding the username, the user of the app could enter the GitHub username that the repositories will be retrieved for. So, let’s add this functionality:
- First, we need a way for the user to enter their GitHub username; the most appropriate way to allow a user to enter a small amount of text is through the use of
UITextField
. - In the main storyboard, find Text Field in the object library, drag it over to the main window, and drop it onto the navigation bar of our
ReposTableViewController
. Now, you need to increase the width of Text Field. For now, just hardcode this to around300px
by highlighting Text Field and selecting the Size Inspector option:
Figure 7.12 – Adding a UITextField instance
Like a table view, UITextField
communicates user events through a delegate, which needs to conform to UITextFieldDelegate
.
- Let’s switch back to
ReposTableViewController
and add conformance toUITextFieldDelegate
; it is a common practice to add protocol conformance to an extension, so add the following at the bottom ofReposTableViewController
:extension ReposTableViewController: UITextFieldDelegate { }
- With this conformance in place, we need to set our view controller to be the delegate of
UITextField
. Head back over to the main storyboard, select the text field, and then open Connections Inspector. You will see that the text field has an outlet for its delegate property. Now, click, hold, and drag from the circle next to our delegate over to the symbol representing ourRepos Table
View Controller
:
Figure 7.13 – UITextField with IBOutlet
The delegate outlet should now have a value:
Figure 7.14 – UITextField delegate outlet
By taking a look at the documentation for UITextFieldDelegate
, we can see that the textFieldShouldReturn
method is called when the user presses the Return button on their keyboard after entering text, so this is the method we will implement.
- Let’s switch back to our
ReposTableViewController
and implement this method in our extension:extension ReposTableViewController: UITextFieldDelegate { public func textFieldShouldReturn(_ textField: UITextField) -> Bool { // TODO: Fetch repositories from username entered into text field // TODO: Dismiss keyboard // Returning true as we want the system to have the default behaviour return true } }
- Since repositories will be fetched here instead of when the view is loaded, let’s move the code from
viewDidLoad
to this method:extension ReposTableViewController: UITextFieldDelegate { public func textFieldShouldReturn(_ textField: UITextField) -> Bool { // If no username, clear the data guard let enteredUsername = textField.text else { repos.removeAll() tableView.reloadData() return true } // Fetch repositories from username entered into text field fetchRepos(forUsername: enteredUsername) { [weak self] result in switch result { case .success(let repos): self?.repos = repos case .failure(let error): self?.repos = [] print("There was an error: \(error)") } DispatchQueue.main.async { self?.tableView.reloadData() } } // TODO: Dismiss keyboard // Returning true as we want the system to have the default behaviour return true } }
Cocoa Touch implements the programming design pattern MVC, which stands for Model View Controller; it is a way of structuring your code to keep its elements reusable, with well-defined responsibilities. In the MVC pattern, all code related to displaying information falls broadly into three areas of responsibility:
- Model objects hold the data that will eventually be displayed on the screen; this might be data that was retrieved from the network or device, or that was generated when the app was running. These objects may be used in multiple places in the app, where different view representations of the same data may be required.
- View objects represent the UI elements that are displayed on the screen; these may just display information that they are provided, or capture input from the user. View objects can be used in multiple places where the same visual element is needed, even if it is showing different data.
- Controller objects act as bridges between the models and the views; they are responsible for obtaining the relevant model objects and for providing the data to be displayed to the right view objects at the right time. Controller objects are also responsible for handling user input from the views and updating the model objects as needed:
Figure 7.15 – MVC overview
With regard to displaying web content, our app provides us with a number of options:
WKWebView
, provided by the WebKit framework, is a view that uses the latest rendering and JavaScript engine for loading and displaying web content. While it is newer, it is less mature in some respects and has issues with caching content.SFSafariViewController
, provided by theSafariServices
framework, is a view controller that displays web content and also provides many of the features that are available in Mobile Safari, including sharing and adding to reading lists and bookmarks. It also provides a convenient button for opening the current site in Mobile Safari.
There’s more...
The last thing we need to do is dismiss the keyboard. Cocoa Touch refers to the object that is currently receiving user events as the first responder. Currently, this is the text field.
It’s the act of the text field becoming the first responder that caused the keyboard to appear on-screen. Therefore, to dismiss the keyboard, the text field just needs to resign its place as first responder:
extension ReposTableViewController: UITextFieldDelegate { public func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } }
Now, click on Build and Run. At this point, you can enter any GitHub account name in the text field to retrieve a list of its public repositories. Note that if your Xcode simulator doesn’t have soft keyboard
enabled, you can just press Enter on your physical keyboard to search for the repo.
See also
For more information regarding what was covered in this recipe, please refer to the following links:
- Apple documentation for GCD: https://developer.apple.com/documentation/dispatch
- Apple documentation for UIKit: https://developer.apple.com/documentation/uikit