Eureka: Elegant form builder in Swift

Build status Platform iOS Swift 5 compatible Carthage compatible CocoaPods compatible License: MIT codebeat badge

Made with ❤️ by XMARTLABS. This is the re-creation of XLForm in Swift.

简体中文

概述

目录

有关更多信息,请查看 我们的博客文章,其中介绍了 Eureka

要求(对于最新版本)

示例项目

您可以克隆并运行示例项目,以查看 Eureka 大多数功能的示例。

用法

如何创建表单

通过扩展 FormViewController,您可以简单地将 Section 和行添加到 form 变量。

import Eureka

class MyFormViewController: FormViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        form +++ Section("Section1")
            <<< TextRow(){ row in
                row.title = "Text Row"
                row.placeholder = "Enter text here"
            }
            <<< PhoneRow(){
                $0.title = "Phone Row"
                $0.placeholder = "And numbers here"
            }
        +++ Section("Section2")
            <<< DateRow(){
                $0.title = "Date Row"
                $0.value = Date(timeIntervalSinceReferenceDate: 0)
            }
    }
}

在示例中,我们创建了两个带有标准行的 Section,结果如下

Screenshot of Custom Cells

您可以通过自己设置 form 属性来创建表单,而无需从 FormViewController 扩展,但是这种方法通常更方便。

配置键盘导航辅助视图

要更改此行为,您应该设置控制器的导航选项。 FormViewController 有一个 navigationOptions 变量,它是一个枚举,可以具有以下一个或多个值

默认值为 enabled & skipCanNotBecomeFirstResponderRow

要启用平滑滚动到屏幕外行,请通过 animateScroll 属性启用它。默认情况下,当用户点击键盘导航辅助视图中的下一个或上一个按钮时,FormViewController 会立即在行之间跳转,包括当下一行在屏幕外时。

要设置键盘和导航事件后突出显示行之间的间距,请设置 rowKeyboardSpacing 属性。默认情况下,当表单滚动到屏幕外视图时,键盘顶部和行底部之间不会留下任何空间。

class MyFormViewController: FormViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        form = ...

	// Enables the navigation accessory and stops navigation when a disabled row is encountered
	navigationOptions = RowNavigationOptions.Enabled.union(.StopDisabledRow)
	// Enables smooth scrolling on navigation to off-screen rows
	animateScroll = true
	// Leaves 20pt of space between the keyboard and the highlighted row after scrolling to an off screen row
	rowKeyboardSpacing = 20
    }
}

如果您想更改整个导航辅助视图,则必须在 FormViewController 的子类中覆盖 navigationAccessoryView 变量。

获取行值

Row 对象保存特定类型的 value。例如,SwitchRow 保存 Bool 值,而 TextRow 保存 String 值。

// Get the value of a single row
let row: TextRow? = form.rowBy(tag: "MyRowTag")
let value = row.value

// Get the value of all rows which have a Tag assigned
// The dictionary contains the 'rowTag':value pairs.
let valuesDictionary = form.values()

运算符

Eureka 包括自定义运算符,使表单创建变得容易

+++       添加 Section

form +++ Section()

// Chain it to add multiple Sections
form +++ Section("First Section") +++ Section("Another Section")

// Or use it with rows and get a blank section for free
form +++ TextRow()
     +++ TextRow()  // Each row will be on a separate section

<<<       插入行

form +++ Section()
        <<< TextRow()
        <<< DateRow()

// Or implicitly create the Section
form +++ TextRow()
        <<< DateRow()

+=        追加数组

// Append Sections into a Form
form += [Section("A"), Section("B"), Section("C")]

// Append Rows into a Section
section += [TextRow(), DateRow()]

Result builders

Eureka 包括 result builders,使表单创建变得容易

@SectionBuilder

// Section + Section
form = (Section("A") +++ {
    URLRow("UrlRow_f1") { $0.title = "Url" }
    if something {
        TwitterRow("TwitterRow_f2") { $0.title = "Twitter" }
    } else {
        TwitterRow("TwitterRow_f1") { $0.title = "Twitter" }
    }
    AccountRow("AccountRow_f1") { $0.title = "Account" }
})

// Form + Section
form +++ {
    if something {
        PhoneRow("PhoneRow_f1") { $0.title = "Phone" }
    } else {
        PhoneRow("PhoneRow_f2") { $0.title = "Phone" }
    }
    PasswordRow("PasswordRow_f1") { $0.title = "Password" }
}

@FormBuilder

