Skip to content

Commit 0a659d2

Browse files
authored
Merge pull request #4967 from woocommerce/issue/4798-sync-countries
2 parents 122eb54 + 8983f0c commit 0a659d2

File tree

8 files changed

+166
-136
lines changed

8 files changed

+166
-136
lines changed

WooCommerce/Classes/ViewRelated/Orders/Order Details/Address Edit/CountrySelectorCommand.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ final class CountrySelectorCommand: ObservableListSelectorCommand {
99

1010
/// Original array of countries.
1111
///
12-
private var countries: [Country]
12+
private let countries: [Country]
1313

1414
/// Data to display
1515
///
@@ -41,13 +41,6 @@ final class CountrySelectorCommand: ObservableListSelectorCommand {
4141
cell.textLabel?.text = model.name
4242
}
4343

44-
/// Resets countries data.
45-
///
46-
func resetCountries(_ countries: [Country]) {
47-
self.countries = countries
48-
self.data = countries
49-
}
50-
5144
/// Filter available countries that contains a given search term.
5245
///
5346
func filterCountries(term: String) {

WooCommerce/Classes/ViewRelated/Orders/Order Details/Address Edit/CountrySelectorViewModel.swift

Lines changed: 3 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,9 @@ final class CountrySelectorViewModel: FilterListSelectorViewModelable, Observabl
1515
}
1616
}
1717

18-
/// Define if the view should show placeholders instead of the real elements.
19-
///
20-
@Published private(set) var showPlaceholders: Bool = false
21-
2218
/// Command that powers the `ListSelector` view.
2319
///
24-
let command = CountrySelectorCommand(countries: [])
20+
let command: CountrySelectorCommand
2521

2622
/// Navigation title
2723
///
@@ -31,93 +27,13 @@ final class CountrySelectorViewModel: FilterListSelectorViewModelable, Observabl
3127
///
3228
let filterPlaceholder = Localization.placeholder
3329

34-
/// ResultsController for stored countries.
35-
///
36-
private lazy var countriesResultsController: ResultsController<StorageCountry> = {
37-
let countriesDescriptor = NSSortDescriptor(key: "name", ascending: true)
38-
return ResultsController<StorageCountry>(storageManager: storageManager, sortedBy: [countriesDescriptor])
39-
}()
40-
41-
/// Trigger to sync countries.
42-
///
43-
private let syncCountriesTrigger = PassthroughSubject<Void, Never>()
44-
45-
/// Storage to fetch countries
46-
///
47-
private let storageManager: StorageManagerType
48-
49-
/// Stores to sync countries
50-
///
51-
private let stores: StoresManager
52-
5330
/// Current `SiteID`, needed to sync countries from a remote source.
5431
///
5532
private let siteID: Int64
5633

57-
init(siteID: Int64, storageManager: StorageManagerType = ServiceLocator.storageManager, stores: StoresManager = ServiceLocator.stores) {
34+
init(siteID: Int64, countries: [Country]) {
5835
self.siteID = siteID
59-
self.storageManager = storageManager
60-
self.stores = stores
61-
bindSyncTrigger()
62-
bindStoredCountries()
63-
}
64-
}
65-
66-
// MARK: Helpers
67-
private extension CountrySelectorViewModel {
68-
/// Fetches & Binds countries from storage, If there are no stored countries, trigger a sync request.
69-
///
70-
func bindStoredCountries() {
71-
72-
// Bind stored countries & command
73-
countriesResultsController.onDidChangeContent = { [weak self] in
74-
guard let self = self else { return }
75-
self.command.resetCountries(self.countriesResultsController.fetchedObjects)
76-
}
77-
78-
// Initial fetch
79-
try? countriesResultsController.performFetch()
80-
81-
// Trigger a sync request if there are no countries.
82-
guard !countriesResultsController.isEmpty else {
83-
return syncCountriesTrigger.send()
84-
}
85-
86-
// Reset countries with fetched
87-
command.resetCountries(countriesResultsController.fetchedObjects)
88-
}
89-
90-
/// Sync countries when requested. Defines the `showPlaceholderState` value depending if countries are being synced or not.
91-
///
92-
func bindSyncTrigger() {
93-
syncCountriesTrigger
94-
.handleEvents(receiveOutput: { // Set `showPlaceholders` to `true` before initiating sync.
95-
self.showPlaceholders = true // I could not find a way to assign this using combine operators. :-(
96-
})
97-
.map { // Sync countries
98-
self.makeSyncCountriesFuture()
99-
.replaceError(with: ()) // TODO: Handle errors
100-
}
101-
.switchToLatest()
102-
.map { _ in // Set `showPlaceholders` to `false` after sync is done.
103-
false
104-
}
105-
.assign(to: &$showPlaceholders)
106-
}
107-
108-
/// Creates a publisher that syncs countries into our storage layer.
109-
///
110-
func makeSyncCountriesFuture() -> AnyPublisher<Void, Error> {
111-
Future<Void, Error> { [weak self] promise in
112-
guard let self = self else { return }
113-
114-
let action = DataAction.synchronizeCountries(siteID: self.siteID) { result in
115-
let newResult = result.map { _ in } // Hides the result success type because we don't need it.
116-
promise(newResult)
117-
}
118-
self.stores.dispatch(action)
119-
}
120-
.eraseToAnyPublisher()
36+
self.command = CountrySelectorCommand(countries: countries)
12137
}
12238
}
12339

