@@ -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