Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

12. Creating a Simple iPhone App, Part 1
Written by Tim Condon

In the previous chapters, you created an API and interacted with it using RESTed. However, users expect something a bit nicer to use TIL! The next two chapters show you how to build a simple iOS app that interacts with the API. In this chapter, you’ll learn how to create different models and get models from the database.

At the end of the two chapters, you’ll have an iOS application that can do everything you’ve learned up to this point. It will look similar to the following:

Getting started

To kick things off, download the materials for this chapter. In Terminal, go the directory where you downloaded the materials and type:

cd TILApp
swift run

This builds and runs the TIL application that the iOS app will talk to. You can use your existing TIL app if you like.

Note: This requires that your Docker container for the database is running. See Chapter 6, “Configuring a Database”, for instructions.

Next, open the TILiOS project. TILiOS contains a skeleton application that interacts with the TIL API. It’s a tab bar application with three tabs:

  • Acronyms: view all acronyms, view details about an acronym and add acronyms.
  • Users: view all users and create users.
  • Categories: view all categories and create categories.

The project contains several empty table view controllers ready for you to configure to display data from the TIL API.

Look at the Models group in the project; it provides three model classes:

  • Acronym
  • User
  • Category

You may recognize the models — these match the models found API application! This shows how powerful using the same language for both client and server can be. It’s even possible to create a separate module both projects use so you don’t have to duplicate code. Because of the way Fluent represents parent-child relationships, the Acronym is slightly different. You can solve this with a DTO like CreateAcronymData, which the project also includes.

Viewing the acronyms

The first tab’s table displays all the acronyms. Create a new Swift file in the Utilities group called ResourceRequest.swift. Open the file and create a type to manage making resource requests:

// 1
struct ResourceRequest<ResourceType>
  where ResourceType: Codable {
  // 2
  let baseURL = "http://localhost:8080/api/"
  let resourceURL: URL

  // 3
  init(resourcePath: String) {
    guard let resourceURL = URL(string: baseURL) else {
      fatalError("Failed to convert baseURL to a URL")
    }
    self.resourceURL =
      resourceURL.appendingPathComponent(resourcePath)
  }
}

Here’s what this does:

  1. Define a generic ResourceRequest type whose generic parameter must conform to Codable.
  2. Set the base URL for the API. This uses localhost for now. Note that this requires you to disable ATS (App Transport Security) in the app’s Info.plist. This is already set up for you in the sample project.
  3. Initialize the URL for the particular resource.

Next, you need a way to fetch all instances of a particular resource type. Add the following method after init(resourcePath:):

// 1
func getAll(
  completion: @escaping
    (Result<[ResourceType], ResourceRequestError>) -> Void
) {
  // 2
  let dataTask = URLSession.shared
    .dataTask(with: resourceURL) { data, _, _ in
      // 3
      guard let jsonData = data else {
        completion(.failure(.noData))
          return
      }
      do {
        // 4
        let resources = try JSONDecoder()
          .decode(
            [ResourceType].self,
            from: jsonData)
        // 5
        completion(.success(resources))
      } catch {
        // 6
        completion(.failure(.decodingError))
      }
    }
    // 7
    dataTask.resume()
}

Here’s what this does:

  1. Define a function to get all values of the resource type from the API. This takes a completion closure as a parameter which uses Swift’s Result type.
  2. Create a data task with the resource URL.
  3. Ensure the response returns some data. Otherwise, call the completion(_:) closure with the appropriate .failure case.
  4. Decode the response data into an array of ResourceTypes.
  5. Call the completion(_:) closure with the .success case and return the array of ResourceTypes.
  6. Catch any errors and return the correct failure case.
  7. Start the dataTask.

Open AcronymsTableViewController.swift and add the following under // MARK: - Properties:

// 1
var acronyms: [Acronym] = []
// 2
let acronymsRequest =
  ResourceRequest<Acronym>(resourcePath: "acronyms")

Here’s what this does:

  1. Declare an array of acronyms. These are the acronyms the table displays.
  2. Create a ResourceRequest for acronyms.

Getting the acronyms

Whenever the view appears on screen, the table view controller calls refresh(_:). Replace the implementation of refresh(_:) with the following:

// 1
acronymsRequest.getAll { [weak self] acronymResult in
  // 2
  DispatchQueue.main.async {
    sender?.endRefreshing()
  }

  switch acronymResult {
  // 3
  case .failure:
    ErrorPresenter.showError(
      message: "There was an error getting the acronyms", 
      on: self)
  // 4
  case .success(let acronyms):
    DispatchQueue.main.async { [weak self] in
      guard let self = self else { return }
      self.acronyms = acronyms
      self.tableView.reloadData()
    }
  }
}

