0%

为iOS应用构建输入表单

了解如何使用更新的集合视图视图模型框架构建复杂的表单,而无需使用 Swift

**提示:**
        此方法不起作用,因为表单中的单元格将被重用,这会导致某些不一致的情况…… 请阅读我的其他文章 。🤷‍♂️


CollectionView和输入表单

        CollectionView框架 刚刚进行了巨大更新。有很多新变化,但是最大的改进之一是我处理视图模型的方式。过去,你必须在视图模型中使用长函数名,包括通用视图和模型类名。如果你曾经阅读过 最终UICollectionView指南 ,那么你应该了解我在说什么。好消息:我现在有一种更好的解决方案! 😉

        此更新不仅可以清理很多代码,还可以添加自定义视图模型处理程序,因此我可以以一种非常简单的方式与输入字段,切换等交互。另一个巨大的改进是,我开始使用视图标识符。那是偶然的发现,我只想寻找 一种通过标签识别视图的替代解决方案 ,然后我有了一个绝妙的主意:为什么不也通过id查找单元格呢?

        结果,我现在可以使用框架来创建表单。我仍然相信集合视图是大多数应用程序的最终构建块。是的,你仍然可以说没有灵丹妙药,但是如果此解决方案可以覆盖我90%的用例,那很好。毕竟,大多数应用程序只是以一种不错的或不太好的方式可视化 JSON数据 。 🤷‍♂️


可重复使用的表单组件

        让我们使用全新的框架来构建表单。 首先,你需要使用包管理器对其进行集成。 我真的希望在几周内我们可以使用 Swift Package Manager ,直到比你应该使用 CocoaPodscarthage 为止。

1
2
3
4
5
6
# cocoapods
source 'https://github.com/CoreKit/CocoaPods.git'
pod 'CollectionView', '~> 2.0.0'

# carthage
github "CoreKit/CollectionView" "2.0.0"

        现在让我们为输入字段创建一个可重用的单元格。 可以像往常一样随意使用 xib文件 ,实现上的唯一区别是,我将在 reset方法 中删除目标侦听器。 我们稍后将在视图模型中添加一个。 🎯

1
2
3
4
5
6
7
8
9
10
11
12
13
import Foundation
import CollectionView

class InputCell: Cell {

@IBOutlet weak var textField: UITextField!

override func reset() {
super.reset()

self.textField.removeTarget(nil, action: nil, for: .editingChanged)
}
}

        我还将创建一个简单的实体,用于在表单字段为空时显示占位符并存储输入字段的实际值,我们将其称为 InputEntity

1
2
3
4
5
6
import Foundation

struct InputEntity {
var placeholder: String
var value: String?
}

        现在最困难的部分是:在视图和模型之间建立连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Foundation
import CollectionView

class InputViewModel: ViewModel<InputCell, InputEntity> {

var editingChangeHandler: ViewModelHandler?

override var height: CGFloat {
return 60
}

override func updateView() {
self.view?.textField.placeholder = self.model.placeholder
self.view?.textField.text = self.model.value

self.view?.textField.addTarget(self,
action: #selector(self.editingChanged(_:)),
for: .editingChanged)
self.view?.textField.addTarget(self,
action: #selector(self.editingDidEnd(_:)),
for: .editingDidEnd)
}

func onEditingChange(_ handler: @escaping ViewModelHandler) -> Self {
self.editingChangeHandler = handler
return self
}

@objc func editingChanged(_ textField: UITextField) {
self.model.value = textField.text
self.editingChangeHandler?(self)
}

@objc func editingDidEnd(_ textField: UITextField) {
print("nothing-to-do-here-now...")
}
}

        这是一个非常复杂的视图模型,但是它也可以做很多事情。 你应该了解的第一件事是 ViewModelHandler ,它基本上是可以在视图模型中使用的通用别名。 它使你能够传递回调的类型安全视图模型。 你稍后会看到。

        第二个主要更改是 updateView方法 ,该方法用于基于来自模型的数据来更新视图。 我还将目标侦听器添加到视图中,以便可以直接在 view-model类 内部处理用户输入。

        onEditingChange 方法是视图模型的 “公共” api 。 我现在使用on前缀将处理程序和侦听器添加到我的视图模型。 如果发生更改事件,它将基本上调用存储的块。 你可以根据需要添加任意数量的事件处理程序块。 我真的希望你能掌握这种方法。

        还有一件事:现在返回单元格的高度是单线! 🎊


