跳转至

第13章:创建一个简单的iPhone应用程序,第2部分

在上一章中,你创建了一个可以创建用户和缩略语的iPhone应用程序。在这一章中,你将扩展这个应用程序,以包括查看单个首字母缩写的细节。你还将学习如何执行最后的CRUD操作:编辑和删除。最后,你将学习如何将首字母缩写添加到类别中。

Note

本章希望你有一个正在运行的TIL Vapor应用程序。它还希望你已经完成了前一章的iOS应用程序。如果没有,请拿起启动项目,从那里开始学习。参见第12章"创建一个简单的iPhone应用程序,第一部分",了解如何运行Vapor应用程序的细节。

开始工作

在上一章中,你学会了如何查看表格中的所有首字母缩写。现在,你想在用户点击一个表格单元格时,显示关于一个缩写的所有信息。启动项目包含了必要的管道,你只需要实现细节。

打开AcronymsTableViewController.swift。把makeAcronymsDetailTableViewController(_:)的实现替换为以下内容:

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

当用户点击一个首字母缩写时,你就会运行这段代码。该代码做了以下工作:

  1. 确保有一个选定的索引路径。
  2. 获取与被点选行相对应的缩写。
  3. 使用选定的首字母缩写创建一个AcronymDetailTableViewController

Utilities组中创建一个名为AcronymRequest.swift的新Swift文件。打开新文件并创建一个新的类型来代表缩写资源请求:

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

这将resource属性设置为该首字母缩写的URL。在AcronymRequest的底部,添加一个方法来获取缩写的用户:

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

下面是这个的作用:

  1. 创建URL以获得首字母缩写的用户。
  2. 使用共享的URLSession创建一个数据任务。
  3. 检查响应是否包含一个主体,否则会出现相应的错误。
  4. 将响应主体解码为User对象,并用成功的结果调用完成处理程序。
  5. 捕获任何解码错误,并以失败的结果调用完成处理程序。
  6. 启动网络任务。

接下来,在getUser(completion:)下面,添加以下方法来获得缩写的类别:

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

这与项目中的其他请求方法完全一样,将响应体解码为[Category]

打开AcronymDetailTableViewController.swift,为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)
  }
}

下面是比赛的过程:

  1. 确保缩略语有一个非零的ID
  2. 创建一个AcronymRequest来收集信息。
  3. 获取首字母缩写的用户。如果请求成功,更新user属性。否则,显示一个适当的错误信息。

  4. 获取首字母缩写的类别。如果请求成功,更新categories属性。否则,显示一个适当的错误信息。

该项目在一个有四个部分的表视图中显示首字母缩写数据。这些部分是:

  • 缩略语
  • 其含义
  • 它的使用者
  • 其类别

建立和运行。点击缩略语表中的一个缩略语,该应用程序将显示所有信息的详细视图:

img

编辑首字母缩写

要编辑一个首字母缩写,用户要在首字母缩写的详细视图中点击Edit按钮。打开CreateAcronymTableViewController.swiftacronym属性的存在是为了存储当前的首字母缩写。如果这个属性被设置--通过AcronymDetailTableViewController.swift中的prepare(for:sender:)--那么用户正在编辑首字母缩写。否则,用户将创建一个新的首字母缩写。

viewDidLoad()中,将populateUsers()替换为:

if let acronym = acronym {
  acronymShortTextField.text = acronym.short
  acronymLongTextField.text = acronym.long
  userLabel.text = selectedUser?.name
  navigationItem.title = "Edit Acronym"
} else {
  populateUsers()
}

如果缩写被设置,你就处于编辑模式,所以用正确的值填充显示字段,并更新视图的标题。如果你是在创建模式下,像以前一样调用populateUsers()

要更新一个首字母缩写,你要向API中的首字母缩写资源发出PUT请求。打开AcronymRequest.swift,在AcronymRequest的底部添加一个方法来更新一个缩写:

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

这种方法的工作方式与你建立的其他请求一样。不同之处在于:

  1. 创建和配置一个URLRequest。该方法必须是PUT,主体包含编码的CreateAcronymData。设置正确的标头,以便Vapor应用程序知道请求包含JSON
  2. 确保响应是一个HTTP响应,状态代码是200,并且响应有一个正文。
  3. 将响应体解码为Acronym,并以成功的结果调用完成处理器。

返回到CreateAcronymTableViewController.swift。在save(_:)之后:

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

这将检查类的acronym属性,看它是否已经被设置。如果该属性为nil,那么用户将保存一个新的首字母缩写,因此该函数将执行与之前相同的保存请求。

// update code goes here之后的if块内,添加以下代码来更新首字母缩写:

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

以下是更新代码的作用:

  1. 确保缩略语有一个有效的ID
  2. 创建一个AcronymRequest并调用update(with:completion:)
  3. 如果更新失败,显示一个错误信息。
  4. 如果更新成功,存储更新的首字母缩写,并触发一个解开的segueAcronymsDetailTableViewController

接下来,打开AcronymsDetailTableViewController.swift,在prepare(for:sender:)的末尾添加以下实现:

if segue.identifier == "EditAcronymSegue" {
  // 1.
  guard
    let destination = segue.destination
      as? CreateAcronymTableViewController else {
    return
  }

  // 2.
  destination.selectedUser = user
  destination.acronym = acronym
}

下面是这个的作用:

  1. 确保目标是CreateAcronymTableViewController
  2. 在目标上设置selectedUseracronym属性。

接下来,在unwind segue的目标updateAcronymDetails(_:)中添加以下实现:

guard let controller = segue.source
  as? CreateAcronymTableViewController else {
  return
}

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

这可以捕捉到更新的首字母缩写(如果设置了)和用户,触发对其自身视图的更新。

