正如我们的开发人员所喜欢的那样,移动应用程序并不总是简单明了。 创建了其他应用程序来解决复杂的用户问题,并包含许多屏幕和脚本。 例如,进行测试,问卷调查和调查的应用程序-您需要在过程中填写许多表格的任何地方。 本文将讨论此应用程序。

我们开始为从事现场保险单注册的代理商开发移动应用程序。 他们在应用程序中用客户数据填写大表格:有关汽车,所有者,驾驶员等的信息。 尽管每种形式都有其自己的部分,单元格和结构,并且每个调查表项目都需要唯一的数据类型(字符串,日期,附加文档),但屏幕形式却非常相似。 但是最主要的是它们的数量。没有人愿意多次重复进行相同元素的可视化和处理。
为了避免在创建表单上花费大量的人工,您需要运用一些独创性和大量的动态UI构造。 在本文中,我们想分享我们如何解决此问题。
为了更好地解决此问题,我们使用了生成对象的机制-ViewModels,该模型用于使用表构建自定义表单。

在正常工作中,对于开发人员要在屏幕上看到的每个单独的表,必须创建一个单独的ViewModel类。 它定义了表格的视觉组件。 我们决定使用Enum字段对结构的简单描述,再上一层并自行动态生成ViewModels和Models。
如何运作
一切始于枚举。 我们为每个配置文件创建一个唯一的枚举-这是配置文件的各个部分。 它的方法之一是返回此部分中的单元格数组。
该表中的单元格也将带有附加的枚举,这些功能将描述单元格的属性。 在这样的函数中,我们设置单元格的名称,即初始值。 后来添加了诸如
- 显示检查:必须隐藏某些单元格,
- “父”单元格列表:此单元格的值,验证或显示所依赖的单元格,
- 单元格类型:具有值的简单单元格,处于切换状态的单元格,具有添加元素功能的单元格等。
我们将所有部分都订阅了一般的QuestionnaireSectionCellType协议,以排除对特定部分的绑定,我们将对表的所有单元格(QuestionnaireCellType)进行相同的操作。
protocol QuestionnaireSectionCellType { var title: String { get } var sectionCellTypes: [QuestionnaireCellType] { get } } protocol QuestionnaireCellType { var title: String { get } var initialValue: Any? { get } var isHidden: Bool { get } var parentFields: [QuestionnaireCellType] { get } … }
这样的模型将非常容易填写。 我们简单地遍历所有部分,在每个部分中遍历一个单元格数组并将它们添加到模型数组中。
在保单持有人屏幕的示例(带有部分的枚举-InsurantSectionType)中:
final class InsurantModel: BaseModel<QuestionnaireCellType> { override init() { super.init() initParameters() } private func initParameters() { InsurantSectionType.allCases.forEach { type in type.sectionCellTypes.forEach { if let valueModel = ValueModel(type: $0, parentFields: $0.parentFields, value: $0.initialValue) { valueModels.append(valueModel) } } } } }
做完了! 现在我们有了一个带有初始值的表。 添加方法以使用QuestionnaireCellType键读取值并将其保存到所需的数组元素。
某些模型可能具有可选字段,因此我们添加了带有可选键的数组。 在模型验证期间,这些键可能不包含值,但是将认为模型已填充。
此外,为方便起见,ValueModel中的所有值我们都订阅了通用协议StringRepresentable协议,以限制可能值的列表,并添加一种在单元格中显示值的方法。
protocol StringRepresentable { var stringValue: String? { get } }
功能不断增强,模型中出现了许多其他属性和方法:清理模型(应在某些模型中设置初始值),支持动态值数组(值:Array)等。
事实证明,这种方法非常方便使用Realm存储在数据库中。 要填写调查表,可以选择先前保存的完整模型。 要扩展CTP策略,代理将不再需要填写用户的文档,用户附加的驱动程序以及新用户的TCP数据。 相反,您可以简单地重用它来填充现有的。
要更改或补充表,您只需要找到与特定屏幕相关的ViewModel,找到负责显示所需块的必要枚举,并添加或修复几种情况。 一切,表将采取必要的形式!
用测试值填写表格也非常方便快捷。 这样,您可以快速生成任何测试数据。 如果您使用初始数据添加一个单独的文件,程序将从该文件中将值带入问卷的每个特定字段,那么即使是初学者也可以生成现成的问卷,而无需进入和分解除特定文件之外的其余代码。
依存关系
我们在开发过程中解决的另一项任务是依赖项处理。 问卷的某些要素是相互联系的。 因此,如果不选择此文件本身的类型,则不能填写文件编号;如果不指示城市和街道,则不能显示房屋编号。

我们通过清除所有相关字段来更新调查表的值(例如,删除或更改文档类型,我们清除“文档编号”字段):
func updateValueModel(value: StringRepresentable?, for type: QuestionnaireCellType) { guard let model = valueModels.first(where: { $0.type.equal(to: type) }) else { return } model.value = value clearRelativeValues(type: type) } func clearRelativeValues(type: QuestionnaireCellType) { _ = valueModels.filter { $0.parentFields.contains(where: { $0.equal(to: type) }) } .compactMap { $0.type } .compactMap { updateValueModel(value: nil, for: $0) } }
开发过程中必须解决的陷阱以及如何管理
显然,此方法对于具有相同功能(在字段中填写)的屏幕很方便,但是如果在一个单独的屏幕上出现的唯一元素或功能不在其他屏幕上,则不是那么方便。 在我们的应用程序中,这些是:
- 具有引擎动力的屏幕,必须单独生成,这就是功能不同的原因。 在此屏幕上,请求将消失,来自服务器的值将被自动替换。 我必须为其单独创建一个类,该类负责显示,加载,验证,从服务器加载以及将值替换为空字段,如果后者决定输入自己的值,则不会打扰用户。
- 注册号屏幕,其中唯一的一个是开关,它影响文本字段的显示或隐藏。 对于这种情况,必须做出一个附加条件,以编程方式确定打开位置为空值的情况。
- 动态列表(例如必须存储并绑定到表单的驱动程序列表)也脱离了概念。
- 独特的数据验证类型。 可能很多面具都混有regex'ami。 以及各个字段的日期验证,其中验证存在显着差异(对最小值/最大值的限制)等。
- 数据输入屏幕将作为collectionView单元格。 因此,设计模式窗口的显示需要对所选索引的精确控制。 我必须检查可填充的字段,并从列表中排除用户不应看到的那些字段。
- 为了正确显示表中的数据,必须对某些屏幕的模型方法进行更改。 诸如名称和地址之类的单元格在表中显示为单个元素,但是需要完全填充几个弹出屏幕。
结论
这种经验使True Engineering的我们能够快速实现易于维护的移动应用程序。 多功能性使您可以快速生成具有不同类型输入数据的表:我们在一周内制作了20个窗口。 这种方法还可以加快应用程序测试过程。 在不久的将来,我们将重用已完成的工厂,以快速生成新表和新功能。