Eureka 5.5.0

Eureka 5.5.0

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最后发布2024 年 5 月
SPM支持 SPM

Martin BarretoMathias ClaassenManuel Lorenze 维护。



Eureka 5.5.0

  • Martin Barreto 和 Mathias Claassen

Eureka: Elegant form builder in Swift

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

使用❤️XMARTLABS 创建。这是在 Swift 中重新创建 XLForm

简体中文

概述

内容

有关更多信息,请参阅我们在博客文章中介绍 Eureka

(最新版本)要求

  • Xcode 11+
  • Swift 5.0+

示例项目

您可以通过克隆和运行示例项目来查看 Eureka 的大部分功能示例。

用法

如何创建一个表单

通过扩展 FormViewController,您可以简单地向 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)
            }
    }
}

在示例中,我们创建了两个带有标准行的部分,结果是这个

Screenshot of Custom Cells

您也可以通过设置 form 属性来创建一个表单,而不需要从 FormViewController 扩展,但这种方法通常更方便。

配置键盘导航插件

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

  • disabled:完全无视图
  • enabled:启用底部视图
  • stopDisabledRow:如果导航在下一个行禁用时应停止
  • skipCanNotBecomeFirstResponderRow:如果导航应跳过返回 canBecomeFirstResponder() 为 false 的行

默认值是 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
    }
}

如果您想更改整个导航插件视图,您必须覆盖 navigationAccessoryView 变量,该变量位于您的 FormViewController 子类中。

获取行值

Row 对象持有特定类型的值。例如,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 包含自定义运算符,以便轻松创建表单。

+++       添加部分

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()]

使用回调函数

Eureka 包含回调函数,用于更改行的外观和行为。

理解行和列

Eureka 中使用的一个抽象概念 Row,它包含一个值得和一个视图 Cell。该 Cell 负责管理视图并继承自 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

回调列表

  • onChange()

    当行的值变化时调用。你可能想在此时调整一些参数,甚至使得其他行显示或消失。

  • onCellSelection()

    每当用户点击行并且行被选中时调用。请注意,这也会对禁用行调用,因此你应在回调内部以如 guard !row.isDisabled else { return } 形式开始你的代码。

  • cellSetup()

    只会在首次配置单元格时调用。在这里设置永久设置。

  • cellUpdate()

    每次单元格出现在屏幕上时调用。你可以在这里使用变量(例如在 cellSetup() 中可能不存在的变量)来更改外观。

  • onCellHighlightChanged()

    当单元格或任何子视图成为或不再是第一响应者时调用。

  • onRowValidationChanged()

    当与行相关联的验证错误发生变化时调用。

  • onExpandInlineRow()

    在展开内联行之前调用。适用于符合 InlineRowType 协议的行。

  • onCollapseInlineRow()

    在折叠内联行之前调用。适用于符合 InlineRowType 协议的行。

  • onPresent()

    由行在其提供另一个视图控制器之前调用。适用于符合 PresenterRowType 协议的行。使用它来设置要提供的控制器。

部分标题和页脚

你可以将字符串 String 或自定义的 View 作为部分标题或页脚设置。

字符串标题

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
        }()
}

动态地隐藏和显示行(或部分)

Screenshot of Hidden Rows

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

为此,每一行都有一个名为hidden的可选类型的Condition变量,可以使用函数或NSPredicate设置。

使用函数条件隐藏

使用Conditionfunction情况

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重新评估新的条件。有关更多信息,请参见此常见问题解答部分

章节

对于章节,这也同样是有效的。这意味着我们可以设置章节的hidden属性来动态显示或隐藏。

禁用行

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

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

列表章节

为了显示选项列表,Eureka 包含一个特殊章节 SelectableSection。在创建一个时,您需要传递用于选项的行的类型和 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
    }
}
可以使用哪些类型的行?

要创建此类章节,您必须创建符合 SelectableRowType 协议的行。

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

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

获取所选行

为了轻松获取 SelectableSection 的所选行,有两种方法:可以调用的 selectedRow()selectedRows(),以获得单个选择部分的所选行或获得多选部分的所选行。

在部分中对选项进行分组

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

  • sectionKeyForValue - 一个闭包,应返回特定行值的键。此键随后用于按部分分割选项。

  • sectionHeaderTitleForKey - 一个闭包,返回为特定键的部分的标题。默认返回键本身。

  • sectionFooterTitleForKey - 一个闭包,返回为特定键的部分的页脚标题。

多层值部分

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

Screenshot of Multivalued Section

如何创建多层值部分

要创建多层值部分,我们必须使用 MultivaluedSection 类型而不是常规的 Section 类型。 MultivaluedSection 扩展 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"
        }
    }

