Skip to content

Commit 83cfd68

Browse files
authored
Bookings: Add basic logic for booking list (#16174)
2 parents 061ac26 + 2ebef2f commit 83cfd68

File tree

5 files changed

+488
-6
lines changed

5 files changed

+488
-6
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import SwiftUI
2+
3+
struct BookingListView: View {
4+
// periphery:ignore
5+
@ObservedObject private var viewModel: BookingListViewModel
6+
7+
init(viewModel: BookingListViewModel) {
8+
self.viewModel = viewModel
9+
}
10+
11+
var body: some View {
12+
Text("Hello, World!")
13+
}
14+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// periphery:ignore:all
2+
import Foundation
3+
import Yosemite
4+
import protocol Storage.StorageManagerType
5+
6+
/// View model for `BookingListView`
7+
final class BookingListViewModel: ObservableObject {
8+
9+
@Published private(set) var bookings: [Booking] = []
10+
11+
private let siteID: Int64
12+
private let stores: StoresManager
13+
private let storage: StorageManagerType
14+
15+
/// Keeps track of the current state of the syncing
16+
@Published private(set) var syncState: SyncState = .empty
17+
18+
/// Tracks if the infinite scroll indicator should be displayed.
19+
@Published private(set) var shouldShowBottomActivityIndicator = false
20+
21+
/// Supports infinite scroll.
22+
private let paginationTracker: PaginationTracker
23+
private let pageFirstIndex: Int = PaginationTracker.Defaults.pageFirstIndex
24+
25+
/// Booking ResultsController.
26+
private lazy var resultsController: ResultsController<StorageBooking> = {
27+
let predicate = NSPredicate(format: "siteID == %lld", siteID)
28+
let sortDescriptorByDate = NSSortDescriptor(key: "dateCreated", ascending: false)
29+
let resultsController = ResultsController<StorageBooking>(storageManager: storage,
30+
matching: predicate,
31+
sortedBy: [sortDescriptorByDate])
32+
return resultsController
33+
}()
34+
35+
init(siteID: Int64,
36+
stores: StoresManager = ServiceLocator.stores,
37+
storage: StorageManagerType = ServiceLocator.storageManager) {
38+
self.siteID = siteID
39+
self.stores = stores
40+
self.storage = storage
41+
self.paginationTracker = PaginationTracker(pageFirstIndex: pageFirstIndex)
42+
43+
configureResultsController()
44+
configurePaginationTracker()
45+
}
46+
47+
/// Called when loading the first page of bookings.
48+
func loadBookings() {
49+
paginationTracker.syncFirstPage()
50+
}
51+
52+
/// Called when the next page should be loaded.
53+
func onLoadNextPageAction() {
54+
paginationTracker.ensureNextPageIsSynced()
55+
}
56+
57+
/// Called when the user pulls down the list to refresh.
58+
/// - Parameter completion: called when the refresh completes.
59+
func onRefreshAction(completion: @escaping () -> Void) {
60+
paginationTracker.resync(reason: nil) {
61+
completion()
62+
}
63+
}
64+
}
65+
66+
// MARK: Configuration
67+
68+
private extension BookingListViewModel {
69+
func configurePaginationTracker() {
70+
paginationTracker.delegate = self
71+
}
72+
73+
/// Performs initial fetch from storage and updates results.
74+
func configureResultsController() {
75+
resultsController.onDidChangeContent = { [weak self] in
76+
self?.updateResults()
77+
}
78+
resultsController.onDidResetContent = { [weak self] in
79+
self?.updateResults()
80+
}
81+
do {
82+
try resultsController.performFetch()
83+
updateResults()
84+
} catch {
85+
ServiceLocator.crashLogging.logError(error)
86+
}
87+
}
88+
89+
/// Updates row view models and sync state.
90+
func updateResults() {
91+
bookings = resultsController.fetchedObjects
92+
transitionToResultsUpdatedState()
93+
}
94+
}
95+
96+
extension BookingListViewModel: PaginationTrackerDelegate {
97+
func sync(pageNumber: Int, pageSize: Int, reason: String?, onCompletion: SyncCompletion?) {
98+
transitionToSyncingState()
99+
let action = BookingAction.synchronizeBookings(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize) { [weak self] result in
100+
switch result {
101+
case .success(let hasNextPage):
102+
onCompletion?(.success(hasNextPage))
103+
104+
case .failure(let error):
105+
DDLogError("⛔️ Error synchronizing bookings: \(error)")
106+
onCompletion?(.failure(error))
107+
}
108+
109+
self?.updateResults()
110+
}
111+
stores.dispatch(action)
112+
}
113+
}
114+
115+
// MARK: State Machine
116+
117+
extension BookingListViewModel {
118+
/// Represents possible states for syncing bookings.
119+
enum SyncState: Equatable {
120+
case syncingFirstPage
121+
case results
122+
case empty
123+
}
124+
125+
/// Update states for sync from remote.
126+
func transitionToSyncingState() {
127+
shouldShowBottomActivityIndicator = true
128+
if bookings.isEmpty {
129+
syncState = .syncingFirstPage
130+
}
131+
}
132+
133+
/// Update states after sync is complete.
134+
func transitionToResultsUpdatedState() {
135+
shouldShowBottomActivityIndicator = false
136+
syncState = bookings.isNotEmpty ? .results : .empty
137+
}
138+
}

WooCommerce/Classes/Bookings/BookingsTabView.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import SwiftUI
33
/// Hosting view for `BookingsTabView`
44
///
55
final class BookingsTabViewHostingController: UIHostingController<BookingsTabView> {
6-
// periphery: ignore
6+
77
init(siteID: Int64) {
8-
super.init(rootView: BookingsTabView())
8+
super.init(rootView: BookingsTabView(siteID: siteID))
99
configureTabBarItem()
1010
}
1111

@@ -17,16 +17,19 @@ final class BookingsTabViewHostingController: UIHostingController<BookingsTabVie
1717
return true
1818
}
1919

20-
// periphery: ignore
2120
func didSwitchStore(id: Int64) {
22-
// TODO: update view
21+
rootView = BookingsTabView(siteID: id)
2322
}
2423
}
2524

2625
private extension BookingsTabViewHostingController {
2726
func configureTabBarItem() {
2827
tabBarItem.image = UIImage(systemName: "calendar")
29-
tabBarItem.title = "Bookings"
28+
tabBarItem.title = NSLocalizedString(
29+
"bookingsTabViewHostingController.tab.title",
30+
value: "Bookings",
31+
comment: "Title of the Bookings tab"
32+
)
3033
tabBarItem.accessibilityIdentifier = "tab-bar-bookings-item"
3134
}
3235
}
@@ -36,9 +39,15 @@ private extension BookingsTabViewHostingController {
3639
struct BookingsTabView: View {
3740
@State private var visibility: NavigationSplitViewVisibility = .all
3841

42+
private let siteID: Int64
43+
44+
init(siteID: Int64) {
45+
self.siteID = siteID
46+
}
47+
3948
var body: some View {
4049
NavigationSplitView(columnVisibility: $visibility) {
41-
Text("Booking List")
50+
BookingListView(viewModel: BookingListViewModel(siteID: siteID))
4251
} detail: {
4352
Text("Booking Detail Screen")
4453
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2846,6 +2846,7 @@
28462846
DED039292BC7A04B005D0571 /* StorePerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED039282BC7A04B005D0571 /* StorePerformanceView.swift */; };
28472847
DED0392B2BC7A076005D0571 /* StorePerformanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED0392A2BC7A076005D0571 /* StorePerformanceViewModel.swift */; };
28482848
DED0392D2BC7DAFD005D0571 /* StatsTimeRangePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED0392C2BC7DAFD005D0571 /* StatsTimeRangePicker.swift */; };
2849+
DED1E3172E8556270089909C /* BookingListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED1E3152E8556270089909C /* BookingListViewModelTests.swift */; };
28492850
DED91DFA2AD78A3A00CDCC53 /* BlazeCampaignItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED91DF92AD78A3A00CDCC53 /* BlazeCampaignItemView.swift */; };
28502851
DED9740B2AD7D09E00122EB4 /* BlazeCampaignListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED9740A2AD7D09E00122EB4 /* BlazeCampaignListView.swift */; };
28512852
DED9740D2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED9740C2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift */; };
@@ -6078,6 +6079,7 @@
60786079
DED039282BC7A04B005D0571 /* StorePerformanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePerformanceView.swift; sourceTree = "<group>"; };
60796080
DED0392A2BC7A076005D0571 /* StorePerformanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePerformanceViewModel.swift; sourceTree = "<group>"; };
60806081
DED0392C2BC7DAFD005D0571 /* StatsTimeRangePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangePicker.swift; sourceTree = "<group>"; };
6082+
DED1E3152E8556270089909C /* BookingListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingListViewModelTests.swift; sourceTree = "<group>"; };
60816083
DED91DF92AD78A3A00CDCC53 /* BlazeCampaignItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignItemView.swift; sourceTree = "<group>"; };
60826084
DED9740A2AD7D09E00122EB4 /* BlazeCampaignListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListView.swift; sourceTree = "<group>"; };
60836085
DED9740C2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListViewModel.swift; sourceTree = "<group>"; };
@@ -13029,6 +13031,7 @@
1302913031
D816DDBA22265D8000903E59 /* ViewRelated */ = {
1303013032
isa = PBXGroup;
1303113033
children = (
13034+
DED1E3162E8556270089909C /* Bookings */,
1303213035
2D7A3E212E7891C400C46401 /* CIAB */,
1303313036
864059FE2C6F67A000DA04DC /* Custom Fields */,
1303413037
86023FAB2B16D80E00A28F07 /* Themes */,
@@ -13758,6 +13761,14 @@
1375813761
path = StoreStats;
1375913762
sourceTree = "<group>";
1376013763
};
13764+
DED1E3162E8556270089909C /* Bookings */ = {
13765+
isa = PBXGroup;
13766+
children = (
13767+
DED1E3152E8556270089909C /* BookingListViewModelTests.swift */,
13768+
);
13769+
path = Bookings;
13770+
sourceTree = "<group>";
13771+
};
1376113772
DED91DF72AD78A0C00CDCC53 /* Blaze */ = {
1376213773
isa = PBXGroup;
1376313774
children = (
@@ -17279,6 +17290,7 @@
1727917290
DE78DE442B2846AF002E58DE /* ThemesCarouselViewModelTests.swift in Sources */,
1728017291
DE4D23B029B1D02A003A4B5D /* WPCom2FALoginViewModelTests.swift in Sources */,
1728117292
03B9E52F2A150EED005C77F5 /* MockCardReaderSupportDeterminer.swift in Sources */,
17293+
DED1E3172E8556270089909C /* BookingListViewModelTests.swift in Sources */,
1728217294
D8C11A6022E2479800D4A88D /* OrderPaymentDetailsViewModelTests.swift in Sources */,
1728317295
B622BC74289CF19400B10CEC /* WaitingTimeTrackerTests.swift in Sources */,
1728417296
023EC2E224DA8BAB0021DA91 /* MockProductSKUValidationStoresManager.swift in Sources */,

0 commit comments

Comments
 (0)