diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift index bfa3e4f..f6a2c27 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift @@ -25,7 +25,7 @@ extension Modern.PlacemarksDemo { Modern.PlacemarksDemo.dataStack.perform( asynchronous: { (transaction) in - let place = self.place.asEditable(in: transaction) + let place = self.$place?.asEditable(in: transaction) place?.annotation = .init(coordinate: coordinate) }, completion: { _ in } @@ -42,7 +42,7 @@ extension Modern.PlacemarksDemo { _ = try? Modern.PlacemarksDemo.dataStack.perform( synchronous: { (transaction) in - let place = self.place.asEditable(in: transaction) + let place = self.$place?.asEditable(in: transaction) place?.setRandomLocation() } ) @@ -71,22 +71,25 @@ extension Modern.PlacemarksDemo { // MARK: Internal - @ObservedObject - var place: ObjectPublisher + @LiveObject(Modern.PlacemarksDemo.placePublisher) + var place: ObjectSnapshot? init() { - self.place = Modern.PlacemarksDemo.placePublisher - self.sinkCancellable = self.place.sink( + self.sinkCancellable = self.$place?.reactive.snapshot().sink( receiveCompletion: { _ in // Deleted, do nothing }, receiveValue: { [self] (snapshot) in + guard let snapshot = snapshot else { + + return + } self.geocoder.geocode(place: snapshot) { (title, subtitle) in - guard self.place.snapshot == snapshot else { + guard self.place == snapshot else { return } @@ -104,22 +107,29 @@ extension Modern.PlacemarksDemo { // MARK: View var body: some View { - Modern.PlacemarksDemo.MapView( - place: self.place.snapshot, - onTap: { coordinate in + + Group { + + if let place = self.place { - self.demoAsynchronousTransaction(coordinate: coordinate) + Modern.PlacemarksDemo.MapView( + place: place, + onTap: { coordinate in + + self.demoAsynchronousTransaction(coordinate: coordinate) + } + ) + .overlay( + InstructionsView( + ("Random", "Sets random coordinate"), + ("Tap", "Sets to tapped coordinate") + ) + .padding(.leading, 10) + .padding(.bottom, 40), + alignment: .bottomLeading + ) } - ) - .overlay( - InstructionsView( - ("Random", "Sets random coordinate"), - ("Tap", "Sets to tapped coordinate") - ) - .padding(.leading, 10) - .padding(.bottom, 40), - alignment: .bottomLeading - ) + } .navigationBarTitle("Placemarks") .navigationBarItems( trailing: Button("Random") { @@ -132,7 +142,7 @@ extension Modern.PlacemarksDemo { // MARK: Private - private var sinkCancellable: AnyCancellable? = nil + private var sinkCancellable: AnyCancellable? private let geocoder = Modern.PlacemarksDemo.Geocoder() } } diff --git a/README.md b/README.md index 81e2482..94a9044 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep ### Features -- **SwiftUI and Combine API utilities.** +- **🆕SwiftUI and Combine API utilities.** `ListPublisher`s and `ObjectPublisher`s now have their `@LiveList` and `@LiveObject` SwiftUI property wrappers. Combine `Publisher` s are also available through the `ListPublisher.reactive`, `ObjectPublisher.reactive`, and `DataStack.reactive` namespaces. - **Backwards-portable DiffableDataSources implementation!** `UITableViews` and `UICollectionViews` now have a new ally: `ListPublisher`s provide diffable snapshots that make reloading animations very easy and very safe. Say goodbye to `UITableViews` and `UICollectionViews` reload errors! - **💎Tight design around Swift’s code elegance and type safety.** CoreStore fully utilizes Swift's community-driven language features. - **🚦Safer concurrency architecture.** CoreStore makes it hard to fall into common concurrency mistakes. The main `NSManagedObjectContext` is strictly read-only, while all updates are done through serial *transactions*. *(See [Saving and processing transactions](#saving-and-processing-transactions))* diff --git a/Sources/ListPublisher.swift b/Sources/ListPublisher.swift index 95446fb..0ac296b 100644 --- a/Sources/ListPublisher.swift +++ b/Sources/ListPublisher.swift @@ -25,16 +25,6 @@ import CoreData -#if canImport(Combine) -import Combine - -#endif - -#if canImport(SwiftUI) -import SwiftUI - -#endif - // MARK: - ListPublisher @@ -89,13 +79,8 @@ public final class ListPublisher: Hashable { */ public fileprivate(set) var snapshot: ListSnapshot = .init() { - willSet { - - self.willChange() - } didSet { - self.didChange() self.notifyObservers() } } @@ -315,11 +300,6 @@ public final class ListPublisher: Hashable { self.fetchedResultsControllerDelegate.fetchedResultsController = nil self.observers.removeAllObjects() } - - - // MARK: FilePrivate - - fileprivate let rawObjectWillChange: Any? // MARK: Private @@ -375,21 +355,6 @@ public final class ListPublisher: Hashable { applyFetchClauses: applyFetchClauses ) - if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { - - #if canImport(Combine) - self.rawObjectWillChange = ObservableObjectPublisher() - - #else - self.rawObjectWillChange = nil - - #endif - } - else { - - self.rawObjectWillChange = nil - } - self.fetchedResultsControllerDelegate.handler = self try! self.fetchedResultsController.performFetchFromSpecifiedStores() @@ -428,143 +393,3 @@ extension ListPublisher: FetchedDiffableDataSourceSnapshotHandler { ) } } - - -#if canImport(Combine) -import Combine - -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -extension ListPublisher: ObservableObject {} - -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -extension ListPublisher: Publisher { - - // MARK: Publisher - - public typealias Output = ListSnapshot - public typealias Failure = Never - - public func receive(subscriber: S) where S.Input == Output, S.Failure == Failure { - - subscriber.receive( - subscription: ListSnapshotSubscription( - publisher: self, - subscriber: subscriber - ) - ) - } - - - // MARK: - ListSnapshotSubscriber - - fileprivate final class ListSnapshotSubscriber: Subscriber { - - // MARK: Subscriber - - typealias Failure = Never - - func receive(subscription: Subscription) { - - subscription.request(.unlimited) - } - - func receive(_ input: Output) -> Subscribers.Demand { - - return .unlimited - } - - func receive(completion: Subscribers.Completion) {} - } - - - // MARK: - ListSnapshotSubscription - - fileprivate final class ListSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { - - // MARK: FilePrivate - - init(publisher: ListPublisher, subscriber: S) { - - self.publisher = publisher - self.subscriber = subscriber - } - - - // MARK: Subscription - - func request(_ demand: Subscribers.Demand) { - - guard demand > 0 else { - - return - } - self.publisher.addObserver(self) { [weak self] (publisher) in - - guard let self = self, let subscriber = self.subscriber else { - - return - } - _ = subscriber.receive(publisher.snapshot) - } - } - - - // MARK: Cancellable - - func cancel() { - self.publisher.removeObserver(self) - self.subscriber = nil - } - - - // MARK: Private - - private let publisher: ListPublisher - private var subscriber: S? - } -} - -#endif - -// MARK: - ListPublisher - -extension ListPublisher { - - // MARK: ObservableObject - - #if canImport(Combine) - - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - public var objectWillChange: ObservableObjectPublisher { - - return self.rawObjectWillChange! as! ObservableObjectPublisher - } - - #endif - - fileprivate func willChange() { - - guard #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) else { - - return - } - #if canImport(Combine) - - #if canImport(SwiftUI) - withAnimation { - - self.objectWillChange.send() - } - - #endif - - self.objectWillChange.send() - - #endif - } - - fileprivate func didChange() { - - // nothing - } -} diff --git a/Sources/LiveObject.swift b/Sources/LiveObject.swift index 76519ad..b415baf 100644 --- a/Sources/LiveObject.swift +++ b/Sources/LiveObject.swift @@ -76,6 +76,11 @@ public struct LiveObject: DynamicProperty { return self.observer.item } + public var projectedValue: ObjectPublisher? { + + return self.observer.objectPublisher + } + // MARK: DynamicProperty diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 958f4ef..c958bee 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -25,16 +25,6 @@ import CoreData -#if canImport(Combine) -import Combine - -#endif - -#if canImport(SwiftUI) -import SwiftUI - -#endif - // MARK: - ObjectPublisher @@ -226,26 +216,10 @@ public final class ObjectPublisher: ObjectRepresentation, Hash // MARK: FilePrivate - fileprivate let rawObjectWillChange: Any? - fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot?) { self.id = objectID self.context = context - if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { - - #if canImport(Combine) - self.rawObjectWillChange = ObservableObjectPublisher() - - #else - self.rawObjectWillChange = nil - - #endif - } - else { - - self.rawObjectWillChange = nil - } self.$lazySnapshot.initialize { [weak self] in guard let self = self else { @@ -262,16 +236,12 @@ public final class ObjectPublisher: ObjectRepresentation, Hash self.object = nil - self.willChange() self.$lazySnapshot.reset({ nil }) - self.didChange() self.notifyObservers() } else if updatedIDs.contains(objectID) { - self.willChange() self.$lazySnapshot.reset({ initializer(objectID, context) }) - self.didChange() self.notifyObservers() } } @@ -305,154 +275,6 @@ public final class ObjectPublisher: ObjectRepresentation, Hash } -#if canImport(Combine) -import Combine - -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -extension ObjectPublisher: ObservableObject {} - -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -extension ObjectPublisher: Publisher { - - // MARK: Publisher - - public typealias Output = ObjectSnapshot - public typealias Failure = Never - - public func receive(subscriber: S) where S.Input == Output, S.Failure == Failure { - - subscriber.receive( - subscription: ObjectSnapshotSubscription( - publisher: self, - subscriber: subscriber - ) - ) - } - - - // MARK: - ObjectSnapshotSubscriber - - fileprivate final class ObjectSnapshotSubscriber: Subscriber { - - // MARK: Subscriber - - typealias Failure = Never - - func receive(subscription: Subscription) { - - subscription.request(.unlimited) - } - - func receive(_ input: Output) -> Subscribers.Demand { - - return .unlimited - } - - func receive(completion: Subscribers.Completion) {} - } - - - // MARK: - ObjectSnapshotSubscription - - fileprivate final class ObjectSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { - - // MARK: FilePrivate - - init(publisher: ObjectPublisher, subscriber: S) { - - self.publisher = publisher - self.subscriber = subscriber - } - - - // MARK: Subscription - - func request(_ demand: Subscribers.Demand) { - - guard demand > 0 else { - - return - } - self.publisher.addObserver(self) { [weak self] (publisher) in - - guard let self = self, let subscriber = self.subscriber else { - - return - } - if let snapshot = publisher.snapshot { - - _ = subscriber.receive(snapshot) - } - else { - - subscriber.receive(completion: .finished) - } - } - } - - - // MARK: Cancellable - - func cancel() { - self.publisher.removeObserver(self) - self.subscriber = nil - } - - - // MARK: Private - - private let publisher: ObjectPublisher - private var subscriber: S? - } -} - -#endif - -// MARK: - ObjectPublisher - -extension ObjectPublisher { - - // MARK: ObservableObject - - #if canImport(Combine) - - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - public var objectWillChange: ObservableObjectPublisher { - - return self.rawObjectWillChange! as! ObservableObjectPublisher - } - - #endif - - fileprivate func willChange() { - - guard #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) else { - - return - } - #if canImport(Combine) - - #if canImport(SwiftUI) - - withAnimation { - - self.objectWillChange.send() - } - - #endif - - self.objectWillChange.send() - - #endif - } - - fileprivate func didChange() { - - // nothing - } -} - - // MARK: - ObjectPublisher where O: NSManagedObject extension ObjectPublisher where O: NSManagedObject {