Skip to content

Commit 72a33ef

Browse files
committed
Add toggle for multiple select and removable tags
1 parent 71c6534 commit 72a33ef

File tree

3 files changed

+87
-33
lines changed

3 files changed

+87
-33
lines changed

Source/BackspaceDetectingTextField.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ protocol BackspaceDetectingTextFieldDelegate: UITextFieldDelegate {
1313
func textFieldDidDeleteBackwards(_ textField: UITextField)
1414
}
1515

16-
class BackspaceDetectingTextField: UITextField {
16+
open class BackspaceDetectingTextField: UITextField {
1717

1818
var onDeleteBackwards: (() -> Void)?
1919

2020
init() {
2121
super.init(frame: CGRect.zero)
2222
}
2323

24-
required init?(coder aDecoder: NSCoder) {
24+
required public init?(coder aDecoder: NSCoder) {
2525
fatalError("init(coder:) has not been implemented")
2626
}
2727

28-
override func deleteBackward() {
28+
override open func deleteBackward() {
2929
onDeleteBackwards?()
3030
// Call super afterwards. The `text` property will return text prior to the delete.
3131
super.deleteBackward()

Source/WSTagView.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import UIKit
1010

1111
open class WSTagView: UIView {
12-
fileprivate let textLabel = UILabel()
12+
let textLabel = UILabel()
1313

1414
open var displayText: String = "" {
1515
didSet {
@@ -78,15 +78,20 @@ open class WSTagView: UIView {
7878

7979
open var selected: Bool = false {
8080
didSet {
81-
if selected && !isFirstResponder {
82-
_ = becomeFirstResponder()
83-
} else
84-
if !selected && isFirstResponder {
85-
_ = resignFirstResponder()
81+
if !allowsMultipleSelection {
82+
if selected && !isFirstResponder {
83+
_ = becomeFirstResponder()
84+
} else
85+
if !selected && isFirstResponder {
86+
_ = resignFirstResponder()
87+
}
8688
}
8789
updateContent(animated: true)
8890
}
8991
}
92+
93+
open var allowsMultipleSelection: Bool = false
94+
open var removable: Bool = true
9095

9196
public init(tag: WSTag) {
9297
super.init(frame: CGRect.zero)
@@ -106,6 +111,7 @@ open class WSTagView: UIView {
106111

107112
self.displayText = tag.text
108113
updateLabelText()
114+
textLabel.translatesAutoresizingMaskIntoConstraints = false
109115

110116
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognizer))
111117
addGestureRecognizer(tapRecognizer)
@@ -208,7 +214,7 @@ open class WSTagView: UIView {
208214

209215
// MARK: - Gesture Recognizers
210216
@objc func handleTapGestureRecognizer(_ sender: UITapGestureRecognizer) {
211-
if selected {
217+
if selected && !allowsMultipleSelection {
212218
return
213219
}
214220
onDidRequestSelection?(self)

Source/WSTagsField.swift

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,16 @@ open class WSTagsField: UIScrollView {
191191
return false
192192
}
193193

194+
open var allowsMultipleSelection: Bool = false {
195+
didSet {
196+
tagViews.forEach { $0.allowsMultipleSelection = allowsMultipleSelection }
197+
}
198+
}
199+
200+
open var autoSelectTagWhenAdded: Bool = false
201+
194202
open fileprivate(set) var tags = [WSTag]()
195-
internal var tagViews = [WSTagView]()
203+
open var tagViews = [WSTagView]()
196204

197205
// MARK: - Events
198206

@@ -317,15 +325,21 @@ open class WSTagsField: UIScrollView {
317325

318326
open func beginEditing() {
319327
self.textField.becomeFirstResponder()
320-
self.unselectAllTagViewsAnimated(false)
328+
if !allowsMultipleSelection {
329+
self.unselectAllTagViewsAnimated(false)
330+
}
321331
}
322332

323333
open func endEditing() {
324-
// NOTE: We used to check if .isFirstResponder and then resign first responder, but sometimes we noticed
325-
// that it would be the first responder, but still return isFirstResponder=NO.
334+
// NOTE: We used to check if .isFirstResponder and then resign first responder, but sometimes we noticed
335+
// that it would be the first responder, but still return isFirstResponder=NO.
326336
// So always attempt to resign without checking.
327337
self.textField.resignFirstResponder()
328338
}
339+
340+
open func addInputAccessoryView(view: UIView) {
341+
textField.inputAccessoryView = view
342+
}
329343

330344
// MARK: - Adding / Removing Tags
331345
open func addTags(_ tags: [String]) {
@@ -362,15 +376,16 @@ open class WSTagsField: UIScrollView {
362376
tagView.borderColor = self.borderColor
363377
tagView.keyboardAppearanceType = self.keyboardAppearance
364378
tagView.layoutMargins = self.layoutMargins
379+
tagView.allowsMultipleSelection = allowsMultipleSelection
365380

366381
tagView.onDidRequestSelection = { [weak self] tagView in
367-
self?.selectTagView(tagView, animated: true)
382+
self?.toggleTagView(tagView, animated: true)
368383
}
369384

370385
tagView.onDidRequestDelete = { [weak self] tagView, replacementText in
371386
// First, refocus the text field
372387
self?.textField.becomeFirstResponder()
373-
if (replacementText?.isEmpty ?? false) == false {
388+
if replacementText?.isEmpty == false {
374389
self?.textField.text = replacementText
375390
}
376391
// Then remove the view from our data
@@ -401,6 +416,15 @@ open class WSTagsField: UIScrollView {
401416
repositionViews()
402417
}
403418

419+
open func setRemovable(tags: [String], removable: Bool = false) {
420+
assert(tagViews.count > 0, "There are no tagViews. Did you call this method after adding tags?")
421+
tagViews.filter { tags.contains($0.textLabel.text ?? "") }.forEach { $0.removable = removable }
422+
}
423+
424+
open func getSelectedTagStrings() -> [String] {
425+
return tagViews.filter { $0.selected }.compactMap { $0.textLabel.text }
426+
}
427+
404428
open func removeTag(_ tag: String) {
405429
removeTag(WSTag(tag))
406430
}
@@ -415,6 +439,10 @@ open class WSTagsField: UIScrollView {
415439
if index < 0 || index >= self.tags.count { return }
416440

417441
let tagView = self.tagViews[index]
442+
if !tagView.removable {
443+
return
444+
}
445+
418446
tagView.removeFromSuperview()
419447
self.tagViews.remove(at: index)
420448

@@ -435,7 +463,18 @@ open class WSTagsField: UIScrollView {
435463
let text = self.textField.text?.trimmingCharacters(in: CharacterSet.whitespaces) ?? ""
436464
if text.isEmpty == false && (onVerifyTag?(self, text) ?? true) {
437465
let tag = WSTag(text)
466+
if self.tags.contains(tag) {
467+
self.textField.text = ""
468+
return nil
469+
}
470+
438471
addTag(tag)
472+
if let tagView = tagViews.last, autoSelectTagWhenAdded {
473+
//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.
474+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
475+
self.toggleTagView(tagView, animated: false)
476+
}
477+
}
439478

440479
self.textField.text = ""
441480
onTextFieldDidChange(self.textField)
@@ -477,23 +516,25 @@ open class WSTagsField: UIScrollView {
477516
}
478517
}
479518

480-
open func selectTagView(_ tagView: WSTagView, animated: Bool = false) {
519+
open func toggleTagView(_ tagView: WSTagView, animated: Bool = false) {
481520
if self.readOnly {
482521
return
483522
}
484-
485-
if tagView.selected {
486-
tagView.onDidRequestDelete?(tagView, nil)
487-
return
523+
524+
tagView.selected = !tagView.selected
525+
526+
if !allowsMultipleSelection {
527+
tagViews.filter { $0 != tagView }.forEach {
528+
$0.selected = false
529+
onDidUnselectTagView?(self, $0)
530+
}
488531
}
489-
490-
tagView.selected = true
491-
tagViews.filter { $0 != tagView }.forEach {
492-
$0.selected = false
493-
onDidUnselectTagView?(self, $0)
532+
533+
if tagView.selected {
534+
onDidSelectTagView?(self, tagView)
535+
} else {
536+
onDidUnselectTagView?(self, tagView)
494537
}
495-
496-
onDidSelectTagView?(self, tagView)
497538
}
498539

499540
open func unselectAllTagViewsAnimated(_ animated: Bool = false) {
@@ -592,10 +633,15 @@ extension WSTagsField {
592633
}
593634

594635
textField.onDeleteBackwards = { [weak self] in
595-
if self?.readOnly ?? true { return }
636+
if self?.readOnly == true { return }
637+
638+
if self?.allowsMultipleSelection == true, self?.textField.text?.isEmpty == true, let lastTag = self?.tags.last {
639+
self?.removeTag(lastTag)
640+
return
641+
}
596642

597-
if self?.textField.text?.isEmpty ?? true, let tagView = self?.tagViews.last {
598-
self?.selectTagView(tagView, animated: true)
643+
if self?.textField.text?.isEmpty == true, let tagView = self?.tagViews.last {
644+
self?.toggleTagView(tagView, animated: true)
599645
self?.textField.resignFirstResponder()
600646
}
601647
}
@@ -709,7 +755,7 @@ extension WSTagsField {
709755
oldIntrinsicContentHeight = newIntrinsicContentHeight
710756
}
711757

712-
if self.enableScrolling {
758+
if self.enableScrolling {
713759
self.isScrollEnabled = contentRect.height + contentInset.top + contentInset.bottom >= newIntrinsicContentHeight
714760
}
715761
self.contentSize.width = self.bounds.width - contentInset.left - contentInset.right
@@ -746,7 +792,9 @@ extension WSTagsField: UITextFieldDelegate {
746792

747793
public func textFieldDidBeginEditing(_ textField: UITextField) {
748794
textDelegate?.textFieldDidBeginEditing?(textField)
749-
unselectAllTagViewsAnimated(true)
795+
if !allowsMultipleSelection {
796+
unselectAllTagViewsAnimated(true)
797+
}
750798
}
751799

752800
public func textFieldDidEndEditing(_ textField: UITextField) {

0 commit comments

Comments
 (0)