Skip to content

Commit 7b36ee5

Browse files
Add async operations and pagination completion (#50)
1 parent bde93ca commit 7b36ee5

6 files changed

Lines changed: 155 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
All notable changes to this project will be documented in this file.
33
`FetchRequests` adheres to [Semantic Versioning](https://semver.org/).
44

5+
## 6.1.0
6+
Release TBD
7+
8+
* Adds async operations for fetch and resort
9+
* Adds completion closure to performPagination, with a boolean value indicating if values were returned or not
10+
511
## [6.0.3](https://github.com/square/FetchRequests/releases/tag/6.0.3)
612
Released on 2024-03-06
713

@@ -17,7 +23,7 @@ Released on 2023-06-23
1723

1824
* The demo now includes a SwiftUI example
1925
* Fix for SwiftUI when a FetchDefinition request is synchronous
20-
26+
2127
## [6.0](https://github.com/square/FetchRequests/releases/tag/6.0.0)
2228
Released on 2023-04-05
2329

FetchRequests/Sources/Controller/FetchedResultsController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ public class FetchedResultsController<FetchedObject: FetchableObject>: NSObject,
238238
return new
239239
}
240240

241-
private let debounceInsertsAndReloads: Bool
241+
internal let debounceInsertsAndReloads: Bool
242+
242243
private var objectsToReload: Set<FetchedObject> = []
243244
private var objectsToInsert: OrderedSet<FetchedObject> = []
244245

FetchRequests/Sources/Controller/FetchedResultsControllerProtocol.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ public protocol FetchedResultsControllerProtocol<FetchedObject>: DoublyObservabl
4848
func indexPath(for object: FetchedObject) -> IndexPath?
4949
}
5050

51+
// MARK: - Async
52+
53+
public extension FetchedResultsControllerProtocol {
54+
@MainActor
55+
func performFetch() async {
56+
await withCheckedContinuation { continuation in
57+
performFetch {
58+
continuation.resume()
59+
}
60+
}
61+
}
62+
63+
@MainActor
64+
func resort(using newSortDescriptors: [NSSortDescriptor]) async {
65+
await withCheckedContinuation { continuation in
66+
resort(using: sortDescriptors) {
67+
continuation.resume()
68+
}
69+
}
70+
}
71+
}
72+
5173
// MARK: - Index Paths
5274

5375
public extension FetchedResultsControllerProtocol {

FetchRequests/Sources/Controller/PausableFetchedResultsController.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ extension PausableFetchedResultsController: FetchedResultsControllerProtocol {
174174
public var sections: [Section] {
175175
sectionsSnapshot ?? controller.sections
176176
}
177+
178+
internal var debounceInsertsAndReloads: Bool {
179+
controller.debounceInsertsAndReloads
180+
}
177181
}
178182

179183
// MARK: - InternalFetchResultsControllerProtocol

FetchRequests/Sources/Requests/PaginatingFetchDefinition.swift

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,29 @@ public class PaginatingFetchDefinition<FetchedObject: FetchableObject>: FetchDef
4141
private extension InternalFetchResultsControllerProtocol {
4242
@MainActor
4343
func performPagination(
44-
with paginationRequest: PaginatingFetchDefinition<FetchedObject>.PaginationRequest
44+
with paginationRequest: PaginatingFetchDefinition<FetchedObject>.PaginationRequest,
45+
willDebounceInsertsAndReloads: Bool,
46+
completion: @escaping @MainActor (_ hasPageResults: Bool) -> Void
4547
) {
4648
let currentResults = self.fetchedObjects
4749
paginationRequest(currentResults) { [weak self] pageResults in
4850
guard let pageResults else {
51+
completion(false)
4952
return
5053
}
54+
5155
performOnMainThread {
5256
self?.manuallyInsert(objects: pageResults, emitChanges: true)
5357
}
58+
59+
if willDebounceInsertsAndReloads {
60+
// force this to run on the _next_ run loop, at which point any debounced insertions should have happened
61+
DispatchQueue.main.async {
62+
completion(!pageResults.isEmpty)
63+
}
64+
} else {
65+
completion(!pageResults.isEmpty)
66+
}
5467
}
5568
}
5669
}
@@ -77,8 +90,21 @@ public class PaginatingFetchedResultsController<
7790
}
7891

