TheDistanceForms 0.4

TheDistanceForms 0.4

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2018年12月
SPM支持 SPM

Vlad AlexaBen BaggleyJames Shaw 维护。



  • The Distance

TheDistanceForms

Carthage Compatible MIT license

TheDistanceForms 是一个用于创建灵活表单的 iOS 框架,作为用户输入元素的通用集合。

特性

  • 多种用户输入类型
    • 文本(单行/多行)
    • 布尔值
    • 日期和时间
    • 选择
    • 媒体(图片/视频)
  • 预建验证类型
    • 空输入验证
    • 电子邮件
    • 邮编
    • 正则表达式
  • 自定义通用验证
  • 可定制布局和样式
  • JSON 表单创建

Default form UI Custom Layout Form Themed Form

组件库

为了保持本库的专注性,其他库中提供额外的功能,这些库可通过 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 是一个提供设置表单、键盘输入和视图层次结构默认功能的设计协议。

创建一个符合 FormContainerUIViewController 并按以下方式设置您的表单

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:) 为管理用户田野间的导航配置一个键盘响应者对象。

Default form UI

仓库项目包含一个演示应用程序,其中包含基本 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》。每种类型都有各种属性,用于定制该问题。

自由文本输入

可以使用 TextSingleTextMultiline 来添加自由文本输入。《TextSingle》背后是由《TextFieldStack》支持的,《TextMultiline》背后是由《TextViewStack》支持的。它们向《UITextField》和《UITextView》分别添加了显示错误和占位符的功能。

TextFieldStack showing an error and placeholder.

一个简单的注释文本字段可以表示为:

{
    "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."
    }
} 

详细信息可以在模式空间中找到

选择

简单的选择可以通过UISegmentedControlUIPickerView实现。分段选择将返回其值的索引作为选择数组中的索引进行验证。表单中唯一可指定的验证是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"
    }
}

Segmented Choice view

下拉选择在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"
    }
}

Dropdown Choices

布尔值

可以使用开关添加简单的是/否问题。这些可以是“订阅我们的通讯”或您可以添加验证以确保用户已同意条款和条件。

{
    "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
}

Switch for boolean form elements

按钮

可以使用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 组件添加了 TKTextFieldTKTextView 和其他 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))

Themed Form

自定义布局

重写默认布局并远离所有表格形式的数据输入。

通过创建一个新的栈而不是使用 formView 变量来实现自定义布局。可以使用来自 KeyedViewelementForKey(_:) 方法访问视图。键是问题键,视图是 QuestionView 的 UIView。通过这种方式,自定义视图可以以不同的方式堆叠视图,从而实现字段的并排布局,例如起始时间和结束时间,或标题、姓氏和名字。

演示应用程序包含一个 EventForm.json 定义,其中包含一个 TextSingle 字段、两个字段 DateTime 和一个 TextMultiline 字段。为了将日期字段并排排列,我们使用了一个名为 EventFormForm 子类,重写了 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 创建 UIStackViewTZStackView

然后可以指定此版本的表单,在调用 FormContainer 方法将表单添加到我们的视图层次结构时覆盖默认表单。

guard let jsonURL = NSBundle.mainBundle().URLForResource("EventForm", withExtension: "json"),
        let form = addFormFromURL(jsonURL, ofType: EventForm.self, toContainerView: formContainer, withInsets: UIEdgeInsetsZero)
        else { return }

表单还将添加到表单容器中,而不是根视图,使表单作为视图层次结构的小部分存在。如果设置了 KeyboardResponder,则导航将按预期工作。可以以此方式创建更复杂的布局。

Custom Layout Form

自定义逻辑

跟踪变化以强制日期选择上的条件。

您可以将表单子类进一步扩展,添加无法在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组件,可以显示占位符、标题和错误消息,使用简单的属性。

  • 参见:TextFieldStackTextViewStackSwitchStack

UIPicker & UIDatePicker便利

一个包含模板代码、可作为UIPickerViewUIDatePickerView代理的NSObject子类。这简化了用于日期和选择选择的UITextField的设置。

通信

  • 如果您发现了错误,请开启一个问题。
  • 如果您有一个功能请求,请开启一个问题。
  • 如果您想贡献力量,请提交一个Merge Request。
  • 如果您想咨询一般性问题,请发送邮件至[email protected]