Skip to content

Commit 76eee26

Browse files
committed
Add subscription endpoints that allow separate predicates for fetching and change tracking
enable-different-request-for-frc-in-subscriptions
1 parent e5ec1cc commit 76eee26

9 files changed

Lines changed: 1269 additions & 75 deletions

Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift

Lines changed: 314 additions & 0 deletions
Large diffs are not rendered by default.

Sources/CoreDataRepository/CoreDataRepository+Fetch.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ extension CoreDataRepository {
3939
}
4040
}
4141

42+
/// Fetch items from the store with a ``NSFetchRequest`` and receive updates as the store changes.
43+
///
44+
/// This endpoint allows separate fetch requests for fetching and change tracking. There are times where CoreData
45+
/// will not recognize changes with a specific predicate. The fix, is to use a simplified predicate for change
46+
/// tracking and the full predicate for fetching.
47+
@inlinable
48+
public func fetchSubscription<Model: FetchableUnmanagedModel>(
49+
request: NSFetchRequest<Model.ManagedModel>,
50+
changeTrackingRequest: NSFetchRequest<Model.ManagedModel>,
51+
of _: Model.Type
52+
) -> AsyncStream<Result<[Model], CoreDataError>> {
53+
AsyncStream { continuation in
54+
let subscription = FetchSubscription(
55+
fetchRequest: request,
56+
fetchResultControllerRequest: changeTrackingRequest,
57+
context: context.childContext(),
58+
continuation: continuation
59+
)
60+
continuation.onTermination = { _ in
61+
subscription.cancel()
62+
}
63+
subscription.manualFetch()
64+
}
65+
}
66+
4267
/// Fetch items from the store with a ``NSFetchRequest`` and receive updates as the store changes.
4368
@inlinable
4469
public func fetchThrowingSubscription<Model: FetchableUnmanagedModel>(
@@ -58,6 +83,31 @@ extension CoreDataRepository {
5883
}
5984
}
6085