Here’s what this does:

  1. Call getAll(completion:) to get all the acronyms. This returns a result in the completion closure.
  2. As the request is complete, call endRefreshing() on the refresh control.
  3. If the fetch fails, use the ErrorPresenter utility to display an alert controller with an appropriate error message.
  4. If the fetch succeeds, update the acronyms array from the result and reload the table.

Displaying acronyms

Still in AcronymsTableViewController.swift, update tableView(_:numberOfRowsInSection:) to return the correct number of acronyms by replacing return 1 with the following:

return acronyms.count

Next, update tableView(_:cellForRowAt:) to display the acronyms in the table. Add the following before return cell:

let acronym = acronyms[indexPath.row]
cell.textLabel?.text = acronym.short
cell.detailTextLabel?.text = acronym.long

This sets the title and subtitle text to the acronym short and long properties for each cell.

Build and run and you’ll see your table populated with acronyms from the database:

Viewing the users

Viewing all the users follows a similar pattern. Most of the view controller is already set up. Open UsersTableViewController.swift and under:

var users: [User] = []

add the following:

let usersRequest = 
  ResourceRequest<User>(resourcePath: "users")

This creates a ResourceRequest to get the users from the API. Next, replace the implementation of refresh(_:) with the following:

// 1
usersRequest.getAll { [weak self] result in
  // 2
  DispatchQueue.main.async {
    sender?.endRefreshing()
  }
  switch result {
  // 3
  case .failure:
    ErrorPresenter.showError(
      message: "There was an error getting the users",
      on: self)
  // 4
  case .success(let users):
    DispatchQueue.main.async { [weak self] in
      guard let self = self else { return }
      self.users = users
      self.tableView.reloadData()
    }
  }
}

Here’s what this does:

  1. Call getAll(completion:) to get all the users. This returns a result in the completion closure.
  2. As the request is complete, call endRefreshing() on the refresh control.
  3. If the fetch fails, use the ErrorPresenter utility to display an alert view with an appropriate error message.
  4. If the fetch succeeds, update the users array from the result and reload the table.

Build and run. Go to the Users tab and you’ll see the table populated with users from your database:

Viewing the categories

Follow a similar pattern to view all the categories. Open CategoriesTableViewController.swift and under:

var categories: [Category] = []

add the following:

let categoriesRequest =
  ResourceRequest<Category>(resourcePath: "categories")

This sets up a ResourceRequest to get the categories from the API. Next, replace the implementation of refresh(_:) with the following:

// 1
categoriesRequest.getAll { [weak self] result in
  // 2
  DispatchQueue.main.async {
    sender?.endRefreshing()
  }
  switch result {
  // 3
  case .failure:
    let message = "There was an error getting the categories"
    ErrorPresenter.showError(message: message, on: self)
  // 4
  case .success(let categories):
    DispatchQueue.main.async { [weak self] in
      guard let self = self else { return }
      self.categories = categories
      self.tableView.reloadData()
    }
  }
}

Here’s what this does:

  1. Call getAll(completion:) to get all the categories. This returns a result in the completion closure.
  2. As the request is complete, call endRefreshing() on the refresh control.
  3. If the fetch fails, use the ErrorPresenter utility to display an alert view with an appropriate error message.
  4. If the fetch succeeds, update the categories array from the result and reload the table.

Build and run. Go to the Categories tab and you’ll see the table populated with categories from the TIL application:

Creating users

In the TIL API, you must have a user to create acronyms, so set up that flow first. Open ResourceRequest.swift and add a new method at the bottom of ResourceRequest to save a model:

// 1
func save<CreateType>(
  _ saveData: CreateType,
  completion: @escaping 
    (Result<ResourceType, ResourceRequestError>) -> Void
) where CreateType: Codable {
  do {
    // 2
    var urlRequest = URLRequest(url: resourceURL)
    // 3
    urlRequest.httpMethod = "POST"
    // 4
    urlRequest.addValue(
      "application/json",
      forHTTPHeaderField: "Content-Type")
    // 5
    urlRequest.httpBody =
      try JSONEncoder().encode(saveData)
    // 6
    let dataTask = URLSession.shared
      .dataTask(with: urlRequest) { data, response, _ in
        // 7
        guard
          let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200,
          let jsonData = data
          else {
            completion(.failure(.noData))
            return
        }

        do {
          // 8
          let resource = try JSONDecoder()
            .decode(ResourceType.self, from: jsonData)
          completion(.success(resource))
        } catch {
          // 9
          completion(.failure(.decodingError))
        }
      }
    // 10
    dataTask.resume()
  // 11
  } catch {
    completion(.failure(.encodingError))
  }
}

