第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)
当用户点击一个首字母缩写时,你就会运行这段代码。该代码做了以下工作:
- 确保有一个选定的索引路径。
- 获取与被点选行相对应的缩写。
- 使用选定的首字母缩写创建一个
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()
}
下面是这个的作用:
- 创建URL以获得首字母缩写的用户。
- 使用共享的
URLSession创建一个数据任务。 - 检查响应是否包含一个主体,否则会出现相应的错误。
- 将响应主体解码为
User对象,并用成功的结果调用完成处理程序。 - 捕获任何解码错误,并以失败的结果调用完成处理程序。
- 启动网络任务。
接下来,在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)
}
}
下面是比赛的过程:
- 确保缩略语有一个非零的
ID。 - 创建一个
AcronymRequest来收集信息。 -
获取首字母缩写的用户。如果请求成功,更新
user属性。否则,显示一个适当的错误信息。 -
获取首字母缩写的类别。如果请求成功,更新
categories属性。否则,显示一个适当的错误信息。
该项目在一个有四个部分的表视图中显示首字母缩写数据。这些部分是:
- 缩略语
- 其含义
- 它的使用者
- 其类别
建立和运行。点击缩略语表中的一个缩略语,该应用程序将显示所有信息的详细视图:

编辑首字母缩写¶
要编辑一个首字母缩写,用户要在首字母缩写的详细视图中点击Edit按钮。打开CreateAcronymTableViewController.swift。acronym属性的存在是为了存储当前的首字母缩写。如果这个属性被设置--通过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))
}
}
这种方法的工作方式与你建立的其他请求一样。不同之处在于:
- 创建和配置一个
URLRequest。该方法必须是PUT,主体包含编码的CreateAcronymData。设置正确的标头,以便Vapor应用程序知道请求包含JSON。 - 确保响应是一个
HTTP响应,状态代码是200,并且响应有一个正文。 - 将响应体解码为
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)
}
}
}
以下是更新代码的作用:
- 确保缩略语有一个有效的
ID。 - 创建一个
AcronymRequest并调用update(with:completion:)。 - 如果更新失败,显示一个错误信息。
- 如果更新成功,存储更新的首字母缩写,并触发一个解开的
segue到AcronymsDetailTableViewController。
接下来,打开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
}
下面是这个的作用:
- 确保目标是
CreateAcronymTableViewController。 - 在目标上设置
selectedUser和acronym属性。
接下来,在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。视图将返回到带有更新值的缩略语详情页:

删除缩略语¶
最后要实现的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()的作用:
- 创建一个
URLRequest并将HTTP方法设置为DELETE。 - 使用共享的
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)
}
这可以在表格视图上实现"滑动-删除"功能。下面是它的工作原理:
- 如果缩写有一个有效的
ID,为该缩写创建一个AcronymRequest并调用delete()在API中删除该缩写。 - 从本地的首字母缩写数组中删除该首字母缩写。
- 从表视图中删除该首字母缩写的行。
建立并运行。在一个首字母缩写上向左滑动,会出现Delete按钮。点选Delete来删除该首字母缩写。
如果你拉动刷新表视图,首字母缩写不会重新出现,因为应用程序已经在API中删除了它:

创建类别¶
设置创建类别表就像设置创建用户表一样。打开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。如果保存成功,屏幕将关闭,新的类别将出现在表中:

为类别添加首字母缩写词¶
最后,你必须实现将首字母缩写添加到类别的能力。在缩略语详细视图中添加一个新的表行部分,其中包含一个将缩略语添加到类别中的按钮。
打开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
}
这些步骤:
- 如果表格单元格在新的章节中,将其标题设置为"添加到类别"。
- 如果该单元格在新的部分,则启用该单元格的选择,否则禁用选择。这允许用户选择新行,但不允许选择其他行。
启动项目已经包含了这个新表格视图的视图控制器。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()
}
}
}
下面是这个的作用:
- 为类别创建一个`资源请求'(ResourceRequest)。
- 从
API中获取所有的类别。 - 如果获取失败,显示一个错误信息。
- 如果获取成功,填充类别数组并重新加载表的数据。
打开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()
}
下面是这个的作用:
- 确保类别有一个有效的
ID,否则调用完成处理程序,并给出失败案例和适当的错误。这使用CategoryAddError,这是启动项目的一部分。 - 为请求建立
URL。 - 创建一个
URLRequest并将HTTP方法设置为POST。 - 从共享的
URLSession中创建一个数据任务。 - 确保响应是一个
HTTP响应,并且响应状态是201 Created。否则,用正确的失败情况调用完成处理程序。 - 用成功案例调用完成处理程序。
打开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)
}
}
}
}
下面是这个函数的作用:
- 获取用户选择的类别。
- 确保缩写有一个有效的
ID;否则,显示一个错误信息。 - 创建一个
AcronymRequest,将首字母缩写添加到该类别。 - 如果请求成功,返回到前一个视图。
- 如果请求失败,显示一个错误信息。
最后,打开AcronymDetailTableViewController.swift来设置AddToCategoryTableViewController。将makeAddToCategoryController(_:)的实现改为如下:
AddToCategoryTableViewController(
coder: coder,
acronym: acronym,
selectedCategories: categories)
这将返回一个AddToCategoryTableViewController,它是用当前的首字母缩写和它的类别创建的。
建立并运行。点击一个首字母缩写,在详细视图中,现在出现了一个新的行,标记为Add To Category。点击这个单元格,类别列表中出现了已经选定的类别。
选择一个新的类别,视图就会关闭。首字母缩写的详细视图现在会在其列表中出现新的类别:

接下来去哪?¶
本章向你展示了如何建立一个与Vapor API交互的iOS应用程序。然而,这个应用程序的功能并不全面,你可以对它进行改进。例如,你可以添加一个类别信息视图,显示某个特定类别的所有首字母缩写。
本书的下一节将向你展示如何建立另一种类型的客户端:网站。