Project Icon

Eureka

Swift表单构建框架简化iOS开发

Eureka是一个Swift编写的iOS表单构建框架,提供丰富的表单控件和功能。它支持动态显示/隐藏、自定义行类型、验证、多值选择等特性。Eureka使用简单,几行代码即可创建复杂表单,同时保持高度可定制性。框架适用于各种复杂程度的表单需求,能有效提升iOS开发效率。

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.

简体中文

Overview

Contents

  • [Requirements]
  • [Usage]
    • [How to create a Form]
    • [Getting row values]
    • [Operators]
    • [Using the callbacks]
    • [Section Header and Footer]
    • [Dynamically hide and show rows (or sections)]
    • [List sections]
    • [Multivalued sections]
    • [Validations]
    • [Swipe Actions]
  • [Custom rows]
    • [Basic custom rows]
    • [Custom inline rows]
    • [Custom presenter rows]
  • [Row catalog]
  • [Installation]
  • [FAQ]

For more information look at [our blog post] that introduces Eureka.

Requirements (for latest release)

  • Xcode 11+
  • Swift 5.0+

Example project

You can clone and run the Example project to see examples of most of Eureka's features.

Usage

How to create a form

By extending FormViewController you can then simply add sections and rows to the form variable.

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

In the example we create two sections with standard rows, the result is this:

Screenshot of Custom Cells

You could create a form by just setting up the form property by yourself without extending from FormViewController but this method is typically more convenient.

Configuring the keyboard navigation accesory

To change the behaviour of this you should set the navigation options of your controller. The FormViewController has a navigationOptions variable which is an enum and can have one or more of the following values:

  • disabled: no view at all
  • enabled: enable view at the bottom
  • stopDisabledRow: if the navigation should stop when the next row is disabled
  • skipCanNotBecomeFirstResponderRow: if the navigation should skip the rows that return false to canBecomeFirstResponder()

The default value is enabled & skipCanNotBecomeFirstResponderRow

To enable smooth scrolling to off-screen rows, enable it via the animateScroll property. By default, the FormViewController jumps immediately between rows when the user hits the next or previous buttons in the keyboard navigation accesory, including when the next row is off screen.

To set the amount of space between the keyboard and the highlighted row following a navigation event, set the rowKeyboardSpacing property. By default, when the form scrolls to an offscreen view no space will be left between the top of the keyboard and the bottom of the row.

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

If you want to change the whole navigation accessory view, you will have to override the navigationAccessoryView variable in your subclass of FormViewController.

Getting row values

The Row object holds a value of a specific type. For example, a SwitchRow holds a Bool value, while a TextRow holds a String value.

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

Operators

Eureka includes custom operators to make form creation easy:

+++       Add a 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

<<<       Insert a row

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

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

+=        Append an array

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

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

Result builders

Eureka includes result builders to make form creation easy:

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

Using the callbacks

Eureka includes callbacks to change the appearance and behavior of a row.

Understanding Row and Cell

A Row is an abstraction Eureka uses which holds a value and contains the view Cell. The Cell manages the view and subclasses UITableViewCell.

Here is an example:

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

Callbacks list

  • onChange()

    Called when the value of a row changes. You might be interested in adjusting some parameters here or even make some other rows appear or disappear.

  • onCellSelection()

    Called each time the user taps on the row and it gets selected. Note that this will also get called for disabled rows so you should start your code inside this callback with something like guard !row.isDisabled else { return }

  • cellSetup()

    Called only once when the cell is first configured. Set permanent settings here.

  • cellUpdate()

    Called each time the cell appears on screen. You can change the appearance here using variables that may not be present on cellSetup().

  • onCellHighlightChanged()

    Called whenever the cell or any subview become or resign the first responder.

  • onRowValidationChanged()

    Called whenever the the validation errors associated with a row changes.

  • onExpandInlineRow()

    Called before expanding the inline row. Applies to rows conforming InlineRowType protocol.

  • onCollapseInlineRow()

    Called before collapsing the inline row. Applies to rows conforming InlineRowType protocol.

  • onPresent()

    Called by a row just before presenting another view controller. Applies to rows conforming PresenterRowType protocol. Use it to set up the presented controller.