Here’s what the new method does:

  1. Declare a method save(_:completion:) that takes a generic Codable type to save and a completion handler that takes the save result. This uses a generic type instead of ResourceRequest because the save Acronym API uses CreateAcronymData instead of Acronym.
  2. Create a URLRequest for the save request.
  3. Set the HTTP method for the request to POST.
  4. Set the Content-Type header for the request to application/json so the API knows there’s JSON data to decode.
  5. Set the request body as the encoded save data.
  6. Create a data task with the request.
  7. Ensure there’s an HTTP response. Check the response status is 200 OK, the code returned by the API upon a successful save. Ensure there’s data in the response body.
  8. Decode the response body into the resource type. Call the completion handler with a success result.
  9. Catch a decode error and call the completion handler with a failure result.
  10. Start the data task.
  11. Catch any encoding errors from try JSONEncoder().encode(resourceToSave) and call the completion handler with a failure result.

Next, open CreateUserTableViewController.swift and replace the implementation of save(_:) with the following:

// 1
guard 
  let name = nameTextField.text,
  !name.isEmpty 
  else {
    ErrorPresenter
      .showError(message: "You must specify a name", on: self)
    return
}

// 2
guard 
  let username = usernameTextField.text,
  !username.isEmpty 
  else {
    ErrorPresenter.showError(
      message: "You must specify a username",
      on: self)
    return
}

// 3
let user = User(name: name, username: username)
// 4
ResourceRequest<User>(resourcePath: "users")
  .save(user) { [weak self] result in
    switch result {
    // 5
    case .failure:
      let message = "There was a problem saving the user"
      ErrorPresenter.showError(message: message, on: self)
    // 6
    case .success:
      DispatchQueue.main.async { [weak self] in
        self?.navigationController?
          .popViewController(animated: true)
      }
    }
}

Here’s what this does:

  1. Ensure the name text field contains a non-empty string.
  2. Ensure the username text field contains a non-empty string.
  3. Create a new user from the provided data.
  4. Create a ResourceRequest for User and call save(_:completion:).
  5. If the save fails, display an error message.
  6. If the save succeeds, return to the previous view: the users table.

Build and run. Go to the Users tab and tap the + button to open the Create User screen. Fill in the two fields and tap Save.

If the save succeeds, the screen closes and the new user appears in the table:

Creating acronyms

Now that you have the ability to create users, it’s time to implement creating acronyms. After all, what good is an acronym dictionary app if you can’t add to it.

Selecting users

When you create an acronym with the API, you must provide a user ID. Asking a user to remember and input a UUID isn’t a good user experience! The iOS app should allow a user to select a user by name.

Open CreateAcronymTableViewController.swift and create a new method under viewDidLoad() to populate the User cell in the create acronym form with a default user:

func populateUsers() {
  // 1
  let usersRequest =
    ResourceRequest<User>(resourcePath: "users")

  usersRequest.getAll { [weak self] result in
    switch result {
    // 2
    case .failure:
      let message = "There was an error getting the users"
      ErrorPresenter
        .showError(message: message, on: self) { _ in
          self?.navigationController?
            .popViewController(animated: true)
        }
    // 3
    case .success(let users):
      DispatchQueue.main.async { [weak self] in
        self?.userLabel.text = users[0].name
      }
      self?.selectedUser = users[0]
    }
  }
}

Here’s what this does:

  1. Get all users from the API.
  2. Show an error if the request fails. Return from the create acronym view when the user dismisses the alert controller. This uses the dismissAction on showError(message:on:dismissAction:).
  3. If the request succeeds, set the user field to the first user’s name and update selectedUser.

At the end of viewDidLoad() add the following:

populateUsers()

Your app’s user can tap the USER cell to select a different user for creating an acronym. This gesture opens the Select A User screen.

Open SelectUserTableViewController.swift. Under:

var users: [User] = []

add the following:

var selectedUser: User

This property holds the selected user. Next, in init?(coder:selectedUser:) assign the provided user to the new property before super.init(coder: coder):

self.selectedUser = selectedUser

Next, add the following implementation to loadData() so the table displays the users when the view loads:

// 1
let usersRequest =
  ResourceRequest<User>(resourcePath: "users")

usersRequest.getAll { [weak self] result in
  switch result {
  // 2
  case .failure:
    let message = "There was an error getting the users"
    ErrorPresenter
      .showError(message: message, on: self) { _ in
        self?.navigationController?
          .popViewController(animated: true)
      }
    // 3
  case .success(let users):
    self?.users = users
    DispatchQueue.main.async { [weak self] in
      self?.tableView.reloadData()
    }
  }
}

