diff --git a/Source/BackspaceDetectingTextField.swift b/Source/BackspaceDetectingTextField.swift index 879a12c..38a35b8 100644 --- a/Source/BackspaceDetectingTextField.swift +++ b/Source/BackspaceDetectingTextField.swift @@ -13,7 +13,7 @@ protocol BackspaceDetectingTextFieldDelegate: UITextFieldDelegate { func textFieldDidDeleteBackwards(_ textField: UITextField) } -class BackspaceDetectingTextField: UITextField { +open class BackspaceDetectingTextField: UITextField { var onDeleteBackwards: (() -> Void)? @@ -21,11 +21,11 @@ class BackspaceDetectingTextField: UITextField { super.init(frame: CGRect.zero) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func deleteBackward() { + override open func deleteBackward() { onDeleteBackwards?() // Call super afterwards. The `text` property will return text prior to the delete. super.deleteBackward() diff --git a/Source/WSTagView.swift b/Source/WSTagView.swift index cbd86ba..68ff484 100755 --- a/Source/WSTagView.swift +++ b/Source/WSTagView.swift @@ -9,7 +9,7 @@ import UIKit open class WSTagView: UIView { - fileprivate let textLabel = UILabel() + let textLabel = UILabel() open var displayText: String = "" { didSet { @@ -78,15 +78,20 @@ open class WSTagView: UIView { open var selected: Bool = false { didSet { - if selected && !isFirstResponder { - _ = becomeFirstResponder() - } else - if !selected && isFirstResponder { - _ = resignFirstResponder() + if !allowsMultipleSelection { + if selected && !isFirstResponder { + _ = becomeFirstResponder() + } else + if !selected && isFirstResponder { + _ = resignFirstResponder() + } } updateContent(animated: true) } } + + open var allowsMultipleSelection: Bool = false + open var removable: Bool = true public init(tag: WSTag) { super.init(frame: CGRect.zero) @@ -106,6 +111,7 @@ open class WSTagView: UIView { self.displayText = tag.text updateLabelText() + textLabel.translatesAutoresizingMaskIntoConstraints = false let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognizer)) addGestureRecognizer(tapRecognizer) @@ -208,7 +214,7 @@ open class WSTagView: UIView { // MARK: - Gesture Recognizers @objc func handleTapGestureRecognizer(_ sender: UITapGestureRecognizer) { - if selected { + if selected && !allowsMultipleSelection { return } onDidRequestSelection?(self) diff --git a/Source/WSTagsField.swift b/Source/WSTagsField.swift index 5dc9ca7..b780ab6 100755 --- a/Source/WSTagsField.swift +++ b/Source/WSTagsField.swift @@ -191,8 +191,16 @@ open class WSTagsField: UIScrollView { return false } + open var allowsMultipleSelection: Bool = false { + didSet { + tagViews.forEach { $0.allowsMultipleSelection = allowsMultipleSelection } + } + } + + open var autoSelectTagWhenAdded: Bool = false + open fileprivate(set) var tags = [WSTag]() - internal var tagViews = [WSTagView]() + open var tagViews = [WSTagView]() // MARK: - Events @@ -317,15 +325,21 @@ open class WSTagsField: UIScrollView { open func beginEditing() { self.textField.becomeFirstResponder() - self.unselectAllTagViewsAnimated(false) + if !allowsMultipleSelection { + self.unselectAllTagViewsAnimated(false) + } } open func endEditing() { - // NOTE: We used to check if .isFirstResponder and then resign first responder, but sometimes we noticed - // that it would be the first responder, but still return isFirstResponder=NO. + // NOTE: We used to check if .isFirstResponder and then resign first responder, but sometimes we noticed + // that it would be the first responder, but still return isFirstResponder=NO. // So always attempt to resign without checking. self.textField.resignFirstResponder() } + + open func addInputAccessoryView(view: UIView) { + textField.inputAccessoryView = view + } // MARK: - Adding / Removing Tags open func addTags(_ tags: [String]) { @@ -362,15 +376,16 @@ open class WSTagsField: UIScrollView { tagView.borderColor = self.borderColor tagView.keyboardAppearanceType = self.keyboardAppearance tagView.layoutMargins = self.layoutMargins + tagView.allowsMultipleSelection = allowsMultipleSelection tagView.onDidRequestSelection = { [weak self] tagView in - self?.selectTagView(tagView, animated: true) + self?.toggleTagView(tagView, animated: true) } tagView.onDidRequestDelete = { [weak self] tagView, replacementText in // First, refocus the text field self?.textField.becomeFirstResponder() - if (replacementText?.isEmpty ?? false) == false { + if replacementText?.isEmpty == false { self?.textField.text = replacementText } // Then remove the view from our data @@ -401,6 +416,15 @@ open class WSTagsField: UIScrollView { repositionViews() } + open func setRemovable(tags: [String], removable: Bool = false) { + assert(tagViews.count > 0, "There are no tagViews. Did you call this method after adding tags?") + tagViews.filter { tags.contains($0.textLabel.text ?? "") }.forEach { $0.removable = removable } + } + + open func getSelectedTagStrings() -> [String] { + return tagViews.filter { $0.selected }.compactMap { $0.textLabel.text } + } + open func removeTag(_ tag: String) { removeTag(WSTag(tag)) } @@ -415,6 +439,10 @@ open class WSTagsField: UIScrollView { if index < 0 || index >= self.tags.count { return } let tagView = self.tagViews[index] + if !tagView.removable { + return + } + tagView.removeFromSuperview() self.tagViews.remove(at: index) @@ -435,7 +463,18 @@ open class WSTagsField: UIScrollView { let text = self.textField.text?.trimmingCharacters(in: CharacterSet.whitespaces) ?? "" if text.isEmpty == false && (onVerifyTag?(self, text) ?? true) { let tag = WSTag(text) + if self.tags.contains(tag) { + self.textField.text = "" + return nil + } + addTag(tag) + if let tagView = tagViews.last, autoSelectTagWhenAdded { + //There's a bug that causes the text to be truncated during animation ("New York" becomes "New Y..."). This delays animating the TagView until after it's set in the TextField. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.toggleTagView(tagView, animated: false) + } + } self.textField.text = "" onTextFieldDidChange(self.textField) @@ -477,23 +516,25 @@ open class WSTagsField: UIScrollView { } } - open func selectTagView(_ tagView: WSTagView, animated: Bool = false) { + open func toggleTagView(_ tagView: WSTagView, animated: Bool = false) { if self.readOnly { return } - - if tagView.selected { - tagView.onDidRequestDelete?(tagView, nil) - return + + tagView.selected = !tagView.selected + + if !allowsMultipleSelection { + tagViews.filter { $0 != tagView }.forEach { + $0.selected = false + onDidUnselectTagView?(self, $0) + } } - - tagView.selected = true - tagViews.filter { $0 != tagView }.forEach { - $0.selected = false - onDidUnselectTagView?(self, $0) + + if tagView.selected { + onDidSelectTagView?(self, tagView) + } else { + onDidUnselectTagView?(self, tagView) } - - onDidSelectTagView?(self, tagView) } open func unselectAllTagViewsAnimated(_ animated: Bool = false) { @@ -592,10 +633,15 @@ extension WSTagsField { } textField.onDeleteBackwards = { [weak self] in - if self?.readOnly ?? true { return } + if self?.readOnly == true { return } + + if self?.allowsMultipleSelection == true, self?.textField.text?.isEmpty == true, let lastTag = self?.tags.last { + self?.removeTag(lastTag) + return + } - if self?.textField.text?.isEmpty ?? true, let tagView = self?.tagViews.last { - self?.selectTagView(tagView, animated: true) + if self?.textField.text?.isEmpty == true, let tagView = self?.tagViews.last { + self?.toggleTagView(tagView, animated: true) self?.textField.resignFirstResponder() } } @@ -709,7 +755,7 @@ extension WSTagsField { oldIntrinsicContentHeight = newIntrinsicContentHeight } - if self.enableScrolling { + if self.enableScrolling { self.isScrollEnabled = contentRect.height + contentInset.top + contentInset.bottom >= newIntrinsicContentHeight } self.contentSize.width = self.bounds.width - contentInset.left - contentInset.right @@ -746,7 +792,9 @@ extension WSTagsField: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { textDelegate?.textFieldDidBeginEditing?(textField) - unselectAllTagViewsAnimated(true) + if !allowsMultipleSelection { + unselectAllTagViewsAnimated(true) + } } public func textFieldDidEndEditing(_ textField: UITextField) {