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

13. Creating a Simple iPhone App, Part 2
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 chapter, you created an iPhone application that can create users and acronyms. In this chapter, you’ll expand the app to include viewing details about a single acronym. You’ll also learn how to perform the final CRUD operations: edit and delete. Finally, you’ll learn how to add acronyms to categories.

Note: This chapter expects you have a TIL Vapor application running. It also expects you’ve completed the iOS app from the previous chapter. If not, grab the starter projects and pick up from there. See Chapter 12, “Creating a Simple iPhone App Part 1”, for details on how to run the Vapor application.

Getting started

In the previous chapter, you learned how to view all the acronyms in a table. Now, you want to show all the information about a single acronym when a user taps a table cell. The starter project contains the necessary plumbing; you simply need to implement the details.

Open AcronymsTableViewController.swift. Replace the implementation for makeAcronymsDetailTableViewController(_:) with the following:

// 1
guard let indexPath = tableView.indexPathForSelectedRow else {
  return nil
}
// 2
let acronym = acronyms[indexPath.row]
// 3
return AcronymDetailTableViewController(
  coder: coder, 
  acronym: acronym)

You run this code when a user taps an acronym. The code does the following:

  1. Ensure that there’s a selected index path.
  2. Get the acronym corresponding to the tapped row.
  3. Create an AcronymDetailTableViewController using the selected acronym.

Create a new Swift file called AcronymRequest.swift in the Utilities group. Open the new file and create a new type to represent an acronym resource request:

struct AcronymRequest {
  let resource: URL

  init(acronymID: UUID) {
    let resourceString =
      "http://localhost:8080/api/acronyms/\(acronymID)"
    guard let resourceURL = URL(string: resourceString) else {
      fatalError("Unable to createURL")
    }
    self.resource = resourceURL
  }
}

This sets the resource property to the URL for that acronym. At the bottom of AcronymRequest, add a method to get the acronym’s user:

func getUser(
  completion: @escaping (
    Result<User, ResourceRequestError>
  ) -> Void
) {
  // 1
  let url = resource.appendingPathComponent("user")

  // 2
  let dataTask = URLSession.shared
    .dataTask(with: url) { data, _, _ in
      // 3
      guard let jsonData = data else {
        completion(.failure(.noData))
        return
      }
      do {
      // 4
        let user = try JSONDecoder()
          .decode(User.self, from: jsonData)
        completion(.success(user))
      } catch {
        // 5
        completion(.failure(.decodingError))
      }
    }
  // 6
  dataTask.resume()
}

Here’s what this does:

  1. Create the URL to get the acronym’s user.
  2. Create a data task using the shared URLSession.
  3. Check the response contains a body, otherwise fail with the appropriate error.
  4. Decode the response body into a User object and call the completion handler with the success result.
  5. Catch any decoding errors and call the completion handler with the failure result.
  6. Start the network task.

Next, below getUser(completion:), add the following method to get the acronym’s categories:

func getCategories(
  completion: @escaping (
    Result<[Category], ResourceRequestError>
  ) -> Void
) {
  let url = resource.appendingPathComponent("categories")
  let dataTask = URLSession.shared
    .dataTask(with: url) { data, _, _ in
      guard let jsonData = data else {
        completion(.failure(.noData))
        return
      }
      do {
        let categories = try JSONDecoder()
          .decode([Category].self, from: jsonData)
        completion(.success(categories))
      } catch {
        completion(.failure(.decodingError))
      }
    }
  dataTask.resume()
}

This works exactly like the other request methods in the project, decoding the response body into [Category].

Open AcronymDetailTableViewController.swift and add the following implementation to getAcronymData():

// 1
guard let id = acronym.id else {
  return
}

// 2
let acronymDetailRequester = AcronymRequest(acronymID: id)
// 3
acronymDetailRequester.getUser { [weak self] result in
  switch result {
  case .success(let user):
    self?.user = user
  case .failure:
    let message =
      "There was an error getting the acronym’s user"
    ErrorPresenter.showError(message: message, on: self)
  }
}

