TheDistanceFormsThemed 0.3.5

TheDistanceFormsThemed 0.3.5

测试已测试
Lang语言 SwiftSwift
许可 MIT
Released最后发布2017年6月
SwiftSwift 版本3.0
SPM支持 SPM

Vlad AlexaBen BaggleyJames Shaw 维护。



  • 作者:
  • The Distance

距离表单

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

功能

  • [x] 多种用户输入类型
    • 文本(单行/多行)
    • 布尔值
    • 日期与时间
    • 选择
    • 媒体(图像/视频)

  • [x] 预建验证类型
    • 空白条目
    • 电子邮件
    • 邮政编码
    • 正则表达式

  • [x] 自定义通用验证
  • [x] 可自定义的布局和样式
  • [x] JSON表单创建

Default form UI Custom Layout Form Themed Form

组件库

为了保持此库的集中性,其他库中提供了额外的功能,这些库由The Distance提供。

需求

  • iOS 8.0+
  • Xcode 7.3+

用法

快速开始

开始的最简单方法是定义一个 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:) 配置一个 KeyboardResponder 对象,以管理用户在字段之间的导航。

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表单。完整的模式定义在存储库中(/Documentation/Schema.json),可用于在类似JSON Schema Lint的网站上进行验证。表单有一个title和一个questions数组,并且应该是JSON的根对象。

{
    "title": "New Booking",
    "questions": [
        ...
    ]
}

每个问题都需要一个key和一个question_type,并且可以选择添加一个validation。每种类型都有各种自定义选项的属性。

自由文本输入

可以使用TextSingleTextMultiline添加自由文本输入。这两个类型分别采用一个TextFieldStackTextViewStack。它们分别为UITextFieldUITextView添加显示错误和占位符功能。

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

UI是通过FormQuestion.init(json:)创建的。对于JSON定义中的每个问题,根据question_type,会调用textSingleViewForQuestion(_:)和其它...ViewForQuestion(_:)方法中的一种。当需要时,这些方法会调用newTextSingleView()和其它new...View()方法来实例化用于显示每个问题的UIView

要自定义UI,可以继承FormQuestion并重写这些方法。创建表单时,可以通过初始化参数或通过在FormContainer创建者方法中指定来指定questionType

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堆栈。如有需要,您可以进一步继承它以更改组件的样式。

以下是一个使用简单的主题(红色强调色和Avenir Next字体样式)创建的测试表单示例。

let form = addFormFromURL(jsonURL, questionType: TKFormQuestion.self, toContainerView: scroll, withInsets: UIEdgeInsetsMake(16.0, 16.0, 16.0, 8.0))

Themed Form

自定义布局

覆盖默认布局,并远离所有表格数据输入。

可以通过创建一个新的堆叠而不是使用formView变量来实现自定义布局。可以使用从KeyedView中获得的elementForKey(_:)方法访问视图。密钥是问题密钥,视图是QuestionView的UIView。通过这种方式,自定义视图可以按不同的方式堆叠视图,以便实现字段如开始和结束时间或标题、名字和姓氏的并列布局。

示例应用程序包含一个EventForm.json定义。它包含一个TextSingle、两个DateTime和一个TextMultiline字段。通过覆盖Form的子类中的createFormView(_:)方法,我们可以将日期字段并排放置,该子类被称为EventForm

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
    }

}

我们使用StackView来创建一个UIStackView(如果您正在运行iOS 9)或TZStackView(如果您正在运行iOS 8)。

然后可以指定这种版本的表单,当调用FormContainer方法将表单添加到我们的视图层次结构时。

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

表单还添加到表单容器中,而不是根视图,这样表单就可以作为视图层次结构中的一部分。提供键盘响应者被设置为之前的状态,导航功能如预期的一样正常工作。可以利用这种方法创建更复杂的布局。

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<String>对象来实现这一点。

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

交流

  • 如果您发现问题,请提交工单。
  • 如果您有功能请求,请提交工单。
  • 如果您想贡献,提交拉取请求。
  • 如果您想提出一个一般性的问题,请通过[email protected]给我们发邮件。