第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
应用程序。然而,这个应用程序的功能并不全面,你可以对它进行改进。例如,你可以添加一个类别信息视图,显示某个特定类别的所有首字母缩写。
本书的下一节将向你展示如何建立另一种类型的客户端:网站。