86+
/// Fetch items from the store with a ``NSFetchRequest`` and receive updates as the store changes.
87+
///
88+
/// This endpoint allows separate fetch requests for fetching and change tracking. There are times where CoreData
89+
/// will not recognize changes with a specific predicate. The fix, is to use a simplified predicate for change
90+
/// tracking and the full predicate for fetching.
91+
@inlinable
92+
public func fetchThrowingSubscription<Model: FetchableUnmanagedModel>(
93+
request: NSFetchRequest<Model.ManagedModel>,
94+
changeTrackingRequest: NSFetchRequest<Model.ManagedModel>,
95+
of _: Model.Type
96+
) -> AsyncThrowingStream<[Model], Error> {
97+
AsyncThrowingStream { continuation in
98+
let subscription = FetchThrowingSubscription(
99+
fetchRequest: request,
100+
fetchResultControllerRequest: changeTrackingRequest,
101+
context: context.childContext(),
102+
continuation: continuation
103+
)
104+
continuation.onTermination = { _ in
105+
subscription.cancel()
106+
}
107+
subscription.manualFetch()
108+
}
109+
}
110+
61111
/// Fetch items from the store with a ``NSFetchRequest`` and transform the results.
62112
@inlinable
63113
public func fetch<Managed: NSManagedObject, Output>(

Sources/CoreDataRepository/Internal/AggregateSubscription.swift

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,39 +52,21 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
5252
) {
5353
let request: NSFetchRequest<NSDictionary>
5454
do {
55-
request = try NSFetchRequest<NSDictionary>.request(
55+
request = try NSFetchRequest.request(
5656
function: function,
5757
predicate: predicate,
5858
entityDesc: entityDesc,
5959
attributeDesc: attributeDesc,
6060
groupBy: groupBy
6161
)
62-
} catch let error as CoreDataError {
63-
self.init(
64-
fetchRequest: NSFetchRequest(),
65-
fetchResultControllerRequest: NSFetchRequest(),
66-
context: context,
67-
continuation: continuation
68-
)
69-
self.fail(error)
70-
return
71-
} catch let error as CocoaError {
72-
self.init(
73-
fetchRequest: NSFetchRequest(),
74-
fetchResultControllerRequest: NSFetchRequest(),
75-
context: context,
76-
continuation: continuation
77-
)
78-
self.fail(.cocoa(error))
79-
return
8062
} catch {
8163
self.init(
8264
fetchRequest: NSFetchRequest(),
8365
fetchResultControllerRequest: NSFetchRequest(),
8466
context: context,
8567
continuation: continuation
8668
)
87-
fail(.unknown(error as NSError))
69+
fail(error)
8870
return
8971
}
9072
guard entityDesc == attributeDesc.entity else {
@@ -112,4 +94,52 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
11294
}
11395
self.init(request: request, context: context, continuation: continuation)
11496
}
97+
98+
@usableFromInline
99+
convenience init(
100+
function: CoreDataRepository.AggregateFunction,
101+
context: NSManagedObjectContext,
102+
predicate: NSPredicate,
103+
changeTrackingRequest: NSFetchRequest<NSManagedObject>,
104+
entityDesc: NSEntityDescription,
105+
attributeDesc: NSAttributeDescription,
106+
groupBy: NSAttributeDescription? = nil,
107+
continuation: AsyncStream<Result<Value, CoreDataError>>.Continuation
108+
) {
109+
let request: NSFetchRequest<NSDictionary>
110+
do {
111+
request = try NSFetchRequest.request(
112+
function: function,
113+
predicate: predicate,
114+
entityDesc: entityDesc,
115+
attributeDesc: attributeDesc,
116+
groupBy: groupBy
117+
)
118+
} catch {
119+
self.init(
120+
fetchRequest: NSFetchRequest(),
121+
fetchResultControllerRequest: NSFetchRequest(),
122+
context: context,
123+
continuation: continuation
124+
)
125+
fail(error)
126+
return
127+
}
128+
guard entityDesc == attributeDesc.entity else {
129+
self.init(
130+
fetchRequest: NSFetchRequest(),
131+
fetchResultControllerRequest: NSFetchRequest(),
132+
context: context,
133+
continuation: continuation
134+
)
135+
fail(.propertyDoesNotMatchEntity)
136+
return
137+
}
138+
self.init(
139+
fetchRequest: request,
140+
fetchResultControllerRequest: changeTrackingRequest,
141+
context: context,
142+
continuation: continuation
143+
)
144+
}
115145
}

Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,43 +52,25 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: ThrowingSu
5252
entityDesc: NSEntityDescription,
5353
attributeDesc: NSAttributeDescription,
5454
groupBy: NSAttributeDescription? = nil,
55-
continuation: AsyncThrowingStream<Value, Error>.Continuation
55+
continuation: AsyncThrowingStream<Value, any Error>.Continuation
5656
) {
5757
let request: NSFetchRequest<NSDictionary>
5858
do {
59-
request = try NSFetchRequest<NSDictionary>.request(
59+
request = try NSFetchRequest.request(
6060
function: function,
6161
predicate: predicate,
6262
entityDesc: entityDesc,
6363
attributeDesc: attributeDesc,
6464
groupBy: groupBy
6565
)
66-
} catch let error as CoreDataError {
67-
self.init(
68-
fetchRequest: NSFetchRequest(),
69-
fetchResultControllerRequest: NSFetchRequest(),
70-
context: context,
71-
continuation: continuation
72-
)
73-
self.fail(error)
74-
return
75-
} catch let error as CocoaError {
76-
self.init(
77-
fetchRequest: NSFetchRequest(),
78-
fetchResultControllerRequest: NSFetchRequest(),
79-
context: context,
80-
continuation: continuation
81-
)
82-
self.fail(.cocoa(error))
83-
return
8466
} catch {
8567
self.init(
8668
fetchRequest: NSFetchRequest(),
8769
fetchResultControllerRequest: NSFetchRequest(),
8870
context: context,
8971
continuation: continuation
9072
)
91-
fail(.unknown(error as NSError))
73+
fail(error)
9274
return
9375
}
9476
guard entityDesc == attributeDesc.entity else {
@@ -116,4 +98,52 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: ThrowingSu
11698
}
11799
self.init(request: request, context: context, continuation: continuation)
118100
}
101+
102+
@usableFromInline
103+
convenience init(
104+
function: CoreDataRepository.AggregateFunction,
105+
context: NSManagedObjectContext,
106+
predicate: NSPredicate,
107+
changeTrackingRequest: NSFetchRequest<NSManagedObject>,
108+
entityDesc: NSEntityDescription,
109+
attributeDesc: NSAttributeDescription,
110+
groupBy: NSAttributeDescription? = nil,
111+
continuation: AsyncThrowingStream<Value, any Error>.Continuation
112+
) {
113+
let request: NSFetchRequest<NSDictionary>
114+
do {
115+
request = try NSFetchRequest.request(
116+
function: function,
117+
predicate: predicate,
118+
entityDesc: entityDesc,
119+
attributeDesc: attributeDesc,
120+
groupBy: groupBy
121+
)
122+
} catch {
123+
self.init(
124+
fetchRequest: NSFetchRequest(),
125+
fetchResultControllerRequest: NSFetchRequest(),
126+
context: context,
127+
continuation: continuation
128+
)
129+
fail(error)
130+
return
131+
}
132+
guard entityDesc == attributeDesc.entity else {
133+
self.init(
134+
fetchRequest: NSFetchRequest(),
135+
fetchResultControllerRequest: NSFetchRequest(),
136+
context: context,
137+
continuation: continuation
138+
)
139+
fail(.propertyDoesNotMatchEntity)
140+
return
141+
}
142+
self.init(
143+
fetchRequest: request,
144+
fetchResultControllerRequest: changeTrackingRequest,
145+
context: context,
146+
continuation: continuation
147+
)
148+
}
119149
}

