Jasper Alblas
Jasper Alblas

Reputation: 348

Need advice on how to perform validation on dynamically built tableviews

I am working on an app that will include 13 forms that can be filled in and send of to a web service. All these forms include multiple fields, although several of the fields are included on more than one form.

Of course I could have taken the route of simply creating 13 view controllers. The forms will not change in the near future, so static cells could do the trick but creating 13 view controllers in code and in the storyboard does not seem elegant.

So I have decided to create one view controller with a variety of dynamic prototype cells which then can be used by the different types of forms when required.

I created a simple model, and use the selected FormType to see which fields (tableview cells) are needed in that form.

struct Form {
let type: FormType
let identifier: String
let cellTypes: [CellType]
let requiredFieldsIndexes: [Int]

}

enum FormType: String {
case permissionEndPoint = "FAP-11"
case other2 = "2"
// etc...

}

Then I create the 13 forms in code:

Form(type: .permissionEndPoint, identifier: "SAP.01", cellTypes: Array([.kilometerConstraints,.additionalInformation,.permissionNumber]),requiredFieldsIndexes: [0])

And use the tableview delegate methods to show the correct fields.` func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return displayedForm.cellTypes.count }

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let identifier = displayedForm.cellTypes[indexPath.row]

    switch identifier {
    case .trafficControlCenter:
        return tableView.dequeueReusableCell(withIdentifier: identifier.rawValue, for: indexPath)
    case .trainInformation:
        return tableView.dequeueReusableCell(withIdentifier: identifier.rawValue, for: indexPath)
    case .permissionAtLocation:
        return tableView.dequeueReusableCell(withIdentifier: identifier.rawValue, for: indexPath)
    case .notUsed:
        let cell = tableView.dequeueReusableCell(withIdentifier: identifier.rawValue, for: indexPath) as! NotUsedTableViewCell
        return cell
    case .additionalInformation:
        let cell = tableView.dequeueReusableCell(withIdentifier: identifier.rawValue, for: indexPath) as! AdditionalInformationCell
        cell.additionalInfoTextView.delegate = self
        return cell
    default:
        return tableView.dequeueReusableCell(withIdentifier: identifier.rawValue, for: indexPath)
    }
}`

This works well, but I need to check if certain if certain fields are filled out, collect the data, and send the data in a certain format to the web. How should I get access to the different fields now that I don't know exactly which fields are shown? Perhaps I could create a reference to all relevant IBOutlets (textfields, textviews and datepickers) to check these?

Is my general setup alright or is there an easier way of modelling this issue?

Any help is very appreciated.

Upvotes: 1

Views: 131

Answers (1)

Matteo Manferdini
Matteo Manferdini

Reputation: 412

There are different moving parts in your solution and the important thing is to keep concerns separated between the different classes.

Following the MVC pattern, this is how I would do it:

  • For each cell, create a subclass of UITableViewCell, which has all the necessary outlets to controls in the cell (text fields, sliders, switches, etc).
  • A cell should also be the delegate of these controls (for example, UITextFieldDelegate) or the receiver of the actions of controls like UISwitch, to detect when the user changes the values in the form.
  • When a value changes, the cell communicates such changes to a custom delegate of its own. You implement this through a custom protocol for your cell subclass. The delegate has to store the cell data. This is because in a table view, when the cell goes out of the screen, it might be reused and not keep its state. So the cell needs to communicate this to an external object, or you will lose the user input.
  • The delegate of all the cells in the table view should be the data source (which might be your view controller, or a separate object). The data source can set itself as a delegate for each cell in the tableView(_:cellForRowAt:) method you show above.
  • Whenever a cell reports a change in its data, the data source stores such data in an array. You can get ask the index path of a cell to the table view, so you know at which index in the array to store such data
  • Every time the tableView(_:cellForRowAt:) method gets called to dequeue a cell, you repopulates the cell with the data you stored in the data source. As I said, cells can be reused, so you could get a reused cell with other data in it, or a newly dequeued and empty one. This is why you need to pass data back to it. If you don't the user will see weird values in the form if he scrolls up and down.
  • When you need the data of the whole form to submit it, you will find it already stored in your array in the data source.

Upvotes: 1

Related Questions