Skip to content

Commit 08a5b43

Browse files
committed
Improve AdaptiveTabBar layout
1 parent b040dbe commit 08a5b43

1 file changed

Lines changed: 113 additions & 40 deletions

File tree

Modules/Sources/WordPressUI/Views/AdaptiveTabBar.swift

Lines changed: 113 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class AdaptiveTabBar: UIControl {
4242
didSet { refreshTabs() }
4343
}
4444

45-
private var buttons: [UIButton] = []
45+
private var buttons: [TabButton] = []
4646

4747
private(set) var selectedIndex: Int = 0 {
4848
didSet {
@@ -95,6 +95,10 @@ public class AdaptiveTabBar: UIControl {
9595
selectionIndicator.heightAnchor.constraint(equalToConstant: 2),
9696
selectionIndicator.bottomAnchor.constraint(equalTo: bottomAnchor)
9797
])
98+
99+
// Accessibility
100+
shouldGroupAccessibilityChildren = true
101+
accessibilityContainerType = .semanticGroup
98102
}
99103

100104
private var separatorHeight: CGFloat {
@@ -124,52 +128,32 @@ public class AdaptiveTabBar: UIControl {
124128
setNeedsLayout()
125129
}
126130

127-
private func createTab(at index: Int) -> UIButton {
131+
private func createTab(at index: Int) -> TabButton {
128132
let item = items[index]
129-
let font = preferredFont
130133
let isFirstItem = index == 0
131134
let isLastItem = index == items.count - 1
132135

133-
var config = UIButton.Configuration.plain()
134-
config.title = item.localizedTitle
135-
config.contentInsets = NSDirectionalEdgeInsets(
136+
let button = TabButton()
137+
button.title = item.localizedTitle
138+
button.font = preferredFont
139+
button.contentInsets = NSDirectionalEdgeInsets(
136140
top: 8,
137-
leading: isFirstItem ? 20 : 10,
141+
leading: isFirstItem ? 20 : 12,
138142
bottom: 8,
139-
trailing: isLastItem ? 20 : 10
143+
trailing: isLastItem ? 20 : 12
140144
)
141-
142-
let button = UIButton(configuration: config, primaryAction: .init { [weak self] _ in
143-
self?.tabButtonTapped(at: index)
144-
})
145-
146-
button.configurationUpdateHandler = { button in
147-
let isSelected = button.state.contains(.selected)
148-
149-
var config = button.configuration ?? .plain()
150-
config.baseBackgroundColor = .clear
151-
config.baseForegroundColor = isSelected ? .label : .secondaryLabel
152-
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
153-
var outgoing = incoming
154-
outgoing.font = font.withWeight(isSelected ? .medium : .regular)
155-
return outgoing
156-
}
157-
button.configuration = config
158-
}
159-
160145
button.accessibilityIdentifier = "\(item)"
161-
button.maximumContentSizeCategory = .extraLarge
162-
button.titleLabel?.numberOfLines = 1
163-
164-
// Measure button width when selected to prevent size changes
165-
button.isSelected = true
166-
let width = button.systemLayoutSizeFitting(CGSize(width: UIView.noIntrinsicMetric, height: tabBarHeight)).width
167-
button.widthAnchor.constraint(greaterThanOrEqualToConstant: width + 2).isActive = true
168-
button.isSelected = false
146+
button.addTarget(self, action: #selector(tabButtonTapped(_:)), for: .touchUpInside)
169147

170148
return button
171149
}
172150

151+
@objc private func tabButtonTapped(_ sender: TabButton) {
152+
guard let index = buttons.firstIndex(of: sender) else { return }
153+
setSelectedIndex(index)
154+
sendActions(for: .valueChanged)
155+
}
156+
173157
private func updateDistribution() {
174158
guard !buttons.isEmpty else { return }
175159

@@ -204,11 +188,6 @@ public class AdaptiveTabBar: UIControl {
204188

205189
// MARK: - Selection
206190

207-
private func tabButtonTapped(at index: Int) {
208-
setSelectedIndex(index)
209-
sendActions(for: .valueChanged)
210-
}
211-
212191
func setSelectedIndex(_ index: Int, animated: Bool = true) {
213192
guard items.indices.contains(index) else { return }
214193

@@ -282,6 +261,100 @@ public class AdaptiveTabBar: UIControl {
282261
}
283262
}
284263

264+
// MARK: - TabButton
265+
266+
private class TabButton: UIControl {
267+
private let label = UILabel()
268+
269+
var title: String = "" {
270+
didSet {
271+
label.text = title
272+
accessibilityLabel = title
273+
invalidateIntrinsicContentSize()
274+
}
275+
}
276+
277+
var font: UIFont = .preferredFont(forTextStyle: .body) {
278+
didSet {
279+
updateAppearance()
280+
invalidateIntrinsicContentSize()
281+
}
282+
}
283+
284+
var contentInsets: NSDirectionalEdgeInsets = .zero {
285+
didSet {
286+
invalidateIntrinsicContentSize()
287+
}
288+
}
289+
290+
override var isSelected: Bool {
291+
didSet {
292+
updateAppearance()
293+
updateAccessibility()
294+
}
295+
}
296+
297+
override init(frame: CGRect) {
298+
super.init(frame: frame)
299+
setup()
300+
}
301+
302+
required init?(coder: NSCoder) {
303+
super.init(coder: coder)
304+
setup()
305+
}
306+
307+
private func setup() {
308+
addSubview(label)
309+
label.textAlignment = .center
310+
label.numberOfLines = 1
311+
label.adjustsFontForContentSizeCategory = true
312+
label.maximumContentSizeCategory = .extraLarge
313+
label.isAccessibilityElement = false
314+
315+
isAccessibilityElement = true
316+
accessibilityTraits = .button
317+
318+
updateAppearance()
319+
updateAccessibility()
320+
}
321+
322+
private func updateAppearance() {
323+
label.font = font.withWeight(isSelected ? .medium : .regular)
324+
label.textColor = isSelected ? .label : .secondaryLabel
325+
}
326+
327+
private func updateAccessibility() {
328+
if isSelected {
329+
accessibilityTraits = [.button, .selected]
330+
} else {
331+
accessibilityTraits = .button
332+
}
333+
}
334+
335+
override func layoutSubviews() {
336+
super.layoutSubviews()
337+
label.frame = bounds.inset(by: UIEdgeInsets(
338+
top: contentInsets.top,
339+
left: contentInsets.leading,
340+
bottom: contentInsets.bottom,
341+
right: contentInsets.trailing
342+
))
343+
}
344+
345+
override var intrinsicContentSize: CGSize {
346+
// Always calculate based on medium weight (selected state)
347+
let mediumFont = font.withWeight(.medium)
348+
let size = title.size(withAttributes: [.font: mediumFont])
349+
350+
// Add small padding to prevent clipping due to rounding
351+
return CGSize(
352+
width: ceil(size.width) + contentInsets.leading + contentInsets.trailing + 2,
353+
height: ceil(size.height) + contentInsets.top + contentInsets.bottom
354+
)
355+
}
356+
}
357+
285358
// MARK: - Preview
286359

287360
#if DEBUG

0 commit comments

Comments
 (0)