首页 > 解决方案 > Reload table view cell, save state, change height

问题描述

I have a tableviewcell inside a tableview. Upon clicking a checkbox a field should be displayed/hidden and the cell height resized to support the change.

Currently, everything works smoothly up until I reload the cell to change the height. Not only does it take TWO taps to work, but it erases the input values of the fields. Its as if it's swapping between two different cells.

Here is a gif below showcasing the problem. Code for the two view controllers is below.

enter image description here

ViewController

import UIKit

class CheckoutViewController: UIViewController {

    let cellSpacingHeight:CGFloat = 30.0

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        tableView.delegate = self
        tableView.dataSource = self
        tableView.separatorStyle = .none
        tableView.allowsSelection = false
        tableView.keyboardDismissMode = .onDrag

        NotificationCenter.default.addObserver(self, selector: #selector(CheckoutViewController.keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(CheckoutViewController.keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardDidHide, object: nil)

    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // MARK: Keyboard Notifications
    @objc func keyboardWillShow(notification: NSNotification) {
        if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height {
            tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0)
        }
    }

    @objc func keyboardWillHide(notification: NSNotification) {
        UIView.animate(withDuration: 0.2, animations: {
            // For some reason adding inset in keyboardWillShow is animated by itself but removing is not, that's why we have to use animateWithDuration here
            self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
        })
    }

    func foo(){

    }


}

// datasource
extension CheckoutViewController: UITableViewDataSource{

    func numberOfSections(in tableView: UITableView) -> Int {
        return 6
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    // Set the spacing between sections
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return cellSpacingHeight
    }

    // Make the background color show through
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = UIView()
        headerView.backgroundColor = UIColor.clear
        return headerView
    }

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

        if(indexPath.section == 0){
            let cellID = "CheckoutEmailTableViewCellReuseID"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! CheckoutEmailTableViewCell

            cell.setUp()

            return cell
        }else if(indexPath.section == 1){
            let cellID = "CheckoutAddressTableViewCellReuseID"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! CheckoutAddressTableViewCell

            cell.setUp(setUpCase: .shipping)
            cell.headerLabel.text = "SHIPPING INFORMATION"

            return cell
        }else if(indexPath.section == 2){
            let cellID = "CheckoutAddressTableViewCellReuseID"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! CheckoutAddressTableViewCell

            cell.setUp(setUpCase: .billing)
            cell.headerLabel.text = "BILLING INFORMATION"



            cell.onMyAction = {
                print("cell.sameAsButtonIsChecked: \(cell.sameAsButtonIsChecked)")
                if let indexPath = tableView.indexPath(for: cell) {
                    tableView.reloadRows(at: [indexPath], with: .automatic)
                }
            }

            return cell
        }else if(indexPath.section == 3){
            let cellID = "CheckoutPaymentMethodTableViewCellReuseID"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! CheckoutPaymentMethodTableViewCell

            cell.setUp()

            return cell
        }else if(indexPath.section == 4){
            let cellID = "CheckoutTotalTableViewCellReuseID"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! CheckoutTotalTableViewCell

            return cell
        }else if(indexPath.section == 5){
            let cellID = "CheckoutButtonTableViewCellReuseID"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! CheckoutButtonTableViewCell

            return cell
        }

        return UITableViewCell()
    }


}

// delegate
extension CheckoutViewController:UITableViewDelegate {}

TableViewCell

import Foundation
import UIKit

class CheckoutAddressTableViewCell: UITableViewCell {

    let addressFieldsHeight:CGFloat = 330

    typealias CellAction = () -> Void
    var onMyAction: CellAction?

    var initialized = false
    var sameAsButtonIsChecked:Bool = true {
        willSet{
            print("will:sameAsButtonIsChecked: \(sameAsButtonIsChecked)")
        }
        didSet {
            print("didStart:sameAsButtonIsChecked: \(sameAsButtonIsChecked)")
            if sameAsButtonIsChecked  {
                print("if")
                sameAsCheckboxImageView.image = #imageLiteral(resourceName: "iconCheckboxChecked")
                addressFieldsView.isHidden = true
                addressFieldsHeightConstraint.constant = 0
            }else{
                print("else")
                sameAsCheckboxImageView.image = #imageLiteral(resourceName: "iconCheckbox")
                addressFieldsView.isHidden = false
                addressFieldsHeightConstraint.constant = addressFieldsHeight
            }
            print("didFinish:sameAsButtonIsChecked: \(sameAsButtonIsChecked)")
            onMyAction?()
        }
    }


    @IBOutlet weak var headerLabel: UILabel!
    @IBOutlet weak var sameAsCheckboxView: UIView!
    @IBOutlet weak var sameAsCheckboxImageView: UIImageView!
    @IBOutlet weak var addressFieldsView: UIView!

    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var addressOneTextField: UITextField!
    @IBOutlet weak var addressTwoTextField: UITextField!
    @IBOutlet weak var cityTextField: UITextField!
    @IBOutlet weak var stateTextField: UITextField!
    @IBOutlet weak var zipCodeTextField: UITextField!
    @IBOutlet weak var countryTextField: UITextField!
    @IBOutlet weak var phoneTextField: UITextField!

    @IBOutlet weak var stateImageView: UIImageView!
    @IBOutlet weak var countryImageView: UIImageView!


    @IBOutlet weak var sameAsHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var addressFieldsHeightConstraint: NSLayoutConstraint!
    enum SetUpCase {
        case shipping
        case billing
    }