// 4
acronymDetailRequester.getCategories { [weak self] result in
  switch result {
  case .success(let categories):
    self?.categories = categories
  case .failure:
    let message =
      "There was an error getting the acronym’s categories"
    ErrorPresenter.showError(message: message, on: self)
  }
}

Here’s the play by play:

  1. Ensure the acronym has a non-nil ID.

  2. Create an AcronymRequest to gather information.

  3. Get the acronym’s user. If the request succeeds, update the user property. Otherwise, display an appropriate error message.

  4. Get the acronym’s categories. If the request succeeds, update the categories property. Otherwise, display an appropriate error message.

The project displays acronym data in a table view with four sections. These are:

  • the acronym
  • its meaning
  • its user
  • its categories

Build and run. Tap an acronym in the Acronyms table and the application will show the detail view with all the information:

Editing acronyms

To edit an acronym, users tap the Edit button in the Acronym detail view. Open CreateAcronymTableViewController.swift. The acronym property exists to store the current acronym. If this property is set — by prepare(for:sender:) in AcronymDetailTableViewController.swift — then the user is editing the acronym. Otherwise, the user is creating a new acronym.

if let acronym = acronym {
  acronymShortTextField.text = acronym.short
  acronymLongTextField.text = acronym.long
  userLabel.text = selectedUser?.name
  navigationItem.title = "Edit Acronym"
} else {
  populateUsers()
}
func update(
  with updateData: CreateAcronymData,
  completion: @escaping (
    Result<Acronym, ResourceRequestError>
  ) -> Void
) {
  do {
    // 1
    var urlRequest = URLRequest(url: resource)
    urlRequest.httpMethod = "PUT"
    urlRequest.httpBody = try JSONEncoder().encode(updateData)
    urlRequest.addValue(
      "application/json",
      forHTTPHeaderField: "Content-Type")
    let dataTask = URLSession.shared
      .dataTask(with: urlRequest) { data, response, _ in
        // 2
        guard
          let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200,
          let jsonData = data
          else {
            completion(.failure(.noData))
            return
        }
        do {
        // 3
          let acronym = try JSONDecoder()
            .decode(Acronym.self, from: jsonData)
          completion(.success(acronym))
        } catch {
          completion(.failure(.decodingError))
        }
      }
    dataTask.resume()
  } catch {
    completion(.failure(.encodingError))
  }
}
let acronymSaveData = acronym.toCreateData()
if self.acronym != nil {
  // update code goes here
} else {
  ResourceRequest<Acronym>(resourcePath: "acronyms")
    .save(acronymSaveData) { [weak self] result in
      switch result {
      case .failure:
        let message = "There was a problem saving the acronym"
        ErrorPresenter.showError(message: message, on: self)
      case .success:
        DispatchQueue.main.async { [weak self] in
          self?.navigationController?
            .popViewController(animated: true)
        }
      }
    }
}
// 1
guard let existingID = self.acronym?.id else {
  let message = "There was an error updating the acronym"
  ErrorPresenter.showError(message: message, on: self)
  return
}
// 2
AcronymRequest(acronymID: existingID)
  .update(with: acronymSaveData) { result in
    switch result {
    // 3
    case .failure:
      let message = "There was a problem saving the acronym"
      ErrorPresenter.showError(message: message, on: self)
    case .success(let updatedAcronym):
      self.acronym = updatedAcronym
      DispatchQueue.main.async { [weak self] in
        // 4
        self?.performSegue(
          withIdentifier: "UpdateAcronymDetails",
          sender: nil)
      }
    }
  }
if segue.identifier == "EditAcronymSegue" {
  // 1.
  guard
    let destination = segue.destination
      as? CreateAcronymTableViewController else {
    return
  }

  // 2.
  destination.selectedUser = user
  destination.acronym = acronym
}
guard let controller = segue.source
  as? CreateAcronymTableViewController else {
  return
}

