Mobile application with automatic form generation: our case

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



    We began to develop a mobile application for agents who are engaged in on-site 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 questionnaire item requires a unique data type (string, date, attached document), the screen forms were quite similar. But the main thing is their number ... Nobody wants to engage in the repetition of visualization and processing of the same elements many times.

    To avoid the many hours of manual work on creating forms, you need to apply a little ingenuity and a lot of dynamic UI construction. In this article, we want to share how we solved this problem.

    For an elegant solution to the problem, we used the mechanism for generating objects - ViewModels, which are used to build custom forms using tables.



    In normal work, for each individual table that the developer wants to see on the screen, a separate ViewModel class must be created. It defines the visual component of the table. We decided to go one level higher and generate ViewModels and Models ourselves dynamically, using a simple description of the structure through the Enum fields.

    How it works


    It all started with enum. For each profile we create a unique enum - these are our sections of the profile. One of its methods is to return the array of cells in this section.

    The cells in 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 parameters such as

    • display check: some cells must be hidden,
    • list of “parent” cells: cells on which the value, validation or display of this cell depends,
    • cell type: simple cells with values, cells in switch, cells with the function of adding elements, etc.

    We subscribe all sections to the general QuestionnaireSectionCellType protocol in order to exclude binding to a specific section, we will do the same with all cells of the table (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 out. 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 {
       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 to read the value with the QuestionnaireCellType key and to save it to the desired array element.

    Some models may have optional fields, so we added an array with optional keys. During model validation, these keys may not contain values, but the model will be considered filled.

    Further, for convenience, all the values ​​in the ValueModel we subscribe to the common protocol StringRepresentable protocol 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 the model (initial values ​​should be set in some models), support for a dynamic array of values ​​(value: Array), etc.

    This approach turned out to be very convenient for storing in the database using Realm. To fill out the questionnaire, it is possible to select a previously saved completed model. To extend the CTP policy, the agent will no longer need to fill out the user's documents, the drivers attached by him, and the TCP data for the new one. Instead, you can simply reuse it to fill in the existing one.

    To change or supplement tables, you just need to find the ViewModel related to a particular screen, find the necessary enum that is responsible for displaying the desired block and add or fix several cases. Everything, the table will take the necessary form!

    Filling out the form with test values ​​was also very convenient and quick. This way you can quickly generate any test data. And if you add a separate file with the initial data, from where the program will take the value into each specific field of the questionnaire, then even a beginner can generate ready-made questionnaires without going 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 dependency handling. Some elements of the questionnaire were interconnected. So, the document number cannot be filled out without choosing the type of this document itself, the house number cannot be indicated without indicating the city and street, etc.



    We made updating the values ​​of the questionnaire with clearing all dependent fields (For example, deleting or changing the type of a document, we clear the field “document number”):

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

    Pitfalls that we had to solve during development, and how we managed


    It is clear that this method is convenient for screens with the same functionality (filling in the 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:

    • A screen with engine power, which had to be generated separately, which is why it differed in functionality. On this screen, the request should go away and the value from the server is automatically substituted. I had to separately create a class for it that would be responsible for displaying, loading, validating, loading from the server and substituting a value in an empty field, without disturbing the user if the latter decides to enter his own value.
    • The registration number screen, in which the only one is the switch, which affects the display or hiding of the text field. For this case, an additional condition had to be made, which would programmatically determine cases with the switch position on as an empty value.
    • Dynamic lists, such as a list of drivers that had to be stored and tied to a form, which also got out of concept.
    • Unique types of data validation. It could be a lot of masks mixed with regex'ami. And date validation for various fields, where the validation differed dramatically (restrictions on the minimum / maximum values), etc.
    • Data entry screens are made as collectionView cells. (That was required by the design!) Because of this, displaying modal windows required precise control over the selected index. I had to check the fields 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 model methods of some screens. Cells such as name and address are displayed in the table as a single element, but require several pop-up screens to be fully populated.

    Conclusion


    This experience allowed us at True Engineering 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 application testing process. In the near future, we will reuse the finished factory to quickly generate new tables and new functionality.

    Also popular now: