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
14 changes: 14 additions & 0 deletions WooCommerce/Classes/Bookings/BookingList/BookingListView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import SwiftUI

struct BookingListView: View {
// periphery:ignore
@ObservedObject private var viewModel: BookingListViewModel

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

var body: some View {
Text("Hello, World!")
}
}
138 changes: 138 additions & 0 deletions WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// periphery:ignore:all
import Foundation
import Yosemite
import protocol Storage.StorageManagerType

/// View model for `BookingListView`
final class BookingListViewModel: ObservableObject {

@Published private(set) var bookings: [Booking] = []

private let siteID: Int64
private let stores: StoresManager
private let storage: StorageManagerType

/// Keeps track of the current state of the syncing
@Published private(set) var syncState: SyncState = .empty

/// Tracks if the infinite scroll indicator should be displayed.
@Published private(set) var shouldShowBottomActivityIndicator = false

/// Supports infinite scroll.
private let paginationTracker: PaginationTracker
private let pageFirstIndex: Int = PaginationTracker.Defaults.pageFirstIndex

/// Booking ResultsController.
private lazy var resultsController: ResultsController<StorageBooking> = {
let predicate = NSPredicate(format: "siteID == %lld", siteID)
let sortDescriptorByDate = NSSortDescriptor(key: "dateCreated", ascending: false)
let resultsController = ResultsController<StorageBooking>(storageManager: storage,
matching: predicate,
sortedBy: [sortDescriptorByDate])
return resultsController
}()

init(siteID: Int64,
stores: StoresManager = ServiceLocator.stores,
storage: StorageManagerType = ServiceLocator.storageManager) {
self.siteID = siteID
self.stores = stores
self.storage = storage
self.paginationTracker = PaginationTracker(pageFirstIndex: pageFirstIndex)

configureResultsController()
configurePaginationTracker()
}

/// Called when loading the first page of bookings.
func loadBookings() {
paginationTracker.syncFirstPage()
}

/// Called when the next page should be loaded.
func onLoadNextPageAction() {
paginationTracker.ensureNextPageIsSynced()
}

/// Called when the user pulls down the list to refresh.
/// - Parameter completion: called when the refresh completes.
func onRefreshAction(completion: @escaping () -> Void) {
paginationTracker.resync(reason: nil) {
completion()
}
}
}

// MARK: Configuration

private extension BookingListViewModel {
func configurePaginationTracker() {
paginationTracker.delegate = self
}

/// Performs initial fetch from storage and updates results.
func configureResultsController() {
resultsController.onDidChangeContent = { [weak self] in
self?.updateResults()
}
resultsController.onDidResetContent = { [weak self] in
self?.updateResults()
}
do {
try resultsController.performFetch()
updateResults()
} catch {
ServiceLocator.crashLogging.logError(error)
}
}

/// Updates row view models and sync state.
func updateResults() {
bookings = resultsController.fetchedObjects
transitionToResultsUpdatedState()
}
}

extension BookingListViewModel: PaginationTrackerDelegate {
func sync(pageNumber: Int, pageSize: Int, reason: String?, onCompletion: SyncCompletion?) {
transitionToSyncingState()
let action = BookingAction.synchronizeBookings(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize) { [weak self] result in
switch result {
case .success(let hasNextPage):
onCompletion?(.success(hasNextPage))

case .failure(let error):
DDLogError("⛔️ Error synchronizing bookings: \(error)")
onCompletion?(.failure(error))
}

self?.updateResults()
}
stores.dispatch(action)
}
}

// MARK: State Machine

extension BookingListViewModel {
/// Represents possible states for syncing bookings.
enum SyncState: Equatable {
case syncingFirstPage
case results
case empty
}

/// Update states for sync from remote.
func transitionToSyncingState() {
shouldShowBottomActivityIndicator = true
if bookings.isEmpty {
syncState = .syncingFirstPage
}
}

/// Update states after sync is complete.
func transitionToResultsUpdatedState() {
shouldShowBottomActivityIndicator = false
syncState = bookings.isNotEmpty ? .results : .empty
}
}
21 changes: 15 additions & 6 deletions WooCommerce/Classes/Bookings/BookingsTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import SwiftUI
/// Hosting view for `BookingsTabView`
///
final class BookingsTabViewHostingController: UIHostingController<BookingsTabView> {
// periphery: ignore

init(siteID: Int64) {
super.init(rootView: BookingsTabView())
super.init(rootView: BookingsTabView(siteID: siteID))
configureTabBarItem()
}

Expand All @@ -17,16 +17,19 @@ final class BookingsTabViewHostingController: UIHostingController<BookingsTabVie
return true
}

