TheDistanceForms
TheDistanceForms 是一个用于创建灵活表单的 iOS 框架,作为用户输入元素的通用集合。
特性
- 多种用户输入类型
- 文本(单行/多行)
- 布尔值
- 日期和时间
- 选择
- 媒体(图片/视频)
- 预建验证类型
- 空输入验证
- 电子邮件
- 邮编
- 正则表达式
- 自定义通用验证
- 可定制布局和样式
- JSON 表单创建
组件库
为了保持本库的专注性,其他库中提供额外的功能,这些库可通过 The Distance 获得。
要求
- iOS 8.0+
- Xcode 7.3+
安装
Carthage
Carthage 是一个去中心的依赖管理器,用于构建你的依赖,并提供二进制框架。
Carthage 是将此功能集成到您的项目的推荐方式。
使用方法
快速开始
开始使用最简单的方法是定义一个 JSON 表单。一旦你定义了你的表单,你将创建一个由你的数据 SwiftyJSON 表示形式的 Form
对象。
guard let data = NSData(contentsOfURL: jsonURL),
let form = Form(definition: JSON(data:data))
else {
return nil
}
这包含了所有表单中的视图和问题对象的引用。FormContainer
是一个提供设置表单、键盘输入和视图层次结构默认功能的设计协议。
创建一个符合 FormContainer
的 UIViewController
并按以下方式设置您的表单
var form:Form?
var keyboardResponder:KeyboardResponder?
override func viewDidLoad() {
super.viewDidLoad()
guard let jsonURL = NSBundle.mainBundle().URLForResource("Form", withExtension: "json"),
let form = addFormFromURL(jsonURL, toContainerView: scrollView, withInsets: UIEdgeInsetsMake(16.0, 16.0, 16.0, 8.0))
else { return }
self.form = form
keyboardResponder = setUpKeyboardResponder(onForm: form, withScrollView: scroll)
}
addFormFromURL(_:toContainerView:withInsets)
创建表单并将所有问题以垂直堆叠的形式添加到 containerView
参数中,在这种情况下是一个 UIScrollView
实例。这是默认 UI
setUpKeyboardResponder(onForm:withScrollView:)
为管理用户田野间的导航配置一个键盘响应者对象。
仓库项目包含一个演示应用程序,其中包含基本 JSON 表单和定制表单的示例。
验证
表单验证按每个问题单独执行,当该问题字段失去首次响应者状态或调用 form.validateForm()
或 form.validateValues()
中的任何一个时(先到的为准),验证就开始。JSON 中指定的验证较为有限,但如果在代码中指定,可以使用验证类型对象执行任何验证。
public struct Validation<Type> { ... }
对于以下类型都有便捷的初始化器可用:
- 非空字符串
- 字符串正则表达式
- 电子邮件
- 邮编
- 电话号码
- 通用数字
验证元素也可以组合,例如电话号码或电子邮件地址。
获取答案
要从表单中获取答案,最简单的方法是添加一个提交按钮。《FormContainer》的默认实现为每个《Button》添加了一个目标,该目标会将调用传递给《buttonTappedForQuestion(_:)》。您可以使用《question.key》来确定哪个按钮被点击,从而响应用户的提交按钮。
form.validateForm()
可以用来获取每个问题的《ValidationResult》数组。您可以对此进行响应,显示警告或如果总体结果是 《.Valid》则将用户的响应提交到服务器。
form.answersJSON()
返回一个关于问题《key》和用户响应的《SwiftyJSON》词典。如果没有响应,将为该《key》添加《null》到词典中。
{
"birthday" : null,
"first_name" : "Josh",
"station" : "London King's Cross",
"newsletter" : true,
"contact" : 1,
"event_start" : "2016-04-25 15:35:03 +0000"
}
JSON 表单设置
要开始,最简单的方法是定义一个 JSON 表单。《full schema》在存储库中定义(/Documentation/Schema.json),并可用于在例如 JSON Schema Lint 等网站上执行验证。一个 Form 有一个《title》和《questions》数组,并且应该是 JSON 的根对象。
{
"title": "New Booking",
"questions": [
...
]
}
每个问题都需要一个《key》和一个《question_type》,并且可以选择带一个《validation》。每种类型都有各种属性,用于定制该问题。
自由文本输入
可以使用 TextSingle
和 TextMultiline
来添加自由文本输入。《TextSingle》背后是由《TextFieldStack》支持的,《TextMultiline》背后是由《TextViewStack》支持的。它们向《UITextField》和《UITextView》分别添加了显示错误和占位符的功能。
一个简单的注释文本字段可以表示为:
{
"key": "notes",
"question_type": "TextMultiline",
"prompt": "Notes"
}
这将向您的表单添加一个带有占位符文本“笔记”的文本视图。
更多的配置可用,一个具有验证和外加大写的标题文本字段可以表示为:
{
"key": "booking_title",
"question_type": "TextSingle",
"prompt": "Title",
"capitalization": "Words",
"validation": {
"type": "NotEmpty",
"value_type": "String",
"message": "Please enter a title for your booking."
}
}
详细信息可以在模式空间中找到
选择
简单的选择可以通过UISegmentedControl
或UIPickerView
实现。分段选择将返回其值的索引作为选择数组中的索引进行验证。表单中唯一可指定的验证是NotEmpty
。因为没有文本视图提示,分段选择有一个标题和副标题。
{
"key": "contact",
"question_type": "ChoiceSegments",
"title": "Contact Preferences",
"subtitle": "How would you like us to keep in touch with you?",
"choices":[
"Email",
"Phone",
"SMS"
],
"validation": {
"type": "NotEmpty",
"value_type": "Number",
"message": "You must select a preference"
}
}
下拉选择在UIPickerView
中显示其选择,因此可以用于较长的选择列表。验证使用字符串,因此有更多的验证类型可用。
{
"key": "station",
"question_type": "ChoiceDropdown",
"prompt": "Select a station",
"choices":[
"London King's Cross",
"Peterborough",
"York",
"Darlington",
"Durham",
"Newcastle",
"Edinburgh Weverley"
],
"validation": {
"type": "NotEmpty",
"value_type": "String",
"message": "You must select a station"
}
}
布尔值
可以使用开关添加简单的是/否问题。这些可以是“订阅我们的通讯”或您可以添加验证以确保用户已同意条款和条件。
{
"key": "newsletter",
"question_type": "Boolean",
"title": "Newsletter",
"subtitle": "Would you like to keep up to date with all our latest news and offers?",
"default": false
}
按钮
可以使用Button
问题类型向表单中添加按钮。
{
"key": "view_terms",
"question_type": "Button",
"title": "View Terms & Conditions"
}
自定义表单
虽然JSON很方便,但它不能表达所有框架的功能。您可以使用Form
的子类创建一个表单,并添加额外的功能。这可以启用自定义UI、逻辑、布局和验证。
自定义UI
用户界面是由 FormQuestion.init(json:)
创建的。对于 JSON 定义中的每个问题,根据 question_type
,会调用 textSingleViewForQuestion(_:)
等 ...ViewForQuestion(_:)
方法中的一个。在必要时,这些方法会调用 newTextSingleView()
等 new...View()
方法来实例化用于显示每个问题的 UIView
。
要自定义用户界面,创建 FormQuestion
的子类并重写这些方法。当创建表单时,可以将 questionType
作为初始化参数指定,或者在 FormContainer
创建方法中指定。
class MyFormQuestion {
override func newTextSingleView() -> TextFieldStack {
let stack = super.newTextSingleView
stack.textField.font = ...
stack.errorLabel.textColor = ...
return stack
}
}
当创建您的表单时
self.form = Form(definition: JSON(data:data), questionType: MyFormQuestion.self)
使用 ThemeKit 进行样式设置
除了创建 FormQuestion
的子类和在 new...View()
方法中配置视图外,TheDistanceForms 还包含一个与 ThemeKit 兼容的框架(TheDistanceFormsThemed),它为 UIKit 组件添加了 TKTextField
、TKTextView
和其他 ThemeKit 子类。它还添加了 TKFormQuestion
类型,该类型重写了 new...View()
以返回 ThemeKit 栈。如果您想更改组件的样式,可以进一步子类化它。
下图中展示了使用简单的 Theme(红色强调色和 Avenir Next 字体样式)创建的测试表单。
let form = addFormFromURL(jsonURL, questionType: TKFormQuestion.self, toContainerView: scroll, withInsets: UIEdgeInsetsMake(16.0, 16.0, 16.0, 8.0))
自定义布局
重写默认布局并远离所有表格形式的数据输入。
通过创建一个新的栈而不是使用 formView
变量来实现自定义布局。可以使用来自 KeyedView
的 elementForKey(_:)
方法访问视图。键是问题键,视图是 QuestionView
的 UIView。通过这种方式,自定义视图可以以不同的方式堆叠视图,从而实现字段的并排布局,例如起始时间和结束时间,或标题、姓氏和名字。
演示应用程序包含一个 EventForm.json
定义,其中包含一个 TextSingle
字段、两个字段 DateTime
和一个 TextMultiline
字段。为了将日期字段并排排列,我们使用了一个名为 EventForm
的 Form
子类,重写了 createFormView(_:)
方法。
class EventForm: Form {
override func createFormView() -> StackView {
// 1. Test Views exist
guard let titleView = self.viewKeys["event_title"],
let startView = self.viewKeys["start_date"],
let endView = self.viewKeys["end_date"],
let notesView = self.viewKeys["notes"]
else { return super.createFormView() }
// 2. Align the dates horizontally
var dateStack = CreateStackView([startView, endView])
dateStack.spacing = 8.0
dateStack.stackDistribution = .FillEqually
// 3. Stack all components vertically
var stack = CreateStackView([titleView, dateStack.view, notesView])
stack.axis = .Vertical
stack.spacing = 16.0
return stack
}
}
当在 iOS 9 或 iOS 8 上运行时,我们使用 StackView 创建 UIStackView
或 TZStackView。
然后可以指定此版本的表单,在调用 FormContainer
方法将表单添加到我们的视图层次结构时覆盖默认表单。
guard let jsonURL = NSBundle.mainBundle().URLForResource("EventForm", withExtension: "json"),
let form = addFormFromURL(jsonURL, ofType: EventForm.self, toContainerView: formContainer, withInsets: UIEdgeInsetsZero)
else { return }
表单还将添加到表单容器中,而不是根视图,使表单作为视图层次结构的小部分存在。如果设置了 KeyboardResponder,则导航将按预期工作。可以以此方式创建更复杂的布局。
自定义逻辑
跟踪变化以强制日期选择上的条件。
您可以将表单子类进一步扩展,添加无法在JSON格式中指定的逻辑。例如,我们可以限制日期为将来日期,并确保结束日期晚于开始日期。
private(set) var startObserver:ObjectObserver!
required init?(definition: JSON, questionType: FormQuestion.Type) {
super.init(definition: definition, questionType: questionType)
// 1. Get the date controllers
guard let startView = questionForKey("start_date")?.questionView,
case let .DateTime(startStack, startController) = startView,
let endView = questionForKey("end_date")?.questionView,
case let .DateTime(_, endController) = endView
else {
return nil
}
// 2. Set the minimum dates to now
startController.datePicker.minimumDate = NSDate()
endController.datePicker.minimumDate = NSDate()
// 3. When the start date changes, update the end date to be later with a minimum duration
startObserver = ObjectObserver(keypath: "text", object: startStack.textField, completion: { (keypath, object, change) in
endController.datePicker.minimumDate = startController.datePicker.date.dateByAddingTimeInterval(30 * 60)
})
}
通过这种方式,我们可以构建之间的复杂关系
自定义验证
根据其他用户输入实现验证
注册示例是一个典型注册表单,包括姓名、电子邮件地址和密码。请注意电子邮件和密码字段的额外自定义。密码字段还使用正则表达式强制执行密码的给定格式。
{
"key": "password",
"question_type": "TextSingle",
"prompt": "Password",
"capitalization": ".None",
"auto_correction": false,
"secure_text_entry": true,
"validation": {
"type": "Regex",
"value_type": "String",
"regex": "^[a-zA-Z0-9]{6,}$",
"message": "Please enter a password at least 6 characters long containing only letters and numbers."
}
}
在JSON中无法指定的是确认字段匹配。我们可以在代码中通过覆盖表单初始化器并手动创建一个Validation
对象来完成。
required init?(definition: JSON, questionType: FormQuestion.Type) {
super.init(definition: definition, questionType: questionType)
guard let passwordView = questionForKey("password")?.questionView,
case let .TextSingle(passwordStack) = passwordView,
let confirmView = questionForKey("confirm_password")?.questionView,
case let .TextSingle(confirmStack) = confirmView
else {
return nil
}
confirmStack.validation = Validation(message: "Your passwords must match.", validation: { (value) -> Bool in
return value == passwordStack.text
})
}
辅助类
为了方便表单创建,已经添加了多个类来补充标准UIKit元素。
错误堆栈
一组UI组件,可以显示占位符、标题和错误消息,使用简单的属性。
- 参见:
TextFieldStack
、TextViewStack
、SwitchStack
UIPicker & UIDatePicker便利
一个包含模板代码、可作为UIPickerView
或UIDatePickerView
代理的NSObject
子类。这简化了用于日期和选择选择的UITextField
的设置。
通信
- 如果您发现了错误,请开启一个问题。
- 如果您有一个功能请求,请开启一个问题。
- 如果您想贡献力量,请提交一个Merge Request。
- 如果您想咨询一般性问题,请发送邮件至[email protected]。