第12章:创建一个简单的iPhone
应用程序,第一部分¶
在前几章中,你创建了一个API
,并使用RESTed
与之进行交互。然而,用户希望使用TIL的东西更漂亮一些!因此,你必须使用TIL
。接下来的两章将告诉你如何建立一个与API
交互的简单iOS
应用。在这一章中,你将学习如何创建不同的模型并从数据库中获取模型。
在这两章结束时,你将拥有一个iOS
应用程序,它可以做你到目前为止所学的一切。它看起来将类似于以下内容:
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
这将建立并运行iOS
应用将与之对话的TIL
应用。如果你愿意,你可以使用你现有的TIL
应用程序。
Note
这需要你的数据库的Docker
容器正在运行。参见第6章,"配置数据库",以获得指导。
接下来,打开TILiOS
项目。TILiOS
包含一个与TIL API
交互的骨架应用程序。它是一个有三个标签的标签栏应用程序:
Acronyms
:查看所有的缩写,查看有关缩写的细节,并添加缩写。Users
:查看所有用户并创建用户。Categories
:查看所有类别并创建类别。
该项目包含几个空的表视图控制器,准备让你配置,以显示来自TIL API
的数据。
看看项目中的Models
组;它提供了三个模型类:
Acronym
User
Category
你可能会认出这些模型--这些模型与发现的API应用相匹配 这表明在客户端和服务器上使用相同的语言可以有多么强大。甚至可以创建一个两个项目都使用的独立模块,这样你就不用重复编写代码了。由于Fluent
表示父子关系的方式,Acronym
略有不同。你可以用一个像CreateAcronymData
这样的DTO
来解决这个问题,该项目也包括这个DTO
。
查看缩略语¶
第一个标签的表格显示所有的首字母缩写。在Utilities
组中创建一个新的Swift
文件,名为ResourceRequest.swift
。打开该文件并创建一个类型来管理资源请求:
// 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)
}
}
下面是这个的作用:
- 定义一个通用的
ResourceRequest
类型,其通用参数必须符合Codable
。 - 设置
API
的基本URL
。现在使用localhost
。
Note
这需要你在应用程序的Info.plist
中禁用ATS(App Transport Security)。这已经在样本项目中为你设置好了。
- 初始化特定资源的
URL
。
接下来,你需要一种方法来获取特定资源类型的所有实例。在init(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()
}
下面是这个的作用:
- 定义一个函数,从
API
中获取资源类型的所有值。这需要一个完成闭包作为参数,它使用Swift
的Result
类型。 - 用资源的
URL
创建一个数据任务。 - 确保响应返回一些数据。否则,用适当的
.failure
情况调用completion(_:)
关闭。 - 将响应数据解码为一个
ResourceType
数组。 - 调用
completion(_:)
闭包的.success
情况,并返回ResourceType
数组。 - 捕获任何错误并返回正确的失败案例。
- 启动
dataTask
。
打开AcronymsTableViewController.swift
,在// MARK: - Properties
下添加以下内容:
// 1
var acronyms: [Acronym] = []
// 2
let acronymsRequest =
ResourceRequest<Acronym>(resourcePath: "acronyms")
下面是这个的作用:
- 声明一个首字母缩写的数组。这些是表格中显示的首字母缩写。
- 为缩写创建一个
ResourceRequest
。
获取首字母缩写¶
每当视图出现在屏幕上时,表格视图控制器会调用refresh(_:)
。将refresh(_:)
的实现替换为以下内容:
// 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()
}
}
}
下面是这个的作用:
- 调用
getAll(completion:)
来获取所有的首字母缩写。这将返回完成度闭包中的一个结果。 - 随着请求的完成,在刷新控件上调用
endRefreshing()
。 - 如果获取失败,使用
ErrorPresenter
工具来显示一个带有适当错误信息的警报控制器。 - 如果获取成功,从结果中更新
acronyms
数组并重新加载表。
显示缩略语¶
还是在AcronymsTableViewController.swift
中,更新tableView(_:numberOfRowsInSection:)
,将return 1
替换为以下内容,以返回正确的缩写词数量:
return acronyms.count
接下来,更新tableView(_:cellForRowAt:)
以显示表格中的缩写。在return cell
前添加以下内容:
let acronym = acronyms[indexPath.row]
cell.textLabel?.text = acronym.short
cell.detailTextLabel?.text = acronym.long
这将标题和副标题文本设置为每个单元格的首字母缩写短和长属性。
建立并运行,你会看到你的表被数据库中的首字母缩写填充:
查看用户¶
查看所有的用户遵循一个类似的模式。大部分的视图控制器已经设置好了。打开UsersTableViewController.swift
,在下面:
var users: [User] = []
增加以下内容:
let usersRequest =
ResourceRequest<User>(resourcePath: "users")
这将创建一个ResourceRequest
来从API
获得用户。接下来,将refresh(_:)
的实现替换为以下内容:
// 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()
}
}
}
下面是这个的作用:
- 调用
getAll(completion:)
来获取所有的用户。这将在完成度闭包中返回一个结果。 - 随着请求的完成,在刷新控件上调用
endRefreshing()
。 - 如果获取失败,使用
ErrorPresenter
工具来显示一个带有适当错误信息的警告视图。 - 如果获取成功,从结果中更新
users
数组并重新加载表。
建立并运行。进入Users
标签,你会看到该表被数据库中的用户填充:
查看类别¶
按照类似的模式来查看所有的类别。打开CategoriesTableViewController.swift
,在下面:
var categories: [Category] = []
增加下面的内容:
let categoriesRequest =
ResourceRequest<Category>(resourcePath: "categories")
这设置了一个ResourceRequest
来从API
中获取类别。接下来,将refresh(_:)
的实现替换为以下内容:
// 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()
}
}
}
下面是这个的作用:
- 调用
getAll(completion:)
来获取所有的类别。这将返回完成度闭包中的一个结果。 - 随着请求的完成,在刷新控件上调用
endRefreshing()
。 - 如果获取失败,使用
ErrorPresenter
工具来显示一个带有适当错误信息的警告视图。 - 如果获取成功,从结果中更新
categories
数组,并重新加载表。
建立并运行。进入Categories
选项卡,你会看到表格被TIL
应用程序的类别填充:
创建用户¶
在TIL API
中,你必须有一个用户来创建缩略语,所以先设置好这个流程。打开ResourceRequest.swift
,在ResourceRequest
的底部添加一个新方法来保存一个模型:
// 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))
}
}
以下是新方法的作用:
- 声明一个方法
save(_:completion:)
,它接收一个要保存的通用Codable
类型和一个接收保存结果的完成处理器。这使用了一个通用类型而不是ResourceRequest
,因为保存Acronym
的API使用了CreateAcronymData
而不是Acronym
。 - 为保存请求创建一个
URLRequest
。 - 设置请求的
HTTP
方法为POST
。 - 设置请求的
Content-Type
头为application/json
,以便API
知道有JSON
数据需要解码。 - 将请求正文设置为编码后的保存数据。
- 用请求创建一个数据任务。
- 确保有一个
HTTP
响应。检查响应状态是否为200 OK
,这是API
在成功保存时返回的代码。确保响应体中有数据。 - 将响应体解码为资源类型。用一个成功的结果调用完成处理程序。
- 捕获一个解码错误并以失败的结果调用完成处理程序。
- 启动数据任务。
- 捕获任何来自
try JSONEncoder().encode(resourceToSave)
的编码错误,并以失败的结果调用完成处理程序。
接下来,打开CreateUserTableViewController.swift
,将save(_:)
的实现替换为以下内容:
// 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)
}
}
}
下面是这个的作用:
- 确保名称文本字段包含一个非空的字符串。
- 确保用户名文本字段包含一个非空的字符串。
- 根据提供的数据创建一个新的用户。
- 为
User
创建一个ResourceRequest
并调用save(_:completion:)
。 - 如果保存失败,显示一个错误信息。
- 如果保存成功,返回到之前的视图:用户表。
建立并运行。进入Users
标签,点击+
按钮,打开创建用户界面。填写两个字段,然后点击Save
。
如果保存成功,屏幕关闭,新用户出现在表格中:
创建首字母缩写词¶
现在你有了创建用户的能力,是时候实现创建缩略语了。毕竟,如果你不能向它添加缩写词,那么一个缩写词词典应用程序有什么用呢。
选择用户¶
当你用API
创建一个首字母缩写时,你必须提供一个用户ID。要求用户记住并输入一个UUID并不是一个好的用户体验! iOS
应用程序应该允许用户通过名字来选择用户。
打开CreateAcronymTableViewController.swift
,在viewDidLoad()
下创建一个新方法,在创建首字母表的User
单元格中填入一个默认用户:
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]
}
}
}
下面是这个的作用:
- 从
API
中获取所有用户。 - 如果请求失败,显示一个错误。当用户解散警报控制器时,从创建缩写视图返回。这使用了
showError(message:on:dismissAction:)
上的dismissAction
。 - 如果请求成功,将用户字段设置为第一个用户的名字并更新
selectedUser
。
在viewDidLoad()
的末尾添加以下内容:
populateUsers()
你的应用程序的用户可以点击USER
单元格来选择一个不同的用户来创建一个缩写。这个手势会打开Select A User
屏幕。
打开SelectUserTableViewController.swift
。在下面的内容:
var users: [User] = []
添加下面的内容:
var selectedUser: User
这个属性持有被选中的用户。接下来,在init?(coder:selectedUser:)
中,在super.init(coder:coder)
之前将提供的用户分配给新的属性:
self.selectedUser = selectedUser
接下来,在loadData()
中添加以下实现,这样当视图加载时,表格就会显示用户:
// 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()
}
}
}
下面是这个的作用:
- 从
API
中获取所有的用户。 - 如果请求失败,显示一个错误信息。一旦用户在警报上点击解散,就返回到之前的视图。
- 如果请求成功,保存用户并重新加载表的数据。
在tableView(_:cellForRowAt:)
中,在return cell
前添加以下内容:
if user.name == selectedUser.name {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
这是将当前单元格与当前选择的用户进行比较。如果它们是相同的,在该单元格上设置一个复选标记。
当用户点击一个单元格时,SelectUserTableViewController
使用一个unwind segue
来导航回CreateAcronymTableViewController
。
在SelectUserTableViewController
中添加以下prepare(for:)
的实现,以设置segue
的选定用户:
// 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]
}
下面是这个的作用:
- 验证这是否是预期的转场。
- 获取触发分离的单元格的索引路径。
- 将
selectedUser
更新为被点击的单元格的用户。
解除分离调用CreateAcronymTableViewController
中的updateSelectedUser(_:)
。打开CreateAcronymTableViewController.swift
,为updateSelectedUser(_:)
添加以下实现:
// 1
guard let controller = segue.source
as? SelectUserTableViewController
else {
return
}
// 2
selectedUser = controller.selectedUser
userLabel.text = selectedUser?.name
下面是这个的作用:
- 确保
segue
来自SelectUserTableViewController
。 - 用新的值更新
selectedUser
,并更新用户标签。
最后,将makeSelectUserViewController(_:)
的实现替换为以下内容:
guard let user = selectedUser else {
return nil
}
return SelectUserTableViewController(
coder: coder,
selectedUser: user)
这确保我们有一个被选中的用户,并为该用户创建一个SelectUserTableViewController
。当用户点击用户字段时,应用程序使用@IBSegueAction
来创建选择用户屏幕。
建立并运行。在缩略语标签中,点击+
,调出Create An Acronym
视图。点击用户行,应用程序打开Select A User
视图,允许你选择一个用户。
当你点击一个用户时,该用户就被设置在Create An Acronym
页面上:
保存缩略语¶
现在你可以成功地选择一个用户,是时候实现将新的首字母缩写保存到数据库中了。将CreateAcronymTableViewController.swift
中的save(_:)
的实现改为以下内容:
// 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)
}
}
}
以下是保存首字母缩写的步骤:
- 确保用户已经填写了首字母缩写和含义。检查所选用户不是
nil
,并且用户有一个有效的ID
。 - 根据提供的数据创建一个新的
Acronym
。使用toCreateData()
辅助方法将缩写转换为CreateAcronymData
。 - 为
Acronym
创建一个ResourceRequest
并使用创建的数据调用`save(_:)'。 - 如果保存请求失败,显示一个错误信息。
- 如果保存请求成功,返回到之前的视图:缩写表。
建立并运行。在Acronyms
标签上,点击+
。填写创建首字母缩写的字段,然后点Save
。
保存的首字母缩写出现在表格中:
接下来去哪?¶
在本章中,你学习了如何从一个iOS
应用程序与API
进行交互。你看到了如何创建不同的模型并从API
中检索它们。你还学习了如何以一种用户友好的方式管理所需的关系。
下一章将在此基础上,查看关于一个缩写的细节。你还将学习如何实现其余的CRUD
操作。最后,你将看到如何在类别和首字母缩写之间建立关系。