// periphery: ignore
func didSwitchStore(id: Int64) {
// TODO: update view
rootView = BookingsTabView(siteID: id)
}
}

private extension BookingsTabViewHostingController {
func configureTabBarItem() {
tabBarItem.image = UIImage(systemName: "calendar")
tabBarItem.title = "Bookings"
tabBarItem.title = NSLocalizedString(
"bookingsTabViewHostingController.tab.title",
value: "Bookings",
comment: "Title of the Bookings tab"
)
tabBarItem.accessibilityIdentifier = "tab-bar-bookings-item"
}
}
Expand All @@ -36,9 +39,15 @@ private extension BookingsTabViewHostingController {
struct BookingsTabView: View {
@State private var visibility: NavigationSplitViewVisibility = .all

private let siteID: Int64

init(siteID: Int64) {
self.siteID = siteID
}

var body: some View {
NavigationSplitView(columnVisibility: $visibility) {
Text("Booking List")
BookingListView(viewModel: BookingListViewModel(siteID: siteID))
} detail: {
Text("Booking Detail Screen")
}
Expand Down
12 changes: 12 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2838,6 +2838,7 @@
DED039292BC7A04B005D0571 /* StorePerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED039282BC7A04B005D0571 /* StorePerformanceView.swift */; };
DED0392B2BC7A076005D0571 /* StorePerformanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED0392A2BC7A076005D0571 /* StorePerformanceViewModel.swift */; };
DED0392D2BC7DAFD005D0571 /* StatsTimeRangePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED0392C2BC7DAFD005D0571 /* StatsTimeRangePicker.swift */; };
DED1E3172E8556270089909C /* BookingListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED1E3152E8556270089909C /* BookingListViewModelTests.swift */; };
DED91DFA2AD78A3A00CDCC53 /* BlazeCampaignItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED91DF92AD78A3A00CDCC53 /* BlazeCampaignItemView.swift */; };
DED9740B2AD7D09E00122EB4 /* BlazeCampaignListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED9740A2AD7D09E00122EB4 /* BlazeCampaignListView.swift */; };
DED9740D2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED9740C2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift */; };
Expand Down Expand Up @@ -6062,6 +6063,7 @@
DED039282BC7A04B005D0571 /* StorePerformanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePerformanceView.swift; sourceTree = "<group>"; };
DED0392A2BC7A076005D0571 /* StorePerformanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePerformanceViewModel.swift; sourceTree = "<group>"; };
DED0392C2BC7DAFD005D0571 /* StatsTimeRangePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangePicker.swift; sourceTree = "<group>"; };
DED1E3152E8556270089909C /* BookingListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingListViewModelTests.swift; sourceTree = "<group>"; };
DED91DF92AD78A3A00CDCC53 /* BlazeCampaignItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignItemView.swift; sourceTree = "<group>"; };
DED9740A2AD7D09E00122EB4 /* BlazeCampaignListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListView.swift; sourceTree = "<group>"; };
DED9740C2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -12981,6 +12983,7 @@
D816DDBA22265D8000903E59 /* ViewRelated */ = {
isa = PBXGroup;
children = (
DED1E3162E8556270089909C /* Bookings */,
2D7A3E212E7891C400C46401 /* CIAB */,
864059FE2C6F67A000DA04DC /* Custom Fields */,
86023FAB2B16D80E00A28F07 /* Themes */,
Expand Down Expand Up @@ -13710,6 +13713,14 @@
path = StoreStats;
sourceTree = "<group>";
};
DED1E3162E8556270089909C /* Bookings */ = {
isa = PBXGroup;
children = (
DED1E3152E8556270089909C /* BookingListViewModelTests.swift */,
);
path = Bookings;
sourceTree = "<group>";
};
DED91DF72AD78A0C00CDCC53 /* Blaze */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -17223,6 +17234,7 @@
DE78DE442B2846AF002E58DE /* ThemesCarouselViewModelTests.swift in Sources */,
DE4D23B029B1D02A003A4B5D /* WPCom2FALoginViewModelTests.swift in Sources */,
03B9E52F2A150EED005C77F5 /* MockCardReaderSupportDeterminer.swift in Sources */,
DED1E3172E8556270089909C /* BookingListViewModelTests.swift in Sources */,
D8C11A6022E2479800D4A88D /* OrderPaymentDetailsViewModelTests.swift in Sources */,
B622BC74289CF19400B10CEC /* WaitingTimeTrackerTests.swift in Sources */,
023EC2E224DA8BAB0021DA91 /* MockProductSKUValidationStoresManager.swift in Sources */,
Expand Down
Loading