ios - 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.
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.
解决方案
您应添加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 的文本。所以它总是出现占位符。
推荐阅读
- python - 在熊猫数据框中获取一周的最小值
- android-studio-3.0 - Android Studio:找不到构建工具修订版 30.0.3
- python - Android 上的 TFLite 模型精度更差
- javascript - Angular + Angularfire 实时更新在移动设备上不起作用
- c++ - 具有不可复制成员的复制赋值运算符
- android-studio - 自动补全在 buildSrc 内的 Android Studio 北极狐中不起作用
- javascript - 是否可以在 Javascript 中动态添加方法?
- react-admin - React-admin:如何使用 ReferenceInput 启用延迟加载
- css - 屏幕分辨率移动/Web 可扩展响应
- pycharm - PyCharm通过文件系统中的标志将目录标记为排除?