Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · 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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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)
  }
}
// 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()
}
// 1
var acronyms: [Acronym] = []
// 2
let acronymsRequest =
  ResourceRequest<Acronym>(resourcePath: "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()
    }
  }
}

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
let acronym = acronyms[indexPath.row]
cell.textLabel?.text = acronym.short
cell.detailTextLabel?.text = acronym.long

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] = []
let usersRequest = 
  ResourceRequest<User>(resourcePath: "users")
// 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()
    }
  }
}

Viewing the categories

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

var categories: [Category] = []
let categoriesRequest =
  ResourceRequest<Category>(resourcePath: "categories")
// 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()
    }
  }
}

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))
  }
}
// 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)
      }
    }
}

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.

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]
    }
  }
}
populateUsers()
var users: [User] = []
var selectedUser: User
self.selectedUser = selectedUser
// 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()
    }
  }
}
if user.name == selectedUser.name {
  cell.accessoryType = .checkmark
} else {
  cell.accessoryType = .none
}
// 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]
}
// 1
guard let controller = segue.source 
  as? SelectUserTableViewController 
  else {
    return
}
// 2
selectedUser = controller.selectedUser
userLabel.text = selectedUser?.name
guard let user = selectedUser else {
  return nil
}
return SelectUserTableViewController(
  coder: coder,
  selectedUser: user)

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)
      }
    }
}

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.

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.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now