Sources/CoreDataRepository/Internal/CountSubscription.swift

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ final class CountSubscription<Value: Numeric & Sendable>: Subscription<Value, NS
1414
{
1515
@usableFromInline
1616
override func fetch() {
17-
frc.managedObjectContext.perform { [weak self, frc] in
17+
frc.managedObjectContext.perform { [weak self, frc, request] in
1818
if (frc.fetchedObjects ?? []).isEmpty {
1919
self?.start()
2020
}
2121
do {
22-
let count = try frc.managedObjectContext.count(for: frc.fetchRequest)
22+
let count = try frc.managedObjectContext.count(for: request)
2323
self?.send(Value(exactly: count) ?? Value.zero)
2424
} catch let error as CocoaError {
2525
self?.fail(.cocoa(error))
@@ -38,38 +38,49 @@ final class CountSubscription<Value: Numeric & Sendable>: Subscription<Value, NS
3838
) {
3939
let request: NSFetchRequest<NSDictionary>
4040
do {
41-
request = try NSFetchRequest<NSDictionary>.countRequest(
41+
request = try NSFetchRequest.countRequest(
4242
predicate: predicate,
4343
entityDesc: entityDesc
4444
)
45-
} catch let error as CoreDataError {
46-
self.init(
47-
fetchRequest: NSFetchRequest(),
48-
fetchResultControllerRequest: NSFetchRequest(),
49-
context: context,
50-
continuation: continuation
51-
)
52-
self.fail(error)
53-
return
54-
} catch let error as CocoaError {
45+
} catch {
5546
self.init(
5647
fetchRequest: NSFetchRequest(),
5748
fetchResultControllerRequest: NSFetchRequest(),
5849
context: context,
5950
continuation: continuation
6051
)
61-
self.fail(.cocoa(error))
52+
fail(error)
6253
return
54+
}
55+
self.init(request: request, context: context, continuation: continuation)
56+
}
57+
58+
@usableFromInline
59+
convenience init(
60+
context: NSManagedObjectContext,
61+
predicate: NSPredicate,
62+
changeTrackingRequest: NSFetchRequest<NSManagedObject>,
63+
entityDesc: NSEntityDescription,
64+
continuation: AsyncStream<Result<Value, CoreDataError>>.Continuation
65+
) {
66+
let request: NSFetchRequest<NSDictionary>
67+
do {
68+
request = try NSFetchRequest.countRequest(predicate: predicate, entityDesc: entityDesc)
6369
} catch {
6470
self.init(
6571
fetchRequest: NSFetchRequest(),
6672
fetchResultControllerRequest: NSFetchRequest(),
6773
context: context,
6874
continuation: continuation
6975
)
70-
fail(.unknown(error as NSError))
76+
fail(error)
7177
return
7278
}
73-
self.init(request: request, context: context, continuation: continuation)
79+
self.init(
80+
fetchRequest: request,
81+
fetchResultControllerRequest: changeTrackingRequest,
82+
context: context,
83+
continuation: continuation
84+
)
7485
}
7586
}

0 commit comments

Comments
 (0)