Section Header and Footer

You can set a title String or a custom View as the header or footer of a Section.

String title

Section("Title")

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

Section(footer: "Footer Title")

Custom view

You can use a Custom View from a .xib file:

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
}

Or a custom UIView created programmatically

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

Or just build the view with a Callback

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

Dynamically hide and show rows (or sections)

Screenshot of Hidden Rows

In this case we are hiding and showing whole sections.

To accomplish this each row has a hidden variable of optional type Condition which can be set using a function or NSPredicate.

Hiding using a function condition

Using the function case of Condition:

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

The array of String to pass should contain the tags of the rows this row depends on. Each time the value of any of those rows changes the function is reevaluated. The function then takes the Form and returns a Bool indicating whether the row should be hidden or not. This the most powerful way of setting up the hidden property as it has no explicit limitations of what can be done.

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

Hiding using an NSPredicate

The hidden variable can also be set with a NSPredicate. In the predicate string you can reference values of other rows by their tags to determine if a row should be hidden or visible. This will only work if the values of the rows the predicate has to check are NSObjects (String and Int will work as they are bridged to their ObjC counterparts, but enums won't work). Why could it then be useful to use predicates when they are more limited? Well, they can be much simpler, shorter and readable than functions. Look at this example:

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

And we can write it even shorter since Condition conforms to ExpressibleByStringLiteral:

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

Note: we will substitute the value of the row whose tag is 'switchTag' instead of '$switchTag'

For all of this to work, all of the implicated rows must have a tag as the tag will identify them.

We can also hide a row by doing:

$0.hidden = true

as Condition conforms to ExpressibleByBooleanLiteral.

Not setting the hidden variable will leave the row always visible.

If you manually set the hidden (or disabled) condition after the form has been displayed you may have to call row.evaluateHidden() to force Eureka to reevaluate the new condition. See this FAQ section for more info.

Sections

For sections this works just the same. That means we can set up section hidden property to show/hide it dynamically.

Disabling rows

To disable rows, each row has an disabled variable which is also an optional Condition type property. This variable also works the same as the hidden variable so that it requires the rows to have a tag.

Note that if you want to disable a row permanently you can also set disabled variable to true.

List Sections

To display a list of options, Eureka includes a special section called SelectableSection. When creating one you need to pass the type of row to use in the options and the selectionType. The selectionType is an enum which can be either multipleSelection or singleSelection(enableDeselection: Bool) where the enableDeselection parameter determines if the selected rows can be deselected or not.

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
    }
}
What kind of rows can be used?

To create such a section you have to create a row that conforms the SelectableRowType protocol.

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

This selectableValue is where the value of the row will be permanently stored. The value variable will be used to determine if the row is selected or not, being 'selectableValue' if selected or nil otherwise. Eureka includes the ListCheckRow which is used for example. In the custom rows of the Examples project you can also find the ImageCheckRow.

Getting the selected rows

To easily get the selected row/s of a SelectableSection there are two methods: selectedRow() and selectedRows() which can be called to get the selected row in case it is a SingleSelection section or all the selected rows if it is a MultipleSelection section.

Grouping options in sections

Additionally you can setup list of options to

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

白日梦AI

白日梦AI提供专注于AI视频生成的多样化功能,包括文生视频、动态画面和形象生成等,帮助用户快速上手,创造专业级内容。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

讯飞绘镜

讯飞绘镜是一个支持从创意到完整视频创作的智能平台,用户可以快速生成视频素材并创作独特的音乐视频和故事。平台提供多样化的主题和精选作品,帮助用户探索创意灵感。

Project Cover

讯飞文书

讯飞文书依托讯飞星火大模型,为文书写作者提供从素材筹备到稿件撰写及审稿的全程支持。通过录音智记和以稿写稿等功能,满足事务性工作的高频需求,帮助撰稿人节省精力,提高效率,优化工作与生活。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号