Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import SwiftUI
import struct Yosemite.Booking

struct BookingListContainerView: View {
@ObservedObject private var viewModel: BookingListContainerViewModel

init(viewModel: BookingListContainerViewModel) {
self.viewModel = viewModel
}

var body: some View {
NavigationStack {
VStack(spacing: 0) {
headerView
TabView(selection: $viewModel.selectedTab) {
ForEach(BookingListTab.allCases, id: \.rawValue) { tab in
BookingListView(viewModel: viewModel.listViewModel(for: tab))
.tag(tab)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.navigationTitle(Localization.viewTitle)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
// TODO
} label: {
Image(systemName: "magnifyingglass")
}
}
}
}
}
}

private extension BookingListContainerView {
var headerView: some View {
VStack(spacing: 0) {
topTabView
Divider()
HStack {
Button {
// TODO
} label: {
Text(Localization.sortBy)
.font(.body)
.foregroundStyle(Color.accentColor)
}
Spacer()
Button {
// TODO
} label: {
Text(Localization.filter)
.font(.body)
.foregroundStyle(Color.accentColor)
}
}
.padding()
.background(Color(.listForeground(modal: false)))
Divider()
}
}

var topTabView: some View {
GeometryReader { geometry in
HStack {
ForEach(BookingListTab.allCases, id: \.rawValue) { tab in
Button {
withAnimation(.easeInOut(duration: 0.3)) {
viewModel.selectedTab = tab
}
} label: {
Text(tab.title)
.font(.subheadline)
.foregroundStyle(viewModel.selectedTab == tab ? Color.accentColor : Color.primary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
}
.overlay(alignment: .bottom) {
Color.accentColor
.frame(width: geometry.size.width / CGFloat(BookingListTab.allCases.count),
height: Layout.selectedTabIndicatorHeight)
.offset(x: tabIndicatorOffset(containerWidth: geometry.size.width,
tabCount: BookingListTab.allCases.count,
selectedIndex: viewModel.selectedTab.rawValue))
.animation(.easeInOut(duration: 0.3), value: viewModel.selectedTab.rawValue)
}
}
.frame(height: Layout.topTabBarHeight)
.background(Color(.listForeground(modal: false)))
}

/// SwiftUI's coordinate system places (0,0) at the center of the container, so we need to:
/// 1. Calculate how far the selected tab is from the left edge
/// 2. Adjust for the center-based coordinate system
/// 3. Center the indicator within the selected tab
///
func tabIndicatorOffset(containerWidth: CGFloat, tabCount: Int, selectedIndex: Int) -> CGFloat {
let tabWidth = containerWidth / CGFloat(tabCount)
let distanceFromLeftEdge = tabWidth * CGFloat(selectedIndex)
let adjustmentForCenterOrigin = containerWidth / 2
let centerWithinTab = tabWidth / 2

return distanceFromLeftEdge - adjustmentForCenterOrigin + centerWithinTab
}
}
private extension BookingListContainerView {
enum Layout {
static let topTabBarHeight: CGFloat = 44
static let selectedTabIndicatorHeight: CGFloat = 3.0
}

enum Localization {
static let viewTitle = NSLocalizedString(
"bookingListView.view.title",
value: "Bookings",
comment: "Title of the booking list view"
)
static let sortBy = NSLocalizedString(
"bookingListView.sortBy",
value: "Sort by",
comment: "Button to select the order of the booking list"
)
static let filter = NSLocalizedString(
"bookingListView.filter",
value: "Filter",
comment: "Button to filter the booking list"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation

/// View model for `BookingListContainerView`
final class BookingListContainerViewModel: ObservableObject {
private let todayListViewModel: BookingListViewModel
private let upcomingListViewModel: BookingListViewModel
private let allListViewModel: BookingListViewModel

@Published var selectedTab: BookingListTab = .today

init(siteID: Int64) {
self.todayListViewModel = BookingListViewModel(siteID: siteID, type: .today)
self.upcomingListViewModel = BookingListViewModel(siteID: siteID, type: .upcoming)
self.allListViewModel = BookingListViewModel(siteID: siteID, type: .all)
}

func listViewModel(for tab: BookingListTab) -> BookingListViewModel {
switch tab {
case .today:
todayListViewModel
case .upcoming:
upcomingListViewModel
case .all:
allListViewModel
}
}
}

enum BookingListTab: Int, CaseIterable {
case today
case upcoming
case all

var title: String {
switch self {
case .today: Localization.today
case .upcoming: Localization.upcoming
case .all: Localization.all
}
}

private enum Localization {
static let today = NSLocalizedString(
"bookingListView.today",
value: "Today",
comment: "Tab title for today's bookings"
)
static let upcoming = NSLocalizedString(
"bookingListView.upcoming",
value: "Upcoming",
comment: "Tab title for upcoming bookings"
)
static let all = NSLocalizedString(
"bookingListView.all",
value: "All",
comment: "Tab title for all bookings"
)
}
}
Loading