Skip to content

Commit e830619

Browse files
authored
Bookings: Update tab handling for booking list (#16188)
2 parents 6c8c1e0 + a95afcc commit e830619

File tree

6 files changed

+262
-177
lines changed

6 files changed

+262
-177
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import SwiftUI
2+
import struct Yosemite.Booking
3+
4+
struct BookingListContainerView: View {
5+
@ObservedObject private var viewModel: BookingListContainerViewModel
6+
7+
init(viewModel: BookingListContainerViewModel) {
8+
self.viewModel = viewModel
9+
}
10+
11+
var body: some View {
12+
NavigationStack {
13+
VStack(spacing: 0) {
14+
headerView
15+
TabView(selection: $viewModel.selectedTab) {
16+
ForEach(BookingListTab.allCases, id: \.rawValue) { tab in
17+
BookingListView(viewModel: viewModel.listViewModel(for: tab))
18+
.tag(tab)
19+
}
20+
}
21+
.tabViewStyle(.page(indexDisplayMode: .never))
22+
}
23+
.navigationTitle(Localization.viewTitle)
24+
.toolbar {
25+
ToolbarItem(placement: .confirmationAction) {
26+
Button {
27+
// TODO
28+
} label: {
29+
Image(systemName: "magnifyingglass")
30+
}
31+
}
32+
}
33+
}
34+
}
35+
}
36+
37+
private extension BookingListContainerView {
38+
var headerView: some View {
39+
VStack(spacing: 0) {
40+
topTabView
41+
Divider()
42+
HStack {
43+
Button {
44+
// TODO
45+
} label: {
46+
Text(Localization.sortBy)
47+
.font(.body)
48+
.foregroundStyle(Color.accentColor)
49+
}
50+
Spacer()
51+
Button {
52+
// TODO
53+
} label: {
54+
Text(Localization.filter)
55+
.font(.body)
56+
.foregroundStyle(Color.accentColor)
57+
}
58+
}
59+
.padding()
60+
.background(Color(.listForeground(modal: false)))
61+
Divider()
62+
}
63+
}
64+
65+
var topTabView: some View {
66+
GeometryReader { geometry in
67+
HStack {
68+
ForEach(BookingListTab.allCases, id: \.rawValue) { tab in
69+
Button {
70+
withAnimation(.easeInOut(duration: 0.3)) {
71+
viewModel.selectedTab = tab
72+
}
73+
} label: {
74+
Text(tab.title)
75+
.font(.subheadline)
76+
.foregroundStyle(viewModel.selectedTab == tab ? Color.accentColor : Color.primary)
77+
}
78+
.frame(maxWidth: .infinity)
79+
.padding(.vertical, 12)
80+
}
81+
}
82+
.overlay(alignment: .bottom) {
83+
Color.accentColor
84+
.frame(width: geometry.size.width / CGFloat(BookingListTab.allCases.count),
85+
height: Layout.selectedTabIndicatorHeight)
86+
.offset(x: tabIndicatorOffset(containerWidth: geometry.size.width,
87+
tabCount: BookingListTab.allCases.count,
88+
selectedIndex: viewModel.selectedTab.rawValue))
89+
.animation(.easeInOut(duration: 0.3), value: viewModel.selectedTab.rawValue)
90+
}
91+
}
92+
.frame(height: Layout.topTabBarHeight)
93+
.background(Color(.listForeground(modal: false)))
94+
}
95+
96+
/// SwiftUI's coordinate system places (0,0) at the center of the container, so we need to:
97+
/// 1. Calculate how far the selected tab is from the left edge
98+
/// 2. Adjust for the center-based coordinate system
99+
/// 3. Center the indicator within the selected tab
100+
///
101+
func tabIndicatorOffset(containerWidth: CGFloat, tabCount: Int, selectedIndex: Int) -> CGFloat {
102+
let tabWidth = containerWidth / CGFloat(tabCount)
103+
let distanceFromLeftEdge = tabWidth * CGFloat(selectedIndex)
104+
let adjustmentForCenterOrigin = containerWidth / 2
105+
let centerWithinTab = tabWidth / 2
106+
107+
return distanceFromLeftEdge - adjustmentForCenterOrigin + centerWithinTab
108+
}
109+
}
110+
private extension BookingListContainerView {
111+
enum Layout {
112+
static let topTabBarHeight: CGFloat = 44
113+
static let selectedTabIndicatorHeight: CGFloat = 3.0
114+
}
115+
116+
enum Localization {
117+
static let viewTitle = NSLocalizedString(
118+
"bookingListView.view.title",
119+
value: "Bookings",
120+
comment: "Title of the booking list view"
121+
)
122+
static let sortBy = NSLocalizedString(
123+
"bookingListView.sortBy",
124+
value: "Sort by",
125+
comment: "Button to select the order of the booking list"
126+
)
127+
static let filter = NSLocalizedString(
128+
"bookingListView.filter",
129+
value: "Filter",
130+
comment: "Button to filter the booking list"
131+
)
132+
}
133+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Foundation
2+
3+
/// View model for `BookingListContainerView`
4+
final class BookingListContainerViewModel: ObservableObject {
5+
private let todayListViewModel: BookingListViewModel
6+
private let upcomingListViewModel: BookingListViewModel
7+
private let allListViewModel: BookingListViewModel
8+
9+
@Published var selectedTab: BookingListTab = .today
10+
11+
init(siteID: Int64) {
12+
self.todayListViewModel = BookingListViewModel(siteID: siteID, type: .today)
13+
self.upcomingListViewModel = BookingListViewModel(siteID: siteID, type: .upcoming)
14+
self.allListViewModel = BookingListViewModel(siteID: siteID, type: .all)
15+
}
16+
17+
func listViewModel(for tab: BookingListTab) -> BookingListViewModel {
18+
switch tab {
19+
case .today:
20+
todayListViewModel
21+
case .upcoming:
22+
upcomingListViewModel
23+
case .all:
24+
allListViewModel
25+
}
26+
}
27+
}
28+
29+
enum BookingListTab: Int, CaseIterable {
30+
case today
31+
case upcoming
32+
case all
33+
34+
var title: String {
35+
switch self {
36+
case .today: Localization.today
37+
case .upcoming: Localization.upcoming
38+
case .all: Localization.all
39+
}
40+
}
41+
42+
private enum Localization {
43+
static let today = NSLocalizedString(
44+
"bookingListView.today",
45+
value: "Today",
46+
comment: "Tab title for today's bookings"
47+
)
48+
static let upcoming = NSLocalizedString(
49+
"bookingListView.upcoming",
50+
value: "Upcoming",
51+
comment: "Tab title for upcoming bookings"
52+
)
53+
static let all = NSLocalizedString(
54+
"bookingListView.all",
55+
value: "All",
56+
comment: "Tab title for all bookings"
57+
)
58+
}
59+
}

0 commit comments

Comments
 (0)