建立和运行。点一个首字母缩写来打开首字母缩写的详细视图,然后点Edit。改变细节并点击Save。视图将返回到带有更新值的缩略语详情页:

img

删除缩略语

最后要实现的CRUD操作是D:删除。打开AcronymRequest.swift,在update(with:completion:)后添加以下方法:

func delete() {
  // 1
  var urlRequest = URLRequest(url: resource)
  urlRequest.httpMethod = "DELETE"
  // 2
  let dataTask = URLSession.shared.dataTask(with: urlRequest)
  dataTask.resume()
}

下面是delete()的作用:

  1. 创建一个URLRequest并将HTTP方法设置为DELETE
  2. 使用共享的URLSession为该请求创建一个数据任务,并发送该请求。这就忽略了请求的结果。

打开AcronymsTableViewController.swift。为了实现删除表行,在tableView(_:cellForRowAt:)后面添加以下内容:

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

这可以在表格视图上实现"滑动-删除"功能。下面是它的工作原理:

  1. 如果缩写有一个有效的ID,为该缩写创建一个AcronymRequest并调用delete()在API中删除该缩写。
  2. 从本地的首字母缩写数组中删除该首字母缩写。
  3. 从表视图中删除该首字母缩写的行。

建立并运行。在一个首字母缩写上向左滑动,会出现Delete按钮。点选Delete来删除该首字母缩写。

如果你拉动刷新表视图,首字母缩写不会重新出现,因为应用程序已经在API中删除了它:

img

创建类别

设置创建类别表就像设置创建用户表一样。打开CreateCategoryTableViewController.swift,将save(_:)的实现替换为:

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

这就像保存用户的save(_:)方法一样。建立并运行。在类别选项卡上,点击+按钮,打开Create Category界面。填写一个名称,然后点击Save。如果保存成功,屏幕将关闭,新的类别将出现在表中:

img

为类别添加首字母缩写词

最后,你必须实现将首字母缩写添加到类别的能力。在缩略语详细视图中添加一个新的表行部分,其中包含一个将缩略语添加到类别中的按钮。

打开AcronymsDetailTableViewController.swift。将numberOfSections(in:)中的return语句改为:

return 5

tableView(_:cellForRowAt:)中,在default前的switch中添加一个新的案例:

// 1
case 4:
  cell.textLabel?.text = "Add To Category"

接下来,在return cell之前添加以下内容:

// 2
if indexPath.section == 4 {
  cell.selectionStyle = .default
  cell.isUserInteractionEnabled = true
} else {
  cell.selectionStyle = .none
  cell.isUserInteractionEnabled = false
}

这些步骤:

  1. 如果表格单元格在新的章节中,将其标题设置为"添加到类别"。
  2. 如果该单元格在新的部分,则启用该单元格的选择,否则禁用选择。这允许用户选择新行,但不允许选择其他行。

启动项目已经包含了这个新表格视图的视图控制器。AddToCategoryTableViewController.swift。该类定义了三个关键属性:

  • categories:一个数组,用于显示从API检索的所有类别。
  • selectedCategories:为首字母缩写选择的类别。
  • acronym:添加到类别的缩写。

该类还包含了对UITableViewDataSource方法的扩展。tableView(_:cellForRowAt:)如果类别在selectedCategories数组中,则在单元格上设置accessoryType

打开AddToCategoryTableViewController.swift,在loadData()中加入以下实现,从API中获取所有类别:

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

下面是这个的作用:

  1. 为类别创建一个`资源请求'(ResourceRequest)。
  2. API中获取所有的类别。
  3. 如果获取失败,显示一个错误信息。
  4. 如果获取成功,填充类别数组并重新加载表的数据。

打开AcronymRequest.swift,在delete()后面添加以下方法:

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

下面是这个的作用:

  1. 确保类别有一个有效的ID,否则调用完成处理程序,并给出失败案例和适当的错误。这使用CategoryAddError,这是启动项目的一部分。
  2. 为请求建立URL
  3. 创建一个URLRequest并将HTTP方法设置为POST
  4. 从共享的URLSession中创建一个数据任务。
  5. 确保响应是一个HTTP响应,并且响应状态是201 Created。否则,用正确的失败情况调用完成处理程序。
  6. 用成功案例调用完成处理程序。

打开AddToCategoryTableViewController.swift,在文件末尾添加以下extension

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

下面是这个函数的作用:

  1. 获取用户选择的类别。
  2. 确保缩写有一个有效的ID;否则,显示一个错误信息。
  3. 创建一个AcronymRequest,将首字母缩写添加到该类别。
  4. 如果请求成功,返回到前一个视图。
  5. 如果请求失败,显示一个错误信息。

最后,打开AcronymDetailTableViewController.swift来设置AddToCategoryTableViewController。将makeAddToCategoryController(_:)的实现改为如下:

AddToCategoryTableViewController(
  coder: coder, 
  acronym: acronym, 
  selectedCategories: categories)

这将返回一个AddToCategoryTableViewController,它是用当前的首字母缩写和它的类别创建的。

建立并运行。点击一个首字母缩写,在详细视图中,现在出现了一个新的行,标记为Add To Category。点击这个单元格,类别列表中出现了已经选定的类别。

选择一个新的类别,视图就会关闭。首字母缩写的详细视图现在会在其列表中出现新的类别:

img

接下来去哪?

本章向你展示了如何建立一个与Vapor API交互的iOS应用程序。然而,这个应用程序的功能并不全面,你可以对它进行改进。例如,你可以添加一个类别信息视图,显示某个特定类别的所有首字母缩写。

本书的下一节将向你展示如何建立另一种类型的客户端:网站。