88
99import UIKit
1010
11- /// A toolbar intended to be used as an inputAccessoryView when traversing through a collection of text fields.
11+ /// An input accessory view used when traversing through a collection of text fields.
1212/// This view contains 3 bar button items: a `previous`, a `next`, and a `done` bar button item.
13- open class TextFieldsTraversalAccessoryView : UIToolbar {
13+ open class TextFieldsTraversalAccessoryView : UIView {
1414
1515 // MARK: - Orientation
1616
@@ -27,9 +27,29 @@ open class TextFieldsTraversalAccessoryView: UIToolbar {
2727
2828 // MARK: - Items
2929
30- public let previousItem = UIBarButtonItem ( )
31- public let nextItem = UIBarButtonItem ( )
32- public let doneItem = UIBarButtonItem ( barButtonSystemItem: . done, target: nil , action: nil )
30+ public let previousItem : UIBarButtonItem = TextFieldsTraversalBarButtonItem ( )
31+ public let nextItem : UIBarButtonItem = TextFieldsTraversalBarButtonItem ( )
32+ public let doneItem : UIBarButtonItem = TextFieldsTraversalBarButtonItem ( )
33+
34+ // MARK: - Views
35+
36+ private let backgroundView : UIVisualEffectView = {
37+ let view = UIVisualEffectView ( effect: UIBlurEffect ( style: . systemMaterial) )
38+ view. clipsToBounds = true
39+ return view
40+ } ( )
41+
42+ private let previousButton = UIButton ( type: . system)
43+ private let nextButton = UIButton ( type: . system)
44+ private let doneButton = UIButton ( type: . system)
45+
46+ private enum Layout {
47+ static let accessoryHeight : CGFloat = 72
48+ static let barInsets = UIEdgeInsets ( top: 8 , left: 16 , bottom: 8 , right: 16 )
49+ static let barContentInset : CGFloat = 18
50+ static let buttonSize = CGSize ( width: 44 , height: 44 )
51+ static let buttonSpacing : CGFloat = 12
52+ }
3353
3454 // MARK: - Lifecycle
3555
@@ -46,74 +66,136 @@ open class TextFieldsTraversalAccessoryView: UIToolbar {
4666 // MARK: - Configuration
4767
4868 private func configure( ) {
49- let flexible = UIBarButtonItem ( barButtonSystemItem: . flexibleSpace, target: nil , action: nil )
50- let fixed = UIBarButtonItem ( barButtonSystemItem: . fixedSpace, target: nil , action: nil )
51- fixed. width = 20
52- setItems ( [ previousItem, fixed, nextItem, flexible, doneItem] , animated: false )
69+ backgroundColor = . clear
70+ addSubview ( backgroundView)
71+ backgroundView. contentView. addSubview ( previousButton)
72+ backgroundView. contentView. addSubview ( nextButton)
73+ backgroundView. contentView. addSubview ( doneButton)
74+
75+ [ previousButton, nextButton, doneButton] . forEach ( configure)
76+ previousButton. addTarget ( self , action: #selector( tappedPrevious) , for: . touchUpInside)
77+ nextButton. addTarget ( self , action: #selector( tappedNext) , for: . touchUpInside)
78+ doneButton. addTarget ( self , action: #selector( tappedDone) , for: . touchUpInside)
79+
80+ bind ( previousItem, to: previousButton)
81+ bind ( nextItem, to: nextButton)
82+ bind ( doneItem, to: doneButton)
5383 applyOrientation ( )
84+ doneItem. image = image ( forSystemName: " checkmark " , pointSize: 26 , weight: . medium)
85+ }
86+
87+ private func configure( _ button: UIButton ) {
88+ button. tintColor = . label
89+ }
90+
91+ private func bind( _ item: UIBarButtonItem , to button: UIButton ) {
92+ guard let item = item as? TextFieldsTraversalBarButtonItem else {
93+ return
94+ }
95+
96+ item. onImageChanged = { [ weak button] image in
97+ button? . setImage ( image, for: . normal)
98+ }
99+ item. onEnabledChanged = { [ weak button] isEnabled in
100+ button? . isEnabled = isEnabled
101+ }
102+ button. setImage ( item. image, for: . normal)
103+ button. isEnabled = item. isEnabled
54104 }
55105
56106 private func applyOrientation( ) {
57107 switch orientation {
58108 case . horizontal:
59- previousItem. image = image ( forDirection : . left)
60- nextItem. image = image ( forDirection : . right)
109+ previousItem. image = image ( forSystemName : " chevron .left" , pointSize : 26 , weight : . medium )
110+ nextItem. image = image ( forSystemName : " chevron .right" , pointSize : 26 , weight : . medium )
61111 case . vertical:
62- previousItem. image = image ( forDirection : . up)
63- nextItem. image = image ( forDirection : . down)
112+ previousItem. image = image ( forSystemName : " chevron .up" , pointSize : 26 , weight : . medium )
113+ nextItem. image = image ( forSystemName : " chevron .down" , pointSize : 26 , weight : . medium )
64114 }
65115 }
116+
117+ open override var intrinsicContentSize : CGSize {
118+ CGSize ( width: UIView . noIntrinsicMetric, height: Layout . accessoryHeight)
119+ }
120+
121+ open override func sizeThatFits( _ size: CGSize ) -> CGSize {
122+ CGSize ( width: size. width, height: intrinsicContentSize. height)
123+ }
124+
125+ open override func layoutSubviews( ) {
126+ super. layoutSubviews ( )
127+
128+ backgroundView. frame = bounds. inset ( by: Layout . barInsets)
129+ backgroundView. layer. cornerRadius = backgroundView. bounds. height / 2
130+
131+ let contentBounds = backgroundView. contentView. bounds. insetBy ( dx: Layout . barContentInset, dy: 0 )
132+ let buttonY = contentBounds. midY - Layout. buttonSize. height / 2
133+ previousButton. frame = CGRect (
134+ x: contentBounds. minX,
135+ y: buttonY,
136+ width: Layout . buttonSize. width,
137+ height: Layout . buttonSize. height
138+ )
139+ nextButton. frame = CGRect (
140+ x: previousButton. frame. maxX + Layout. buttonSpacing,
141+ y: buttonY,
142+ width: Layout . buttonSize. width,
143+ height: Layout . buttonSize. height
144+ )
145+ doneButton. frame = CGRect (
146+ x: contentBounds. maxX - Layout. buttonSize. width,
147+ y: buttonY,
148+ width: Layout . buttonSize. width,
149+ height: Layout . buttonSize. height
150+ )
151+ }
66152
67153 // MARK: - Image
68154
69- private func image( forDirection direction: Direction ) -> UIImage {
70- let upArrowImage = imageForUpArrow ( )
71- guard let cgImage = upArrowImage. cgImage else {
72- return upArrowImage
155+ private func image( forSystemName systemName: String , pointSize: CGFloat , weight: UIImage . SymbolWeight ) -> UIImage ? {
156+ let configuration = UIImage . SymbolConfiguration ( pointSize: pointSize, weight: weight)
157+ return UIImage ( systemName: systemName, withConfiguration: configuration)
158+ }
159+
160+ // MARK: Actions
161+
162+ @objc private func tappedPrevious( ) {
163+ sendAction ( for: previousItem)
164+ }
165+
166+ @objc private func tappedNext( ) {
167+ sendAction ( for: nextItem)
168+ }
169+
170+ @objc private func tappedDone( ) {
171+ sendAction ( for: doneItem)
172+ }
173+
174+ private func sendAction( for item: UIBarButtonItem ) {
175+ guard item. isEnabled,
176+ let target = item. target,
177+ let action = item. action else {
178+ return
73179 }
74180
75- return UIImage (
76- cgImage: cgImage,
77- scale: upArrowImage. scale,
78- orientation: direction. imageOrientation
79- ) . withRenderingMode ( . alwaysTemplate)
181+ UIApplication . shared. sendAction ( action, to: target, from: item, for: nil )
80182 }
81-
82- private func imageForUpArrow( ) -> UIImage {
83- let lineWidth : CGFloat = 1.5
84- let ratio : CGFloat = 5.0 / 9.0
85- let dimension : CGFloat = 20
86- let size = CGSize ( width: dimension, height: dimension * ratio)
87- let bounds = CGRect ( origin: . zero, size: size) . insetBy ( dx: lineWidth / 2 , dy: lineWidth / 2 )
88-
89- let path = UIBezierPath ( )
90- path. lineWidth = lineWidth
91- path. lineCapStyle = . round
92- path. move ( to: CGPoint ( x: bounds. minX, y: bounds. maxY) )
93- path. addLine ( to: CGPoint ( x: bounds. midX, y: bounds. minY) )
94- path. addLine ( to: CGPoint ( x: bounds. maxX, y: bounds. maxY) )
95-
96- let renderer = UIGraphicsImageRenderer ( size: size)
97- return renderer. image { _ in
98- UIColor . black. setStroke ( )
99- path. stroke ( )
183+ }
184+
185+ private final class TextFieldsTraversalBarButtonItem : UIBarButtonItem {
186+
187+ var onImageChanged : ( ( UIImage ? ) -> Void ) ?
188+ var onEnabledChanged : ( ( Bool ) -> Void ) ?
189+
190+ override var image : UIImage ? {
191+ didSet {
192+ onImageChanged ? ( image)
100193 }
101194 }
102-
103- // MARK: Direction
104-
105- private enum Direction {
106- case left
107- case right
108- case up
109- case down
110- var imageOrientation : UIImage . Orientation {
111- switch self {
112- case . up: return . up
113- case . down: return . down
114- case . left: return . left
115- case . right: return . right
116- }
195+
196+ override var isEnabled : Bool {
197+ didSet {
198+ onEnabledChanged ? ( isEnabled)
117199 }
118200 }
119201}
0 commit comments