Here’s what this does:

  1. Get all the users from the API.
  2. If the request fails, show an error message. Return to the previous view once a user taps dismiss on the alert.
  3. If the request succeeds, save the users and reload the table data.

In tableView(_:cellForRowAt:) before return cell add the following:

if user.name == selectedUser.name {
  cell.accessoryType = .checkmark
} else {
  cell.accessoryType = .none
}

This compares the current cell against the currently selected user. If they are the same, set a checkmark on that cell.

SelectUserTableViewController uses an unwind segue to navigate back to the CreateAcronymTableViewController when a user taps a cell.

Add the following implementation of prepare(for:) in SelectUserTableViewController to set the selected user for the segue:

// 1
if segue.identifier == "UnwindSelectUserSegue" {
  // 2
  guard
    let cell = sender as? UITableViewCell,
    let indexPath = tableView.indexPath(for: cell)
    else {
      return
  }
  // 3
  selectedUser = users[indexPath.row]
}

Here’s what this does:

  1. Verify this is the expected segue.
  2. Get the index path of the cell that triggered the segue.
  3. Update selectedUser to the user for the tapped cell.

The unwind segue calls updateSelectedUser(_:) in CreateAcronymTableViewController. Open CreateAcronymTableViewController.swift and add the following implementation to the updateSelectedUser(_:):

// 1
guard let controller = segue.source 
  as? SelectUserTableViewController 
  else {
    return
}
// 2
selectedUser = controller.selectedUser
userLabel.text = selectedUser?.name

Here’s what this does:

  1. Ensure the segue came from SelectUserTableViewController.
  2. Update selectedUser with the new value and update the user label.

Finally, replace the implementation for makeSelectUserViewController(_:) with the following:

guard let user = selectedUser else {
  return nil
}
return SelectUserTableViewController(
  coder: coder,
  selectedUser: user)

This ensures we have a selected user and creates a SelectUserTableViewController with that user. When a user taps the user field, the app uses the @IBSegueAction to create the select user screen.

Build and run. In the Acronyms tab, tap + to bring up the Create An Acronym view. Tap the user row and the application opens the Select A User view, allowing you to select a user.

When you tap a user, that user is then set on the Create An Acronym page:

Saving acronyms

Now that you can successfully select a user, it’s time to implement saving the new acronym to the database. Replace the implementation of save(_:) in CreateAcronymTableViewController.swift with the following:

// 1
guard
  let shortText = acronymShortTextField.text,
  !shortText.isEmpty 
  else {
    ErrorPresenter.showError(
      message: "You must specify an acronym!",
      on: self)
    return
}
guard
  let longText = acronymLongTextField.text,
  !longText.isEmpty 
  else {
    ErrorPresenter.showError(
      message: "You must specify a meaning!",
      on: self)
    return
}
guard let userID = selectedUser?.id else {
  let message = "You must have a user to create an acronym!"
  ErrorPresenter.showError(message: message, on: self)
  return
}

// 2
let acronym = Acronym(
  short: shortText,
  long: longText,
  userID: userID)
let acronymSaveData = acronym.toCreateData()
// 3
ResourceRequest<Acronym>(resourcePath: "acronyms")
  .save(acronymSaveData) { [weak self] result in
    switch result {
    // 4
    case .failure:
      let message = "There was a problem saving the acronym"
      ErrorPresenter.showError(message: message, on: self)
    // 5
    case .success:
      DispatchQueue.main.async { [weak self] in
        self?.navigationController?
          .popViewController(animated: true)
      }
    }
}

Here are the steps to save the acronym:

  1. Ensure the user has filled in the acronym and meaning. Check the selected user is not nil and the user has a valid ID.
  2. Create a new Acronym from the supplied data. Convert the acronym to CreateAcronymData using the toCreateData() helper method.
  3. Create a ResourceRequest for Acronym and call save(_:) using the create data.
  4. If the save request fails, show an error message.
  5. If the save request succeeds, return to the previous view: the acronyms table.

Build and run. On the Acronyms tab, tap +. Fill in the fields to create an acronym and tap Save.

The saved acronym appears in the table:

Where to go from here?

In this chapter, you learned how to interact with the API from an iOS application. You saw how to create different models and retrieve them from the API. You also learned how to manage the required relationships in a user-friendly way.

The next chapter builds upon this to view details about a single acronym. You’ll also learn how to implement the rest of the CRUD operations. Finally, you’ll see how to set up relationships between categories and acronyms.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.