Mobile applications are not always simple and concise, as we like - the developers. Other applications are created to solve complex user problems and contain many screens and scenarios. For example, applications for testing, questionnaires and surveys - wherever in the process you need to fill out many forms. About this application and will be discussed in this article.

We began to develop a mobile application for agents who are engaged in field registration of insurance policies. They fill out large forms in the application with customer data: information about the car, owners, drivers, etc. Although each form has its own sections, cells and structure, and each item of the questionnaire requires a unique data type (row, date, sub-document), the screen forms were quite similar. But the main thing is their number ... Nobody wants to repeat the visualization and processing of the same type elements many times.
')
To avoid many hours of manual work on creating forms, you need to use a little quick wits and a lot of dynamic UI construction. In this article, we want to share a way to solve this problem.
For an elegant solution of the problem, we used the object generation mechanism - ViewModels, which are used to build custom forms using tables.

In normal operation, for each individual table that the developer wants to see on the screen, its own ViewModel class should be created. It determines the visual component of the table. We decided to go up one level and generate the ViewModels and Models themselves dynamically using a simple structure description via the Enum fields.
How it works
It all started with enum'a. For each questionnaire we create a unique enum - these are our sections of the questionnaire. One of its methods is to return the cell array of this section.
The cells of the table will also be enum with additional functions that will describe the properties of the cells. In such functions we set the name of the cell, the initial value. Later added options such as
- display check: some cells must be hidden,
- list of “parent” cells: the cells on which the value, validation or display of this cell depends,
- cell type: simple cells with values, cells in the switch, cells with the function of adding elements, etc.
We subscribe all sections to the common QuestionnaireSectionCellType protocol in order to eliminate binding to a specific section, we will do the same with all table cells (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 } … }
Such a model will be very easy to fill. We simply run through all sections, in each section we run through an array of cells and add them to the model array.
On the example of the policyholder's screen (enum with sections - 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) } } } } }
Done! Now we have a table with initial values. Add methods for reading the value by the QuestionnaireCellType key and for saving to the required element of the array.
Some models may have optional fields, so we added an array with optional keys. When validating a model, these keys may not contain values, but the model will be considered full.
Further, for convenience, all the values in the ValueModel are signed by the StringRepresentable protocol to the general protocol in order to limit the list of possible values and add a method to display the value in the cell.
protocol StringRepresentable { var stringValue: String? { get } }
The functionality grew, and many other properties and methods appeared in the models: cleaning up the model (some models need to set initial values), support for a dynamic array of values (value: Array), etc.
This approach was very convenient for storage in the database using Realm. To fill in the questionnaire, you can choose a previously saved filled model. To extend the CTP policy, the agent will no longer need to fill in the user's documents, the drivers attached to them, and the data on the PTS for the new one. Instead, you can simply reuse to fill the existing one.
To modify or supplement the tables, you only need to find the ViewModel related to a specific screen, find the required enum, which is responsible for displaying the required block, and add or correct several case'ov. Everything, the table will take the necessary form!
Filling out the form with test values was also very convenient and fast. This way you can quickly generate any test data. And if you add a separate file with initial data, from where the program will take the value into each specific field of the questionnaire, then even a beginner will be able to generate ready-made questionnaires, without delving into and disassembling the rest of the code, except for a specific file.
Dependencies
A separate task that we solved during the development process is handling dependencies. Some elements of the questionnaire were related. Thus, the document number cannot be filled without choosing the type of this document, the house number cannot be specified without specifying the city and street, etc.

We made an update of the questionnaire values with clearing all dependent fields (For example, removing or changing the document type, we clear the “document number” field):
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) } }
The pitfalls that had to be solved during the development, and how we managed
It is clear that this method is convenient for screens with the same functionality (filling in fields), but not so convenient if unique elements or functions appear on one separate screen that are not on other screens. In our application, these were:
- The screen with the power of the engine, which had to be generated separately, which is why it differed in functionality. On this screen, the query should go away and automatically substitute the value from the server. It was necessary to separately create a class for it that would be responsible for displaying it, loading it, validating it, loading it from the server, and substituting the value into an empty field without disturbing the user if the latter decides to enter his value.
- The registration number screen, in which the only one is a switch that affects the display or hiding of the text field. For this case, we had to make an additional condition that would programmatically determine cases with the switch position turned on as an empty value.
- Dynamic lists, such as the list of drivers that had to be stored and tied to the form, which was also out of concept.
- Unique types of data validation. It could be many masks interspersed with regexs. And validation of dates for different fields, where validation differed strikingly (minimum / maximum values), etc.
- Data entry screens are made as collectionView cells. (Design demanded that!) Because of this, the display of modal windows required precise control over the selected index. We had to check the fields that are available for filling, and exclude from the list those that the user should not see.
- To correctly display the data in the table, it was necessary to make changes to the methods of the model of some screens. Cells such as name and address are displayed in the table as one element, but require several pop-up screens to fill.
Conclusion
This experience allowed us to quickly implement a mobile application that is easy to maintain. Versatility allows you to quickly generate tables with different types of input data: we made 20 windows in just a week. This approach also speeds up the process of testing the application. In the near future we will reuse the finished factory for the rapid generation of new tables and new functionality.