FXForms 是一个 Objective-C 库,可以轻松在 iOS 上创建基于表格的表单。它非常适合设置页面或数据输入任务。
与其他解决方案不同,FXForms 直接与您提供的强类型数据模型一起工作(而不是字典或复杂的 dataSource 协议),并通过反射尽可能多地从您的模型中推断信息,以避免重复复制类型信息的繁琐过程。
注意:“支持”意味着该库已与该版本进行了测试。“兼容”意味着该库应该能够在该 iOS 版本上运行(即它不依赖于任何不可用的 SDK 功能),但不再进行兼容性测试,并且可能需要调整或修复错误才能正确运行。
FXForms 需要 ARC。如果希望在非 ARC 项目中使用 FXForms,只需将 -fobjc-arc
编译器标志添加到 FXForms.m
类中。要执行此操作,请转到目标设置中的构建阶段选项卡,打开编译源组,双击列表中的 FXForms.m
,然后在弹出窗口中键入 -fobjc-arc
。
如果要将整个项目转换为 ARC,请注释掉 FXForms.m
中的 #error 行,然后在 Xcode 中运行编辑 > 重构 > 转换为 Objective-C ARC... 工具,并确保所有您希望使用 ARC 的文件(包括 FXForms.m
)都已勾选。
要创建表单对象,只需创建任何新的 NSObject
子类,使其遵守 FXForm
协议,如下所示
@interface MyForm : NSObject <FXForm>
@end
FXForm
协议没有任何强制方法或属性。FXForms 库将检查您的对象,并识别所有公共和私有属性,并使用它们来生成表单。例如,假设您想要一个包含“电子邮件”和“密码”字段以及“记住我”开关的表单;您可以将其定义如下
@interface MyForm : NSObject <FXForm>
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, assign) BOOL rememberMe;
@end
这就是您需要做的全部工作。FXForms 真的是非常智能;比你想象的要聪明得多
UISwitch
显示,email 字段将自动具有类型为 UIKeyboardTypeEmailAddress
的键盘,密码字段将自动启用 secureTextEntry
。FXForm
协议的属性),或视图控制器(例如条款和条件页面),如果它们为nil,将自动实例化 - 无需设置默认值。这些默认行为都是通过Objective-C运行时API检查属性类型和名称推断出来的,但如果愿意,也可以全部覆盖 - 这将在下面的“调整表单行为”中介绍。
要在视图控制器中显示您的表单,您有两种选择:FXForms
提供了一个旨在尽量简化开发生成的UIViewController
子类,命名为FXFormViewController
。要设置FXFormViewController
,只需像平常一样创建它,并按如下方式设置您的表单。
FXFormViewController *controller = [[FXFormViewController alloc] init];
controller.formController.form = [[MyForm alloc] init];
然后您就可以像显示任何普通视图控制器一样显示表单控制器。FXFormViewController
包含一个UITableView
,它会在需要时自动创建。但是,如果您愿意,可以将自己的UITableView
分配给tableView
属性并使用该表。您甚至可以使用创建tableView的nib文件来初始化FXFormViewController
。
FXFormViewController
设计用于被继承,就像普通的UIViewController
或UITableViewController
一样。在大多数情况下,您将想要继承FXFormViewController
以便可以添加您的表单设置逻辑和动作处理器。
将FXFormViewController
(或其子类)放入UINavigationController
中是个好主意。这不强制,但如果表单包含子表单,这些子表单将推送到其navigationController,如果没有这个,表单将不会显示。
与UITableViewController
类似,FXFormViewController
通常将tableView作为控制器的主视图。与UITableViewController
不同,它不一定非得是 - 如果您愿意,可以让您的tableView成为主视图的子视图。
与UITableViewController
类似,FXFormViewController
实现了UITableViewDelegate
协议。因此,如果进行子类化,可以覆盖UITableViewDelegate
和UIScrollViewDelegate
的方法来实现自定义行为。FXFormViewController
实际上不是tableView的直接代理,它是其FXFormController
实例(一个FXFormController
对象)的代理。表单控制器作为tableView的代理和数据源,并通过FXFormControllerDelegate
协议将UITableViewDelegate
方法代理回FXFormViewController
。
与UITableViewController
不同,FXFormViewController
没有实现UITableViewDataSource
协议。这完全由FXFormController
处理,不推荐尝试覆盖或拦截任何数据源方法。
FXFormViewController
相当灵活,但有时被迫使用特定的基类控制器可能不是很方便。例如,您可能希望为所有视图控制器使用一个共同的基类,或在没有相关控制器的视图中显示表单。
在前一种情况下,你可以添加一个作为子控制器的 FXFormViewController
,但在后一种情况下这不会起作用。要在不使用 FXFormViewController
的情况下使用 FXForms,您可以直接使用 FXFormController
。要使用 FXFormController
显示表单,您只需设置表单和 tableView 属性,然后它会完成剩余的工作。您可以选择将 FXFormController
的委托属性绑定,以接收 UITableView
事件的通知。
使用这种自定义表单视图控制器时,一些交互仍然会为您处理(例如,当键盘出现时调整表格视图内容的缩进),但您需要自己添加其他视图逻辑,例如,当 UIViewController
出现在屏幕上时重新加载表格。
下面是一个自定义表单视图控制器的示例代码
@interface MyFormViewController : UIViewController <FXFormControllerDelegate>
@property (nonatomic, strong) IBOutlet UITableView *tableView;
@property (nonatomic, strong) FXFormController *formController;
@end
@implementation MyFormViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//we'll assume that tableView has already been set via a nib or the -loadView method
self.formController = [[FXFormController alloc] init];
self.formController.tableView = self.tableView;
self.formController.delegate = self;
self.formController.form = [[MyForm alloc] init];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//reload the table
[self.tableView reloadData];
}
@end
FXForm 的最大优势在于尽可能少的猜测来消除工作量。但它无法猜测一切,有时甚至猜错了。那么你该如何纠正它呢?
你可能发现,你不想将所有对象属性都变成表单字段;例如,你可能有一些私有属性,这些属性是你的表单模型内部使用的,或者你可能只是希望字段顺序与接口中属性排列不同。
要覆盖表单字段列表,实现你表单的任意 -fields
方法
- (NSArray *)fields
{
return @[@"field1", @"field2", @"field3"];
}
fields
方法应该返回一个字符串、字典或混合数组。在示例中,我们返回了字符串;它们映射到表单对象的属性名称。如果你返回这样的名称数组,这些字段将替换自动生成的字段列表。
每次表单重新分配给表单控制器时,都会再次调用 -fields
方法。这意味着您可以根据应用逻辑动态生成字段。例如,您可以根据其他属性显示或隐藏特定的字段。
除了省略和重新排列字段外,您还可能希望覆盖它们的属性。有两种方法可以这样做:一种方法是在表单对象中添加一个方法,例如 -(NSDictionary *)[name]Field;
其中 name 是字段相关的属性。此方法返回一个包含要覆盖的属性的字典(参见表单字段属性)。例如,如果我们想覆盖电子邮件字段的标题,我们可以这样做
- (NSDictionary *)emailField
{
return @{FXFormFieldTitle: @"Email Address"};
}
或者,您可以在 -fields
数组中返回一个字典而不是字符串。如果您这样做,您必须包含 FXFormFieldKey
,这样 FXForms 就知道您要覆盖哪个字段。
- (NSArray *)fields
{
return @[
@{FXFormFieldKey: @"email", FXFormFieldTitle: @"Email Address"},
…other fields…
];
}
这两种方法等效。
您可能希望添加额外的虚拟表单字段(例如按钮或标签),这些字段与您的表单类上的任何属性都不对应。您可以通过实现 -fields
方法来完成此操作,但如果您对默认字段满意,只想在末尾添加一些额外字段,则可以改用覆盖 -extraFields
方法,这与其工作方式相同,但它保留从表单类中推断出的默认字段。
- (NSArray *)extraFields
{
return @[
@{FXFormFieldTitle: @"Extra Field"},
];
}
同样,如果您只想在表单中显示表单对象属性的一个子集,但不想只为了排除几个属性而必须列出所有要在 -fields
方法中保留的属性,则可以使用 -excludedFields
方法仅指定不希望包含的属性名称。
- (NSArray *)excludedFields
{
return @[
@"someProperty",
@"someOtherProperty",
];
}
您可能希望在表单中将表单字段分组到不同的部分中,以使其更易于使用。FXForms对分组的方式非常简单 - 您只需将任何字段的FXFormFieldHeader
或FXFormFieldFooter
属性添加到任何位置,它就会在该点开始/结束一个部分。FXFormFieldHeader/Footer
是一个字符串,它将被用作该部分标题或页眉文本。如果您不希望有任何文本,只需提供一个空字符串即可。
在以下示例中,我们有四个字段,并将它们分成了两个组,每个组都有一个标题
- (NSArray *)fields
{
return @[
@{FXFormFieldKey: @"field1", FXFormFieldHeader: @"Section 1"},
@"field2",
@{FXFormFieldKey: @"field3", FXFormFieldHeader: @"Section 2"},
@"field4",
];
}
或者,因为我们没有覆盖任何其他字段的属性,所以我们也可以使用以下方法来更整洁地完成此操作
- (NSDictionary *)field1Field
{
return @{FXFormFieldHeader: @"Section 1"};
}
- (NSDictionary *)field3Field
{
return @{FXFormFieldHeader: @"Section 2"};
}
您可以设置的表单字段属性列表如下。大多数这些都有合理的默认值被自动设置。请注意,这些常量的字符串值在接口中声明 - 您可以假设这些常量的字符串值在未来版本中不会更改,并且您可以在(例如)用于配置表单的plist中安全地使用这些值。
static NSString *const FXFormFieldKey = @"key";
这是相关表单对象的属性的名称。如果您的字段没有实际属性作为后盾,这可能是一个用于填充字段值的getter方法的名称。也可以有完全虚拟的字段(如按钮),这些字段根本没有任何键。
static NSString *const FXFormFieldType = @"type";
这是字段类型,它用于决定字段在表格中的显示方式。类型用于确定表示字段的单元格类型,但它也可能用于配置单元格(单个单元格类可能支持多种字段类型)。类型自动从字段属性声明中推断出来,但可以被覆盖。支持的类型在下面的“表单字段类型”中列出,但是您可以提供任何字符串作为类型,并实现一个自定义表单单元格来显示和/或编辑它。
static NSString *const FXFormFieldClass = @"class";
这是表示字段值的类。对于原始类型,当通过KVC访问时将使用用于装箱值的类(例如,NSNumber
用于数值,或NSValue
用于struct
类型)。这对于表单的所有属性自动确定,因此您很少需要自己设置它。对于您使用-fields
或-extraFields
方法添加的自定义表单属性,有时明确指定此属性是有帮助的。一个好的例子是,如果您的表单属性是视图控制器或子表单字段,通常无法自动推断其类。提供的值可以是Class
对象或表示类名的字符串。
static NSString *const FXFormFieldCell = @"cell";
这是表示字段的单元格的类。默认情况下,在字段级别上不指定此值;相反,FXFormController
维护一个字段类型到单元格类的映射,这允许您在每个表单级别而不是在每个字段级别覆盖用于显示给定字段类型的默认单元格。如果您确实需要提供一个特殊的单个单元格类型,您可以使用此属性来完成。提供的值可以是Class
对象或表示类名的字符串。
static NSString *const FXFormFieldTitle = @"title";
这是字段的显示标题。这是通过将camelCase转换为标题大小写并通过运行通过NSLocalizedString()
宏进行本地化自动生成的。这意味着您可以通过使用此密钥替换标题,也可以通过您的字符串文件替换,如果您更愿意的话。
static NSString *const FXFormFieldPlaceholder = @"placeholder";
这是当字段值为空或nil时显示的占位符值。这通常是字符串,但不限于字符串,例如,对于日期字段可以是一个NSDate,对于图像字段可以是一个UIImage。当与选项或多选字段一起使用时,占位符将作为选项列表中的第一个项出现,并且可以用来重置字段值为nil/无值。
static NSString *const FXFormFieldDefaultValue = @"default";
这是当字段值为nil时的默认值。在动态创建表单时非常有用,可以直接设置字段数组的默认值,而无需在表单的值中分别设置。这也是确保用户永远不会将字段值设置为nil的一种方式。请注意,默认值与占位符值不同,因为它实际上将存储在表单中。如果设置了默认值,占位符将永远不会出现。默认值仅适用于对象类型,因为零可能是整数或浮点属性的一个有意义的值,所以它不会用默认值替换。
static NSString *const FXFormFieldOptions = @"options";
对于任何字段类型,您可以提供一组支持值,这将覆盖标准字段,以代替选项列表进行选择。选项可以是NSStrings、NSNumbers或任何其他对象类型。您可以提供FXFormFieldValueTransformer
来控制选项值在列表中的显示方式。或者,如果您使用自定义对象作为值,您可以实现-(NSString *)fieldDescription;
方法来控制其显示方式。有关更多详细信息,请参阅以下表单字段选项。
static NSString *const FXFormFieldTemplate = @"template";
如果字段是一个NSArray或NSOrderedSet,FXForms允许用户添加、编辑和删除项目。默认情况下,FXForms假定集合中的值为FXFormFieldTypeText类型,但您可以使用FXFormFieldTemplate字典来覆盖此设置。FXFormFieldTemplate字典可以包含与普通字段字典大部分相同类型的值,并应用于描述集合中元素的属性。
static NSString *const FXFormFieldValueTransformer = @"valueTransformer";
有时您希望为字段显示的值与存储的值不匹配。例如,您可能希望以特定格式显示日期,或将区域代码转换为其可读等效形式。FXFormFieldValueTransformer
属性允许您指定一个用于获取字段值的转换块或NSValueTransformer
。如果提供了值转换器,它将用作调用字段值对象的-fieldDescription
方法。您可以提供NSValueTransformer
的实例或其子类的名称。如果表单字段有选项数组,值转换器也会用于控制列表中选项的显示方式。FXFormFieldValueTransformer
是可逆的,在这种情况下,它还将用于在将输入值存储在表单之前将其转换为字符串。
static NSString *const FXFormFieldAction = @"action";
这是由字段执行的可选操作。值可以是表示选择器名称的字符串或是一个块,当字段被激活时将执行。如果操作指定为选择器,则目标对象由响应者链从单元格向上传递并遇到能响应选择器的对象确定。这意味着您可以选择在tableView、其父视图、视图控制器、app委托或甚至窗口上实现此动作方法。如果表单作为另一个表单的子表单呈现,您也可以在视图控制器为其父表单实现子表单的动作方法。
对于非交互字段,选中单元格时将调用操作;对于开关或文本字段等字段,当值改变时将触发。当使用选择器时,操作方法可以接受零个或一个参数。提供的参数是发送者,通常是表单字段单元格,(一个遵循 UITableViewCell
且符合 FXFormFieldCell
协议的单元格),您可以从中访问字段模型,以及从那里访问表单本身。
static NSString *const FXFormFieldSegue = @"segue";
这是当字段被点击时将执行 UIStoryboardSegue
。这可以是 UIStoryboardSegue
的子类(或包含 UIStoryboardSegue
子类名称的字符串),UIStoryboardSegue
子类的实例,或者表示附加到表单视图控制器的标识符的字符串。注意,在后一种情况下,交织作用必须附加到与表单相同的控制器上,否则调用时将崩溃。
如果 FXFormFieldSegue 属性是交织实例或标识符,则在点击字段时将调用。如果它是交织子类,则此交织将被实例化并用于处理显示子表单或子控制器时的过渡。
static NSString *const FXFormFieldHeader = @"header";
此属性提供可选的分区标题字符串,用于在字段之前显示。提供空字符串以创建不带标题的分区。
static NSString *const FXFormFieldFooter = @"footer";
此属性提供可选的分区页脚字符串,用于在字段之后显示。提供空字符串以创建不带页脚的分区。
static NSString *const FXFormFieldInline = @"inline";
值是另一个 FXForm 或提供选项数组的字段通常会在新 FXFormViewController
中显示,当您点击字段时会将其推入导航堆栈。您可能希望在同一表格视图内联显示此类字段。您可以通过将 FXFormFieldInline
属性设置为 @YES
来这样做。
static NSString *const FXFormFieldSortable = @"sortable";
类型为 NSArray 或 NSOrderedSet 的字段可以可选地显示排序控件,以便用户可以更改项顺序。将 FXFormFieldSortable 属性设置为 YES 以启用此功能。
static NSString *const FXFormFieldViewController = @"controller";
某些类型的字段可能显示在另一个视图控制器中,当字段被选中时将将其推入导航堆栈。默认情况下,此类在字段级别上未指定;相反,FXFormController
维护一个字段类型到控制器类的映射,这允许您在每个表单级别上覆盖用于显示给定字段类型的默认控制器,而不是在每个字段级别上进行操作。如果您必须提供特定的一次性控制器类型,FXFormFieldViewController 属性允许您按字段级指定要使用的控制器。指定的控制器必须遵循 FXFormFieldViewController
协议。默认情况下,此类字段将使用 FXFormViewController
类进行显示。
static NSString *const FXFormFieldTypeDefault = @"default";
这是默认字段类型,如果没有确定特定类型则使用。
static NSString *const FXFormFieldTypeLabel = @"label";
如果希望将字段视为非交互式/只读字段,可以使用此类型。通过使用 -fieldDescription
方法将值转换为字符串来显示表单值。这映射到所有内置类型的标准 NSObject -description
方法,但您可以为其自定义值类重写它。
static NSString *const FXFormFieldTypeText = @"text";
默认情况下,此字段类型将由具有默认自动修正的普通 UITextField
表示。
static NSString *const FXFormFieldTypeLongText = @"longtext";
此类型表示多行文本。默认情况下,此字段类型将由扩展 UITextView
表示。
static NSString *const FXFormFieldTypeURL = @"url";
与 FXFormFieldTypeText
类似,但具有 UIKeyboardTypeURL
的键盘类型且无自动修正。
static NSString *const FXFormFieldTypeEmail = @"email";
与 FXFormFieldTypeText
类似,但具有 UIKeyboardTypeEmailAddress
的键盘类型且无自动修正。
static NSString *const FXFormFieldTypePassword = @"password";
与 FXFormFieldTypeText
类似,但启用了安全文本输入且无自动修正。
static NSString *const FXFormFieldTypeNumber = @"number";
与 FXFormFieldTypeText
类似,但具有数字键盘,并将输入限制为有效数字。
static NSString *const FXFormFieldTypeInteger = @"integer";
与 FXFormFieldTypeNumber
类似,但仅限制为整数输入。
static NSString *const FXFormFieldTypeUnsigned = @"unsigned";
与 FXFormFieldTypeInteger
类似,但适用于无符号值。默认使用数字键盘。
static NSString *const FXFormFieldTypeFloat = @"float";
与 FXFormFieldTypeNumber
类似,但表示值是基本类型(不可为空)
static NSString *const FXFormFieldTypeBoolean = @"boolean";
一个布尔值,默认通过 UISwitch
控件设置。
static NSString *const FXFormFieldTypeOption = @"option";
与 FXFormFieldTypeBoolean
类似,但此类型用于切换选项,默认创建复选标记控件而不是开关。
static NSString *const FXFormFieldTypeDate = @"date";
日期值,通过 UIDatePicker
选择。
static NSString *const FXFormFieldTypeTime = @"time";
时间值,通过 UIDatePicker
选择。
static NSString *const FXFormFieldTypeDateTime = @"datetime";
日期和时间,通过 UIDatePicker
选择。
static NSString *const FXFormFieldTypeImage = @"image"
图片,通过 UIImagePickerController
选择。
当你为表单字段提供一个选项数组时,字段输入将展示为一系列可供勾选的选项。如何将这些选项列表转换为表单值取决于字段类型
如果字段类型匹配选项数组的值,选择选项将直接设置选择值,但这可能不是你想要的。例如,如果你有一个字符串列表,你可能更感兴趣的是选中的索引而不是值(值可能已本地化和格式化以供人类消费,而不是用于机器解释)。如果字段类型是数字,而选项值不是数字,则假定字段值应设置为所选项的 索引,而不是值。
如果字段是集合类型(例如 NSArray、NSSet 等),表单将允许用户选择多个选项而不是一个。集合的处理方式如下,取决于属性的类:如果你使用 NSArray
、NSSet
和 NSOrderedSet
,所选项将直接存储在集合中;如果你使用 NSIndexSet
,将存储值的索引;如果你使用 NSDictionary
,将存储值及其索引。对于有序集合类型,所选项的顺序将与选项列表中的顺序相匹配。
多选字段也可以与 NS_OPTIONS
样式的位字段枚举值一起使用。只需将整数或枚举用作属性类型,然后指定字段类型为 FXFormFieldTypeBitfield
。然后,你可以通过使用 NSNumber
值明确指定选项中的位值,或者让 FXForms 从选项索引中推断位值。
**注意**:在运行时,FXForms 无法访问枚举中定义的实际值,因此所选择值将完全由 FXFormFieldOptions
中的选项值决定。如果枚举值非连续或不从零开始,则索引将不与选项索引匹配。要定义具有非连续值的枚举选项,您可以指定显式的数值选项,并使用 FXFormFieldValueTransformer
显示人类可读的标签,如下所示
typedef NS_ENUM(NSInteger, Gender)
{
GenderMale = 10,
GenderFemale = 15,
GenderOther = -1
};
- (NSDictionary *)genderField
{
return @{FXFormFieldOptions: @[@(GenderMale), @(GenderFemale), @(GenderOther)],
FXFormFieldValueTransformer: ^(id input) {
return @{@(GenderMale): @"Male",
@(GenderFemale): @"Female",
@(GenderOther): @"Other"}[input];
}};
}
如果您想调整字段单元格的一些属性,而不是创建子类,您实际上可以通过添加额外的值到字段字典中通过 keyPath 设置任何单元格属性。例如,此代码将电子邮件字段的文本标签设置为红色
- (NSDictionary *)emailField
{
return @{@"textLabel.color": [UIColor redColor]};
}
此代码将禁用姓名字段的自动首字母大写
- (NSDictionary *)nameField
{
return @{@"textField.autocapitalizationType": @(UITextAutocapitalizationTypeNone)};
}
在 FXForm 控制器中不会回收单元格,因此您不需要担心以这种方式设置的任何属性清理。然而,注意过度使用类似这样的“字符串型”代码,因为错误无法在编译时捕获。对于大量的自定义,最好创建单元格子类,并在 -setField:
方法中覆盖属性。
FXForms为所有支持的字段提供了默认单元格实现。您可能需要为自定义字段类型提供额外的单元格类,甚至可以将所有FXForm单元格替换为您应用程序的定制版本。
单元格的自定义可以有二级。最简单的方法是创建一个从FXFormBaseCell
继承的现有FXFormCell
子类,这些细胞类包含处理各种不同字段类型的逻辑,同时还暴露了用于视图和控制的接口,便于自定义。
在创建现有单例类型子类时,您可以通过重写setUp
、update
和didSelectWithTableView:controller:
方法来覆盖相应的行为(可选地调用[super …],如果需要继承原始单例的行为)。setUp
方法仅在单例创建时调用一次,而update
方法将在字段值每次更新时调用。
如果您已经有一个基本单例类,而且不想以FXFormBaseCell
为基础,则可以通过创建一个继承自UITableViewCell
的自定义单元格来实现FXForms兼容的单例,该单元格采用FXFormFieldCell
协议。
您的自定义单元格必须有一个名为field的属性,其类型为FXFormField
。FXFormField
是一个封装类,用于封装字段属性,同时也提供了设置和获取相关表单值的方式(通过字段值的virtual属性)。您不能直接实例化FXFormField
对象,然而,它们可以通过FXFormController
的方法访问和枚举。
创建了自己的自定义单例后,可以这样使用它
FXFormFieldCell
属性将它用于特定的表单字段-registerCellClass:forFieldType:
方法将您的自定义单例类告诉表单控制器以用于特定类型。-registerCellClass:forFieldClass:
方法告诉表单控制器以用于特定值类。FXFormController
的-registerDefaultFieldCellClass:
方法。这将用新的单例类替换所有字段类型的默认单例关联。然后,您可以使用-registerCellClass:forFieldType:
添加特定类型的附加单例类。版1.2beta
版本 1.1.6
版本 1.1.5
版本 1.1.4
版本 1.1.3
版本 1.1.2
版本 1.1.1
版本 1.1
版本 1.0.2
版本 1.0.1
版本 1.0