iOS Swift中TableView单元格TextField与TextView重叠及动态添加问题
Got it, let's fix this step by step. The core issue here is that you're looping through your entire array in cellForRowAt, which adds every control type to every cell—causing that overlapping mess. We'll restructure the data and cell logic to make each cell show only one control, plus add the dynamic button functionality.
1. Create a Clear Data Model
First, replace your separate widgetInstockArray and labelInstockArray with a single array of structured items. This lets each row know exactly what control to show and what placeholder to use:
enum InputControlType { case textField case textView } struct InputRowItem { let controlType: InputControlType let placeholder: String } // Initialize your data array (replace with your existing values) var inputRowItems: [InputRowItem] = []
2. Build a Custom TableView Cell
Make a custom cell that contains both a TextField and TextView, but keeps them hidden by default. This way we can toggle visibility based on the row's data, and avoid re-adding controls every time a cell is reused.
class InputTableViewCell: UITableViewCell { // Add these via Interface Builder or programmatically let textField: UITextField = { let tf = UITextField() tf.borderStyle = .roundedRect // Add constraints to pin it to cell's contentView edges (adjust padding as needed) tf.translatesAutoresizingMaskIntoConstraints = false return tf }() let textView: UITextView = { let tv = UITextView() tv.layer.borderWidth = 1 tv.layer.borderColor = UIColor.systemGray4.cgColor tv.layer.cornerRadius = 8 // Add same constraints as textField to ensure they overlap perfectly tv.translatesAutoresizingMaskIntoConstraints = false return tv }() // Optional: Add placeholder support for TextView (since it doesn't have a native one) private let textViewPlaceholderLabel: UILabel = { let label = UILabel() label.textColor = .systemGray label.translatesAutoresizingMaskIntoConstraints = false return label }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupViews() setupTextViewPlaceholderObserver() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() setupTextViewPlaceholderObserver() } private func setupViews() { contentView.addSubview(textField) contentView.addSubview(textView) contentView.addSubview(textViewPlaceholderLabel) // Pin textField to contentView NSLayoutConstraint.activate([ textField.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), textField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), textField.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) ]) // Pin textView to same bounds as textField NSLayoutConstraint.activate([ textView.topAnchor.constraint(equalTo: textField.topAnchor), textView.leadingAnchor.constraint(equalTo: textField.leadingAnchor), textView.trailingAnchor.constraint(equalTo: textField.trailingAnchor), textView.bottomAnchor.constraint(equalTo: textField.bottomAnchor) ]) // Pin placeholder label inside textView NSLayoutConstraint.activate([ textViewPlaceholderLabel.topAnchor.constraint(equalTo: textView.topAnchor, constant: 8), textViewPlaceholderLabel.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 8) ]) // Hide both controls initially textField.isHidden = true textView.isHidden = true } private func setupTextViewPlaceholderObserver() { textView.addObserver(self, forKeyPath: "text", options: .new, context: nil) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "text" { textViewPlaceholderLabel.isHidden = !textView.text.isEmpty } } // Call this to configure the cell with a row item func configure(with item: InputRowItem) { // Hide both controls first to avoid reuse issues textField.isHidden = true textView.isHidden = true switch item.controlType { case .textField: textField.isHidden = false textField.placeholder = item.placeholder case .textView: textView.isHidden = false textViewPlaceholderLabel.text = item.placeholder textViewPlaceholderLabel.isHidden = !textView.text.isEmpty } } deinit { textView.removeObserver(self, forKeyPath: "text") } }
3. Fix cellForRowAt Logic
Now, instead of looping through your entire array, just grab the item for the current row and configure the cell accordingly:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "InputCell", for: indexPath) as? InputTableViewCell else { return UITableViewCell() } let rowItem = inputRowItems[indexPath.row] cell.configure(with: rowItem) return cell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return inputRowItems.count }
4. Implement Dynamic Add Functionality
When your button is tapped, add a new InputRowItem to your data array and update the table view (using insertRows is more efficient than full reload):
@IBAction func addControlButtonTapped(_ sender: UIButton) { // Example: Add a TextField by default, adjust type/placeholder as needed let newItem = InputRowItem(controlType: .textField, placeholder: "New Input") inputRowItems.append(newItem) // Insert the new row with animation let newIndexPath = IndexPath(row: inputRowItems.count - 1, section: 0) tableView.insertRows(at: [newIndexPath], with: .automatic) }
5. Key Notes
- Cell Reuse: Always hide all controls before showing the correct one—this prevents old controls from sticking around when cells are reused.
- TextView Placeholder: Since UITextView doesn't have a native placeholder, we added a label and observer to mimic it. You can adjust this logic to fit your design.
- Data Initialization: Populate your
inputRowItemsarray with your existingwidgetInstockArrayandlabelInstockArrayvalues (map each pair to anInputRowItem).
内容的提问来源于stack exchange,提问作者iOS Developer