前面的代码片段显示了如何创建一个多层值部分。在这种情况下,我们希望根据多层值参数指示插入、删除和重新排序行。

addButtonProvider 允许我们自定义按钮行,当触摸时插入新行,而 multivaluedOptions 包含 .Insert 值。

每当需要插入新行时,Eureka 都会调用 multivaluedRowToInsertAt 隐藏属性。为了提供要添加到多值部分的行,我们应该设置此属性。Eureka 通过闭包参数传递索引。请注意,我们可以返回任何类型的行,甚至是自定义行,尽管在大多数情况下,多值部分的行具有相同的类型。

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

在创建插入部分时,我们需要注意一些事项。任何添加到可插入多值部分的行应位于 Eureka 自动添加的新行之上。可以通过将额外的行添加到初始化器闭包(初始化器的最后一个参数)内部来轻松实现这一点,然后 Eureka 将插入按钮添加到部分的末尾。

编辑模式

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

有关如何使用多值部分的更多信息,请参阅 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 集合和一个特定配置,用于确定何时评估验证规则。

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

提供的规则有:

  • RuleRequired
  • RuleEmail
  • RuleURL
  • RuleGreaterThan, RuleGreaterOrEqualThan, RuleSmallerThan, RuleSmallerOrEqualThan
  • 规则最小长度,规则最大长度
  • 规则闭合

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

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:) 函数,一次性设置多个规则。

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

有时候,我们希望在行上使用的一些规则集合,我们还想在其他行上使用。在这种情况下,我们可以使用一个 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 属性来实现,它可以有以下值:

  • .validatesOnChange - 当行值发生变化时进行验证。
  • .validatesOnBlur - (默认值)在单元格放弃第一响应者后立即进行验证。不适用于所有行。
  • .validatesOnChangeAfterBlurred - 在行值首次在放弃第一响应者后发生变化时进行验证。
  • .validatesOnDemand - 我们应手动调用 validate 方法来手动验证行或表单。

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

如何获取验证错误

每个行都有一个 validationErrors 属性,可以用来检索所有验证错误。这个属性只是存储了最新的行验证执行中的验证错误列表,这意味着它不会评估行的验证规则。

关于类型说明

如预期,规则必须使用与行对象相同的类型。请特别小心检查使用的行类型。您可能会看到编译器错误(“在调用中不正确的参数标记(期望 'rule:' 而得到 'ruleSet:'”)而不是指向问题的地方,当混合类型时。

滑动操作

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

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

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。如果有表单中的多值部分(在viewWillAppear),Eureka将自动设置为true。如果你在同一个表单中既有多值部分又有滑动操作,你应该根据需求设置isEditing

自定义行

通常,你可能需要一个与Eureka包中包括的行不同的行。如果是这种情况,你将不得不创建自己的行,但这并不困难。你可以阅读这个创建自定义行的教程以开始。你可能还想看看EurekaCommunity,其中包含一些可添加到Eureka的额外行。

基本自定义行

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

记住,Row是Eureka使用的抽象,而Cell是负责视图的真正的UITableViewCell。由于Row包含Cell,因此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回调类似,单元格也有设置和更新方法,您可以在其中自定义它。

自定义内联行

内联行是一种特定的行,它浮动在下面的行上方显示,通常在行被点击时,内联行会在展开和折叠模式之间切换。

因此,要创建内联行,我们需要两个行,一个是“始终”可见的行,另一个将展开/折叠。

另一个要求是这两个行的值类型必须相同。这意味着如果一个行包含一个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` 调用 `expandInlineRow` 和 `collapseInlineRow`。

最后,当行被选中时,必须调用 `toggleInlineRow()`,例如覆盖 `customDidSelect`。

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

自定义呈现者行

注意: 呈现者行是一个呈现新的 UIViewController 的行。

要创建一个自定义呈现者行,必须创建一个遵守 `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 类、以模态方式呈现控制器或将控制器推送到特定视图控制器。例如,可以像这样定义一个自定义 PushRow

让我们看一个例子。

/// 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,它已经移动到了那里。

您需要做的如下

  • 创建一个包含您想创建的单元格的 nib 文件。
  • 然后设置单元格类为您想要修改的现有单元格(如果您想改变更多东西,除纯 UI 之外,你应该继承该单元格)。确保该类的模块设置正确。
  • 将出口连接到您的类
  • 告诉您的行使用新的 nib 文件。这是通过设置 `cellProvider` 变量来实现使用这个 nib 的。您应该在初始化器中这样做,无论是在每个具体实例化还是在 `defaultRowInitializer` 中。例如
<<< PostalAddressRow() {
     $0.cellProvider = CellProvider<PostalAddressCell>(nibName: "CustomNib", bundle: Bundle.main)
}

您也可以为此创建一个新的行。在这种情况下,尝试从您想要修改的行的同一线公有类继承,以继承其逻辑。

在此过程中有一些需要考虑的事情

  • 如果您想看一个例子,请看看PostalAddressRowCreditCardRow,它们在其用例中使用了自定义 nib 文件。
  • 如果收到错误信息说 "Unknown class <YOUR_CLASS_NAME> in Interface Builder file",可能是您必须在代码中某处实例化该新类型,以便在运行时加载它。在 my 案例中,调用 "let t = YourClass.self" 有所帮助。

行目录

控制行

标签行


按钮行


复选行


开关行


滑块行


步进器行


文本区行


字段行

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

文本行

姓名行

网址行

整数行

电话行

密码行

电子邮件行

小数行

推特行

账号行

邮编行

所有上述 FieldRow 子类型都具有一个类型为 NSFormatterformatter 属性,它可以被设置以确定该行值应该如何显示。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 属性。当使用允许小数值并符合用户区域设置的格式化器(例如 DecimalFormatter)和一个 DecimalRow 时,如果 useFormatterDuringInput 设置为 false,则必须将 useFormatterOnDidBeginEditing 设置为 true,以便正在编辑的值中的小数点和键盘上的小数点相匹配

日期行

日期行包含日期,并允许我们通过UIDatePicker控件设置新值。UIDatePicker的模式和日期选择器视图显示的方式是它们之间变化的。

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

使用这3种样式(普通、内联与选择器),Eureka包括了

  • 日期行
  • 时间行
  • 日期时间行
  • 倒计时行

选项行

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

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

可以显示一个警报,用户可以从其中选择选项。
动作栏行

可以显示一个操作栏,用户可以从其中选择选项。
推送行

可以从使用复选行列出的选项生成一个新的控制器。
多选选择行

类似于推送行,但允许选择多个选项。
分段行
分段行(带标题)
选择器行

通过选择器视图显示通用类型的选项。
(也有内联选择器行)

自己构建自定义行?

让我们知道,我们很高兴在这里提到它。:)

  • 位置行(作为示例项目中自定义行包含)