Composing forms and more

        目前,该计划将具有一个包含两个输入字段的输入表单。 一个用于电子邮件地址,另一个将用于密码。 诀窍是,这次我不会向你展示整个代码,但是你必须弄清楚其余的代码。

        但是,我将向你展示制作自己的表单所需的所有知识,甚至包括一些复杂的表单。 不用担心,这只是几行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import UIKit
import CollectionView

class ViewController: CollectionViewController {

override func viewDidLoad() {
super.viewDidLoad()

let grid = Grid(columns: 1, margin: UIEdgeInsets(all: 16), padding: .zero)
self.collectionView.source = .init(grid: grid, [
[
InputViewModel(id: "email-input", .init(placeholder: "Email", value: nil))
.onEditingChange { viewModel in
guard let passwordViewModel = viewModel.by(id: "password-input") as? InputViewModel else {
return
}
passwordViewModel.model.value = viewModel.model.value ?? ""
passwordViewModel.updateView()
},
InputViewModel(id: "password-input", .init(placeholder: "Password", value: nil)),
],
])
self.collectionView.reloadData()
}
}

        如果你曾经使用过集合视图框架,那么你应该知道我一直使用网格系统,因为我真的不喜欢计算数字。

        源是一组按部分分组的视图模型。这里唯一有趣的部分是,现在可以使用节和视图模型的数组来初始化源。

        如果你使用和标识符初始化一个视图模型,则以后可以通过ID查询该视图模型。这正是在编辑更改处理程序块内发生的事情。每个视图模型都可以通过id返回其他一些视图模型。默认情况下,视图模型是类型安全的,由于通用的 ViewModelHandler 别名, viewModel 也在块内传递。

        因此,在这个小示例中,如果你在第一个输入字段中键入内容,则完全相同的文本将出现在第二个文本字段中。你可以在需要时按ID获取所有视图模型。例如,如果你必须提交此表单,则可以使用相同的方法来获取电子邮件和密码字段。


建立登录表单

        我要求你使用我的框架自行构建一个登录表单。 我保证不超过30分钟的工作时间。 我将向你展示我将使用的最终视图控制器,因此这可能会给你一些帮助。

        如果你想稍微增加点趣味,甚至可以添加一个复选框以接受隐私策略。 这里的主要思想是你应该为表单中的每个项目创建可重用的组件。 因此,例如,具有相应视图模型的 ToggleView 是一种很好的方法(也适用于按钮)。 🤫

        这是最终的提示,你只需要创建自己的视图模型和视图即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import UIKit
import CollectionView

class ViewController: CollectionViewController {

enum Ids: String {
case email = "email-input"
case password = "password-input"
case privacyPolicy = "privacy-policy-checkbox"
case submit = "submit-button"
}

override func viewDidLoad() {
super.viewDidLoad()

let grid = Grid(columns: 1, margin: UIEdgeInsets(all: 16), padding: .zero)
self.collectionView.source = .init(grid: grid, [
[
InputViewModel(id: Ids.email.rawValue, .init(placeholder: "Email", value: nil))
.onEditingEnd { viewModel in
guard let passwordViewModel = viewModel.by(id: Ids.password.rawValue) as? InputViewModel else {
return
}
passwordViewModel.view?.textField.becomeFirstResponder()
},
InputViewModel(id: Ids.password.rawValue, .init(placeholder: "Password", value: nil, secure: true))
.onEditingEnd { viewModel in
viewModel.view?.textField.endEditing(true)
},
],
[
ToggleViewModel(id: Ids.privacyPolicy.rawValue, .init(label: "Privacy policy", value: false))
.onValueChange { viewModel in
guard let submitViewModel = viewModel.by(id: Ids.submit.rawValue) as? ButtonViewModel else {
return
}
var model = submitViewModel.model
model.enabled = viewModel.model.value
submitViewModel.model = model
submitViewModel.updateView()
},
],
[
ButtonViewModel(id: Ids.submit.rawValue, .init(title: "Submit", enabled: false))
.onSubmit { viewModel in
guard
let emailViewModel = viewModel.by(id: Ids.email.rawValue) as? InputViewModel,
let passwordViewModel = viewModel.by(id: Ids.password.rawValue) as? InputViewModel
else {
return
}
/* ... */
},
],
])
self.collectionView.reloadData()
}
}

        到此为止,这是一个几乎完整的登录表单,仅包含几行代码。 当然有一个基础框架,但是如果你检查 源代码 ,你实际上会发现它不包含任何被视为黑魔法的东西。 💫

坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道