| // Copyright 2019-present the Material Components for iOS authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import UIKit |
| |
| class UITextFieldWithChipsExample: UIViewController { |
| |
| fileprivate let textField = InsetTextField() |
| private var leftView = UIView() |
| fileprivate var leadingConstraint: NSLayoutConstraint? |
| |
| override func viewDidLoad() { |
| super.viewDidLoad() |
| |
| self.view.backgroundColor = UIColor.white |
| |
| setupExample() |
| additionalTextField() |
| |
| // this fixes the issue of the cursor becoming half size when the field is empty |
| DispatchQueue.main.async { |
| self.textField.becomeFirstResponder() |
| } |
| } |
| |
| private func setupExample() { |
| |
| textField.translatesAutoresizingMaskIntoConstraints = false |
| textField.backgroundColor = UIColor.black.withAlphaComponent(0.05) |
| textField.layer.borderWidth = 1.0 |
| textField.layer.borderColor = UIColor.black.cgColor |
| textField.textColor = .darkGray |
| textField.text = "With Chips (Hit Enter)" |
| |
| // when on, enter responds to auto-correction which is confusing when we're trying to create "chips" |
| textField.autocorrectionType = UITextAutocorrectionType.no |
| |
| textField.delegate = self |
| |
| textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) |
| |
| view.addSubview(textField) |
| |
| // position the textfield somewhere in the screen |
| if #available(iOSApplicationExtension 11.0, *) { |
| let guide = view.safeAreaLayoutGuide |
| textField.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 20.0).isActive = true |
| textField.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -20.0).isActive = true |
| textField.topAnchor.constraint(equalTo: guide.topAnchor, constant: 40.0).isActive = true |
| } else if #available(iOSApplicationExtension 9.0, *) { |
| textField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true |
| textField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true |
| textField.topAnchor.constraint(equalTo: view.topAnchor, constant: 40.0).isActive = true |
| } else { |
| // Fallback on earlier versions |
| print("This example is supported on iOS version 9 or later.") |
| } |
| |
| leftView.translatesAutoresizingMaskIntoConstraints = false |
| leftView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5) |
| |
| leftView.clipsToBounds = true |
| textField.leftView = leftView |
| textField.leftViewMode = .always |
| } |
| |
| private func additionalTextField() { |
| let additionalTextField = PlainTextField() |
| additionalTextField.translatesAutoresizingMaskIntoConstraints = false |
| additionalTextField.backgroundColor = UIColor.black.withAlphaComponent(0.05) |
| additionalTextField.layer.borderWidth = 1.0 |
| additionalTextField.layer.borderColor = UIColor.black.cgColor |
| additionalTextField.textColor = .darkGray |
| additionalTextField.text = "Regular UITextfield" |
| |
| // when on, enter responds to auto-correction which is confusing when we're trying to create "chips" |
| additionalTextField.autocorrectionType = UITextAutocorrectionType.no |
| |
| view.addSubview(additionalTextField) |
| |
| // position the textfield somewhere in the screen |
| if #available(iOSApplicationExtension 9.0, *) { |
| additionalTextField.leadingAnchor.constraint(equalTo: textField.leadingAnchor).isActive = true |
| additionalTextField.trailingAnchor.constraint(equalTo: textField.trailingAnchor).isActive = true |
| additionalTextField.topAnchor.constraint(equalTo: textField.topAnchor, constant: 60.0).isActive = true |
| } else { |
| // Fallback on earlier versions |
| print("This example is supported on iOS version 9 or later.") |
| } |
| } |
| |
| fileprivate func appendLabel(text: String) { |
| |
| let pad: CGFloat = 5.0 |
| |
| // create label and add to left view |
| let label = newLabel(text: text) |
| let lastLabel = leftView.subviews.last |
| leftView.addSubview(label) |
| |
| // add constraints |
| if #available(iOSApplicationExtension 9.0, *) { |
| label.topAnchor.constraint(equalTo: leftView.topAnchor).isActive = true |
| label.bottomAnchor.constraint(equalTo: leftView.bottomAnchor).isActive = true |
| if let lastLabel = lastLabel { |
| label.leadingAnchor.constraint(equalTo: lastLabel.trailingAnchor, constant: pad).isActive = true |
| //label.leadingAnchor.constraint(equalTo: lastLabel.trailingAnchor, constant: pad).isActive = true |
| //lastmax = lastLabel.frame.maxX |
| } else { |
| leadingConstraint = label.leadingAnchor.constraint(equalTo: leftView.leadingAnchor) |
| leadingConstraint?.priority = UILayoutPriority.defaultLow |
| leadingConstraint?.isActive = true |
| } |
| } else { |
| // Fallback on earlier versions |
| print("This example is supported on iOS version 9 or later.") |
| } |
| |
| // adjust text field's inset and width |
| leftView.layoutIfNeeded() |
| textField.insetX = label.frame.maxX |
| } |
| |
| private func newLabel(text: String) -> UILabel { |
| // create label and add to left view |
| let label = UILabel() |
| label.translatesAutoresizingMaskIntoConstraints = false |
| label.backgroundColor = UIColor.red.withAlphaComponent(0.4) |
| label.text = " " + text + " " |
| label.textColor = .white |
| label.layer.cornerRadius = 3.0 |
| label.layer.masksToBounds = true |
| return label |
| } |
| } |
| |
| // MARK: Example Extensions |
| |
| extension UITextFieldWithChipsExample: UITextFieldDelegate { |
| |
| // listen to "enter" key |
| func textField(_ textField: UITextField, |
| shouldChangeCharactersIn range: NSRange, |
| replacementString string: String) -> Bool { |
| if string == "\n" { |
| if let trimmedText = textField.text?.trimmingCharacters(in: .whitespaces), |
| trimmedText.count > 0 { |
| appendLabel(text: trimmedText) |
| textField.text = "" |
| } |
| } |
| return true |
| } |
| |
| func setupEditingRect(string: String) { |
| |
| let textrect = textField.textRect(forBounds: textField.bounds) |
| let insetTextField = textField as InsetTextField |
| if let textrange = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument) { |
| let firstrect = textField.firstRect(for: textrange) |
| |
| // if space is too small for typing, we need to make more room - by moving the split point |
| let textWidth = firstrect.width |
| let fieldWidth = textrect.width |
| let space = fieldWidth - textWidth |
| if space < 0 { |
| insetTextField.insetX += space |
| if let leadingConstraint = leadingConstraint { |
| //leftView.removeConstraint(leadingConstraint) |
| leadingConstraint.constant += space |
| } |
| insetTextField.layoutIfNeeded() |
| } |
| |
| } |
| } |
| |
| @objc func textFieldDidChange(_ textField: UITextField) { |
| setupEditingRect(string: textField.text ?? "") |
| } |
| } |
| |
| extension UITextFieldWithChipsExample { |
| override var preferredStatusBarStyle: UIStatusBarStyle { |
| return .lightContent |
| } |
| } |
| |
| extension UITextFieldWithChipsExample { |
| |
| class func catalogMetadata() -> [String: Any] { |
| return [ |
| "breadcrumbs": ["Chip Text Field", "UI Text Fields With Label Chips"], |
| "primaryDemo": false, |
| "presentable": false, |
| ] |
| } |
| } |
| |
| // MARK: UITextField Subclass |
| |
| private class InsetTextField: UITextField { |
| |
| // the split point: this is the x position where chips view ends and text begins. |
| // Updating this property moves the split point between chips & text. |
| var insetX: CGFloat = 8.0 |
| |
| // default padding for the textfield, taking insetX into account for the left position |
| let insetRect = UIEdgeInsets(top: 5.0, left: 8.0, bottom: 5.0, right: 8.0) |
| |
| // text bounds |
| override func textRect(forBounds bounds: CGRect) -> CGRect { |
| let superbounds = super.textRect(forBounds: bounds) |
| var newbounds = superbounds.inset(by: insetRect) |
| newbounds.origin.x = insetX |
| return newbounds |
| } |
| |
| // text bounds while editing |
| override func editingRect(forBounds bounds: CGRect) -> CGRect { |
| let superbounds = super.editingRect(forBounds: bounds) |
| var newbounds = superbounds.inset(by: insetRect) |
| newbounds.origin.x = insetX |
| return newbounds |
| } |
| |
| // left view bounds |
| override func leftViewRect(forBounds bounds: CGRect) -> CGRect { |
| let superbounds = super.leftViewRect(forBounds: bounds) |
| var newbounds = superbounds |
| newbounds.size.width = insetX |
| return newbounds |
| } |
| } |
| |
| class PlainTextField: UITextField { |
| |
| // default padding for the textfield, taking insetX into account for the left position |
| let insetRect = UIEdgeInsets(top: 5.0, left: 8.0, bottom: 5.0, right: 8.0) |
| |
| // text bounds |
| override func textRect(forBounds bounds: CGRect) -> CGRect { |
| let superbounds = super.textRect(forBounds: bounds) |
| let newbounds = superbounds.inset(by: insetRect) |
| return newbounds |
| } |
| |
| // text bounds while editing |
| override func editingRect(forBounds bounds: CGRect) -> CGRect { |
| let superbounds = super.editingRect(forBounds: bounds) |
| let newbounds = superbounds.inset(by: insetRect) |
| return newbounds |
| } |
| } |