0

This question might be very broad but I can't find any similar approaches. I have a tableView that consists of several sections. It's like a profile page where user can edit their bio i.e. name, date of birth, gender etc. As the bio is separated into different sections. How would I get all the data to be saved at once when tapped in the tableViewController and NOT the tableViewCell's.

I'm used to sending data from TableViewController to a TableViewCell to populate the cells. But I now need the opposite action where the data from TableViewCell gets sent back to the TableViewController in order for me to define what to save to the database.

Here is an example of how I would save in a normal ViewController:

func saveData() {

    guard let firstNameText = firstNameField.text, let lastNameText = lastNameField.text else { return }

    guard let uid = Auth.auth().currentUser else { return }

    let databseRef = Database.database().reference(withPath: "users/\(uid)")

    let bioItem: [String : Any] = ["firstName" : firstNameText, "lastName" : lastNameText]

    databseRef.updateChildValues(bioItem)

} 

Update

Here is what I have attempted using the solution suggested below:

protocol TextFieldCellDelegate: class {
    func textFieldCell(_ cell: FirstNameCell, didChangeText text: String)
}

class FirstNameCell: UITableViewCell {

    weak var delegate: TextFieldCellDelegate?
    var indexPath: IndexPath!

    var user: User? {
        didSet {
            guard let user = user else { return }
            nameField.text = user.firstName
        }
    }

    lazy var nameField: UITextField = {
        let field = UITextField()
        return field
    }()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupViews()

    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    override func setupViews() {
        addSubview(nameField)
        // Adding constraints...
    }

    @objc private func textFieldDidChange(_ textField: UITextField) {
        guard let text = nameField.text else {
            return
        }

        delegate?.textFieldCell(self, didChangeText: text)
    }

}

ViewController:

var user: User!

func textFieldCell(_ cell: FirstNameCell, didChangeText text: String) {
    switch cell.indexPath.section {
    case 0:
        if cell.indexPath.row == 0 {
            user.firstName = text

            print("New name: \(text)")
        }
    // handle other rows
    default:
        break
        // handle other sections and rows
    }
}



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

    switch (indexPath.section) {
    case 0:

        let cell = tableView.dequeueReusableCell(withIdentifier: firstNameCell, for: indexPath) as! FirstNameCell
        cell.user = self.user
        cell.delegate = self //error
        cell.indexPath = indexPath
        return cell

    case 1:
        let cell = tableView.dequeueReusableCell(withIdentifier: secondNameCell, for: indexPath) as! SecondNameCell
        cell.user = self.user
        return cell
    default:
        break
    }
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    return cell

}
Chace
  • 561
  • 10
  • 28

1 Answers1

1

An example project can be found on GitHub.

I've solved this a few different ways. The way I think would work best for you knowing little about your application's architecture is as follows assuming your example of a edit profile view:

Create a mutable User struct and create a local variable that will be edited each time a table row is edited.

struct User {
    var firstName: String?
    var lastName: String?
    var birthday: Date?
}

Then in your edit profile view controller:

var userModel = User()

When creating your cell subclass you need register the action UIControlEventEditingChanged as stated in this SO post and then each time the textfield is changed notify a delegate that the change happened so your view controller can receive the change event.

protocol TextFieldCellDelegate: class {
    func textFieldCell(_ cell: TextFieldCell, didChangeText text: String)
}

In the cell subclass create a variable for the delegate and create a variable for an indexPath:

weak var delegate: TextFieldCellDelegate?
var indexPath: IndexPath!

The indexPath variable will be used by the view controller to determine which cell is being modified.

Lastly in the cell subclass inside the editing changed action you hooked up earlier:

@objc private func textFieldDidChange(_ textField: UITextField) {
    guard let text = textField.text else {
        return
    }

    delegate?.textFieldCell(self, didChangeText: text)
}

In your view controller set the cell's delegate to the view controller in cell for row and set the indexPath:

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

    cell.delegate = self
    cell.indexPath = indexPath

    //...
}

Lastly in the view controller conform to the TextFieldCellDelegate:

func textFieldCell(_ cell: TextFieldCell, didChangeText text: String) {
    switch cell.indexPath.section {
    case TableSection.user.rawValue:
        if cell.indexPath.row == 0 {
            userModel.firstName = text
        }
        // handle other rows
    default:
        // handle other sections and rows
    }
}

Now every time you edit a row in the table update the userModel. Once the user is done editing their profile and tap a save button and you call your saveData() function check each property on the userModel and see what you need to save:

// ...

var bioItemToUpdate = [String : Any]()

if let firstName = userModel.firstName {
    bioItemToUpdate["firstName"] = firstName
}

if let lastName = userModel.lastName {
    bioItemToUpdate["lastName"] = lastName
}

if let birthday = userModel.birthday {
    bioItemToUpdate["birthday"] = birthday
}

databseRef.updateChildValues(bioItemToUpdate)

Using this approach can also yield the benefit of requiring certain fields to be valid before enabling the save button.

I hope this helps you come to a good solution for your app!

naturaln0va
  • 755
  • 6
  • 8
  • Are you able to show how you would update the `userModel` when a textField has been filled? – Chace Jan 10 '18 at 17:27
  • @mninety5 I added an example of what it'd look like using a table cell with a textfield. – naturaln0va Jan 10 '18 at 18:25
  • @natualn0va Your solution is very helpful. However I'm a little confused how you conform to the `TextFieldCellDelegate`. It seems as though this takes in a particular cell therefore how can I affect other cells in the function? I am also unable to call `cell.delegate = self` as I just get an error. My app already consists of a userModel as it is used to populate the cell's in the first place. Please see my attempt with your solution. – Chace Jan 11 '18 at 10:45
  • @mninety5 you shouldn't create separate cell subclasses for each item in the user model, it'd be better to just have one cell subclass for a text field cell. Then you can setup that cell's info in the `cellForRowAt` in the view controller. To conform to the delegate at the top of the file where you'd see the class declaration like `class ViewController: UIViewController {` add a colon and the delegate name like so `class ViewController: UIViewController, TextFieldCellDelegate {`. – naturaln0va Jan 11 '18 at 21:23
  • @natualn0va So you're suggesting that my "sections" should rather be rows instead? There are apps where you can edit your profile which might have several sections. And once you tap Done. It then saves your details. Therefore I do need extra subclasses in order have separate sections no? Also I'm unable to fix the cell.delegate = self issue – Chace Jan 12 '18 at 09:22
  • 1
    No not at all. For example if you had 3 sections that each had 2 rows and each row was a cell with a text field in it you only need 1 `UITableViewCell` subclass. Then in your `cellForRow` you can set up each as you need based on your model. I've created a small example project you can find here: https://github.com/naturaln0va/ProfileEdit. Hopefully this helps tie everything together! – naturaln0va Jan 12 '18 at 13:58
  • Brilliant example! You're a life saver. – Chace Jan 12 '18 at 14:08
  • @mninety5 I'm glad that helped! – naturaln0va Jan 12 '18 at 21:10