7992
@MainActor
80-
public func performPagination() {
81-
performPagination(with: paginatingDefinition.paginationRequest)
93+
public func performPagination(completion: @escaping @MainActor (_ hasPageResults: Bool) -> Void = { _ in }) {
94+
performPagination(
95+
with: paginatingDefinition.paginationRequest,
96+
willDebounceInsertsAndReloads: debounceInsertsAndReloads,
97+
completion: completion
98+
)
99+
}
100+
101+
@MainActor
102+
public func performPagination() async -> Bool {
103+
await withCheckedContinuation { continuation in
104+
performPagination { hasPageResults in
105+
continuation.resume(returning: hasPageResults)
106+
}
107+
}
82108
}
83109
}
84110

@@ -105,6 +131,10 @@ public class PausablePaginatingFetchedResultsController<
105131

106132
@MainActor
107133
public func performPagination() {
108-
performPagination(with: paginatingDefinition.paginationRequest)
134+
performPagination(
135+
with: paginatingDefinition.paginationRequest,
136+
willDebounceInsertsAndReloads: debounceInsertsAndReloads,
137+
completion: { _ in }
138+
)
109139
}
110140
}

FetchRequests/Tests/Controllers/PaginatingFetchedResultsControllerTestCase.swift

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
1717
private(set) var fetchCompletion: (([TestObject]) -> Void)!
1818

1919
private var paginationCurrentResults: [TestObject]!
20-
private var paginationCompletion: (([TestObject]?) -> Void)!
20+
private var paginationRequestCompletion: (([TestObject]?) -> Void)!
21+
22+
private var performPaginationCompletionResult: Bool!
2123

2224
private var associationRequest: TestObject.AssociationRequest!
2325

@@ -35,7 +37,7 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
3537
}
3638
let paginationRequest: PaginatingFetchDefinition<TestObject>.PaginationRequest = { [unowned self] currentResults, completion in
3739
self.paginationCurrentResults = currentResults
38-
self.paginationCompletion = completion
40+
self.paginationRequestCompletion = completion
3941
}
4042