@FormBuilder
var form: Form {
    Section("Section A") { section in
        section.tag = "Section_A"
    }
    if true {
        Section("Section B") { section in
            section.tag = "Section_B"
        }
    }
    NameRow("NameRow_f1") { $0.title = "Name" }
}

使用回调

Eureka 包括回调以更改行的外观和行为。

理解 Row 和 Cell

Row 是 Eureka 使用的抽象,它保存一个 value 并包含视图 CellCell 管理视图并继承 UITableViewCell

这是一个例子

let row  = SwitchRow("SwitchRow") { row in      // initializer
                        row.title = "The title"
                    }.onChange { row in
                        row.title = (row.value ?? false) ? "The title expands when on" : "The title"
                        row.updateCell()
                    }.cellSetup { cell, row in
                        cell.backgroundColor = .lightGray
                    }.cellUpdate { cell, row in
                        cell.textLabel?.font = .italicSystemFont(ofSize: 18.0)
                }

Screenshot of Disabled Row

回调列表

Section 标题和页脚

您可以将标题 String 或自定义 View 设置为 Section 的标题或页脚。

字符串标题

Section("Title")

Section(header: "Title", footer: "Footer Title")

Section(footer: "Footer Title")

自定义视图

您可以使用来自 .xib 文件的自定义视图

Section() { section in
    var header = HeaderFooterView<MyHeaderNibFile>(.nibFile(name: "MyHeaderNibFile", bundle: nil))

    // Will be called every time the header appears on screen
    header.onSetupView = { view, _ in
        // Commonly used to setup texts inside the view
        // Don't change the view hierarchy or size here!
    }
    section.header = header
}

或以编程方式创建的自定义 UIView

Section(){ section in
    var header = HeaderFooterView<MyCustomUIView>(.class)
    header.height = {100}
    header.onSetupView = { view, _ in
        view.backgroundColor = .red
    }
    section.header = header
}

或者只是使用回调构建视图

Section(){ section in
    section.header = {
          var header = HeaderFooterView<UIView>(.callback({
              let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
              view.backgroundColor = .red
              return view
          }))
          header.height = { 100 }
          return header
        }()
}

动态隐藏和显示行(或 Section)

Screenshot of Hidden Rows

在这种情况下,我们正在隐藏和显示整个 Sections。

为了实现这一点,每行都有一个可选类型 Conditionhidden 变量,可以使用函数或 NSPredicate 进行设置。

使用函数条件隐藏

使用 Conditionfunction case

Condition.function([String], (Form)->Bool)

要传递的 String 数组应包含此行依赖的行的标签。每次任何这些行的值更改时,都会重新评估该函数。然后,该函数接受 Form 并返回一个 Bool,指示该行是否应隐藏。这是设置 hidden 属性的最强大方法,因为它对可以完成的操作没有明确的限制。

form +++ Section()
            <<< SwitchRow("switchRowTag"){
                $0.title = "Show message"
            }
            <<< LabelRow(){

                $0.hidden = Condition.function(["switchRowTag"], { form in
                    return !((form.rowBy(tag: "switchRowTag") as? SwitchRow)?.value ?? false)
                })
                $0.title = "Switch is on!"
        }

Screenshot of Hidden Rows

public enum Condition {
    case function([String], (Form)->Bool)
    case predicate(NSPredicate)
}

使用 NSPredicate 隐藏

hidden 变量也可以使用 NSPredicate 设置。在谓词字符串中,您可以参考其他行的标签值,以确定行是否应隐藏或可见。这仅在谓词必须检查的行的值是 NSObjects 时才有效(String 和 Int 将起作用,因为它们桥接到它们的 ObjC 对等项,但枚举将不起作用)。那么,当谓词更受限制时,为什么使用谓词可能有用呢?嗯,它们可以比函数更简单、更短和更易读。看看这个例子

$0.hidden = Condition.predicate(NSPredicate(format: "$switchTag == false"))

我们可以写得更短,因为 Condition 符合 ExpressibleByStringLiteral

$0.hidden = "$switchTag == false"

注意:我们将替换标签为 'switchTag' 的行的值,而不是 '$switchTag'

为了使所有这些都有效,所有相关的行都必须有一个标签,因为标签将标识它们。

我们也可以通过执行以下操作来隐藏行

$0.hidden = true

因为 Condition 符合 ExpressibleByBooleanLiteral

不设置 hidden 变量将使行始终可见。

