Skip to content

Commit bb0b2cf

Browse files
authored
iOS 26: AdaptiveTabBar Improvements (#24799)
2 parents 4ea2ac8 + b847b80 commit bb0b2cf

File tree

6 files changed

+81
-38
lines changed

6 files changed

+81
-38
lines changed

Modules/Sources/WordPressUI/Views/AdaptiveTabBar.swift

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import UIKit
22
import WordPressShared
33

4-
public protocol AdaptiveTabBarItem {
4+
public protocol AdaptiveTabBarItem: Identifiable {
55
var localizedTitle: String { get }
66
}
77

@@ -38,7 +38,7 @@ public class AdaptiveTabBar: UIControl {
3838

3939
// MARK: - Properties
4040

41-
var items: [AdaptiveTabBarItem] = [] {
41+
var items: [any AdaptiveTabBarItem] = [] {
4242
didSet { refreshTabs() }
4343
}
4444

@@ -51,9 +51,12 @@ public class AdaptiveTabBar: UIControl {
5151
}
5252
}
5353

54+
public var preferredFont = UIFont.preferredFont(forTextStyle: .body)
55+
5456
private var widthConstraint: NSLayoutConstraint!
5557
private var indicatorWidthConstraint: NSLayoutConstraint?
5658
private var indicatorCenterXConstraint: NSLayoutConstraint?
59+
private var previousWidth: CGFloat?
5760

5861
public let tabBarHeight: CGFloat = 44
5962

@@ -83,7 +86,7 @@ public class AdaptiveTabBar: UIControl {
8386

8487
widthConstraint = stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
8588

86-
let separator = SeparatorView.horizontal()
89+
let separator = SeparatorView.horizontal(height: separatorHeight)
8790
addSubview(separator)
8891
separator.pinEdges([.horizontal, .bottom])
8992

@@ -94,10 +97,17 @@ public class AdaptiveTabBar: UIControl {
9497
])
9598
}
9699

100+
private var separatorHeight: CGFloat {
101+
if #available(iOS 26, *) { 1 } else { 0.33 }
102+
}
103+
97104
public override func layoutSubviews() {
98105
super.layoutSubviews()
99106

100-
updateDistribution()
107+
if previousWidth != bounds.width {
108+
previousWidth = bounds.width
109+
updateDistribution()
110+
}
101111
}
102112

103113
// MARK: - Tab Management
@@ -116,10 +126,16 @@ public class AdaptiveTabBar: UIControl {
116126

117127
private func createTab(at index: Int) -> UIButton {
118128
let item = items[index]
129+
let font = preferredFont
119130

120131
var config = UIButton.Configuration.plain()
121132
config.title = item.localizedTitle
122-
config.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)
133+
config.contentInsets = NSDirectionalEdgeInsets(
134+
top: 8,
135+
leading: index == 0 ? 20 : 6,
136+
bottom: 8,
137+
trailing: 6
138+
)
123139

124140
let button = UIButton(configuration: config, primaryAction: .init { [weak self] _ in
125141
self?.tabButtonTapped(at: index)
@@ -133,8 +149,7 @@ public class AdaptiveTabBar: UIControl {
133149
config.baseForegroundColor = isSelected ? .label : .secondaryLabel
134150
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
135151
var outgoing = incoming
136-
outgoing.font = UIFont.preferredFont(forTextStyle: .headline)
137-
.withWeight(isSelected ? .medium : .regular)
152+
outgoing.font = font.withWeight(isSelected ? .medium : .regular)
138153
return outgoing
139154
}
140155
button.configuration = config
@@ -143,18 +158,28 @@ public class AdaptiveTabBar: UIControl {
143158
button.accessibilityIdentifier = "\(item)"
144159
button.maximumContentSizeCategory = .extraLarge
145160

161+
button.titleLabel?.numberOfLines = 1
162+
163+
button.isSelected = true
164+
let width = button.systemLayoutSizeFitting(CGSize(width: UIView.noIntrinsicMetric, height: tabBarHeight)).width
165+
button.widthAnchor.constraint(greaterThanOrEqualToConstant: width + 2).isActive = true // just in case add more space
166+
button.isSelected = false
167+
146168
return button
147169
}
148170

149171
private func updateDistribution() {
150172
guard !buttons.isEmpty else { return }
151173

152-
let totalPreferredWidth = buttons.reduce(0) { total, tab in
153-
total + tab.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: tabBarHeight)).width
154-
}
174+
let maxWidth = buttons.map {
175+
$0.systemLayoutSizeFitting(CGSize(width: UIView.noIntrinsicMetric, height: tabBarHeight)).width
176+
}.max() ?? 0
177+
178+
let totalPreferredWidth = maxWidth * CGFloat(buttons.count)
155179

156180
// If the items don't fit, enable scrolling
157-
let shouldFillWidth = totalPreferredWidth <= bounds.width
181+
// Adding 2 just in case if there is some rounding error somewhere
182+
let shouldFillWidth = (totalPreferredWidth + 2) <= safeAreaLayoutGuide.layoutFrame.width
158183
if shouldFillWidth {
159184
stackView.distribution = .fillEqually
160185
widthConstraint.isActive = true
@@ -224,6 +249,13 @@ public class AdaptiveTabBar: UIControl {
224249

225250
let selectedTab = buttons[selectedIndex]
226251
let tabFrame = scrollView.convert(selectedTab.frame, from: stackView)
252+
let visibleRect = scrollView.bounds
253+
254+
// Only scroll if the button is not fully visible
255+
guard !visibleRect.contains(tabFrame) else {
256+
return
257+
}
258+
227259
let targetRect = CGRect(
228260
x: max(0, tabFrame.midX - bounds.width / 2),
229261
y: 0,
@@ -234,7 +266,7 @@ public class AdaptiveTabBar: UIControl {
234266
scrollView.scrollRectToVisible(targetRect, animated: animated)
235267
}
236268

237-
var currentlySelectedItem: AdaptiveTabBarItem? {
269+
var currentlySelectedItem: (any AdaptiveTabBarItem)? {
238270
return items[safe: selectedIndex]
239271
}
240272
}

Modules/Sources/WordPressUI/Views/AdaptiveTabBarController.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
import UIKit
22

33
@MainActor
4-
public final class AdaptiveTabBarController<Item: AdaptiveTabBarItem> where Item: Equatable {
4+
public final class AdaptiveTabBarController<Item: AdaptiveTabBarItem> {
55
private(set) public var items: [Item] = []
66

77
public var selection: Item? {
88
didSet {
9-
guard oldValue != selection else { return }
10-
if let index = items.firstIndex(where: { $0 == selection }) {
9+
guard oldValue?.id != selection?.id else { return }
10+
if let index = selectionIndex {
1111
if filterBar.selectedIndex != index {
1212
filterBar.setSelectedIndex(index)
1313
}
14-
if segmentedControl.selectedSegmentIndex != index {
14+
if segmentedControl.selectedSegmentIndex != index, segmentedControl.numberOfSegments >= index {
1515
segmentedControl.selectedSegmentIndex = index
1616
}
1717
}
1818
}
1919
}
2020

21+
public var selectionIndex: Int? {
22+
items.firstIndex(where: { $0.id == selection?.id })
23+
}
24+
2125
public var accessibilityIdentifier: String? {
2226
didSet {
2327
filterBar.accessibilityIdentifier = accessibilityIdentifier
2428
segmentedControl.accessibilityIdentifier = accessibilityIdentifier
2529
}
2630
}
2731

28-
private let filterBar = AdaptiveTabBar()
32+
public let filterBar = AdaptiveTabBar()
2933
private var filterBarContainer = UIView()
3034

31-
private let segmentedControl = UISegmentedControl()
35+
public let segmentedControl = UISegmentedControl()
3236

3337
private weak var viewController: UIViewController?
3438

Modules/Sources/WordPressUI/Views/SeparatorView.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import UIKit
22

33
public final class SeparatorView: UIView {
4-
public static func horizontal() -> SeparatorView {
4+
public static func horizontal(height: CGFloat = 0.33) -> SeparatorView {
55
let view = SeparatorView()
6-
view.heightAnchor.constraint(equalToConstant: 0.33).isActive = true
6+
view.heightAnchor.constraint(equalToConstant: height).isActive = true
77
return view
88
}
99

10-
public static func vertical() -> SeparatorView {
10+
public static func vertical(width: CGFloat = 0.33) -> SeparatorView {
1111
let view = SeparatorView()
12-
view.widthAnchor.constraint(equalToConstant: 0.33).isActive = true
12+
view.widthAnchor.constraint(equalToConstant: width).isActive = true
1313
return view
1414
}
1515

WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ class AbstractPostListViewController: UIViewController,
6666

6767
lazy var noResultsViewController = NoResultsViewController.controller()
6868

69-
lazy var filterSettings: PostListFilterSettings = {
70-
return PostListFilterSettings(blog: self.blog, postType: self.postTypeToSync())
71-
}()
69+
lazy var filterSettings = PostListFilterSettings(blog: blog, postType: postTypeToSync())
7270

7371
let filterTabBar = FilterTabBar()
7472

@@ -100,7 +98,6 @@ class AbstractPostListViewController: UIViewController,
10098
configureFetchResultsController()
10199
configureTableView()
102100
configureFilterBar()
103-
configureTableView()
104101
configureSearchController()
105102
configureAuthorFilter()
106103

@@ -156,7 +153,7 @@ class AbstractPostListViewController: UIViewController,
156153
func configureTableView() {
157154
view.addSubview(tableView)
158155
tableView.translatesAutoresizingMaskIntoConstraints = false
159-
view.pinSubviewToAllEdges(tableView)
156+
tableView.pinEdges()
160157

161158
tableView.dataSource = self
162159
tableView.delegate = self
@@ -452,6 +449,14 @@ class AbstractPostListViewController: UIViewController,
452449
}
453450
}
454451

452+
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
453+
return section == 0 ? 1 : 0
454+
}
455+
456+
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
457+
return section == 0 ? UIView() : nil
458+
}
459+
455460
// MARK: - Actions
456461

457462
@objc private func refresh(_ sender: AnyObject) {
@@ -485,7 +490,7 @@ class AbstractPostListViewController: UIViewController,
485490
refreshResults()
486491
}
487492

488-
@objc func updateFilter(_ filter: PostListFilter, withSyncedPosts posts: [AbstractPost], hasMore: Bool) {
493+
func updateFilter(_ filter: PostListFilter, withSyncedPosts posts: [AbstractPost], hasMore: Bool) {
489494
guard posts.count > 0 else {
490495
wpAssertionFailure("This method should not be called with no posts.")
491496
return

WordPress/Classes/ViewRelated/Post/Utils/PostListFilterSettings.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import WordPressShared
44

55
/// `PostListFilterSettings` manages settings for filtering posts (by author or status)
66
/// - Note: previously found within `AbstractPostListViewController`
7-
class PostListFilterSettings: NSObject {
7+
class PostListFilterSettings {
88
fileprivate static let currentPostAuthorFilterKey = "CurrentPostAuthorFilterKey"
99
fileprivate static let currentPageListStatusFilterKey = "CurrentPageListStatusFilterKey"
1010

11-
@objc let blog: Blog
12-
@objc let postType: PostServiceType
11+
let blog: Blog
12+
let postType: PostServiceType
1313
fileprivate var allPostListFilters: [PostListFilter]?
1414

1515
enum AuthorFilter: UInt {
@@ -19,12 +19,12 @@ class PostListFilterSettings: NSObject {
1919
/// Initializes a new PostListFilterSettings instance
2020
/// - Parameter blog: the blog which owns the list of posts
2121
/// - Parameter postType: the type of post being listed
22-
@objc init(blog: Blog, postType: PostServiceType) {
22+
init(blog: Blog, postType: PostServiceType) {
2323
self.blog = blog
2424
self.postType = postType
2525
}
2626

27-
@objc func availablePostListFilters() -> [PostListFilter] {
27+
func availablePostListFilters() -> [PostListFilter] {
2828

2929
if allPostListFilters == nil {
3030
allPostListFilters = PostListFilter.postListFilters()
@@ -72,11 +72,11 @@ class PostListFilterSettings: NSObject {
7272
// MARK: - Current filter
7373

7474
/// - returns: the last active PostListFilter
75-
@objc func currentPostListFilter() -> PostListFilter {
75+
func currentPostListFilter() -> PostListFilter {
7676
return availablePostListFilters()[currentFilterIndex()]
7777
}
7878

79-
@objc func keyForCurrentListStatusFilter() -> String {
79+
func keyForCurrentListStatusFilter() -> String {
8080
switch postType {
8181
case .page:
8282
return type(of: self).currentPageListStatusFilterKey
@@ -88,7 +88,7 @@ class PostListFilterSettings: NSObject {
8888
}
8989

9090
/// currentPostListFilter: returns the index of the last active PostListFilter
91-
@objc func currentFilterIndex() -> Int {
91+
func currentFilterIndex() -> Int {
9292

9393
let userDefaults = UserPersistentStoreFactory.instance()
9494

@@ -101,7 +101,7 @@ class PostListFilterSettings: NSObject {
101101
}
102102

103103
/// setCurrentFilterIndex: stores the index of the last active PostListFilter
104-
@objc func setCurrentFilterIndex(_ newIndex: Int) {
104+
func setCurrentFilterIndex(_ newIndex: Int) {
105105
let index = self.currentFilterIndex()
106106

107107
guard newIndex != index else {
@@ -156,7 +156,7 @@ class PostListFilterSettings: NSObject {
156156

157157
// MARK: - Analytics
158158

159-
@objc func propertiesForAnalytics() -> [String: AnyObject] {
159+
func propertiesForAnalytics() -> [String: AnyObject] {
160160
var properties = [String: AnyObject]()
161161

162162
properties["type"] = postType.rawValue as AnyObject?

WordPress/Classes/ViewRelated/Stats/SiteStatsDashboardViewController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ enum StatsTabType: Int, AdaptiveTabBarItem, CaseIterable {
1212
case traffic
1313
case subscribers
1414

15+
var id: StatsTabType { self }
16+
1517
var localizedTitle: String {
1618
switch self {
1719
case .insights: return NSLocalizedString("Insights", comment: "Title of Insights stats filter.")

0 commit comments

Comments
 (0)