user = controller.selectedUser
if let acronym = controller.acronym {
  self.acronym = acronym
}

Deleting acronyms

The final CRUD operation to implement is D: delete. Open AcronymRequest.swift and add the following method after update(with:completion:):

func delete() {
  // 1
  var urlRequest = URLRequest(url: resource)
  urlRequest.httpMethod = "DELETE"
  // 2
  let dataTask = URLSession.shared.dataTask(with: urlRequest)
  dataTask.resume()
}
override func tableView(
  _ tableView: UITableView,
  commit editingStyle: UITableViewCell.EditingStyle,
  forRowAt indexPath: IndexPath
) {
  if let id = acronyms[indexPath.row].id {
    // 1
    let acronymDetailRequester = AcronymRequest(acronymID: id)
    acronymDetailRequester.delete()
  }

  // 2
  acronyms.remove(at: indexPath.row)
  // 3
  tableView.deleteRows(at: [indexPath], with: .automatic)
}

Creating categories

Setting up the create category table is like setting up the create users table. Open CreateCategoryTableViewController.swift and replace the implementation of save(_:) with:

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

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

Adding acronyms to categories

The finish up, you must implement the ability to add acronyms to categories. Add a new table row section to the acronym detail view that contains a button to add the acronym to a category.

return 5
// 1
case 4:
  cell.textLabel?.text = "Add To Category"
// 2
if indexPath.section == 4 {
  cell.selectionStyle = .default
  cell.isUserInteractionEnabled = true
} else {
  cell.selectionStyle = .none
  cell.isUserInteractionEnabled = false
}
// 1
let categoriesRequest =
  ResourceRequest<Category>(resourcePath: "categories")
// 2
categoriesRequest.getAll { [weak self] result in
  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):
    self?.categories = categories
    DispatchQueue.main.async { [weak self] in
      self?.tableView.reloadData()
    }
  }
}
func add(
  category: Category,
  completion: @escaping (Result<Void, CategoryAddError>) -> Void
) {
  // 1
  guard let categoryID = category.id else {
    completion(.failure(.noID))
    return
  }
  // 2
  let url = resource
    .appendingPathComponent("categories")
    .appendingPathComponent("\(categoryID)")
  // 3
  var urlRequest = URLRequest(url: url)
  urlRequest.httpMethod = "POST"
  // 4
  let dataTask = URLSession.shared
    .dataTask(with: urlRequest) { _, response, _ in
      // 5
      guard
        let httpResponse = response as? HTTPURLResponse,
        httpResponse.statusCode == 201
        else {
          completion(.failure(.invalidResponse))
          return
      }
      // 6
      completion(.success(()))
    }
  dataTask.resume()
}
// MARK: - UITableViewDelegate
extension AddToCategoryTableViewController {
  override func tableView(
    _ tableView: UITableView,
    didSelectRowAt indexPath: IndexPath
  ) {
    // 1
    let category = categories[indexPath.row]
    // 2
    guard let acronymID = acronym.id else {
      let message = """
        There was an error adding the acronym
        to the category - the acronym has no ID
        """
      ErrorPresenter.showError(message: message, on: self)
      return
    }
    // 3
    let acronymRequest = AcronymRequest(acronymID: acronymID)
    acronymRequest
      .add(category: category) { [weak self] result in
        switch result {
        // 4
        case .success:
          DispatchQueue.main.async { [weak self] in
            self?.navigationController?
              .popViewController(animated: true)
          }
        // 5
        case .failure:
          let message = """
            There was an error adding the acronym
            to the category
            """
          ErrorPresenter.showError(message: message, on: self)
        }
      }
  }
}
AddToCategoryTableViewController(
  coder: coder, 
  acronym: acronym, 
  selectedCategories: categories)

Where to go from here?

This chapter has shown you how to build an iOS application that interacts with the Vapor API. The application isn’t fully-featured, however, and you could improve it. For example, you could add a category information view that displays all the acronyms for a particular category.

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