generated from bitwarden/template
-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[BITAU-148] [BITAU-154] Add Sync Service to the PM app (#977)
- Loading branch information
1 parent
59c7e1a
commit bc5a4b3
Showing
18 changed files
with
1,115 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import Combine | ||
import CoreData | ||
|
||
// MARK: - FetchedResultsPublisher | ||
|
||
/// A Combine publisher that publishes the initial result set and any future data changes for a | ||
/// Core Data fetch request. | ||
/// | ||
/// Adapted from https://gist.github.com/darrarski/28d2f5a28ef2c5669d199069c30d3d52 | ||
/// | ||
class FetchedResultsPublisher<ResultType>: Publisher where ResultType: NSFetchRequestResult { | ||
// MARK: Types | ||
|
||
typealias Output = [ResultType] | ||
|
||
typealias Failure = Error | ||
|
||
// MARK: Properties | ||
|
||
/// The managed object context that the fetch request is executed against. | ||
let context: NSManagedObjectContext | ||
|
||
/// The fetch request used to get the objects. | ||
let request: NSFetchRequest<ResultType> | ||
|
||
// MARK: Initialization | ||
|
||
/// Initialize a `FetchedResultsPublisher`. | ||
/// | ||
/// - Parameters: | ||
/// - context: The managed object context that the fetch request is executed against. | ||
/// - request: The fetch request used to get the objects. | ||
/// | ||
init(context: NSManagedObjectContext, request: NSFetchRequest<ResultType>) { | ||
self.context = context | ||
self.request = request | ||
} | ||
|
||
// MARK: Publisher | ||
|
||
func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output { | ||
subscriber.receive(subscription: FetchedResultsSubscription( | ||
context: context, | ||
request: request, | ||
subscriber: subscriber | ||
)) | ||
} | ||
} | ||
|
||
// MARK: - FetchedResultsSubscription | ||
|
||
/// A `Subscription` to a `FetchedResultsPublisher` which fetches results from Core Data via a | ||
/// `NSFetchedResultsController` and notifies the subscriber of any changes to the data. | ||
/// | ||
private final class FetchedResultsSubscription<SubscriberType, ResultType>: NSObject, Subscription, | ||
NSFetchedResultsControllerDelegate | ||
where SubscriberType: Subscriber, | ||
SubscriberType.Input == [ResultType], | ||
SubscriberType.Failure == Error, | ||
ResultType: NSFetchRequestResult { | ||
// MARK: Properties | ||
|
||
/// The fetched results controller to manage the results of a Core Data fetch request. | ||
private var controller: NSFetchedResultsController<ResultType>? | ||
|
||
/// The current demand from the subscriber. | ||
private var demand: Subscribers.Demand = .none | ||
|
||
/// Whether the subscription has changes to send to the subscriber. | ||
private var hasChangesToSend = false | ||
|
||
/// The subscriber to the subscription that is notified of the fetched results. | ||
private var subscriber: SubscriberType? | ||
|
||
// MARK: Initialization | ||
|
||
/// Initialize a `FetchedResultsSubscription`. | ||
/// | ||
/// - Parameters: | ||
/// - context: The managed object context that the fetch request is executed against. | ||
/// - request: The fetch request used to get the objects. | ||
/// - subscriber: The subscriber to the subscription that is notified of the fetched results. | ||
/// | ||
init( | ||
context: NSManagedObjectContext, | ||
request: NSFetchRequest<ResultType>, | ||
subscriber: SubscriberType | ||
) { | ||
controller = NSFetchedResultsController( | ||
fetchRequest: request, | ||
managedObjectContext: context, | ||
sectionNameKeyPath: nil, | ||
cacheName: nil | ||
) | ||
self.subscriber = subscriber | ||
|
||
super.init() | ||
|
||
controller?.delegate = self | ||
|
||
do { | ||
try controller?.performFetch() | ||
if controller?.fetchedObjects != nil { | ||
hasChangesToSend = true | ||
fulfillDemand() | ||
} | ||
} catch { | ||
subscriber.receive(completion: .failure(error)) | ||
} | ||
} | ||
|
||
// MARK: Subscription | ||
|
||
func request(_ demand: Subscribers.Demand) { | ||
self.demand += demand | ||
fulfillDemand() | ||
} | ||
|
||
// MARK: Cancellable | ||
|
||
func cancel() { | ||
controller = nil | ||
subscriber = nil | ||
} | ||
|
||
// MARK: NSFetchedResultsControllerDelegate | ||
|
||
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { | ||
hasChangesToSend = true | ||
fulfillDemand() | ||
} | ||
|
||
// MARK: Private | ||
|
||
private func fulfillDemand() { | ||
guard demand > 0, hasChangesToSend, | ||
let subscriber, | ||
let fetchedObjects = controller?.fetchedObjects | ||
else { return } | ||
|
||
hasChangesToSend = false | ||
demand -= 1 | ||
demand += subscriber.receive(fetchedObjects) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Combine | ||
|
||
extension Future { | ||
/// Initialize a `Future` with an async throwing closure. | ||
/// | ||
/// - Parameter attemptToFulfill: A closure that the publisher invokes when it emits a value or | ||
/// an error occurs. | ||
/// | ||
convenience init(_ attemptToFulfill: @Sendable @escaping () async throws -> Output) where Failure == Error { | ||
self.init { promise in | ||
Task { | ||
do { | ||
let result = try await attemptToFulfill() | ||
promise(.success(result)) | ||
} catch { | ||
promise(.failure(error)) | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import Combine | ||
|
||
extension Publisher { | ||
/// Maps the output of a publisher to a different type, discarding any `nil` values. | ||
/// | ||
/// - Parameters: | ||
/// - maxPublishers: The maximum number of concurrent publisher subscriptions. | ||
/// - transform: The transform to apply to each output. | ||
/// - Returns: A publisher containing any non-`nil` mapped values. | ||
/// | ||
func asyncCompactMap<T>( | ||
maxPublishers: Subscribers.Demand = .max(1), | ||
_ transform: @escaping (Output) async -> T? | ||
) -> Publishers.CompactMap<Publishers.FlatMap<Future<T?, Never>, Self>, T> { | ||
asyncMap(maxPublishers: maxPublishers, transform) | ||
.compactMap { $0 } | ||
} | ||
|
||
/// Maps the output of a publisher to a different type. | ||
/// | ||
/// - Parameters: | ||
/// - maxPublishers: The maximum number of concurrent publisher subscriptions. | ||
/// - transform: The transform to apply to each output. | ||
/// - Returns: A publisher containing the mapped values. | ||
/// | ||
func asyncMap<T>( | ||
maxPublishers: Subscribers.Demand = .max(1), | ||
_ transform: @escaping (Output) async -> T | ||
) -> Publishers.FlatMap<Future<T, Never>, Self> { | ||
flatMap(maxPublishers: maxPublishers) { value in | ||
Future { promise in | ||
Task { | ||
let output = await transform(value) | ||
promise(.success(output)) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension Publisher where Failure == Error { | ||
/// Maps the output of a publisher to a different type which could throw an error. | ||
/// | ||
/// - Parameters: | ||
/// - maxPublishers: The maximum number of concurrent publisher subscriptions. | ||
/// - transform: The transform to apply to each output. | ||
/// - Returns: A publisher containing the mapped values. | ||
/// | ||
func asyncTryMap<T>( | ||
maxPublishers: Subscribers.Demand = .max(1), | ||
_ transform: @escaping (Output) async throws -> T | ||
) -> Publishers.FlatMap<Future<T, Failure>, Self> { | ||
flatMap(maxPublishers: maxPublishers) { value in | ||
Future { | ||
try await transform(value) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.