mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-01-15 13:43:43 +01:00
SwiftUI utilities done (for now)
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
275
Sources/DataStack+Reactive.swift
Normal file
275
Sources/DataStack+Reactive.swift
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -220,7 +220,7 @@ extension DiffableDataSource {
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Deprecated
|
||||
// MARK: Deprecated
|
||||
|
||||
extension DiffableDataSource {
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ extension DiffableDataSource {
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Deprecated
|
||||
// MARK: Deprecated
|
||||
|
||||
extension DiffableDataSource {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
132
Sources/ListSnapshot.SectionInfo.swift
Normal file
132
Sources/ListSnapshot.SectionInfo.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
52
Sources/PublisherConvertible.swift
Normal file
52
Sources/PublisherConvertible.swift
Normal 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
|
||||
46
Sources/PublisherProvider.swift
Normal file
46
Sources/PublisherProvider.swift
Normal 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
|
||||
Reference in New Issue
Block a user