Skip to content

Commit 207277c

Browse files
authored
Merge pull request #4949 from woocommerce/issue/4780-fetch-country-data
2 parents a23223e + 4b3b176 commit 207277c

File tree

11 files changed

+163
-54
lines changed

11 files changed

+163
-54
lines changed

WooCommerce/Classes/ViewRelated/ListSelector/ListSelector.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import SwiftUI
22

3+
/// Protocol required to re-render the view when the command updates any of it's content.
4+
///
5+
protocol ObservableListSelectorCommand: ListSelectorCommand, ObservableObject {}
6+
37
/// `SwiftUI` wrapper for `ListSelectorViewController`
48
///
5-
struct ListSelector<Command: ListSelectorCommand>: UIViewControllerRepresentable {
9+
struct ListSelector<Command: ObservableListSelectorCommand>: UIViewControllerRepresentable {
610

711
/// Command that defines cell style and provide data.
812
///
9-
let command: Command
13+
@ObservedObject var command: Command
1014

1115
/// Table view style.
1216
///

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

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ import Yosemite
33

44
/// Command to be used to select a country when editing addresses.
55
///
6-
final class CountrySelectorCommand: ListSelectorCommand {
6+
final class CountrySelectorCommand: ObservableListSelectorCommand {
77
typealias Model = Country
88
typealias Cell = BasicTableViewCell
99

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

1414
/// Data to display
1515
///
16-
private(set) var data: [Country]
16+
@Published private(set) var data: [Country]
1717

1818
/// Current selected country
1919
///
@@ -23,7 +23,7 @@ final class CountrySelectorCommand: ListSelectorCommand {
2323
///
2424
let navigationBarTitle: String? = ""
2525

26-
init(countries: [Country] = temporaryCountries, selected: Country? = nil) {
26+
init(countries: [Country], selected: Country? = nil) {
2727
self.countries = countries
2828
self.data = countries
2929
self.selected = selected
@@ -41,6 +41,13 @@ final class CountrySelectorCommand: ListSelectorCommand {
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+
4451
/// Filter available countries that contains a given search term.
4552
///
4653
func filterCountries(term: String) {
@@ -51,16 +58,3 @@ final class CountrySelectorCommand: ListSelectorCommand {
5158
data = countries.filter { $0.name.localizedCaseInsensitiveContains(term) }
5259
}
5360
}
54-
55-
// MARK: Temporary Methods
56-
extension CountrySelectorCommand {
57-
58-
// Supported countries will come from the view model later.
59-
//
60-
private static let temporaryCountries: [Country] = {
61-
return Locale.isoRegionCodes.map { regionCode in
62-
let name = Locale.current.localizedString(forRegionCode: regionCode) ?? ""
63-
return Country(code: regionCode, name: name, states: [])
64-
}
65-
}()
66-
}

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

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import Combine
22
import SwiftUI
3+
import Yosemite
4+
import protocol Storage.StorageManagerType
35

46
/// View Model for the `CountrySelector` view.
57
///
@@ -10,13 +12,12 @@ final class CountrySelectorViewModel: FilterListSelectorViewModelable, Observabl
1012
var searchTerm: String = "" {
1113
didSet {
1214
command.filterCountries(term: searchTerm)
13-
objectWillChange.send()
1415
}
1516
}
1617

1718
/// Command that powers the `ListSelector` view.
1819
///
19-
let command = CountrySelectorCommand()
20+
let command = CountrySelectorCommand(countries: [])
2021

2122
/// Navigation title
2223
///
@@ -25,6 +26,56 @@ final class CountrySelectorViewModel: FilterListSelectorViewModelable, Observabl
2526
/// Filter text field placeholder
2627
///
2728
let filterPlaceholder = Localization.placeholder
29+
30+
/// ResultsController for stored countries.
31+
///
32+
private lazy var countriesResultsController: ResultsController<StorageCountry> = {
33+
let countriesDescriptor = NSSortDescriptor(key: "name", ascending: true)
34+
return ResultsController<StorageCountry>(storageManager: storageManager, sortedBy: [countriesDescriptor])
35+
}()
36+
37+
/// Storage to fetch countries
38+
///
39+
private let storageManager: StorageManagerType
40+
41+
/// Stores to sync countries
42+
///
43+
private let stores: StoresManager
44+
45+
/// Current `SiteID`, needed to sync countries from a remote source.
46+
///
47+
private let siteID: Int64
48+
49+
init(siteID: Int64, storageManager: StorageManagerType = ServiceLocator.storageManager, stores: StoresManager = ServiceLocator.stores) {
50+
self.siteID = siteID
51+
self.storageManager = storageManager
52+
self.stores = stores
53+
fetchAndBindCountries()
54+
}
55+
}
56+
57+
// MARK: Helpers
58+
private extension CountrySelectorViewModel {
59+
/// Fetches & Binds countries from storage, If there are no stored countries, sync them from a remote source.
60+
///
61+
func fetchAndBindCountries() {
62+
// Bind stored countries & command
63+
countriesResultsController.onDidChangeContent = { [weak self] in
64+
guard let self = self else { return }
65+
self.command.resetCountries(self.countriesResultsController.fetchedObjects)
66+
}
67+
68+
// Initial fetch
69+
try? countriesResultsController.performFetch()
70+
71+
// Reset countries with fetched data or sync countries if needed.
72+
if !countriesResultsController.isEmpty {
73+
command.resetCountries(countriesResultsController.fetchedObjects)
74+
} else {
75+
let action = DataAction.synchronizeCountries(siteID: siteID, onCompletion: { _ in })
76+
stores.dispatch(action)
77+
}
78+
}
2879
}
2980

3081
// MARK: Constants

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,7 @@ struct EditAddressForm: View {
136136
.disabled(!viewModel.isDoneButtonEnabled))
137137

138138
// Go to edit country
139-
// TODO: Move `CountrySelectorViewModel` creation to the VM when it exists.
140-
NavigationLink(destination: FilterListSelector(viewModel: CountrySelectorViewModel()), isActive: $showCountrySelector) {
139+
NavigationLink(destination: FilterListSelector(viewModel: viewModel.createCountryViewModel()), isActive: $showCountrySelector) {
141140
EmptyView()
142141
}
143142

@@ -197,7 +196,7 @@ struct EditAddressForm_Previews: PreviewProvider {
197196
country: "US",
198197
phone: "333-333-3333",
199198
200-
static let sampleViewModel = EditAddressFormViewModel(address: sampleAddress)
199+
static let sampleViewModel = EditAddressFormViewModel(siteID: 123, address: sampleAddress)
201200

202201
static var previews: some View {
203202
NavigationView {

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import Yosemite
22

33
final class EditAddressFormViewModel: ObservableObject {
44

5-
init(address: Address?) {
5+
/// Current site ID
6+
///
7+
private let siteID: Int64
8+
9+
init(siteID: Int64, address: Address?) {
10+
self.siteID = siteID
611
self.originalAddress = address ?? .empty
712
updateFieldsWithOriginalAddress()
813
}
@@ -33,6 +38,12 @@ final class EditAddressFormViewModel: ObservableObject {
3338
var isDoneButtonEnabled: Bool {
3439
return originalAddress != addressFromFields
3540
}
41+
42+
/// Creates a view model to be used when selecting a country
43+
///
44+
func createCountryViewModel() -> CountrySelectorViewModel {
45+
CountrySelectorViewModel(siteID: siteID)
46+
}
3647
}
3748

3849
private extension EditAddressFormViewModel {

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import SwiftUI
22

33
protocol FilterListSelectorViewModelable: ObservableObject {
44

5-
associatedtype Command: ListSelectorCommand
5+
associatedtype Command: ObservableListSelectorCommand
66

77
/// Binding variable for the filter search term
88
///
@@ -85,11 +85,3 @@ private extension SearchHeader {
8585
static let externalPadding = EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)
8686
}
8787
}
88-
89-
struct FilterListSelector_Previews: PreviewProvider {
90-
static var previews: some View {
91-
NavigationView {
92-
FilterListSelector(viewModel: CountrySelectorViewModel())
93-
}
94-
}
95-
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Yosemite
33

44
/// Command to be used to select a state when editing addresses.
55
///
6-
final class StateSelectorCommand: ListSelectorCommand {
6+
final class StateSelectorCommand: ObservableListSelectorCommand {
77
typealias Model = StateOfACountry
88
typealias Cell = BasicTableViewCell
99

WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ private extension OrderDetailsViewController {
661661
}
662662

663663
func editShippingAddressTapped() {
664-
let viewModel = EditAddressFormViewModel(address: viewModel.order.shippingAddress)
664+
let viewModel = EditAddressFormViewModel(siteID: viewModel.order.siteID, address: viewModel.order.shippingAddress)
665665
let editAddressViewController = EditAddressHostingController(viewModel: viewModel)
666666
show(editAddressViewController, sender: self)
667667
}

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

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,69 @@ import TestKit
55

66
final class CountrySelectorViewModelTests: XCTestCase {
77

8+
let sampleSiteID: Int64 = 123
9+
let testingStorage = MockStorageManager()
10+
11+
override func setUp () {
12+
super.setUp()
13+
14+
testingStorage.reset()
15+
testingStorage.insertSampleCountries(readOnlyCountries: Self.sampleCountries)
16+
}
17+
818
func test_filter_countries_return_expected_results() {
919
// Given
10-
let viewModel = CountrySelectorViewModel()
20+
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, storageManager: testingStorage)
1121

1222
// When
1323
viewModel.searchTerm = "Co"
1424
let countries = viewModel.command.data.map { $0.name }
25+
1526
// Then
1627
assertEqual(countries, [
1728
"Cocos (Keeling) Islands",
18-
"Congo - Kinshasa",
29+
"Colombia",
30+
"Comoros",
1931
"Congo - Brazzaville",
32+
"Congo - Kinshasa",
2033
"Cook Islands",
21-
"Colombia",
2234
"Costa Rica",
23-
"Comoros",
24-
"Morocco",
25-
"Monaco",
2635
"Mexico",
36+
"Monaco",
37+
"Morocco",
2738
"Puerto Rico",
2839
"Turks & Caicos Islands"
2940
])
3041
}
3142

3243
func test_filter_countries_with_uppercase_letters_return_expected_results() {
3344
// Given
34-
let viewModel = CountrySelectorViewModel()
45+
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, storageManager: testingStorage)
3546

3647
// When
3748
viewModel.searchTerm = "CO"
3849
let countries = viewModel.command.data.map { $0.name }
50+
3951
// Then
4052
assertEqual(countries, [
4153
"Cocos (Keeling) Islands",
42-
"Congo - Kinshasa",
54+
"Colombia",
55+
"Comoros",
4356
"Congo - Brazzaville",
57+
"Congo - Kinshasa",
4458
"Cook Islands",
45-
"Colombia",
4659
"Costa Rica",
47-
"Comoros",
48-
"Morocco",
49-
"Monaco",
5060
"Mexico",
61+
"Monaco",
62+
"Morocco",
5163
"Puerto Rico",
5264
"Turks & Caicos Islands"
5365
])
5466
}
5567

5668
func test_cleaning_search_terms_return_all_countries() {
5769
// Given
58-
let viewModel = CountrySelectorViewModel()
70+
let viewModel = CountrySelectorViewModel(siteID: sampleSiteID, storageManager: testingStorage)
5971
let totalNumberOfCountries = viewModel.command.data.count
6072

6173
// When
@@ -66,4 +78,36 @@ final class CountrySelectorViewModelTests: XCTestCase {
6678
// Then
6779
XCTAssertEqual(viewModel.command.data.count, totalNumberOfCountries)
6880
}
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+
}
103+
}
104+
105+
// MARK: Helpers
106+
private extension CountrySelectorViewModelTests {
107+
static let sampleCountries: [Country] = {
108+
return Locale.isoRegionCodes.map { regionCode in
109+
let name = Locale.current.localizedString(forRegionCode: regionCode) ?? ""
110+
return Country(code: regionCode, name: name, states: [])
111+
}
112+
}()
69113
}

0 commit comments

Comments
 (0)