How to implement a text field cell with custom content configuration in iOS 14?

Viewed 641

Apple's Implementing Modern Collection Views has an example of implementing custom UICollectionViewCell in Modern Collection Views/Cell Configurations. It demonstrates how to add custom views to UICollectionViewCell.

What I want is it's a UITextField cell. So I added a UITextField and the code was:

class CustomConfigurationCell: UICollectionViewListCell {

  // Trying to gain access to UITextField
  // But contentView is not a CustomContentView
  var textField: UITextField? {
    guard let contentView = contentView as? CustomContentView else {
      return nil
    }
    return contentView.textField
  }

  override func updateConfiguration(using state: UICellConfigurationState) {
    var content = CustomContentConfiguration().updated(for: state)
    content.name = name
    contentConfiguration = content
  }
}

struct CustomContentConfiguration: UIContentConfiguration, Hashable {
  func makeContentView() -> UIView & UIContentView {
    return CustomContentView(configuration: self)
  }
}

class CustomContentView: UIView, UIContentView {
  public let textField = UITextField();
}

In my UIViewController:

let textFieldCellRegistration = UICollectionView.CellRegistration<CustomConfigurationCell, Key> { (cell, indexPath, item) in
   cell.textField?.delegate = self
}

The text field showed up as expected. But unfortunately, cell.textField was nil. I couldn't retrieve user input without UITextViewDelegate.

So how can I implement a text field cell with content configuration?

1 Answers

You just need a custom content configuration and view. Note: you'll might have to implement your own indentation and margins to match UIListContentConfiguration.

import UIKit

struct TextFieldContentConfiguration: UIContentConfiguration {
    var text: String? = nil
    var textChanged: ((String?) -> Void)?

    func makeContentView() -> UIView & UIContentView {
        return TextFieldContentView(configuration: self)
    }

    func updated(for state: UIConfigurationState) -> TextFieldContentConfiguration {
        return self
    }
}

class TextFieldContentView: UIView, UIContentView, UITextFieldDelegate {
    public var configuration: UIContentConfiguration {
        get {
            return appliedConfiguration
        }
        set {
            if let config = newValue as? TextFieldContentConfiguration {
                apply(configuration: config)
            }
        }
    }

    private var appliedConfiguration: TextFieldContentConfiguration = TextFieldContentConfiguration()

    private func apply(configuration: TextFieldContentConfiguration) {
        textField.text = configuration.text
        self.appliedConfiguration = configuration
    }

    required init(configuration: TextFieldContentConfiguration) {
        textField = UITextField(frame: .zero)
        textField.translatesAutoresizingMaskIntoConstraints = false

        super.init(frame: .zero)

        addViews()
        apply(configuration: configuration)

        textFieldToken = NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: textField, queue: .main, using: { [weak self] notification in
            guard let textField = notification.object as? UITextField else {
                return
            }
            self?.appliedConfiguration.textChanged?(textField.text)
        })
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError()
    }

    private func addViews() {
        addSubview(textField)

        let guide = layoutMarginsGuide
        NSLayoutConstraint.activate([
            textField.topAnchor.constraint(equalTo: guide.topAnchor),
            textField.bottomAnchor.constraint(equalTo: guide.bottomAnchor),
            textField.leadingAnchor.constraint(equalTo: guide.leadingAnchor),
            textField.trailingAnchor.constraint(equalTo: guide.trailingAnchor),
        ])
    }

    private let textField: UITextField
    private var textFieldToken: Any?
}

Then you use the custom configuration with a UICollectionViewListCell, and pass a block that will get the value back.

let textCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String?> {
    [weak self] cell, indexPath, model in
    var config = TextFieldContentConfiguration()
    config.text = self?.timer.displayName
    config.textChanged = { newText in
        self?.timer.displayName = newText ?? ""
    }

    cell.contentConfiguration = config
}
Related