SwiftUI utilities done (for now)

This commit is contained in:
John Estropia
2021-02-21 10:56:27 +09:00
parent f2efe175e5
commit d7b852fca4
25 changed files with 1154 additions and 203 deletions

View File

@@ -72,7 +72,7 @@ extension Modern.ColorsDemo {
// MARK: Private
@LiveList(Modern.ColorsDemo.palettesPublisher)
private var palettes: LiveList<Modern.ColorsDemo.Palette>.Items
private var palettes: ListSnapshot
private let listView: (
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,

View File

@@ -30,14 +30,19 @@ extension Modern.ColorsDemo.UIKit {
return UIViewControllerType(self.palette)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
uiViewController.palette = Modern.ColorsDemo.dataStack.monitorObject(
self.palette.object!
)
}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
// MARK: Private
private let palette: ObjectPublisher<Modern.ColorsDemo.Palette>
private var palette: ObjectPublisher<Modern.ColorsDemo.Palette>
}
}

View File

@@ -46,7 +46,10 @@ extension Modern {
static let palettesPublisher: ListPublisher<Modern.ColorsDemo.Palette> = Modern.ColorsDemo.dataStack.publishList(
From<Modern.ColorsDemo.Palette>()
.sectionBy(\.$colorGroup)
.sectionBy(
\.$colorGroup,
sectionIndexTransformer: { $0?.first?.uppercased() }
)
.where(Modern.ColorsDemo.filter.whereClause())
.orderBy(.ascending(\.$hue))
)
@@ -57,7 +60,10 @@ extension Modern {
try! Modern.ColorsDemo.palettesPublisher.refetch(
From<Modern.ColorsDemo.Palette>()
.sectionBy(\.$colorGroup)
.sectionBy(
\.$colorGroup,
sectionIndexTransformer: { $0?.first?.uppercased() }
)
.where(self.filter.whereClause())
.orderBy(.ascending(\.$hue))
)

View File

@@ -15,10 +15,10 @@ extension Modern.ColorsDemo.SwiftUI {
struct DetailView: View {
/**
Sample 1: Setting an `ObjectPublisher` declared as an `@ObservedObject`
Sample 1: Using a `LiveObject` to observe object changes. Note that the `ObjectSnapshot` is always `Optional`
*/
@LiveObject
private var palette: LiveObject<Modern.ColorsDemo.Palette>.Item?
private var palette: ObjectSnapshot<Modern.ColorsDemo.Palette>?
/**
Sample 2: Setting properties that can be binded to controls (`Slider` in this case) by creating custom `@Binding` instances that updates the store when the values change.

View File

@@ -14,22 +14,22 @@ extension Modern.ColorsDemo.SwiftUI {
struct ItemView: View {
/**
Sample 1: Setting an `ObjectPublisher` declared as an `@ObservedObject`
Sample 1: Using a `LiveObject` to observe object changes. Note that the `ObjectSnapshot` is always `Optional`
*/
@LiveObject
private var palette: LiveObject<Modern.ColorsDemo.Palette>.Item?
// MARK: Internal
private var palette: ObjectSnapshot<Modern.ColorsDemo.Palette>?
/**
Sample 2: Initializing a `LiveObject` from an existing `ObjectPublisher`
*/
internal init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
self._palette = .init(palette)
}
// MARK: View
/**
Sample 3: Readding values directly from the `ObjectSnapshot`
*/
var body: some View {
if let palette = self.palette {

View File

@@ -14,22 +14,35 @@ extension Modern.ColorsDemo.SwiftUI {
struct ListView: View {
/**
Sample 1: Setting a sectioned `ListPublisher` declared as an `@ObservedObject`
Sample 1: Using a `LiveList` to observe list changes
*/
@LiveList
private var palettes: LiveList<Modern.ColorsDemo.Palette>.Items
private var palettes: ListSnapshot<Modern.ColorsDemo.Palette>
/**
Sample 2: Assigning sections and items of the `ListPublisher` to corresponding `View`s
Sample 2: Initializing a `LiveList` from an existing `ListPublisher`
*/
var body: some View {
return List {
init(
listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) {
self._palettes = .init(listPublisher)
self.onPaletteTapped = onPaletteTapped
}
/**
Sample 3: Assigning sections and items of the `ListSnapshot` to corresponding `View`s by using the correct `ForEach` overloads.
*/
var body: some View {
List {
ForEachSection(in: self.palettes) { sectionID, palettes in
ForEach(sectionIn: self.palettes) { section in
Section(header: Text(sectionID)) {
Section(header: Text(section.sectionID)) {
ForEach(palettes) { palette in
ForEach(objectIn: section) { palette in
Button(
action: {
@@ -45,7 +58,7 @@ extension Modern.ColorsDemo.SwiftUI {
}
.onDelete { itemIndices in
self.deleteColors(at: itemIndices, in: sectionID)
self.deleteColors(at: itemIndices, in: section.sectionID)
}
}
}
@@ -53,18 +66,6 @@ extension Modern.ColorsDemo.SwiftUI {
.animation(.default)
.listStyle(PlainListStyle())
.edgesIgnoringSafeArea([])
}
// MARK: Internal
init(
listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) {
self._palettes = .init(listPublisher)
self.onPaletteTapped = onPaletteTapped
}

View File

@@ -40,6 +40,15 @@ extension Modern.ColorsDemo.UIKit {
/**
Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ObjectMonitor`s safely remove deallocated observers automatically.
*/
var palette: ObjectMonitor<Modern.ColorsDemo.Palette> {
didSet {
oldValue.removeObserver(self)
self.startMonitoringObject()
}
}
deinit {
self.palette.removeObserver(self)
@@ -183,9 +192,8 @@ extension Modern.ColorsDemo.UIKit {
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 10
),
containerView.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -10
containerView.centerYAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.centerYAnchor
),
containerView.trailingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
@@ -223,8 +231,6 @@ extension Modern.ColorsDemo.UIKit {
// MARK: Private
private let palette: ObjectMonitor<Modern.ColorsDemo.Palette>
@available(*, unavailable)
required init?(coder: NSCoder) {

View File

@@ -15,9 +15,9 @@ extension Modern.ColorsDemo.UIKit {
final class ListViewController: UITableViewController {
/**
Sample 1: Setting up a `DiffableDataSource.TableViewAdapter` that will manage tableView snapshot updates automatically. We can use the built-in `DiffableDataSource.TableViewAdapter` type directly, but in our case we want to enabled `UITableView` cell deletions so we create a custom subclass `DeletionEnabledDataSource` (see declaration below).
Sample 1: Setting up a `DiffableDataSource.TableViewAdapter` that will manage tableView snapshot updates automatically. We can use the built-in `DiffableDataSource.TableViewAdapter` type directly, but in our case we want to enabled `UITableView` cell deletions so we create a custom subclass `CustomDataSource` (see declaration below).
*/
private lazy var dataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> = DeletionEnabledDataSource(
private lazy var dataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> = CustomDataSource(
tableView: self.tableView,
dataStack: Modern.ColorsDemo.dataStack,
cellProvider: { (tableView, indexPath, palette) in
@@ -54,9 +54,11 @@ extension Modern.ColorsDemo.UIKit {
}
/**
Sample 4: This is the custom `DiffableDataSource.TableViewAdapter` subclass we wrote that enabled swipe-to-delete gestures on the `UITableView`.
Sample 4: This is the custom `DiffableDataSource.TableViewAdapter` subclass we wrote that enabled swipe-to-delete gestures and section index titles on the `UITableView`.
*/
final class DeletionEnabledDataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> {
final class CustomDataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> {
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
@@ -79,6 +81,16 @@ extension Modern.ColorsDemo.UIKit {
break
}
}
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return self.sectionIndexTitlesForAllSections().compactMap({ $0 })
}
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return index
}
}

View File

@@ -56,6 +56,60 @@ extension DataStack {
return context.objectPublisher(objectID: objectID)
}
/**
Creates a `ListPublisher` that satisfy the specified `FetchChainableBuilderType` built from a chain of clauses.
```
let listPublisher = dataStack.listPublisher(
From<MyPersonEntity>()
.where(\.age > 18)
.orderBy(.ascending(\.age))
)
```
Multiple objects may then register themselves to be notified when changes are made to the fetched results.
```
listPublisher.addObserver(self) { (listPublisher) in
// handle changes
}
```
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
- returns: a `ListPublisher` that broadcasts changes to the fetched results
*/
public func publishList<B: FetchChainableBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
return self.publishList(
clauseChain.from,
clauseChain.fetchClauses
)
}
/**
Creates a `ListPublisher` for a sectioned list that satisfy the specified `FetchChainableBuilderType` built from a chain of clauses.
```
let listPublisher = dataStack.listPublisher(
From<MyPersonEntity>()
.sectionBy(\.age, { "\($0!) years old" })
.where(\.age > 18)
.orderBy(.ascending(\.age))
)
```
Multiple objects may then register themselves to be notified when changes are made to the fetched results.
```
listPublisher.addObserver(self) { (listPublisher) in
// handle changes
}
```
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
- returns: a `ListPublisher` that broadcasts changes to the fetched results
*/
public func publishList<B: SectionMonitorBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
return self.publishList(
clauseChain.from,
clauseChain.sectionBy,
clauseChain.fetchClauses
)
}
/**
Creates a `ListPublisher` for the specified `From` and `FetchClause`s. Multiple objects may then register themselves to be notified when changes are made to the fetched results.
@@ -93,32 +147,6 @@ extension DataStack {
)
}
/**
Creates a `ListPublisher` that satisfy the specified `FetchChainableBuilderType` built from a chain of clauses.
```
let listPublisher = dataStack.listPublisher(
From<MyPersonEntity>()
.where(\.age > 18)
.orderBy(.ascending(\.age))
)
```
Multiple objects may then register themselves to be notified when changes are made to the fetched results.
```
listPublisher.addObserver(self) { (listPublisher) in
// handle changes
}
```
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
- returns: a `ListPublisher` that broadcasts changes to the fetched results
*/
public func publishList<B: FetchChainableBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
return self.publishList(
clauseChain.from,
clauseChain.fetchClauses
)
}
/**
Creates a `ListPublisher` for a sectioned list that satisfy the fetch clauses. Multiple objects may then register themselves to be notified when changes are made to the fetched results.
@@ -161,37 +189,9 @@ extension DataStack {
}
)
}
/**
Creates a `ListPublisher` for a sectioned list that satisfy the specified `FetchChainableBuilderType` built from a chain of clauses.
```
let listPublisher = dataStack.listPublisher(
From<MyPersonEntity>()
.sectionBy(\.age, { "\($0!) years old" })
.where(\.age > 18)
.orderBy(.ascending(\.age))
)
```
Multiple objects may then register themselves to be notified when changes are made to the fetched results.
```
listPublisher.addObserver(self) { (listPublisher) in
// handle changes
}
```
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
- returns: a `ListPublisher` that broadcasts changes to the fetched results
*/
public func publishList<B: SectionMonitorBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
return self.publishList(
clauseChain.from,
clauseChain.sectionBy,
clauseChain.fetchClauses
)
}
// MARK: - Deprecated
// MARK: Deprecated
@available(*, deprecated, renamed: "publishObject(_:)")
public func objectPublisher<O: DynamicObject>(_ object: O) -> ObjectPublisher<O> {

View File

@@ -0,0 +1,275 @@
//
// DataStack+Reactive.swift
// CoreStore
//
// Copyright © 2021 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
#if canImport(Combine)
import Combine
// MARK: - DataStack
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
extension DataStack: PublisherCompatible {}
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
extension PublisherProvider where Base == DataStack {
// MARK: Public
/**
Reactive extension for `CoreStore.DataStack`'s `importObject(...)` API. Creates an `ImportableObject` by importing from the specified import source. The event value will be the object instance correctly associated for the `DataStack`.
```
dataStack.reactive
.importObject(
Into<Person>(),
source: ["name": "John"]
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (person) in
XCTAssertNotNil(person)
// ...
}
)
.store(in: &cancellables)
```
- parameter into: an `Into` clause specifying the entity type
- parameter source: the object to import values from
- returns: A `Future` that publishes the imported object. The event value, if not `nil`, will be the object instance correctly associated for the `DataStack`.
*/
public func importObject<O: DynamicObject & ImportableObject>(
_ into: Into<O>,
source: O.ImportSource
) -> Future<O?, CoreStoreError> {
return .init { (promise) in
self.base.perform(
asynchronous: { (transaction) -> O? in
return try transaction.importObject(
into,
source: source
)
},
success: { promise(.success($0.flatMap(self.base.fetchExisting))) },
failure: { promise(.failure($0)) }
)
}
}
/**
Reactive extension for `CoreStore.DataStack`'s `importObject(...)` API. Updates an existing `ImportableObject` by importing values from the specified import source. The event value will be the object instance correctly associated for the `DataStack`.
```
let existingPerson: Person = // ...
dataStack.reactive
.importObject(
existingPerson,
source: ["name": "John", "age": 30]
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (person) in
XCTAssertEqual(person?.age, 30)
// ...
}
)
.store(in: &cancellables)
```
- parameter object: the object to update
- parameter source: the object to import values from
- returns: A `Future` that publishes the imported object. The event value, if not `nil`, will be the object instance correctly associated for the `DataStack`.
*/
public func importObject<O: DynamicObject & ImportableObject>(
_ object: O,
source: O.ImportSource
) -> Future<O?, CoreStoreError> {
return .init { (promise) in
self.base.perform(
asynchronous: { (transaction) -> O? in
guard let object = transaction.edit(object) else {
try transaction.cancel()
}
try transaction.importObject(
object,
source: source
)
return object
},
success: { promise(.success($0.flatMap(self.base.fetchExisting))) },
failure: { promise(.failure($0)) }
)
}
}
/**
Reactive extension for `CoreStore.DataStack`'s `importUniqueObject(...)` API. Updates an existing `ImportableUniqueObject` or creates a new instance by importing from the specified import source. The event value will be the object instance correctly associated for the `DataStack`.
```
dataStack.reactive
.importUniqueObject(
Into<Person>(),
source: ["name": "John", "age": 30]
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (person) in
XCTAssertEqual(person?.age, 30)
// ...
}
)
.store(in: &cancellables)
```
- parameter into: an `Into` clause specifying the entity type
- parameter source: the object to import values from
- returns: A `Future` for the imported object. The event value, if not `nil`, will be the object instance correctly associated for the `DataStack`.
*/
public func importUniqueObject<O: DynamicObject & ImportableUniqueObject>(
_ into: Into<O>,
source: O.ImportSource
) -> Future<O?, CoreStoreError> {
return .init { (promise) in
self.base.perform(
asynchronous: { (transaction) -> O? in
return try transaction.importUniqueObject(
into,
source: source
)
},
success: { promise(.success($0.flatMap(self.base.fetchExisting))) },
failure: { promise(.failure($0)) }
)
}
}
/**
Reactive extension for `CoreStore.DataStack`'s `importUniqueObjects(...)` API. Updates existing `ImportableUniqueObject`s or creates them by importing from the specified array of import sources. `ImportableUniqueObject` methods are called on the objects in the same order as they are in the `sourceArray`, and are returned in an array with that same order. The event values will be object instances correctly associated for the `DataStack`.
```
dataStack.reactive
.importUniqueObjects(
Into<Person>(),
sourceArray: [
["name": "John"],
["name": "Bob"],
["name": "Joe"]
]
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (people) in
XCTAssertEqual(people?.count, 3)
// ...
}
)
.store(in: &cancellables)
```
- Warning: If `sourceArray` contains multiple import sources with same ID, no merging will occur and ONLY THE LAST duplicate will be imported.
- parameter into: an `Into` clause specifying the entity type
- parameter sourceArray: the array of objects to import values from
- parameter preProcess: a closure that lets the caller tweak the internal `UniqueIDType`-to-`ImportSource` mapping to be used for importing. Callers can remove from/add to/update `mapping` and return the updated array from the closure.
- returns: A `Future` for the imported objects. The event values will be the object instances correctly associated for the `DataStack`.
*/
public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence>(
_ into: Into<O>,
sourceArray: S,
preProcess: @escaping (_ mapping: [O.UniqueIDType: O.ImportSource]) throws -> [O.UniqueIDType: O.ImportSource] = { $0 }
) -> Future<[O], CoreStoreError> where S.Iterator.Element == O.ImportSource {
return .init { (promise) in
self.base.perform(
asynchronous: { (transaction) -> [O] in
return try transaction.importUniqueObjects(
into,
sourceArray: sourceArray,
preProcess: preProcess
)
},
success: { promise(.success(self.base.fetchExisting($0))) },
failure: { promise(.failure($0)) }
)
}
}
/**
Reactive extension for `CoreStore.DataStack`'s `perform(asynchronous:...)` API. Performs a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. The event value will be the value returned from the `task` closure. Any errors thrown from inside the `task` will be wrapped in a `CoreStoreError` and reported to the completion `.failure`. To cancel/rollback changes, call `transaction.cancel()`, which throws a `CoreStoreError.userCancelled`.
```
dataStack.reactive
.perform(
asynchronous: { (transaction) -> (inserted: Set<NSManagedObject>, deleted: Set<NSManagedObject>) in
// ...
return (
transaction.insertedObjects(),
transaction.deletedObjects()
)
}
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { value in
let inserted = dataStack.fetchExisting(value0.inserted)
let deleted = dataStack.fetchExisting(value0.deleted)
// ...
}
)
.store(in: &cancellables)
```
- parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: A `Future` whose event value be the value returned from the `task` closure.
*/
public func perform<Output>(
_ asynchronous: @escaping (AsynchronousDataTransaction) throws -> Output
) -> Future<Output, CoreStoreError> {
return .init { (promise) in
self.base.perform(
asynchronous: asynchronous,
success: { promise(.success($0)) },
failure: { promise(.failure($0)) }
)
}
}
}
#endif

View File

@@ -209,6 +209,25 @@ extension DiffableDataSource {
return self.dispatcher.indexPath(for: itemID)
}
/**
Returns the section index title for the specified `section` if the `SectionBy` for this list has provided a `sectionIndexTransformer`
- parameter section: the section index to search for
- returns: the section index title for the specified `section`, or `nil` if not found
*/
public func sectionIndexTitle(for section: Int) -> String? {
return self.dispatcher.sectionIndexTitle(for: section)
}
/**
Returns the section index titles for all sections if the `SectionBy` for this list has provided a `sectionIndexTransformer`
*/
public func sectionIndexTitlesForAllSections() -> [String?] {
return self.dispatcher.sectionIndexTitlesForAllSections()
}
// MARK: Internal

View File

@@ -220,7 +220,7 @@ extension DiffableDataSource {
}
// MARK: - Deprecated
// MARK: Deprecated
extension DiffableDataSource {

View File

@@ -220,7 +220,7 @@ extension DiffableDataSource {
}
// MARK: - Deprecated
// MARK: Deprecated
extension DiffableDataSource {

View File

@@ -157,6 +157,18 @@ extension DiffableDataSource {
@objc
open dynamic func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {}
@objc
open dynamic func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return nil
}
@objc
open dynamic func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return index
}
// MARK: Private
@@ -255,7 +267,7 @@ extension DiffableDataSource {
}
// MARK: - Deprecated
// MARK: Deprecated
extension DiffableDataSource {

View File

@@ -36,21 +36,137 @@ extension ForEach where Content: View {
// MARK: Public
/**
Creates an instance that creates views for each object in a `ListSnapshot`.
```
@LiveList
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(objectIn: self.people) { person in
ProfileView(person)
}
}
.animation(.default)
}
```
- parameter listSnapshot: The `ListSnapshot` that the `ForEach` instance uses to create views dynamically
- parameter content: The view builder that receives an `ObjectPublisher` instance and creates views dynamically.
*/
public init<O: DynamicObject>(
_ listSnapshot: Data,
objectIn listSnapshot: Data,
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
) where Data == LiveList<O>.Items, ID == O.ObjectID {
) where Data == ListSnapshot<O>, ID == O.ObjectID {
self.init(listSnapshot, id: \.cs_objectID, content: content)
}
/**
Creates an instance that creates views for each object in a collection of `ObjectPublisher`s.
```
let people: [ObjectPublisher<Person>]
var body: some View {
List {
ForEach(objectIn: self.people) { person in
ProfileView(person)
}
}
.animation(.default)
}
```
- parameter objectPublishers: The collection of `ObjectPublisher`s that the `ForEach` instance uses to create views dynamically
- parameter content: The view builder that receives an `ObjectPublisher` instance and creates views dynamically.
*/
public init<O: DynamicObject>(
_ objectPublishers: Data,
objectIn objectPublishers: Data,
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
) where Data.Element == ObjectPublisher<O>, ID == O.ObjectID {
self.init(objectPublishers, id: \.cs_objectID, content: content)
}
/**
Creates an instance that creates views for `ListSnapshot` sections.
```
@LiveList
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.people) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { person in
ProfileView(person)
}
}
}
}
.animation(.default)
}
```
- parameter listSnapshot: The `ListSnapshot` that the `ForEach` instance uses to create views dynamically
- parameter content: The view builder that receives a `ListSnapshot.SectionInfo` instance and creates views dynamically.
*/
public init<O: DynamicObject>(
sectionIn listSnapshot: ListSnapshot<O>,
@ViewBuilder content: @escaping (ListSnapshot<O>.SectionInfo) -> Content
) where Data == [ListSnapshot<O>.SectionInfo], ID == ListSnapshot<O>.SectionID {
let sections = listSnapshot.sections()
self.init(sections, id: \.sectionID, content: content)
}
/**
Creates an instance that creates views for each object in a `ListSnapshot.SectionInfo`.
```
@LiveList
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.people) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { person in
ProfileView(person)
}
}
}
}
.animation(.default)
}
```
- parameter sectionInfo: The `ListSnapshot.SectionInfo` that the `ForEach` instance uses to create views dynamically
- parameter content: The view builder that receives an `ObjectPublisher` instance and creates views dynamically.
*/
public init<O: DynamicObject>(
objectIn sectionInfo: Data,
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
) where Data == ListSnapshot<O>.SectionInfo, ID == O.ObjectID {
self.init(sectionInfo, id: \.cs_objectID, content: content)
}
}
#endif

View File

@@ -1,59 +0,0 @@
//
// ForEachSection.swift
// CoreStore
//
// Created by John Rommel Estropia on 2021/02/13.
// Copyright © 2021 John Rommel Estropia. All rights reserved.
//
#if canImport(Combine) && canImport(SwiftUI)
import Combine
import SwiftUI
// MARK: - ForEachSection
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public struct ForEachSection<Object: DynamicObject, Sections: View>: View {
// MARK: Internal
public init(
in listSnapshot: LiveList<Object>.Items,
@ViewBuilder sections: @escaping (
_ sectionID: ListSnapshot<Object>.SectionID,
_ objects: [ObjectPublisher<Object>]
) -> Sections
) {
self.listSnapshot = listSnapshot
self.sections = sections
}
// MARK: View
public var body: some View {
ForEach(self.listSnapshot.sectionIDs, id: \.self) { sectionID in
self.sections(
sectionID,
self.listSnapshot.items(inSectionWithID: sectionID)
)
}
}
// MARK: Private
private let listSnapshot: LiveList<Object>.Items
private let sections: (
_ sectionID: ListSnapshot<Object>.SectionID,
_ objects: [ObjectPublisher<Object>]
) -> Sections
}
#endif

View File

@@ -192,9 +192,18 @@ extension Internals {
return self.sections[section].elements.count
}
func sectionIndexTitles() -> [String] {
func sectionIndexTitle(for section: Int) -> String? {
return self.sections.compactMap({ $0.indexTitle })
guard self.sections.indices.contains(section) else {
return nil
}
return self.sections[section].indexTitle
}
func sectionIndexTitlesForAllSections() -> [String?] {
return self.sections.map({ $0.indexTitle })
}

View File

@@ -31,24 +31,69 @@ import SwiftUI
// MARK: - ListReader
/**
A container view that reads list changes in a `ListPublisher`
*/
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public struct ListReader<Object: DynamicObject, Content: View, Value>: View {
// MARK: Internal
/**
Creates an instance that creates views for `ListPublisher` changes.
```
let people: ListPublisher<Person>
var body: some View {
List {
ListReader(self.people) { listSnapshot in
ForEach(objectIn: listSnapshot) { person in
ProfileView(person)
}
}
}
.animation(.default)
}
```
- parameter listPublisher: The `ListPublisher` that the `ListReader` instance uses to create views dynamically
- parameter content: The view builder that receives an `ListSnapshot` instance and creates views dynamically.
*/
public init(
_ listPublisher: ListPublisher<Object>,
@ViewBuilder content: @escaping (Value) -> Content
) where Value == LiveList<Object>.Items {
@ViewBuilder content: @escaping (ListSnapshot<Object>) -> Content
) where Value == ListSnapshot<Object> {
self._list = .init(listPublisher)
self.content = content
self.keyPath = \.self
}
/**
Creates an instance that creates views for `ListPublisher` changes.
```
let people: ListPublisher<Person>
var body: some View {
ListReader(self.people, keyPath: \.count) { count in
Text("Number of members: \(count)")
}
}
```
- parameter listPublisher: The `ListPublisher` that the `ListReader` instance uses to create views dynamically
- parameter keyPath: A `KeyPath` for a property in the `ListSnapshot` whose value will be sent to the views
- parameter content: The view builder that receives the value from the property `KeyPath` and creates views dynamically.
*/
public init(
_ listPublisher: ListPublisher<Object>,
keyPath: KeyPath<LiveList<Object>.Items, Value>,
keyPath: KeyPath<ListSnapshot<Object>, Value>,
@ViewBuilder content: @escaping (Value) -> Content
) {
@@ -69,10 +114,10 @@ public struct ListReader<Object: DynamicObject, Content: View, Value>: View {
// MARK: Private
@LiveList
private var list: LiveList<Object>.Items
private var list: ListSnapshot<Object>
private let content: (Value) -> Content
private let keyPath: KeyPath<LiveList<Object>.Items, Value>
private let keyPath: KeyPath<ListSnapshot<Object>, Value>
}
#endif

View File

@@ -0,0 +1,132 @@
//
// ListSnapshot.SectionInfo.swift
// CoreStore
//
// Copyright © 2018 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
// MARK: - ListSnapshot
extension ListSnapshot {
// MARK: - SectionInfo
public struct SectionInfo: Hashable, RandomAccessCollection {
// MARK: Private
public let sectionID: SectionID
public let itemIDs: [ItemID]
// MARK: RandomAccessCollection
public var startIndex: Index {
return self.itemIDs.startIndex
}
public var endIndex: Index {
return self.itemIDs.endIndex
}
public func index(after i: Index) -> Index {
return self.itemIDs.index(after: i)
}
public func formIndex(after i: inout Index) {
self.itemIDs.formIndex(after: &i)
}
public func index(before i: Index) -> Index {
return self.itemIDs.index(before: i)
}
public func formIndex(before i: inout Index) {
self.itemIDs.formIndex(before: &i)
}
// MARK: BidirectionalCollection
public subscript(position: Int) -> ObjectPublisher<O> {
let itemID = self.itemIDs[position]
return self.context.objectPublisher(objectID: itemID)
}
public func index(_ i: Index, offsetBy distance: Int) -> Index {
return self.itemIDs.index(i, offsetBy: distance)
}
public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Int) -> Index? {
return self.itemIDs.index(i, offsetBy: distance, limitedBy: limit)
}
public func distance(from start: Index, to end: Index) -> Int {
return self.itemIDs.distance(from: start, to: end)
}
public subscript(bounds: Range<Index>) -> ArraySlice<Element> {
let itemIDs = self.itemIDs[bounds]
return ArraySlice(itemIDs.map(self.context.objectPublisher(objectID:)))
}
// MARK: Sequence
public typealias Element = ObjectPublisher<O>
public typealias Index = Int
// MARK: Internal
internal let context: NSManagedObjectContext
internal init?(
sectionID: SectionID,
listSnapshot: ListSnapshot<O>
) {
guard let context = listSnapshot.context else {
return nil
}
self.sectionID = sectionID
self.itemIDs = listSnapshot.itemIDs(inSectionWithID: sectionID)
self.context = context
}
}
}

View File

@@ -283,6 +283,29 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
return self.diffableSnapshot.sectionIdentifier(containingItem: itemID)
}
/**
Returns an array of `SectionInfo` instances that contains a collection of `ObjectPublisher<O>` items for each section.
*/
public func sections() -> [SectionInfo] {
return self
.sectionIDs
.compactMap({ SectionInfo(sectionID: $0, listSnapshot: self) })
}
/**
Returns the `SectionInfo` that the specified `ItemID` belongs to, or `nil` if it is not in the list.
- parameter itemID: the `ItemID`
- returns: the `SectionInfo` that the specified `ItemID` belongs to, or `nil` if it is not in the list
*/
public func section(containingItemWithID itemID: ItemID) -> SectionInfo? {
return self
.sectionID(containingItemWithID: itemID)
.flatMap({ SectionInfo(sectionID: $0, listSnapshot: self) })
}
/**
All object identifiers in the `ListSnapshot`
@@ -688,6 +711,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
// MARK: Internal
internal let context: NSManagedObjectContext?
internal private(set) var diffableSnapshot: Internals.DiffableDataSourceSnapshot
internal init() {
@@ -696,7 +720,10 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
self.context = nil
}
internal init(diffableSnapshot: Internals.DiffableDataSourceSnapshot, context: NSManagedObjectContext) {
internal init(
diffableSnapshot: Internals.DiffableDataSourceSnapshot,
context: NSManagedObjectContext
) {
self.diffableSnapshot = diffableSnapshot
self.context = context
@@ -706,6 +733,5 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
// MARK: Private
private let id: UUID = .init()
private let context: NSManagedObjectContext?
}

View File

@@ -31,14 +31,41 @@ import SwiftUI
// MARK: - LiveList
/**
A property wrapper type that can read `ListPublisher` changes.
*/
@propertyWrapper
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public struct LiveList<Object: DynamicObject>: DynamicProperty {
// MARK: Public
public typealias Items = ListSnapshot<Object>
/**
Creates an instance that observes `ListPublisher` changes and exposes a `ListSnapshot` value.
```
@LiveList
var people: ListSnapshot<Person>
init(listPublisher: ListPublisher<Person>) {
self._people = .init(listPublisher)
}
var body: some View {
List {
ForEach(objectIn: self.people) { person in
ProfileView(person)
}
}
.animation(.default)
}
```
- parameter listPublisher: The `ListPublisher` that the `LiveList` will observe changes for
*/
public init(
_ listPublisher: ListPublisher<Object>
) {
@@ -46,6 +73,105 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
self.observer = .init(listPublisher: listPublisher)
}
/**
Creates an instance that observes the specified `FetchChainableBuilderType` and exposes a `ListSnapshot` value.
```
@LiveList(
From<Person>()
.where(\.isMember == true)
.orderBy(.ascending(\.lastName))
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(objectIn: self.people) { person in
ProfileView(person)
}
}
.animation(.default)
}
```
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
*/
public init<B: FetchChainableBuilderType>(
_ clauseChain: B,
in dataStack: DataStack
) where B.ObjectType == Object {
self.init(dataStack.publishList(clauseChain))
}
/**
Creates an instance that observes the specified `SectionMonitorBuilderType` and exposes a `ListSnapshot` value.
```
@LiveList(
From<Person>()
.sectionBy(\.age)
.where(\.isMember == true)
.orderBy(.ascending(\.lastName))
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.people) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { person in
ProfileView(person)
}
}
}
}
.animation(.default)
}
```
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/
public init<B: SectionMonitorBuilderType>(
_ clauseChain: B,
in dataStack: DataStack
) where B.ObjectType == Object {
self.init(dataStack.publishList(clauseChain))
}
/**
Creates an instance that observes the specified `From` and `FetchClause`s and exposes a `ListSnapshot` value.
```
@LiveList(
From<Person>(),
Where<Person>(\.isMember == true),
OrderBy<Person>(.ascending(\.lastName))
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(objectIn: self.people) { person in
ProfileView(person)
}
}
.animation(.default)
}
```
- parameter from: a `From` clause indicating the entity type
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public init(
_ from: From<Object>,
_ fetchClauses: FetchClause...,
@@ -55,6 +181,34 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
self.init(from, fetchClauses, in: dataStack)
}
/**
Creates an instance that observes the specified `From` and `FetchClause`s and exposes a `ListSnapshot` value.
```
@LiveList(
From<Person>(),
[
Where<Person>(\.isMember == true),
OrderBy<Person>(.ascending(\.lastName))
]
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(objectIn: self.people) { person in
ProfileView(person)
}
}
.animation(.default)
}
```
- parameter from: a `From` clause indicating the entity type
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public init(
_ from: From<Object>,
_ fetchClauses: [FetchClause],
@@ -64,14 +218,40 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
self.init(dataStack.publishList(from, fetchClauses))
}
public init<B: FetchChainableBuilderType>(
_ clauseChain: B,
in dataStack: DataStack
) where B.ObjectType == Object {
self.init(dataStack.publishList(clauseChain))
}
/**
Creates an instance that observes the specified `From`, `SectionBy`, and `FetchClause`s and exposes a sectioned `ListSnapshot` value.
```
@LiveList(
From<Person>(),
SectionBy(\.age),
Where<Person>(\.isMember == true),
OrderBy<Person>(.ascending(\.lastName))
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.people) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { person in
ProfileView(person)
}
}
}
}
.animation(.default)
}
```
- parameter from: a `From` clause indicating the entity type
- parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections.
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public init(
_ from: From<Object>,
_ sectionBy: SectionBy<Object>,
@@ -82,6 +262,42 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
self.init(from, sectionBy, fetchClauses, in: dataStack)
}
/**
Creates an instance that observes the specified `From`, `SectionBy`, and `FetchClause`s and exposes a sectioned `ListSnapshot` value.
```
@LiveList(
From<Person>(),
SectionBy(\.age),
[
Where<Person>(\.isMember == true),
OrderBy<Person>(.ascending(\.lastName))
]
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.people) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { person in
ProfileView(person)
}
}
}
}
.animation(.default)
}
```
- parameter from: a `From` clause indicating the entity type
- parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections.
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public init(
_ from: From<Object>,
_ sectionBy: SectionBy<Object>,
@@ -92,18 +308,10 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
self.init(dataStack.publishList(from, sectionBy, fetchClauses))
}
public init<B: SectionMonitorBuilderType>(
_ clauseChain: B,
in dataStack: DataStack
) where B.ObjectType == Object {
self.init(dataStack.publishList(clauseChain))
}
// MARK: @propertyWrapper
public var wrappedValue: Items {
public var wrappedValue: ListSnapshot<Object> {
return self.observer.items
}
@@ -133,7 +341,7 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
private final class Observer: ObservableObject {
@Published
var items: Items
var items: ListSnapshot<Object>
let listPublisher: ListPublisher<Object>

View File

@@ -31,14 +31,38 @@ import SwiftUI
// MARK: - LiveObject
/**
A property wrapper type that can read `ObjectPublisher` changes.
*/
@propertyWrapper
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public struct LiveObject<O: DynamicObject>: DynamicProperty {
// MARK: Public
public typealias Item = ObjectSnapshot<O>
/**
Creates an instance that observes `ObjectPublisher` changes and exposes an `Optional<ObjectSnapshot<O>>` value.
```
@LiveObject
var person: ObjectSnapshot<Person>?
init(objectPublisher: ObjectPublisher<Person>) {
self._person = .init(objectPublisher)
}
var body: some View {
HStack {
AsyncImage(self.person?.$avatarURL)
Text(self.person?.$fullName ?? "")
}
}
```
- parameter objectPublisher: The `ObjectPublisher` that the `LiveObject` will observe changes for
*/
public init(_ objectPublisher: ObjectPublisher<O>?) {
self.observer = .init(objectPublisher: objectPublisher)
@@ -47,7 +71,7 @@ public struct LiveObject<O: DynamicObject>: DynamicProperty {
// MARK: @propertyWrapper
public var wrappedValue: Item? {
public var wrappedValue: ObjectSnapshot<O>? {
return self.observer.item
}
@@ -72,7 +96,7 @@ public struct LiveObject<O: DynamicObject>: DynamicProperty {
private final class Observer: ObservableObject {
@Published
var item: Item?
var item: ObjectSnapshot<O>?
let objectPublisher: ObjectPublisher<O>?

View File

@@ -31,24 +31,40 @@ import SwiftUI
// MARK: - ObjectReader
/**
A container view that reads changes to an `ObjectPublisher`
*/
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public struct ObjectReader<Object: DynamicObject, Content: View, Value>: View {
// MARK: Internal
/**
Creates an instance that creates views for `ObjectPublisher` changes.
- parameter objectPublisher: The `ObjectPublisher` that the `ObjectReader` instance uses to create views dynamically
- parameter content: The view builder that receives an `Optional<ObjectSnapshot<O>>` instance and creates views dynamically.
*/
public init(
_ objectPublisher: ObjectPublisher<Object>?,
@ViewBuilder content: @escaping (Value) -> Content
) where Value == LiveObject<Object>.Item {
@ViewBuilder content: @escaping (ObjectSnapshot<Object>) -> Content
) where Value == ObjectSnapshot<Object> {
self._object = .init(objectPublisher)
self.content = content
self.keyPath = \.self
}
/**
Creates an instance that creates views for `ObjectPublisher` changes.
- parameter objectPublisher: The `ObjectPublisher` that the `ObjectReader` instance uses to create views dynamically
- parameter keyPath: A `KeyPath` for a property in the `ObjectSnapshot` whose value will be sent to the views
- parameter content: The view builder that receives the value from the property `KeyPath` and creates views dynamically.
*/
public init(
_ objectPublisher: ObjectPublisher<Object>?,
keyPath: KeyPath<LiveObject<Object>.Item, Value>,
keyPath: KeyPath<ObjectSnapshot<Object>, Value>,
@ViewBuilder content: @escaping (Value) -> Content
) {
@@ -72,10 +88,10 @@ public struct ObjectReader<Object: DynamicObject, Content: View, Value>: View {
// MARK: Private
@LiveObject
private var object: LiveObject<Object>.Item?
private var object: ObjectSnapshot<Object>?
private let content: (Value) -> Content
private let keyPath: KeyPath<LiveObject<Object>.Item, Value>
private let keyPath: KeyPath<ObjectSnapshot<Object>, Value>
}
#endif

View File

@@ -0,0 +1,52 @@
//
// PublisherCompatible.swift
// CoreStore
//
// Copyright © 2021 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
#if canImport(Combine)
import Combine
// MARK: - PublisherCompatible
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public protocol PublisherCompatible {
associatedtype ReactiveBase
var reactive: PublisherProvider<ReactiveBase> { get }
}
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
extension PublisherCompatible {
// MARK: Public
public var reactive: PublisherProvider<Self> {
return .init(self)
}
}
#endif

View File

@@ -0,0 +1,46 @@
//
// PublisherProvider.swift
// CoreStore
//
// Copyright © 2021 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
#if canImport(Combine)
import Combine
// MARK: - PublisherProvider
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public struct PublisherProvider<Base> {
// MARK: Public
public let base: Base
public init(_ base: Base) {
self.base = base
}
}
#endif