Skip to content

Commit 1d619ef

Browse files
committed
Update accessory bar appearance
1 parent 4f6d3e5 commit 1d619ef

10 files changed

Lines changed: 147 additions & 56 deletions

Classes/TextFieldsTraversalAccessoryView.swift

Lines changed: 138 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
import 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
}

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ textFieldsTraversalController.accessoryView.orientation = .vertical
3535

3636
![image](https://github.com/danielinoa/TextFieldsTraversalController/blob/master/Screenshots/vertical.png)
3737

38+
The arrow buttons are disabled automatically at the first and last enabled text fields.
39+
40+
<img src="https://github.com/danielinoa/TextFieldsTraversalController/blob/master/Screenshots/previous-disabled-full.png" height="640"></a>
41+
<img src="https://github.com/danielinoa/TextFieldsTraversalController/blob/master/Screenshots/next-disabled-full.png" height="640"></a>
42+
43+
![previous disabled](https://github.com/danielinoa/TextFieldsTraversalController/blob/master/Screenshots/previous-disabled.png)
44+
45+
![next disabled](https://github.com/danielinoa/TextFieldsTraversalController/blob/master/Screenshots/next-disabled.png)
46+
3847

3948
## Requirements
4049

Screenshots/SS1.png

160 KB
Loading

Screenshots/SS2.png

160 KB
Loading

Screenshots/horizontal.png

38.3 KB
Loading

Screenshots/next-disabled-full.png

253 KB
Loading

Screenshots/next-disabled.png

48 KB
Loading
253 KB
Loading

Screenshots/previous-disabled.png

48 KB
Loading

Screenshots/vertical.png

38.4 KB
Loading

0 commit comments

Comments
 (0)