WooCommerce/Classes/ViewRelated/Orders/Order Details/Address Edit/EditAddressForm.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ struct EditAddressForm: View {
134134
// TODO: save changes
135135
}
136136
.disabled(!viewModel.isDoneButtonEnabled))
137+
.redacted(reason: viewModel.showPlaceholders ? .placeholder : [])
138+
.shimmering(active: viewModel.showPlaceholders)
139+
.onAppear {
140+
viewModel.onLoadTrigger.send()
141+
}
137142

138143
// Go to edit country
139144
LazyNavigationLink(destination: FilterListSelector(viewModel: viewModel.createCountryViewModel()), isActive: $showCountrySelector) {

WooCommerce/Classes/ViewRelated/Orders/Order Details/Address Edit/EditAddressFormViewModel.swift

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,59 @@
11
import Yosemite
2+
import Storage
3+
import Combine
24

35
final class EditAddressFormViewModel: ObservableObject {
46

57
/// Current site ID
68
///
79
private let siteID: Int64
810

9-
init(siteID: Int64, address: Address?) {
11+
/// ResultsController for stored countries.
12+
///
13+
private lazy var countriesResultsController: ResultsController<StorageCountry> = {
14+
let countriesDescriptor = NSSortDescriptor(key: "name", ascending: true)
15+
return ResultsController<StorageCountry>(storageManager: storageManager, sortedBy: [countriesDescriptor])
16+
}()
17+
18+
/// Trigger to sync countries.
19+
///
20+
private let syncCountriesTrigger = PassthroughSubject<Void, Never>()
21+
22+
/// Storage to fetch countries
23+
///
24+
private let storageManager: StorageManagerType
25+
26+
/// Stores to sync countries
27+
///
28+
private let stores: StoresManager
29+
30+
/// Store for publishers subscriptions
31+
///
32+
private var subscriptions = Set<AnyCancellable>()
33+
34+
35+
init(siteID: Int64, address: Address?, storageManager: StorageManagerType = ServiceLocator.storageManager, stores: StoresManager = ServiceLocator.stores) {
1036
self.siteID = siteID
1137
self.originalAddress = address ?? .empty
38+
self.storageManager = storageManager
39+
self.stores = stores
1240
updateFieldsWithOriginalAddress()
41+
42+
// Listen only to the first emitted event.
43+
onLoadTrigger.first().sink {
44+
self.bindSyncTrigger()
45+
self.fetchStoredCountriesAndTriggerSyncIfNeeded()
46+
}.store(in: &subscriptions)
1347
}
1448

1549
/// Original `Address` model.
1650
///
1751
private let originalAddress: Address
1852

53+
/// Trigger to perform any one time setups.
54+
///
55+
let onLoadTrigger: PassthroughSubject<Void, Never> = PassthroughSubject()
56+
1957
// MARK: User Fields
2058

2159
@Published var firstName: String = ""
@@ -33,6 +71,10 @@ final class EditAddressFormViewModel: ObservableObject {
3371

3472
// MARK: Navigation and utility
3573

74+
/// Define if the view should show placeholders instead of the real elements.
75+
///
76+
@Published private(set) var showPlaceholders: Bool = false
77+
3678
/// Return `true` if the done button should be enabled.
3779
///
3880
var isDoneButtonEnabled: Bool {
@@ -42,7 +84,7 @@ final class EditAddressFormViewModel: ObservableObject {
4284
/// Creates a view model to be used when selecting a country
4385
///
4486
func createCountryViewModel() -> CountrySelectorViewModel {
45-
CountrySelectorViewModel(siteID: siteID)
87+
CountrySelectorViewModel(siteID: siteID, countries: countriesResultsController.fetchedObjects)
4688
}
4789
}
4890

@@ -75,4 +117,50 @@ private extension EditAddressFormViewModel {
75117
phone: phone.isEmpty ? nil : phone,
76118
email: email.isEmpty ? nil : email)
77119
}
120+
121+
122+
/// Fetches countries from storage, If there are no stored countries, trigger a sync request.
123+
///
124+
func fetchStoredCountriesAndTriggerSyncIfNeeded() {
125+
// Initial fetch
126+
try? countriesResultsController.performFetch()
127+
128+
// Trigger a sync request if there are no countries.
129+
guard !countriesResultsController.isEmpty else {
130+
return syncCountriesTrigger.send()
131+
}
132+
}
133+
134+
/// Sync countries when requested. Defines the `showPlaceholderState` value depending if countries are being synced or not.
135+
///
136+
func bindSyncTrigger() {
137+
syncCountriesTrigger
138+
.handleEvents(receiveOutput: { // Set `showPlaceholders` to `true` before initiating sync.
139+
self.showPlaceholders = true // I could not find a way to assign this using combine operators. :-(
140+
})
141+
.map { // Sync countries
142+
self.makeSyncCountriesFuture()
143+
.replaceError(with: ()) // TODO: Handle errors
144+
}
145+
.switchToLatest()
146+
.map { _ in // Set `showPlaceholders` to `false` after sync is done.
147+
false
148+
}
149+
.assign(to: &$showPlaceholders)
150+
}
151+
152+
/// Creates a publisher that syncs countries into our storage layer.
153+
///
154+
func makeSyncCountriesFuture() -> AnyPublisher<Void, Error> {
155+
Future<Void, Error> { [weak self] promise in
156+
guard let self = self else { return }
157+
158+
let action = DataAction.synchronizeCountries(siteID: self.siteID) { result in
159+
let newResult = result.map { _ in } // Hides the result success type because we don't need it.
160+
promise(newResult)
161+
}
162+
self.stores.dispatch(action)
163+
}
164+
.eraseToAnyPublisher()
165+
}
78166
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/Address Edit/FilterListSelector.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ protocol FilterListSelectorViewModelable: ObservableObject {
2020
/// Placeholder for the filter text field
2121
///
2222
var filterPlaceholder: String { get }
23-
24-
/// Value to indicate if the views should be redacted
25-
///
26-
var showPlaceholders: Bool { get }
2723
}
2824

2925
/// Filterable List Selector View
@@ -42,8 +38,6 @@ struct FilterListSelector<ViewModel: FilterListSelectorViewModelable>: View {
4238
ListSelector(command: viewModel.command, tableStyle: .plain)
4339
}
4440
.navigationTitle(viewModel.navigationTitle)
45-
.redacted(reason: viewModel.showPlaceholders ? .placeholder : [])
46-
.shimmering(active: viewModel.showPlaceholders)
4741
}
4842
}
4943

WooCommerce/Classes/ViewRelated/Orders/Order Details/Address Edit/StateSelectorViewModel.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ final class StateSelectorViewModel: FilterListSelectorViewModelable, ObservableO
2525
/// Filter text field placeholder
2626
///
2727
let filterPlaceholder = Localization.placeholder
28-
29-
/// States do not require data loading
30-
///
31-
let showPlaceholders = false
3228
}
3329

3430
// MARK: Constants

WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/Addresses/CountrySelector/CountrySelectorViewModelTests.swift

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,14 @@ import TestKit
66
final class CountrySelectorViewModelTests: XCTestCase {
77

88
let sampleSiteID: Int64 = 123
9-
let testingStorage = MockStorageManager()
109

1110
override func setUp () {
1211
super.setUp()
13-
14-
testingStorage.reset()
15-
testingStorage.insertSampleCountries(readOnlyCountries: Self.sampleCountries)
1612
}
1713

1814
func test_filter_countries_return_expected_results() {
1915
// Given
20-
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, storageManager: testingStorage)
16+
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, countries: Self.sampleCountries)
2117

2218
// When
2319
viewModel.searchTerm = "Co"
@@ -42,7 +38,7 @@ final class CountrySelectorViewModelTests: XCTestCase {
4238

4339
func test_filter_countries_with_uppercase_letters_return_expected_results() {
4440
// Given
45-
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, storageManager: testingStorage)
41+
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, countries: Self.sampleCountries)
4642

4743
// When
4844
viewModel.searchTerm = "CO"
@@ -67,7 +63,7 @@ final class CountrySelectorViewModelTests: XCTestCase {
6763

6864
func test_cleaning_search_terms_return_all_countries() {
6965
// Given
70-
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, storageManager: testingStorage)
66+
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, countries: Self.sampleCountries)
7167
let totalNumberOfCountries = viewModel.command.data.count
7268

7369
// When
@@ -78,28 +74,6 @@ final class CountrySelectorViewModelTests: XCTestCase {
7874
// Then
7975
XCTAssertEqual(viewModel.command.data.count, totalNumberOfCountries)
8076
}
81-
82-
func test_starting_view_model_without_stored_countries_fetches_them_remotely() {
83-
// Given
84-
testingStorage.reset()
85-
let testingStores = MockStoresManager(sessionManager: .testingInstance)
86-
87-
88-
// When
89-
let countriesFetched: Bool = waitFor { promise in
90-
testingStores.whenReceivingAction(ofType: DataAction.self) { action in
91-
switch action {
92-
case .synchronizeCountries:
93-
promise(true)
94-
}
95-
}
96-
97-
_ = CountrySelectorViewModel(siteID: self.sampleSiteID, storageManager: self.testingStorage, stores: testingStores)
98-
}
99-
100-
// Then
101-
XCTAssertTrue(countriesFetched)
102-
}
10377
}
10478

10579
// MARK: Helpers
@@ -108,6 +82,8 @@ private extension CountrySelectorViewModelTests {
10882
return Locale.isoRegionCodes.map { regionCode in
10983
let name = Locale.current.localizedString(forRegionCode: regionCode) ?? ""
11084
return Country(code: regionCode, name: name, states: [])
85+
}.sorted { a, b in
86+
a.name <= b.name
11187
}
11288
}()
11389
}

0 commit comments

Comments
 (0)