Screenshot of Location Row

安装

CocoaPods

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

在项目的 Podfile 中指定 Eureka

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

pod 'Eureka'

然后运行以下命令

$ pod install

Swift 包管理器

Swift 包管理器是用于管理 Swift 代码分布的工具。

设置好您的 Package.swift 清单文件后,您可以添加 Eureka 作为一个依赖,通过将其添加到 Package.swift 的依赖值中来实现。

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

Carthage

Carthage 是一个简单的、去中心化的 Cocoa 依赖管理器。

在您的项目的 Cartfile 中指定 Eureka。

github "xmartlabs/Eureka" ~> 5.4

作为嵌入式框架手动添加

  • 从您的项目的 git 根目录运行以下命令来克隆 Eureka 作为 git 子模块
$ git submodule add https://github.com/xmartlabs/Eureka.git
  • 打开由上一个 git 子模块命令创建的 Eureka 文件夹,并将 Eureka.xcodeproj 拖到您应用程序 Xcode 项目的 Project Navigator 中。

  • 在 Project Navigator 中选择 Eureka.xcodeproj 并验证部署目标与您的应用程序部署目标相匹配。

  • 在 Xcode 导航中选择您的项目,然后在侧边栏中选择您的应用程序目标。然后选择“General”选项卡,在“嵌入式二进制文件”部分下点击 + 按钮。

  • 选择 Eureka.framework,完成操作!

参与

  • 如果您想 贡献,请随时 提交拉取请求
  • 如果您有 功能请求,请 开启一个问题
  • 如果您发现 bug,在提交问题之前请检查旧的问题。
  • 如果您需要 帮助 或者想 提问,请使用 StackOverflow。 (标签 eureka-forms)。

在贡献力量之前,请查看 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 以根据您想要显示的字符串值来设置。

如何通过标签值获取Row

我们可以通过调用 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 才会调用

查看这个问题。

如何更新 Section 的头部/尾部

  • 设置新的头部/尾部数据……
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 以执行更改
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协议。表单是一个部分集合,部分是由行集合组成的。

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的位置添加它,然后将输出口连接到FormViewController的tableView变量。这允许你为表单定义一个自定义框架(可能带有约束)。

这一切也可以通过程序化地更改FormViewController的tableView的框架、边距等来实现。

捐赠给 Eureka

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

变更日志

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