如果您在表单显示后手动设置隐藏(或禁用)条件,您可能需要调用 row.evaluateHidden() 以强制 Eureka 重新评估新条件。有关更多信息,请参阅 此 FAQ 部分

Sections

对于 Sections,这同样适用。这意味着我们可以设置 Section 的 hidden 属性以动态显示/隐藏它。

禁用行

要禁用行,每行都有一个 disabled 变量,它也是一个可选的 Condition 类型属性。此变量的工作方式与 hidden 变量相同,因此它要求行具有标签。

请注意,如果您想永久禁用行,您也可以将 disabled 变量设置为 true

列表 Sections

要显示选项列表,Eureka 包括一个名为 SelectableSection 的特殊 Section。创建时,您需要传递要在选项中使用的行类型和 selectionTypeselectionType 是一个枚举,可以是 multipleSelectionsingleSelection(enableDeselection: Bool),其中 enableDeselection 参数确定是否可以取消选择选定的行。

form +++ SelectableSection<ListCheckRow<String>>("Where do you live", selectionType: .singleSelection(enableDeselection: true))

let continents = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"]
for option in continents {
    form.last! <<< ListCheckRow<String>(option){ listRow in
        listRow.title = option
        listRow.selectableValue = option
        listRow.value = nil
    }
}
可以使用哪种类型的行?

要创建这样的 Section,您必须创建一个符合 SelectableRowType 协议的行。

public protocol SelectableRowType : RowType {
    var selectableValue : Value? { get set }
}

selectableValue 是行的值将永久存储的位置。value 变量将用于确定行是否被选中,如果被选中,则为 'selectableValue',否则为 nil。Eureka 包括 ListCheckRow,例如使用它。在 Examples 项目的自定义行中,您还可以找到 ImageCheckRow

获取选定的行

为了轻松获取 SelectableSection 的选定行,有两种方法:selectedRow()selectedRows(),可以调用它们以获取 SingleSelection Section 中的选定行,或者如果是 MultipleSelection Section,则获取所有选定的行。

在 Sections 中对选项进行分组

此外,您可以使用 SelectorViewController 的以下属性设置按 Sections 分组的选项列表

多值 Sections

Eureka 通过使用多值 Sections 支持某个字段的多个值(例如联系人中的电话号码)。它允许我们轻松创建可插入、可删除和可重新排序的 Sections。

Screenshot of Multivalued Section

如何创建多值 Section

为了创建多值 Section,我们必须使用 MultivaluedSection 类型而不是常规的 Section 类型。MultivaluedSection 扩展了 Section,并具有一些额外的属性来配置多值 Section 的行为。

让我们深入研究一个代码示例...

form +++
    MultivaluedSection(multivaluedOptions: [.Reorder, .Insert, .Delete],
                       header: "Multivalued TextField",
                       footer: ".Insert adds a 'Add Item' (Add New Tag) button row as last cell.") {
        $0.addButtonProvider = { section in
            return ButtonRow(){
                $0.title = "Add New Tag"
            }
        }
        $0.multivaluedRowToInsertAt = { index in
            return NameRow() {
                $0.placeholder = "Tag Name"
            }
        }
        $0 <<< NameRow() {
            $0.placeholder = "Tag Name"
        }
    }

之前的代码片段显示了如何创建多值 Section。在这种情况下,我们希望插入、删除和重新排序行,如 multivaluedOptions 参数所示。

addButtonProvider 允许我们自定义按钮行,该按钮行在点击时插入新行,而 multivaluedOptions 包含 .Insert 值。

每次需要插入新行时,Eureka 都会调用 multivaluedRowToInsertAt 闭包属性。为了提供要添加到多值 Section 的行,我们应该设置此属性。Eureka 将索引作为闭包参数传递。请注意,我们可以返回任何类型的行,甚至是自定义行,即使在大多数情况下,多值 Section 行的类型都相同。

当我们创建一个可插入的多值 Section 时,Eureka 会自动添加一个按钮行。我们可以自定义此按钮行的外观,正如我们之前解释的那样。showInsertIconInAddButton 属性指示加号按钮(插入样式)是否应出现在按钮的左侧,默认为 true。

在创建可插入的 Sections 时,我们需要考虑一些事项。添加到可插入的多值 Section 的任何行都应放置在 Eureka 自动添加以插入新行的行之上。这可以通过从 Section 的初始化程序闭包(Section 初始化程序的最后一个参数)内部将这些附加行添加到 Section 来轻松实现,这样 Eureka 就会在 Section 的末尾添加添加插入按钮。

编辑模式