4143
let desiredAssociations = TestObject.fetchRequestAssociations(
@@ -72,7 +74,8 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
7274
controller = nil
7375
fetchCompletion = nil
7476
paginationCurrentResults = nil
75-
paginationCompletion = nil
77+
paginationRequestCompletion = nil
78+
performPaginationCompletionResult = nil
7679
associationRequest = nil
7780
inclusionCheck = nil
7881

@@ -124,7 +127,8 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
124127

125128
fetchCompletion = nil
126129
paginationCurrentResults = nil
127-
paginationCompletion = nil
130+
paginationRequestCompletion = nil
131+
performPaginationCompletionResult = nil
128132
changeEvents.removeAll()
129133

130134
// Trigger pagination
@@ -133,6 +137,8 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
133137

134138
performPagination(paginationObjectIDs)
135139

140+
XCTAssertTrue(performPaginationCompletionResult)
141+
136142
XCTAssertEqual(controller.sections.count, 1)
137143
XCTAssertEqual(controller.sections[0].fetchedIDs, objectIDs + paginationObjectIDs)
138144

@@ -161,7 +167,8 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
161167

162168
fetchCompletion = nil
163169
paginationCurrentResults = nil
164-
paginationCompletion = nil
170+
paginationRequestCompletion = nil
171+
performPaginationCompletionResult = nil
165172
changeEvents.removeAll()
166173

167174
// Trigger pagination
@@ -170,9 +177,12 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
170177

171178
performPagination(paginationObjectIDs)
172179

180+
XCTAssertTrue(performPaginationCompletionResult)
181+
173182
fetchCompletion = nil
174183
paginationCurrentResults = nil
175-
paginationCompletion = nil
184+
paginationRequestCompletion = nil
185+
performPaginationCompletionResult = nil
176186
changeEvents.removeAll()
177187

178188
// Trigger insert
@@ -184,7 +194,7 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
184194

185195
XCTAssertNil(fetchCompletion)
186196
XCTAssertNil(paginationCurrentResults)
187-
XCTAssertNil(paginationCompletion)
197+
XCTAssertNil(paginationRequestCompletion)
188198

189199
XCTAssertEqual(controller.sections.count, 1)
190200
XCTAssertEqual(controller.sections[0].fetchedIDs, ["a", "b", "c", "d", "e", "f"])
@@ -193,6 +203,71 @@ class PaginatingFetchedResultsControllerTestCase: XCTestCase, FetchedResultsCont
193203
XCTAssertEqual(changeEvents[0].change, FetchedResultsChange.insert(location: IndexPath(item: 4, section: 0)))
194204
XCTAssertEqual(changeEvents[0].object.id, "e")
195205
}
206+
207+
func testPaginationHasObjects() async throws {
208+
controller = PaginatingFetchedResultsController(
209+
definition: createFetchDefinition(),
210+
debounceInsertsAndReloads: false
211+
)
212+
controller.setDelegate(self)
213+
214+
// Fetch some objects
215+
216+
let objectIDs = ["a", "b", "c"]
217+
218+
try performFetch(objectIDs)
219+
220+
fetchCompletion = nil
221+
paginationCurrentResults = nil
222+
paginationRequestCompletion = nil
223+
performPaginationCompletionResult = nil
224+
changeEvents.removeAll()
225+
226+
// Trigger pagination
227+
228+
let paginationObjectIDs = ["d", "f"]
229+
230+
performPagination(paginationObjectIDs)
231+
232+
XCTAssertTrue(performPaginationCompletionResult)
233+
234+
XCTAssertEqual(controller.sections.count, 1)
235+
XCTAssertEqual(controller.sections[0].fetchedIDs, objectIDs + paginationObjectIDs)
236+
237+
XCTAssertEqual(changeEvents.count, 2)
238+
XCTAssertEqual(changeEvents[0].change, FetchedResultsChange.insert(location: IndexPath(item: 3, section: 0)))
239+
XCTAssertEqual(changeEvents[0].object.id, "d")
240+
XCTAssertEqual(changeEvents[1].change, FetchedResultsChange.insert(location: IndexPath(item: 4, section: 0)))
241+
XCTAssertEqual(changeEvents[1].object.id, "f")
242+
}
243+
244+
func testPaginationDoesNotHaveObjects() async throws {
245+
controller = PaginatingFetchedResultsController(
246+
definition: createFetchDefinition(),
247+
debounceInsertsAndReloads: false
248+
)
249+
controller.setDelegate(self)
250+
251+
// Fetch some objects
252+
253+
let objectIDs = ["a", "b", "c"]
254+
255+
try performFetch(objectIDs)
256+
257+
fetchCompletion = nil
258+
paginationCurrentResults = nil
259+
paginationRequestCompletion = nil
260+
performPaginationCompletionResult = nil
261+
changeEvents.removeAll()
262+
263+
// Trigger pagination
264+
265+
let paginationObjectIDs: [String] = []
266+
267+
performPagination(paginationObjectIDs)
268+
269+
XCTAssertFalse(performPaginationCompletionResult)
270+
}
196271
}
197272

198273
// MARK: - FetchedResultsControllerDelegate
@@ -217,9 +292,11 @@ private extension PaginatingFetchedResultsControllerTestCase {
217292
}
218293

219294
func performPagination(_ objects: [TestObject], file: StaticString = #file, line: UInt = #line) {
220-
controller.performPagination()
295+
controller.performPagination { hasPageResults in
296+
self.performPaginationCompletionResult = hasPageResults
297+
}
221298

222-
self.paginationCompletion(objects)
299+
self.paginationRequestCompletion(objects)
223300

224301
let hasAllObjects = objects.allSatisfy { controller.fetchedObjects.contains($0) }
225302
XCTAssertTrue(hasAllObjects, file: file, line: line)

0 commit comments

Comments
 (0)