123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- // FieldRow.swift
- // Eureka ( https://github.com/xmartlabs/Eureka )
- //
- // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com )
- //
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- import Foundation
- import UIKit
- public protocol InputTypeInitiable {
- init?(string stringValue: String)
- }
- public protocol FieldRowConformance : FormatterConformance {
- var titlePercentage : CGFloat? { get set }
- var placeholder : String? { get set }
- var placeholderColor : UIColor? { get set }
- }
- extension Int: InputTypeInitiable {
- public init?(string stringValue: String) {
- self.init(stringValue, radix: 10)
- }
- }
- extension Float: InputTypeInitiable {
- public init?(string stringValue: String) {
- self.init(stringValue)
- }
- }
- extension String: InputTypeInitiable {
- public init?(string stringValue: String) {
- self.init(stringValue)
- }
- }
- extension URL: InputTypeInitiable {}
- extension Double: InputTypeInitiable {
- public init?(string stringValue: String) {
- self.init(stringValue)
- }
- }
- open class FormatteableRow<Cell: CellType>: Row<Cell>, FormatterConformance where Cell: BaseCell, Cell: TextInputCell {
- /// A formatter to be used to format the user's input
- open var formatter: Formatter?
- /// If the formatter should be used while the user is editing the text.
- open var useFormatterDuringInput = false
- open var useFormatterOnDidBeginEditing: Bool?
- public required init(tag: String?) {
- super.init(tag: tag)
- displayValueFor = { [unowned self] value in
- guard let v = value else { return nil }
- guard let formatter = self.formatter else { return String(describing: v) }
- if (self.cell.textInput as? UIView)?.isFirstResponder == true {
- return self.useFormatterDuringInput ? formatter.editingString(for: v) : String(describing: v)
- }
- return formatter.string(for: v)
- }
- }
- }
- open class FieldRow<Cell: CellType>: FormatteableRow<Cell>, FieldRowConformance, KeyboardReturnHandler where Cell: BaseCell, Cell: TextFieldCell {
- /// Configuration for the keyboardReturnType of this row
- open var keyboardReturnType: KeyboardReturnTypeConfiguration?
- /// The percentage of the cell that should be occupied by the textField
- @available (*, deprecated, message: "Use titlePercentage instead")
- open var textFieldPercentage : CGFloat? {
- get {
- return titlePercentage.map { 1 - $0 }
- }
- set {
- titlePercentage = newValue.map { 1 - $0 }
- }
- }
- /// The percentage of the cell that should be occupied by the title (i.e. the titleLabel and optional imageView combined)
- open var titlePercentage: CGFloat?
- /// The placeholder for the textField
- open var placeholder: String?
- /// The textColor for the textField's placeholder
- open var placeholderColor: UIColor?
- public required init(tag: String?) {
- super.init(tag: tag)
- }
- }
- /**
- * Protocol for cells that contain a UITextField
- */
- public protocol TextInputCell {
- var textInput: UITextInput { get }
- }
- public protocol TextFieldCell: TextInputCell {
- var textField: UITextField! { get }
- }
- extension TextFieldCell {
- public var textInput: UITextInput {
- return textField
- }
- }
- open class _FieldCell<T> : Cell<T>, UITextFieldDelegate, TextFieldCell where T: Equatable, T: InputTypeInitiable {
- @IBOutlet public weak var textField: UITextField!
- @IBOutlet public weak var titleLabel: UILabel?
- fileprivate var observingTitleText = false
- private var awakeFromNibCalled = false
- open var dynamicConstraints = [NSLayoutConstraint]()
- private var calculatedTitlePercentage: CGFloat = 0.7
- public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- let textField = UITextField()
- self.textField = textField
- textField.translatesAutoresizingMaskIntoConstraints = false
- super.init(style: style, reuseIdentifier: reuseIdentifier)
- setupTitleLabel()
- contentView.addSubview(titleLabel!)
- contentView.addSubview(textField)
- NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in
- guard let me = self else { return }
- guard me.observingTitleText else { return }
- me.titleLabel?.removeObserver(me, forKeyPath: "text")
- me.observingTitleText = false
- }
- NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
- guard let me = self else { return }
- guard !me.observingTitleText else { return }
- me.titleLabel?.addObserver(me, forKeyPath: "text", options: [.new, .old], context: nil)
- me.observingTitleText = true
- }
- NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in
- self?.setupTitleLabel()
- self?.setNeedsUpdateConstraints()
- }
- }
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
- open override func awakeFromNib() {
- super.awakeFromNib()
- awakeFromNibCalled = true
- }
- deinit {
- textField?.delegate = nil
- textField?.removeTarget(self, action: nil, for: .allEvents)
- guard !awakeFromNibCalled else { return }
- if observingTitleText {
- titleLabel?.removeObserver(self, forKeyPath: "text")
- }
- imageView?.removeObserver(self, forKeyPath: "image")
- NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
- NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
- NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil)
- }
- open override func setup() {
- super.setup()
- selectionStyle = .none
- if !awakeFromNibCalled {
- titleLabel?.addObserver(self, forKeyPath: "text", options: [.new, .old], context: nil)
- observingTitleText = true
- imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil)
- }
- textField.addTarget(self, action: #selector(_FieldCell.textFieldDidChange(_:)), for: .editingChanged)
- }
- open override func update() {
- super.update()
- detailTextLabel?.text = nil
- if !awakeFromNibCalled {
- if let title = row.title {
- switch row.cellStyle {
- case .subtitle:
- textField.textAlignment = .left
- textField.clearButtonMode = .whileEditing
- default:
- textField.textAlignment = title.isEmpty ? .left : .right
- textField.clearButtonMode = title.isEmpty ? .whileEditing : .never
- }
- } else {
- textField.textAlignment = .left
- textField.clearButtonMode = .whileEditing
- }
- } else {
- textLabel?.text = nil
- titleLabel?.text = row.title
- if #available(iOS 13.0, *) {
- titleLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label
- } else {
- titleLabel?.textColor = row.isDisabled ? .gray : .black
- }
- }
- textField.delegate = self
- textField.text = row.displayValueFor?(row.value)
- textField.isEnabled = !row.isDisabled
- if #available(iOS 13.0, *) {
- textField.textColor = row.isDisabled ? .tertiaryLabel : .label
- } else {
- textField.textColor = row.isDisabled ? .gray : .black
- }
- textField.font = .preferredFont(forTextStyle: .body)
- if let placeholder = (row as? FieldRowConformance)?.placeholder {
- if let color = (row as? FieldRowConformance)?.placeholderColor {
- textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.foregroundColor: color])
- } else {
- textField.placeholder = (row as? FieldRowConformance)?.placeholder
- }
- }
- if row.isHighlighted {
- titleLabel?.textColor = tintColor
- }
- }
- open override func cellCanBecomeFirstResponder() -> Bool {
- return !row.isDisabled && textField?.canBecomeFirstResponder == true
- }
- open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool {
- return textField?.becomeFirstResponder() ?? false
- }
- open override func cellResignFirstResponder() -> Bool {
- return textField?.resignFirstResponder() ?? true
- }
- open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
- let obj = object as AnyObject?
- if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey],
- ((obj === titleLabel && keyPathValue == "text") || (obj === imageView && keyPathValue == "image")) &&
- (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue {
- setNeedsUpdateConstraints()
- updateConstraintsIfNeeded()
- }
- }
- // MARK: Helpers
- open func customConstraints() {
- guard !awakeFromNibCalled else { return }
- contentView.removeConstraints(dynamicConstraints)
- dynamicConstraints = []
-
- switch row.cellStyle {
- case .subtitle:
- var views: [String: AnyObject] = ["textField": textField]
-
- if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
- views["titleLabel"] = titleLabel
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:[titleLabel]-3-[textField]", options: .alignAllLeading, metrics: nil, views: views)
- // Here we are centering the textField with an offset of -4. This replicates the exact behavior of the default UITableViewCell with .subtitle style
- dynamicConstraints.append(NSLayoutConstraint(item: textField!, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: -4))
- dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: textField, attribute: .centerX, multiplier: 1, constant: 0))
- } else {
- dynamicConstraints.append(NSLayoutConstraint(item: textField!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0))
- }
-
- if let imageView = imageView, let _ = imageView.image {
- views["imageView"] = imageView
- if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-|", options: [], metrics: nil, views: views)
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views)
- } else {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views)
- }
- } else {
- if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-|", options: [], metrics: nil, views: views)
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: [], metrics: nil, views: views)
- } else {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views)
- }
- }
-
- default:
- var views: [String: AnyObject] = ["textField": textField]
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-11-[textField]-11-|", options: .alignAllLastBaseline, metrics: nil, views: views)
-
- if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
- views["titleLabel"] = titleLabel
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-11-[titleLabel]-11-|", options: .alignAllLastBaseline, metrics: nil, views: views)
- dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: textField, attribute: .centerY, multiplier: 1, constant: 0))
- }
-
- if let imageView = imageView, let _ = imageView.image {
- views["imageView"] = imageView
- if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views)
- dynamicConstraints.append(NSLayoutConstraint(item: titleLabel,
- attribute: .width,
- relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual,
- toItem: contentView,
- attribute: .width,
- multiplier: calculatedTitlePercentage,
- constant: 0.0))
- } else {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views)
- }
- } else {
- if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views)
- dynamicConstraints.append(NSLayoutConstraint(item: titleLabel,
- attribute: .width,
- relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual,
- toItem: contentView,
- attribute: .width,
- multiplier: calculatedTitlePercentage,
- constant: 0.0))
- } else {
- dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views)
- }
- }
- }
- contentView.addConstraints(dynamicConstraints)
- }
- open override func updateConstraints() {
- customConstraints()
- super.updateConstraints()
- }
- @objc open func textFieldDidChange(_ textField: UITextField) {
- guard let textValue = textField.text else {
- row.value = nil
- return
- }
- guard let fieldRow = row as? FieldRowConformance, let formatter = fieldRow.formatter else {
- row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value)
- return
- }
- if fieldRow.useFormatterDuringInput {
- let unsafePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
- defer {
- unsafePointer.deallocate()
- }
- let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(unsafePointer)
- let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
- if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
- row.value = value.pointee as? T
- guard var selStartPos = textField.selectedTextRange?.start else { return }
- let oldVal = textField.text
- textField.text = row.displayValueFor?(row.value)
- selStartPos = (formatter as? FormatterProtocol)?.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text) ?? selStartPos
- textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos)
- return
- }
- } else {
- let unsafePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
- defer {
- unsafePointer.deallocate()
- }
- let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(unsafePointer)
- let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
- if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
- row.value = value.pointee as? T
- } else {
- row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value)
- }
- }
- }
- // MARK: Helpers
- private func setupTitleLabel() {
- titleLabel = self.textLabel
- titleLabel?.translatesAutoresizingMaskIntoConstraints = false
- titleLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal)
- titleLabel?.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1000), for: .horizontal)
- }
- private func displayValue(useFormatter: Bool) -> String? {
- guard let v = row.value else { return nil }
- if let formatter = (row as? FormatterConformance)?.formatter, useFormatter {
- return textField?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v)
- }
- return String(describing: v)
- }
- // MARK: TextFieldDelegate
- open func textFieldDidBeginEditing(_ textField: UITextField) {
- formViewController()?.beginEditing(of: self)
- formViewController()?.textInputDidBeginEditing(textField, cell: self)
- if let fieldRowConformance = row as? FormatterConformance, let _ = fieldRowConformance.formatter, fieldRowConformance.useFormatterOnDidBeginEditing ?? fieldRowConformance.useFormatterDuringInput {
- textField.text = displayValue(useFormatter: true)
- } else {
- textField.text = displayValue(useFormatter: false)
- }
- }
- open func textFieldDidEndEditing(_ textField: UITextField) {
- formViewController()?.endEditing(of: self)
- formViewController()?.textInputDidEndEditing(textField, cell: self)
- textFieldDidChange(textField)
- textField.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil)
- }
- open func textFieldShouldReturn(_ textField: UITextField) -> Bool {
- return formViewController()?.textInputShouldReturn(textField, cell: self) ?? true
- }
- open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
- return formViewController()?.textInput(textField, shouldChangeCharactersInRange:range, replacementString:string, cell: self) ?? true
- }
- open func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
- return formViewController()?.textInputShouldBeginEditing(textField, cell: self) ?? true
- }
- open func textFieldShouldClear(_ textField: UITextField) -> Bool {
- return formViewController()?.textInputShouldClear(textField, cell: self) ?? true
- }
- open func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
- return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true
- }
- open override func layoutSubviews() {
- super.layoutSubviews()
- guard let row = (row as? FieldRowConformance) else { return }
- defer {
- // As titleLabel is the textLabel, iOS may re-layout without updating constraints, for example:
- // swiping, showing alert or actionsheet from the same section.
- // thus we need forcing update to use customConstraints()
- setNeedsUpdateConstraints()
- updateConstraintsIfNeeded()
- }
- guard let titlePercentage = row.titlePercentage else { return }
- var targetTitleWidth = bounds.size.width * titlePercentage
- if let imageView = imageView, let _ = imageView.image, let titleLabel = titleLabel {
- var extraWidthToSubtract = titleLabel.frame.minX - imageView.frame.minX // Left-to-right interface layout
- if UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute) == .rightToLeft {
- extraWidthToSubtract = imageView.frame.maxX - titleLabel.frame.maxX
- }
- targetTitleWidth -= extraWidthToSubtract
- }
- calculatedTitlePercentage = targetTitleWidth / contentView.bounds.size.width
- }
- }
|