默认情况下,仅当表单中存在 MultivaluedSection 时,Eureka 才会将 tableView 的 isEditing 设置为 true。这将在首次呈现表单的 viewWillAppear 中完成。

有关如何使用多值 Sections 的更多信息,请查看 Eureka 示例项目,其中包含多个用法示例。

自定义添加按钮

如果您想使用不是 ButtonRow 的添加按钮,则可以使用 GenericMultivaluedSection<AddButtonType>,其中 AddButtonType 是您要用作添加按钮的行的类型。如果您想使用自定义行来更改按钮的 UI,这将非常有用。

例子

GenericMultivaluedSection<LabelRow>(multivaluedOptions: [.Reorder, .Insert, .Delete], {
    $0.addButtonProvider = { section in
        return LabelRow(){
            $0.title = "A Label row as add button"
        }
    }
    // ...
}

验证

Eureka 2.0.0 引入了广受欢迎的内置验证功能。

行具有 Rules 的集合和特定的配置,该配置确定何时应评估验证规则。

默认情况下提供了一些规则,但您也可以创建自己的规则。

提供的规则是

让我们看看如何设置验证规则。

override func viewDidLoad() {
        super.viewDidLoad()
        form
          +++ Section(header: "Required Rule", footer: "Options: Validates on change")

            <<< TextRow() {
                $0.title = "Required Rule"
                $0.add(rule: RuleRequired())

		// This could also have been achieved using a closure that returns nil if valid, or a ValidationError otherwise.
		/*
		let ruleRequiredViaClosure = RuleClosure<String> { rowValue in
		return (rowValue == nil || rowValue!.isEmpty) ? ValidationError(msg: "Field required!") : nil
		}
		$0.add(rule: ruleRequiredViaClosure)
		*/

                $0.validationOptions = .validatesOnChange
            }
            .cellUpdate { cell, row in
                if !row.isValid {
                    cell.titleLabel?.textColor = .systemRed
                }
            }

          +++ Section(header: "Email Rule, Required Rule", footer: "Options: Validates on change after blurred")

            <<< TextRow() {
                $0.title = "Email Rule"
                $0.add(rule: RuleRequired())
                $0.add(rule: RuleEmail())
                $0.validationOptions = .validatesOnChangeAfterBlurred
            }
            .cellUpdate { cell, row in
                if !row.isValid {
                    cell.titleLabel?.textColor = .systemRed
                }
            }

正如您在前面的代码片段中看到的,我们可以在行中通过调用行的 add(rule:) 函数来设置任意数量的规则。

行还提供 func remove(ruleWithIdentifier identifier: String) 来删除规则。为了使用它,我们必须在创建规则后为其分配一个 id。

有时我们想要在行上使用的规则集合与我们想要在许多其他行上使用的规则集合相同。在这种情况下,我们可以使用 RuleSet 设置所有验证规则,RuleSet 是验证规则的集合。

var rules = RuleSet<String>()
rules.add(rule: RuleRequired())
rules.add(rule: RuleEmail())

let row = TextRow() {
            $0.title = "Email Rule"
            $0.add(ruleSet: rules)
            $0.validationOptions = .validatesOnChangeAfterBlurred
        }

Eureka 允许我们指定何时应评估验证规则。我们可以通过设置 validationOptions 行的属性来做到这一点,该属性可以具有以下值

如果您想验证整个表单(所有行),您可以手动调用 Form validate() 方法。

如何获取验证错误

每行都有 validationErrors 属性,可用于检索所有验证错误。此属性仅保存最新行验证执行的验证错误列表,这意味着它不会评估行的验证规则。

关于类型的说明

正如预期的那样,Rules 必须使用与 Row 对象相同的类型。请格外小心检查使用的行类型。当混合类型时,您可能会看到编译器错误(“调用中的不正确的参数标签(具有 'rule:',预期为 'ruleSet:')”,但未指向问题)。

滑动操作

通过使用滑动操作,我们可以为每行定义多个 leadingSwipetrailingSwipe 操作。由于滑动操作依赖于 iOS 系统功能,因此 leadingSwipe 仅在 iOS 11.0+ 上可用。

让我们看看如何定义滑动操作。

let row = TextRow() {
            let deleteAction = SwipeAction(
                style: .destructive,
                title: "Delete",
                handler: { (action, row, completionHandler) in
                    //add your code here.
                    //make sure you call the completionHandler once done.
                    completionHandler?(true)
                })
            deleteAction.image = UIImage(named: "icon-trash")

            $0.trailingSwipe.actions = [deleteAction]
            $0.trailingSwipe.performsFirstActionWithFullSwipe = true

            //please be aware: `leadingSwipe` is only available on iOS 11+ only
            let infoAction = SwipeAction(
                style: .normal,
                title: "Info",
                handler: { (action, row, completionHandler) in
                    //add your code here.
                    //make sure you call the completionHandler once done.
                    completionHandler?(true)
                })
            infoAction.actionBackgroundColor = .blue
            infoAction.image = UIImage(named: "icon-info")

            $0.leadingSwipe.actions = [infoAction]
            $0.leadingSwipe.performsFirstActionWithFullSwipe = true
        }

滑动操作需要将 tableView.isEditing 设置为 false。如果表单中存在 MultivaluedSection(在 viewWillAppear 中),Eureka 将将其设置为 true。如果您的同一个表单中同时具有 MultivaluedSections 和滑动操作,则应根据需要设置 isEditing

自定义行

您经常需要与 Eureka 中包含的行不同的行。如果是这种情况,您将必须创建自己的行,但这应该不困难。您可以阅读 关于如何创建自定义行的本教程 以开始使用。您可能还想查看 EurekaCommunity,其中包含一些可以添加到 Eureka 的额外行。

基本自定义行

要创建具有自定义行为和外观的行,您可能需要创建 RowCell 的子类。

请记住,Row 是 Eureka 使用的抽象,而 Cell 是负责视图的实际 UITableViewCell。由于 Row 包含 Cell,因此必须为相同的 value 类型定义 RowCell

// Custom Cell with value type: Bool
// The cell is defined using a .xib, so we can set outlets :)
public class CustomCell: Cell<Bool>, CellType {
    @IBOutlet weak var switchControl: UISwitch!
    @IBOutlet weak var label: UILabel!

    public override func setup() {
        super.setup()
        switchControl.addTarget(self, action: #selector(CustomCell.switchValueChanged), for: .valueChanged)
    }

    func switchValueChanged(){
        row.value = switchControl.on
        row.updateCell() // Re-draws the cell which calls 'update' bellow
    }

    public override func update() {
        super.update()
        backgroundColor = (row.value ?? false) ? .white : .black
    }
}

// The custom Row also has the cell: CustomCell and its correspond value
public final class CustomRow: Row<CustomCell>, RowType {
    required public init(tag: String?) {
        super.init(tag: tag)
        // We set the cellProvider to load the .xib corresponding to our cell
        cellProvider = CellProvider<CustomCell>(nibName: "CustomCell")
    }
}

结果
Screenshot of Disabled Row


自定义行需要继承 `Row` 并符合 `RowType` 协议。自定义单元格需要继承 `Cell` 并符合 `CellType` 协议。

就像回调 cellSetup 和 CellUpdate 一样,Cell 具有 setup 和 update 方法,您可以在其中自定义它。

自定义内联行

内联行是一种特定类型的行,它动态地在其下方显示一行,通常内联行在展开和折叠模式之间切换,只要点击该行即可。

因此,要创建内联行,我们需要 2 行,即“始终”可见的行和将展开/折叠的行。

另一个要求是这两行的值类型必须相同。这意味着如果一行保存 String 值,则另一行也必须具有 String 值。

一旦我们有了这两行,我们应该使顶行类型符合 InlineRowType。此协议要求您定义一个 InlineRow 类型别名和一个 setupInlineRow 函数。InlineRow 类型将是要展开/折叠的行的类型。以此为例

class PickerInlineRow<T> : Row<PickerInlineCell<T>> where T: Equatable {

    public typealias InlineRow = PickerRow<T>
    open var options = [T]()

    required public init(tag: String?) {
        super.init(tag: tag)
    }

    public func setupInlineRow(_ inlineRow: InlineRow) {
        inlineRow.options = self.options
        inlineRow.displayValueFor = self.displayValueFor
        inlineRow.cell.height = { UITableViewAutomaticDimension }
    }
}

InlineRowType 还会向您的内联行添加一些方法

func expandInlineRow()
func collapseInlineRow()
func toggleInlineRow()

这些方法应该可以正常工作,但如果您想覆盖它们,请记住,必须是 toggleInlineRow 调用 expandInlineRowcollapseInlineRow

最后,您必须在选择行时调用 toggleInlineRow(),例如覆盖 customDidSelect

public override func customDidSelect() {
    super.customDidSelect()
    if !isDisabled {
        toggleInlineRow()
    }
}

自定义 Presenter 行

注意: Presenter 行是呈现新 UIViewController 的行。

要创建自定义 Presenter 行,您必须创建一个符合 PresenterRowType 协议的类。强烈建议继承 SelectorRow,因为它确实符合该协议并添加了其他有用的功能。

PresenterRowType 协议定义如下

public protocol PresenterRowType: TypedRowType {

     associatedtype PresentedControllerType : UIViewController, TypedRowControllerType

     /// Defines how the view controller will be presented, pushed, etc.
     var presentationMode: PresentationMode<PresentedControllerType>? { get set }

     /// Will be called before the presentation occurs.
     var onPresentCallback: ((FormViewController, PresentedControllerType) -> Void)? { get set }
}

当行即将呈现另一个视图控制器时,将调用 onPresentCallback。这在 SelectorRow 中完成,因此如果您不继承它,则必须自己调用它。

presentationMode 定义了如何呈现控制器以及呈现哪个控制器。此呈现可以使用 Segue 标识符、segue 类、模态呈现控制器或推送到特定的视图控制器。例如,可以像这样定义 CustomPushRow

让我们看一个例子..

/// Generic row type where a user must select a value among several options.
open class SelectorRow<Cell: CellType>: OptionsRow<Cell>, PresenterRowType where Cell: BaseCell {


    /// Defines how the view controller will be presented, pushed, etc.
    open var presentationMode: PresentationMode<SelectorViewController<SelectorRow<Cell>>>?

    /// Will be called before the presentation occurs.
    open var onPresentCallback: ((FormViewController, SelectorViewController<SelectorRow<Cell>>) -> Void)?

    required public init(tag: String?) {
        super.init(tag: tag)
    }

    /**
     Extends `didSelect` method
     */
    open override func customDidSelect() {
        super.customDidSelect()
        guard let presentationMode = presentationMode, !isDisabled else { return }
        if let controller = presentationMode.makeController() {
            controller.row = self
            controller.title = selectorTitle ?? controller.title
            onPresentCallback?(cell.formViewController()!, controller)
            presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!)
        } else {
            presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!)
        }
    }

    /**
     Prepares the pushed row setting its title and completion callback.
     */
    open override func prepare(for segue: UIStoryboardSegue) {
        super.prepare(for: segue)
        guard let rowVC = segue.destination as Any as? SelectorViewController<SelectorRow<Cell>> else { return }
        rowVC.title = selectorTitle ?? rowVC.title
        rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback
        onPresentCallback?(cell.formViewController()!, rowVC)
        rowVC.row = self
    }
}


// SelectorRow conforms to PresenterRowType
public final class CustomPushRow<T: Equatable>: SelectorRow<PushSelectorCell<T>>, RowType {

    public required init(tag: String?) {
        super.init(tag: tag)
        presentationMode = .show(controllerProvider: ControllerProvider.callback {
            return SelectorViewController<T>(){ _ in }
            }, onDismiss: { vc in
                _ = vc.navigationController?.popViewController(animated: true)
        })
    }
}

使用同一行继承单元格

有时我们想更改我们其中一行的 UI 外观,但不更改行类型以及与一行关联的所有逻辑。目前有一种方法可以做到这一点,如果您正在使用从 nib 文件实例化的单元格。目前,Eureka 的核心行都不是从 nib 文件实例化的,但 EurekaCommunity 中的一些自定义行是,特别是 PostalAddressRow,它已移至此处。

您必须做的是

<<< PostalAddressRow() {
     $0.cellProvider = CellProvider<PostalAddressCell>(nibName: "CustomNib", bundle: Bundle.main)
}

您也可以为此创建一个新行。在这种情况下,尝试从与您要更改的行相同的超类继承,以继承其逻辑。

当您这样做时,需要考虑一些事项

行目录

控件行

标签行


按钮行


复选框行


开关行


滑块行


步进器行


文本区域行


字段行

这些行在单元格的右侧有一个文本字段。它们之间的区别在于不同的大小写、自动更正和键盘类型配置。

TextRow

NameRow

URLRow

IntRow

PhoneRow

PasswordRow

EmailRow

DecimalRow

TwitterRow

AccountRow

ZipCodeRow

上面所有 FieldRow 子类型都具有 NSFormatter 类型的 formatter 属性,可以设置该属性以确定应如何显示该行的值。Eureka 包含一个用于显示小数点后两位的数字的自定义格式化程序 (DecimalFormatter)。示例项目还包含一个 CurrencyFormatter,它根据用户的语言环境将数字显示为货币。

默认情况下,设置行的 formatter 仅影响在未编辑值时值的显示方式。要在编辑行时也格式化值,请在初始化行时将 useFormatterDuringInput 设置为 true。在编辑值时格式化值可能需要更新光标位置,Eureka 提供了以下协议,您的格式化程序应符合该协议以处理光标位置

public protocol FormatterProtocol {
    func getNewPosition(forPosition forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition
}

此外,FieldRow 子类型具有 useFormatterOnDidBeginEditing 属性。当将 DecimalRow 与允许十进制值并符合用户语言环境的格式化程序(例如 DecimalFormatter)一起使用时,如果 useFormatterDuringInputfalse,则必须将 useFormatterOnDidBeginEditing 设置为 true,以便正在编辑的值中的小数点与键盘上的小数点匹配。

日期行

日期行保存日期,并允许我们通过 UIDatePicker 控件设置新值。UIDatePicker 的模式以及日期选择器视图的显示方式是它们之间的区别。

日期行
选择器显示在键盘中。
日期行(内联)
行展开。
日期行(选择器)
选择器始终可见。

通过这 3 种样式(普通、内联和选择器),Eureka 包括

选项行

这些行具有与之关联的选项列表,用户必须从中选择。

<<< ActionSheetRow<String>() {
                $0.title = "ActionSheetRow"
                $0.selectorTitle = "Pick a number"
                $0.options = ["One","Two","Three"]
                $0.value = "Two"    // initially selected
            }
Alert Row

将显示一个警报,其中包含要选择的选项。
ActionSheet Row

将显示一个操作表,其中包含要选择的选项。
Push Row

将推送到一个新的控制器,从中选择使用复选框行列出的选项。
Multiple Selector Row

类似于 PushRow,但允许选择多个选项。
Segmented Row
Segmented Row(带标题)
Picker Row

通过选择器视图呈现通用类型的选项
(还有 Picker Inline Row)

构建了您自己的自定义行?

请告诉我们,我们很乐意在此处提及它。:)

Screenshot of Location Row

安装

CocoaPods

CocoaPods 是 Cocoa 项目的依赖项管理器。

将 Eureka 指定到您项目的 Podfile

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

pod 'Eureka'

然后运行以下命令

$ pod install

Swift Package Manager

Swift Package Manager 是用于管理 Swift 代码分发的工具。

设置 Package.swift 清单文件后,可以通过将其添加到 Package.swift 的 dependencies 值中,将 Eureka 添加为依赖项。

dependencies: [ .package(url: "https://github.com/xmartlabs/Eureka.git", from: "5.5.0") ]

Carthage

Carthage 是一个简单、分散的 Cocoa 依赖项管理器。

将 Eureka 指定到您项目的 Cartfile

github "xmartlabs/Eureka" ~> 5.5

手动作为嵌入式框架

$ git submodule add https://github.com/xmartlabs/Eureka.git

参与进来

在贡献代码之前,请查看 CONTRIBUTING 文件以获取更多信息。

如果您在您的应用中使用 Eureka,我们很乐意听到!请在 Twitter 上给我们留言。

作者

FAQ

如何更改单元格中显示的行值的文本表示形式。

每一行都有以下属性

/// Block variable used to get the String that should be displayed for the value of this row.
public var displayValueFor: ((T?) -> String?)? = {
    return $0.map { String(describing: $0) }
}

您可以根据要显示的字符串值设置 displayValueFor

如何使用其标签值获取行

我们可以通过调用 Form 类公开的以下任何函数来获取特定行

public func rowBy<T: Equatable>(tag: String) -> RowOf<T>?
public func rowBy<Row: RowType>(tag: String) -> Row?
public func rowBy(tag: String) -> BaseRow?

例如

let dateRow : DateRow? = form.rowBy(tag: "dateRowTag")
let labelRow: LabelRow? = form.rowBy(tag: "labelRowTag")

let dateRow2: Row<DateCell>? = form.rowBy(tag: "dateRowTag")

let labelRow2: BaseRow? = form.rowBy(tag: "labelRowTag")

如何使用标签值获取 Section

let section: Section?  = form.sectionBy(tag: "sectionTag")

如何使用字典设置表单值

调用 Form 类公开的 setValues(values: [String: Any?]) 方法。

例如

form.setValues(["IntRowTag": 8, "TextRowTag": "Hello world!", "PushRowTag": Company(name:"Xmartlabs")])

其中 "IntRowTag""TextRowTag""PushRowTag" 是行标签(每个标签唯一标识一行),而 8"Hello world!"Company(name:"Xmartlabs") 是要分配的相应行值。

行的值类型必须与相应字典的值类型匹配,否则将分配 nil。

如果表单已经显示,我们必须重新加载可见行,可以通过重新加载表视图 tableView.reloadData() 或为每个可见行调用 updateCell() 来实现。

更改隐藏或禁用条件后,行不会更新

设置条件后,此条件不会自动评估。 如果您希望立即评估,可以调用 .evaluateHidden().evaluateDisabled()

这些函数仅在将行添加到表单以及它所依赖的行发生更改时调用。 如果在显示行时条件发生更改,则必须手动重新评估。

除非还定义了 onCellHighlight,否则不会调用 onCellUnHighlight

请查看此 issue

如何更新 Section 的 header/footer

section.header = HeaderFooterView(title: "Header title \(variable)") // use String interpolation
//or
var header = HeaderFooterView<UIView>(.class) // most flexible way to set up a header using any view type
header.height = { 60 }  // height can be calculated
header.onSetupView = { view, section in  // each time the view is about to be displayed onSetupView is invoked.
    view.backgroundColor = .orange
}
section.header = header
section.reload()

如何自定义 Selector 和 MultipleSelector 选项单元格

提供了 selectableRowSetupselectableRowCellUpdateselectableRowCellSetup 属性,以便能够自定义 SelectorViewController 和 MultipleSelectorViewController 的可选择单元格。

let row = PushRow<Emoji>() {
              $0.title = "PushRow"
              $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻]
              $0.value = 👦🏼
              $0.selectorTitle = "Choose an Emoji!"
          }.onPresent { from, to in
              to.dismissOnSelection = false
              to.dismissOnChange = false
              to.selectableRowSetup = { row in
                  row.cellProvider = CellProvider<ListCheckCell<Emoji>>(nibName: "EmojiCell", bundle: Bundle.main)
              }
              to.selectableRowCellUpdate = { cell, row in
                  cell.textLabel?.text = "Text " + row.selectableValue!  // customization
                  cell.detailTextLabel?.text = "Detail " +  row.selectableValue!
              }
          }