    func setUp(setUpCase:SetUpCase) {
        if(setUpCase == .shipping){
            sameAsHeightConstraint.constant = 0
            sameAsCheckboxView.isHidden = true
        }else if(setUpCase == .billing){
            if !initialized {
                sameAsButtonIsChecked = true
            }
        }

        setUpTextFields()

        sameAsCheckboxView.addTapGesture(tapNumber: 1, target: self, action: #selector(sameAsTap))
        stateImageView.addTapGesture(tapNumber: 1, target: self, action: #selector(focusStateTextField))
        countryImageView.addTapGesture(tapNumber: 1, target: self, action: #selector(focusCountryTextField))

        initialized = true;
    }




    private func setUpTextFields(){
        nameTextField.delegate = self
        addressOneTextField.delegate = self
        addressTwoTextField.delegate = self
        cityTextField.delegate = self
        stateTextField.delegate = self
        zipCodeTextField.delegate = self
        countryTextField.delegate = self
        phoneTextField.delegate = self

        nameTextField.setBothPaddingPoints(10, 10)
        addressOneTextField.setBothPaddingPoints(10, 10)
        addressTwoTextField.setBothPaddingPoints(10, 10)
        cityTextField.setBothPaddingPoints(10, 10)
        stateTextField.setBothPaddingPoints(10, 10)
        zipCodeTextField.setBothPaddingPoints(10, 10)
        countryTextField.setBothPaddingPoints(10, 10)
        phoneTextField.setBothPaddingPoints(10, 10)
    }

    @objc func sameAsTap(){
        self.endEditing(true)
        sameAsButtonIsChecked = !sameAsButtonIsChecked
    }


    @objc func focusStateTextField(){
        stateTextField.becomeFirstResponder()
    }
    @objc func focusCountryTextField(){
        countryTextField.becomeFirstResponder()
    }

}

// Unfortunately we need to tell iOS explicitly where to go next and
// explicitly where to look after the return key
extension CheckoutAddressTableViewCell: UITextFieldDelegate{
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if(textField.returnKeyType == .next){
            if(textField == stateTextField || textField == countryTextField){
                textField.superview?.superview?.superview?.viewWithTag(textField.tag+1)?.becomeFirstResponder()
            }else{
                textField.superview?.superview?.viewWithTag(textField.tag+1)?.becomeFirstResponder()
            }
        }else{
            textField.resignFirstResponder()
        }

        return true
    }

    func textFieldDidBeginEditing(_ textField: UITextField) {
//        textField
    }
}

Occasional error messages (I think this is a separate matter as it does not always show)

2018-09-03 09:52:48.642169+0900 nameOfApp[33568:2874030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x60000029e910 UIView:0x7f9e36d33aa0.height == 40   (active)>",
    "<NSLayoutConstraint:0x60000029f180 UIView:0x7f9e36dd5260.height == 63   (active)>",
    "<NSLayoutConstraint:0x60000049d970 UIView:0x7f9e36dd5960.height == 0   (active)>",
    "<NSLayoutConstraint:0x60000049e0f0 V:|-(0)-[UIView:0x7f9e36d33aa0]   (active, names: '|':UIView:0x7f9e36d8f380 )>",
    "<NSLayoutConstraint:0x60000049e230 V:[UIView:0x7f9e36d33aa0]-(0)-[UIView:0x7f9e36dd5260]   (active)>",
    "<NSLayoutConstraint:0x60000049e320 V:[UIView:0x7f9e36dd5260]-(0)-[UIView:0x7f9e36dd5960]   (active)>",
    "<NSLayoutConstraint:0x60000049e370 V:[UIView:0x7f9e36dd5960]-(0)-|   (active, names: '|':UIView:0x7f9e36d8f380 )>",
    "<NSLayoutConstraint:0x60000049e460 V:[UIView:0x7f9e36d8f380]-(0)-|   (active, names: '|':UITableViewCellContentView:0x7f9e36d90ed0 )>",
    "<NSLayoutConstraint:0x60000049e4b0 V:|-(0)-[UIView:0x7f9e36d8f380]   (active, names: '|':UITableViewCellContentView:0x7f9e36d90ed0 )>",
    "<NSLayoutConstraint:0x600000683480 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7f9e36d90ed0.height == 433   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000029f180 UIView:0x7f9e36dd5260.height == 63   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

标签: iosswift

解决方案


您应添加textField.text = "text kept when hidden"以下方法。

func setUp(setUpCase:SetUpCase) {
        if(setUpCase == .shipping){
            sameAsHeightConstraint.constant = 0
            sameAsCheckboxView.isHidden = true
        }else if(setUpCase == .billing){
            if !initialized {
                sameAsButtonIsChecked = true
            }
        }

        setUpTextFields()

        sameAsCheckboxView.addTapGesture(tapNumber: 1, target: self, action: #selector(sameAsTap))
        stateImageView.addTapGesture(tapNumber: 1, target: self, action: #selector(focusStateTextField))
        countryImageView.addTapGesture(tapNumber: 1, target: self, action: #selector(focusCountryTextField))

        initialized = true;
    }

sameAsCheckboxView.addTapGesture(tapNumber: 1, target: self, action: #selector(sameAsTap))提取到awakeFromNib:更好。

NSLayoutConstraint 错误应该用 xib 文件调试。

因为 UITableViewCell 是可重用的。因此,您将单元格的元素从元素中隐藏起来,然后再次显示它。

cell.onMyAction = {
                print("cell.sameAsButtonIsChecked: \(cell.sameAsButtonIsChecked)")
                if let indexPath = tableView.indexPath(for: cell) {
                    tableView.reloadRows(at: [indexPath], with: .automatic)
                }
            }

下面的代码被称为

let cellID = "CheckoutAddressTableViewCellReuseID"
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! CheckoutAddressTableViewCell

cell.setUp(setUpCase: .billing)

而且您没有保留 textField 的文本。所以它总是出现占位符。


推荐阅读