Skip to content

Commit cee365c

Browse files
author
Artyom Vlasov
authored
Load User Defaults list asynchronously to improve performance (#22535)
2 parents e1b24c8 + a4e6276 commit cee365c

2 files changed

Lines changed: 80 additions & 42 deletions

File tree

WordPress/Classes/ViewRelated/Me/App Settings/Boolean User Defaults/BooleanUserDefaultsDebugViewModel.swift

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,75 @@
11
import SwiftUI
2+
import Combine
23

34
final class BooleanUserDefaultsDebugViewModel: ObservableObject {
5+
46
private let persistentRepository: UserPersistentRepository
5-
private let coreDataStack: CoreDataStack
7+
private let coreDataStack: CoreDataStackSwift
8+
private var allUserDefaultsSections = Sections()
9+
private var cancellables = Set<AnyCancellable>()
610

7-
private var allUserDefaultsSections = Sections() {
8-
didSet {
9-
self.reloadSections()
10-
}
11-
}
11+
// MARK: - State
1212

1313
@Published var searchQuery: String = "" {
1414
didSet {
15-
self.reloadSections()
15+
Task { @MainActor in
16+
self.reloadSections()
17+
}
1618
}
1719
}
1820

19-
@Published var userDefaultsSections: Sections = []
21+
@MainActor @Published private(set) var userDefaultsSections: Sections = []
22+
23+
// MARK: - Init
2024

21-
init(coreDataStack: CoreDataStack = ContextManager.shared,
25+
init(coreDataStack: CoreDataStackSwift = ContextManager.shared,
2226
persistentRepository: UserPersistentRepository = UserPersistentStoreFactory.instance()) {
2327
self.coreDataStack = coreDataStack
2428
self.persistentRepository = persistentRepository
2529
}
2630

31+
// MARK: - Load
32+
2733
func load() {
34+
Task {
35+
await self.load()
36+
}
37+
}
38+
39+
func load() async {
2840
let allUserDefaults = persistentRepository.dictionaryRepresentation()
2941
var loadedUserDefaultsSections = Sections()
3042
var otherSection = [Row]()
3143

3244
for (key, value) in allUserDefaults {
3345
if let groupedUserDefaults = value as? [String: Bool], !isFeatureFlagsSection(key) {
46+
let rows = await processGroupedUserDefaults(groupedUserDefaults)
3447
let section = Section(
3548
key: key,
36-
rows: processGroupedUserDefaults(groupedUserDefaults)
49+
rows: rows
3750
)
3851
loadedUserDefaultsSections.append(section)
3952
} else if let booleanUserDefault = value as? Bool, !isGutenbergUserDefault(key) {
4053
otherSection.append(.init(key: key, title: key, value: booleanUserDefault))
4154
}
4255
}
56+
4357
if !otherSection.isEmpty {
4458
let rows = otherSection.sorted { $0.title < $1.title }
4559
let section = Section(key: Strings.otherBooleanUserDefaultsSectionID, rows: rows)
4660
loadedUserDefaultsSections.append(section)
4761
}
48-
allUserDefaultsSections = loadedUserDefaultsSections
62+
63+
self.allUserDefaultsSections = loadedUserDefaultsSections
64+
65+
await MainActor.run {
66+
self.reloadSections()
67+
}
4968
}
5069

51-
private func reloadSections() {
70+
// MARK: - Helpers
71+
72+
@MainActor private func reloadSections() {
5273
self.userDefaultsSections = filterUserDefaults(by: searchQuery)
5374
}
5475

@@ -73,22 +94,28 @@ final class BooleanUserDefaultsDebugViewModel: ObservableObject {
7394
return filteredSections
7495
}
7596

76-
private func processGroupedUserDefaults(_ userDefaults: [String: Bool]) -> [Row] {
77-
var rows = userDefaults.reduce(into: [Row]()) { result, keyValue in
78-
let (key, value) = keyValue
79-
result.append(processSingleUserDefault(key: key, value: value))
97+
private func processGroupedUserDefaults(_ userDefaults: [String: Bool]) async -> [Row] {
98+
let rows = try? await self.coreDataStack.performAndSave { [weak self] context -> [Row] in
99+
guard let self else {
100+
return []
101+
}
102+
return userDefaults
103+
.reduce(into: [Row]()) { result, keyValue in
104+
let (key, value) = keyValue
105+
let row = self.processSingleUserDefault(key: key, value: value, in: context)
106+
result.append(row)
107+
}.sorted { $0.title < $1.title }
80108
}
81-
rows = rows.sorted { $0.title < $1.title }
82-
return rows
109+
return rows ?? []
83110
}
84111

85-
private func processSingleUserDefault(key: String, value: Bool) -> Row {
86-
let title = findBlog(byID: key)?.url ?? key
112+
private func processSingleUserDefault(key: String, value: Bool, in context: NSManagedObjectContext) -> Row {
113+
let title = findBlog(byID: key, in: context)?.url ?? key
87114
return Row(key: key, title: title, value: value)
88115
}
89116

90-
private func findBlog(byID id: String) -> Blog? {
91-
return try? Blog.lookup(withID: Int(id) ?? 0, in: coreDataStack.mainContext)
117+
private func findBlog(byID id: String, in context: NSManagedObjectContext) -> Blog? {
118+
return try? Blog.lookup(withID: Int(id) ?? 0, in: context)
92119
}
93120

94121
func updateUserDefault(_ newValue: Bool, section: Section, row: Row) {

WordPress/WordPressTest/Me/App Settings/Boolean User Defaults/BooleanUserDefaultsDebugViewModelTests.swift

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import XCTest
2+
import Combine
23
@testable import WordPress
34

45
private typealias Section = BooleanUserDefaultsDebugViewModel.Section
56
private typealias Row = BooleanUserDefaultsDebugViewModel.Row
67

7-
class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
8+
@MainActor final class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
89

910
var viewModel: BooleanUserDefaultsDebugViewModel!
1011
var mockPersistentRepository: InMemoryUserDefaults!
12+
var cancellables: Set<AnyCancellable>!
1113

1214
override func setUp() {
1315
super.setUp()
16+
cancellables = .init()
1417
mockPersistentRepository = InMemoryUserDefaults()
1518
viewModel = BooleanUserDefaultsDebugViewModel(coreDataStack: contextManager,
1619
persistentRepository: mockPersistentRepository)
@@ -22,12 +25,12 @@ class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
2225
super.tearDown()
2326
}
2427

25-
func testLoadUserDefaults_WithOtherSection() {
28+
func testLoadUserDefaults_WithOtherSection() async {
2629
// Given
2730
mockPersistentRepository.set(true, forKey: "entry1")
2831

2932
// When
30-
viewModel.load()
33+
await viewModel.load()
3134

3235
// Then
3336
XCTAssertTrue(viewModel.userDefaultsSections.count == 1)
@@ -38,12 +41,12 @@ class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
3841
XCTAssertTrue(viewModel.userDefaultsSections.first?.rows.first?.value == true)
3942
}
4043

41-
func testLoadUserDefaults_WithoutOtherSection() {
44+
func testLoadUserDefaults_WithoutOtherSection() async {
4245
// Given
4346
mockPersistentRepository.set(["entry1": true], forKey: "section1")
4447

4548
// When
46-
viewModel.load()
49+
await viewModel.load()
4750

4851
// Then
4952
XCTAssertTrue(viewModel.userDefaultsSections.count == 1)
@@ -54,10 +57,10 @@ class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
5457
XCTAssertTrue(viewModel.userDefaultsSections.first?.rows.first?.value == true)
5558
}
5659

57-
func testUserDefaultsSections_MatchingQuery() {
60+
func testUserDefaultsSections_MatchingQuery() async {
5861
// Given
5962
mockPersistentRepository.set(["match": true], forKey: "section1")
60-
viewModel.load()
63+
await viewModel.load()
6164

6265
// When
6366
viewModel.searchQuery = "mat"
@@ -71,25 +74,33 @@ class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
7174
XCTAssertTrue(viewModel.userDefaultsSections.first?.rows.first?.value == true)
7275
}
7376

74-
func testUserDefaultsSections_NotMatchingQuery() {
77+
func testUserDefaultsSections_NotMatchingQuery() async {
7578
// Given
79+
let expectation = expectation(description: "List should be empty when no matching entries")
7680
mockPersistentRepository.set(["entry1": true], forKey: "section1")
77-
viewModel.load()
81+
await viewModel.load()
7882

7983
// When
84+
viewModel.$userDefaultsSections
85+
.dropFirst()
86+
.sink { sections in
87+
XCTAssertTrue(sections.isEmpty)
88+
expectation.fulfill()
89+
}
90+
.store(in: &cancellables)
8091
viewModel.searchQuery = "noMatch"
8192

8293
// Then
83-
XCTAssertTrue(viewModel.userDefaultsSections.isEmpty)
94+
await fulfillment(of: [expectation], timeout: 1)
8495
}
8596

86-
func testUserDefaultsSections_WithFilteredOutNonBooleanEntries() {
97+
func testUserDefaultsSections_WithFilteredOutNonBooleanEntries() async {
8798
// Given
8899
mockPersistentRepository.set(["entry1": "NotBoolean"], forKey: "section1")
89100
mockPersistentRepository.set(["entry2": false], forKey: "section1")
90101

91102
// When
92-
viewModel.load()
103+
await viewModel.load()
93104

94105
// Then
95106
XCTAssertTrue(viewModel.userDefaultsSections.count == 1)
@@ -100,32 +111,32 @@ class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
100111
XCTAssertTrue(viewModel.userDefaultsSections.first?.rows.first?.value == false)
101112
}
102113

103-
func testUserDefaultsSections_WithFilteredOutGutenbergItems() {
114+
func testUserDefaultsSections_WithFilteredOutGutenbergItems() async {
104115
// Given
105116
mockPersistentRepository.set(true, forKey: "com.wordpress.gutenberg-entry")
106117

107118
// When
108-
viewModel.load()
119+
await viewModel.load()
109120

110121
// Then
111122
XCTAssertTrue(viewModel.userDefaultsSections.isEmpty)
112123
}
113124

114-
func testUserDefaultsSections_WithFilteredOutFeatureFlagSection() {
125+
func testUserDefaultsSections_WithFilteredOutFeatureFlagSection() async {
115126
// Given
116127
mockPersistentRepository.set(["entry1": true], forKey: "FeatureFlagStoreCache")
117128

118129
// When
119-
viewModel.load()
130+
await viewModel.load()
120131

121132
// Then
122133
XCTAssertTrue(viewModel.userDefaultsSections.isEmpty)
123134
}
124135

125-
func testUpdateUserDefault_OtherSection() {
136+
func testUpdateUserDefault_OtherSection() async {
126137
// Given
127138
mockPersistentRepository.set(true, forKey: "entry1")
128-
viewModel.load()
139+
await viewModel.load()
129140

130141
// When
131142
let section = viewModel.userDefaultsSections[0]
@@ -141,10 +152,10 @@ class BooleanUserDefaultsDebugViewModelTests: CoreDataTestCase {
141152
XCTAssertTrue(viewModel.userDefaultsSections.first?.rows.first?.value == false)
142153
}
143154

144-
func testUpdateUserDefault_GivenSection() {
155+
func testUpdateUserDefault_GivenSection() async {
145156
// Given
146157
mockPersistentRepository.set(["entry1": true], forKey: "section1")
147-
viewModel.load()
158+
await viewModel.load()
148159

149160
// When
150161
let section = viewModel.userDefaultsSections[0]

0 commit comments

Comments
 (0)