不想使用 Eureka 自定义运算符?

正如我们所说,FormSection 类型符合 MutableCollectionRangeReplaceableCollection 协议。 Form 是 Section 的集合,Section 是 Row 的集合。

RangeReplaceableCollection 协议扩展提供了许多有用的方法来修改集合。

extension RangeReplaceableCollection {
    public mutating func append(_ newElement: Self.Element)
    public mutating func append<S>(contentsOf newElements: S) where S : Sequence, Self.Element == S.Element
    public mutating func insert(_ newElement: Self.Element, at i: Self.Index)
    public mutating func insert<S>(contentsOf newElements: S, at i: Self.Index) where S : Collection, Self.Element == S.Element
    public mutating func remove(at i: Self.Index) -> Self.Element
    public mutating func removeSubrange(_ bounds: Range<Self.Index>)
    public mutating func removeFirst(_ n: Int)
    public mutating func removeFirst() -> Self.Element
    public mutating func removeAll(keepingCapacity keepCapacity: Bool)
    public mutating func reserveCapacity(_ n: Self.IndexDistance)
}

这些方法在内部用于实现自定义运算符,如下所示

public func +++(left: Form, right: Section) -> Form {
    left.append(right)
    return left
}

public func +=<C : Collection>(inout lhs: Form, rhs: C) where C.Element == Section {
    lhs.append(contentsOf: rhs)
}

public func <<<(left: Section, right: BaseRow) -> Section {
    left.append(right)
    return left
}

public func +=<C : Collection>(inout lhs: Section, rhs: C) where C.Element == BaseRow {
    lhs.append(contentsOf: rhs)
}

您可以在 此处 查看其余自定义运算符的实现方式。

是否要使用 Eureka 自定义运算符取决于您自己。

如何从 storyboard 设置表单

表单始终显示在 UITableView 中。 您可以在 storyboard 中设置您的视图控制器,并在您希望的位置添加 UITableView,然后将 outlet 连接到 FormViewController 的 tableView 变量。 这允许您为表单定义自定义框架(可能带有约束)。

所有这些也可以通过以编程方式更改 FormViewController 的 tableView 的框架、边距等来完成。

捐赠给 Eureka

这样我们就可以让 Eureka 变得更好!

变更日志

这可以在 CHANGELOG.md 文件中找到。