mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-03-26 11:21:32 +01:00
SwiftUI utilities done (for now)
This commit is contained in:
@@ -72,7 +72,7 @@ extension Modern.ColorsDemo {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
@LiveList(Modern.ColorsDemo.palettesPublisher)
|
@LiveList(Modern.ColorsDemo.palettesPublisher)
|
||||||
private var palettes: LiveList<Modern.ColorsDemo.Palette>.Items
|
private var palettes: ListSnapshot
|
||||||
|
|
||||||
private let listView: (
|
private let listView: (
|
||||||
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
|
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
|
||||||
|
|||||||
@@ -30,14 +30,19 @@ extension Modern.ColorsDemo.UIKit {
|
|||||||
return UIViewControllerType(self.palette)
|
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) {}
|
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||||
|
|
||||||
|
|
||||||
// MARK: Private
|
// 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(
|
static let palettesPublisher: ListPublisher<Modern.ColorsDemo.Palette> = Modern.ColorsDemo.dataStack.publishList(
|
||||||
From<Modern.ColorsDemo.Palette>()
|
From<Modern.ColorsDemo.Palette>()
|
||||||
.sectionBy(\.$colorGroup)
|
.sectionBy(
|
||||||
|
\.$colorGroup,
|
||||||
|
sectionIndexTransformer: { $0?.first?.uppercased() }
|
||||||
|
)
|
||||||
.where(Modern.ColorsDemo.filter.whereClause())
|
.where(Modern.ColorsDemo.filter.whereClause())
|
||||||
.orderBy(.ascending(\.$hue))
|
.orderBy(.ascending(\.$hue))
|
||||||
)
|
)
|
||||||
@@ -57,7 +60,10 @@ extension Modern {
|
|||||||
|
|
||||||
try! Modern.ColorsDemo.palettesPublisher.refetch(
|
try! Modern.ColorsDemo.palettesPublisher.refetch(
|
||||||
From<Modern.ColorsDemo.Palette>()
|
From<Modern.ColorsDemo.Palette>()
|
||||||
.sectionBy(\.$colorGroup)
|
.sectionBy(
|
||||||
|
\.$colorGroup,
|
||||||
|
sectionIndexTransformer: { $0?.first?.uppercased() }
|
||||||
|
)
|
||||||
.where(self.filter.whereClause())
|
.where(self.filter.whereClause())
|
||||||
.orderBy(.ascending(\.$hue))
|
.orderBy(.ascending(\.$hue))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ extension Modern.ColorsDemo.SwiftUI {
|
|||||||
struct DetailView: View {
|
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
|
@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.
|
⭐️ 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 {
|
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
|
@LiveObject
|
||||||
private var palette: LiveObject<Modern.ColorsDemo.Palette>.Item?
|
private var palette: ObjectSnapshot<Modern.ColorsDemo.Palette>?
|
||||||
|
|
||||||
|
|
||||||
// MARK: Internal
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
⭐️ Sample 2: Initializing a `LiveObject` from an existing `ObjectPublisher`
|
||||||
|
*/
|
||||||
internal init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
|
internal init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
|
||||||
|
|
||||||
self._palette = .init(palette)
|
self._palette = .init(palette)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
// MARK: View
|
⭐️ Sample 3: Readding values directly from the `ObjectSnapshot`
|
||||||
|
*/
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
if let palette = self.palette {
|
if let palette = self.palette {
|
||||||
|
|||||||
@@ -14,22 +14,35 @@ extension Modern.ColorsDemo.SwiftUI {
|
|||||||
struct ListView: View {
|
struct ListView: View {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
⭐️ Sample 1: Setting a sectioned `ListPublisher` declared as an `@ObservedObject`
|
⭐️ Sample 1: Using a `LiveList` to observe list changes
|
||||||
*/
|
*/
|
||||||
@LiveList
|
@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 {
|
init(
|
||||||
return List {
|
listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
|
||||||
|
onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
|
||||||
|
) {
|
||||||
|
|
||||||
ForEachSection(in: self.palettes) { sectionID, palettes in
|
self._palettes = .init(listPublisher)
|
||||||
|
self.onPaletteTapped = onPaletteTapped
|
||||||
|
}
|
||||||
|
|
||||||
Section(header: Text(sectionID)) {
|
/**
|
||||||
|
⭐️ Sample 3: Assigning sections and items of the `ListSnapshot` to corresponding `View`s by using the correct `ForEach` overloads.
|
||||||
|
*/
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
ForEach(palettes) { palette in
|
List {
|
||||||
|
|
||||||
|
ForEach(sectionIn: self.palettes) { section in
|
||||||
|
|
||||||
|
Section(header: Text(section.sectionID)) {
|
||||||
|
|
||||||
|
ForEach(objectIn: section) { palette in
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
action: {
|
action: {
|
||||||
@@ -45,7 +58,7 @@ extension Modern.ColorsDemo.SwiftUI {
|
|||||||
}
|
}
|
||||||
.onDelete { itemIndices in
|
.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)
|
.animation(.default)
|
||||||
.listStyle(PlainListStyle())
|
.listStyle(PlainListStyle())
|
||||||
.edgesIgnoringSafeArea([])
|
.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.
|
⭐️ 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 {
|
deinit {
|
||||||
|
|
||||||
self.palette.removeObserver(self)
|
self.palette.removeObserver(self)
|
||||||
@@ -183,9 +192,8 @@ extension Modern.ColorsDemo.UIKit {
|
|||||||
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
|
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
|
||||||
constant: 10
|
constant: 10
|
||||||
),
|
),
|
||||||
containerView.bottomAnchor.constraint(
|
containerView.centerYAnchor.constraint(
|
||||||
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
|
equalTo: view.safeAreaLayoutGuide.centerYAnchor
|
||||||
constant: -10
|
|
||||||
),
|
),
|
||||||
containerView.trailingAnchor.constraint(
|
containerView.trailingAnchor.constraint(
|
||||||
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
|
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
|
||||||
@@ -223,8 +231,6 @@ extension Modern.ColorsDemo.UIKit {
|
|||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private let palette: ObjectMonitor<Modern.ColorsDemo.Palette>
|
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ extension Modern.ColorsDemo.UIKit {
|
|||||||
final class ListViewController: UITableViewController {
|
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,
|
tableView: self.tableView,
|
||||||
dataStack: Modern.ColorsDemo.dataStack,
|
dataStack: Modern.ColorsDemo.dataStack,
|
||||||
cellProvider: { (tableView, indexPath, palette) in
|
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) {
|
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||||
|
|
||||||
@@ -79,6 +81,16 @@ extension Modern.ColorsDemo.UIKit {
|
|||||||
break
|
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)
|
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.
|
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.
|
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.
|
||||||
|
|
||||||
@@ -162,36 +190,8 @@ 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(
|
// MARK: Deprecated
|
||||||
clauseChain.from,
|
|
||||||
clauseChain.sectionBy,
|
|
||||||
clauseChain.fetchClauses
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Deprecated
|
|
||||||
|
|
||||||
@available(*, deprecated, renamed: "publishObject(_:)")
|
@available(*, deprecated, renamed: "publishObject(_:)")
|
||||||
public func objectPublisher<O: DynamicObject>(_ object: O) -> ObjectPublisher<O> {
|
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
|
||||||
@@ -210,6 +210,25 @@ extension DiffableDataSource {
|
|||||||
return self.dispatcher.indexPath(for: itemID)
|
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
|
// MARK: Internal
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ extension DiffableDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Deprecated
|
// MARK: Deprecated
|
||||||
|
|
||||||
extension DiffableDataSource {
|
extension DiffableDataSource {
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ extension DiffableDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Deprecated
|
// MARK: Deprecated
|
||||||
|
|
||||||
extension DiffableDataSource {
|
extension DiffableDataSource {
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,18 @@ extension DiffableDataSource {
|
|||||||
@objc
|
@objc
|
||||||
open dynamic func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {}
|
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
|
// MARK: Private
|
||||||
|
|
||||||
@@ -255,7 +267,7 @@ extension DiffableDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Deprecated
|
// MARK: Deprecated
|
||||||
|
|
||||||
extension DiffableDataSource {
|
extension DiffableDataSource {
|
||||||
|
|
||||||
|
|||||||
@@ -36,21 +36,137 @@ extension ForEach where Content: View {
|
|||||||
|
|
||||||
// MARK: Public
|
// 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>(
|
public init<O: DynamicObject>(
|
||||||
_ listSnapshot: Data,
|
objectIn listSnapshot: Data,
|
||||||
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
|
@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)
|
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>(
|
public init<O: DynamicObject>(
|
||||||
_ objectPublishers: Data,
|
objectIn objectPublishers: Data,
|
||||||
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
|
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
|
||||||
) where Data.Element == ObjectPublisher<O>, ID == O.ObjectID {
|
) where Data.Element == ObjectPublisher<O>, ID == O.ObjectID {
|
||||||
|
|
||||||
self.init(objectPublishers, id: \.cs_objectID, content: content)
|
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
|
#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
|
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
|
// 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, *)
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
public struct ListReader<Object: DynamicObject, Content: View, Value>: View {
|
public struct ListReader<Object: DynamicObject, Content: View, Value>: View {
|
||||||
|
|
||||||
// MARK: Internal
|
// 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(
|
public init(
|
||||||
_ listPublisher: ListPublisher<Object>,
|
_ listPublisher: ListPublisher<Object>,
|
||||||
@ViewBuilder content: @escaping (Value) -> Content
|
@ViewBuilder content: @escaping (ListSnapshot<Object>) -> Content
|
||||||
) where Value == LiveList<Object>.Items {
|
) where Value == ListSnapshot<Object> {
|
||||||
|
|
||||||
self._list = .init(listPublisher)
|
self._list = .init(listPublisher)
|
||||||
self.content = content
|
self.content = content
|
||||||
self.keyPath = \.self
|
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(
|
public init(
|
||||||
_ listPublisher: ListPublisher<Object>,
|
_ listPublisher: ListPublisher<Object>,
|
||||||
keyPath: KeyPath<LiveList<Object>.Items, Value>,
|
keyPath: KeyPath<ListSnapshot<Object>, Value>,
|
||||||
@ViewBuilder content: @escaping (Value) -> Content
|
@ViewBuilder content: @escaping (Value) -> Content
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -69,10 +114,10 @@ public struct ListReader<Object: DynamicObject, Content: View, Value>: View {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
@LiveList
|
@LiveList
|
||||||
private var list: LiveList<Object>.Items
|
private var list: ListSnapshot<Object>
|
||||||
|
|
||||||
private let content: (Value) -> Content
|
private let content: (Value) -> Content
|
||||||
private let keyPath: KeyPath<LiveList<Object>.Items, Value>
|
private let keyPath: KeyPath<ListSnapshot<Object>, Value>
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -284,6 +284,29 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
|
|||||||
return self.diffableSnapshot.sectionIdentifier(containingItem: itemID)
|
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`
|
All object identifiers in the `ListSnapshot`
|
||||||
*/
|
*/
|
||||||
@@ -688,6 +711,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
|
|||||||
|
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
|
internal let context: NSManagedObjectContext?
|
||||||
internal private(set) var diffableSnapshot: Internals.DiffableDataSourceSnapshot
|
internal private(set) var diffableSnapshot: Internals.DiffableDataSourceSnapshot
|
||||||
|
|
||||||
internal init() {
|
internal init() {
|
||||||
@@ -696,7 +720,10 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
|
|||||||
self.context = nil
|
self.context = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
internal init(diffableSnapshot: Internals.DiffableDataSourceSnapshot, context: NSManagedObjectContext) {
|
internal init(
|
||||||
|
diffableSnapshot: Internals.DiffableDataSourceSnapshot,
|
||||||
|
context: NSManagedObjectContext
|
||||||
|
) {
|
||||||
|
|
||||||
self.diffableSnapshot = diffableSnapshot
|
self.diffableSnapshot = diffableSnapshot
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -706,6 +733,5 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private let id: UUID = .init()
|
private let id: UUID = .init()
|
||||||
private let context: NSManagedObjectContext?
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,41 @@ import SwiftUI
|
|||||||
|
|
||||||
// MARK: - LiveList
|
// MARK: - LiveList
|
||||||
|
|
||||||
|
/**
|
||||||
|
A property wrapper type that can read `ListPublisher` changes.
|
||||||
|
*/
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
||||||
|
|
||||||
// MARK: Public
|
// 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(
|
public init(
|
||||||
_ listPublisher: ListPublisher<Object>
|
_ listPublisher: ListPublisher<Object>
|
||||||
) {
|
) {
|
||||||
@@ -46,6 +73,105 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
|||||||
self.observer = .init(listPublisher: listPublisher)
|
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(
|
public init(
|
||||||
_ from: From<Object>,
|
_ from: From<Object>,
|
||||||
_ fetchClauses: FetchClause...,
|
_ fetchClauses: FetchClause...,
|
||||||
@@ -55,6 +181,34 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
|||||||
self.init(from, fetchClauses, in: dataStack)
|
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(
|
public init(
|
||||||
_ from: From<Object>,
|
_ from: From<Object>,
|
||||||
_ fetchClauses: [FetchClause],
|
_ fetchClauses: [FetchClause],
|
||||||
@@ -64,14 +218,40 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
|||||||
self.init(dataStack.publishList(from, fetchClauses))
|
self.init(dataStack.publishList(from, fetchClauses))
|
||||||
}
|
}
|
||||||
|
|
||||||
public init<B: FetchChainableBuilderType>(
|
/**
|
||||||
_ clauseChain: B,
|
Creates an instance that observes the specified `From`, `SectionBy`, and `FetchClause`s and exposes a sectioned `ListSnapshot` value.
|
||||||
in dataStack: DataStack
|
```
|
||||||
) where B.ObjectType == Object {
|
@LiveList(
|
||||||
|
From<Person>(),
|
||||||
|
SectionBy(\.age),
|
||||||
|
Where<Person>(\.isMember == true),
|
||||||
|
OrderBy<Person>(.ascending(\.lastName))
|
||||||
|
)
|
||||||
|
var people: ListSnapshot<Person>
|
||||||
|
|
||||||
self.init(dataStack.publishList(clauseChain))
|
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(
|
public init(
|
||||||
_ from: From<Object>,
|
_ from: From<Object>,
|
||||||
_ sectionBy: SectionBy<Object>,
|
_ sectionBy: SectionBy<Object>,
|
||||||
@@ -82,6 +262,42 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
|||||||
self.init(from, sectionBy, fetchClauses, in: dataStack)
|
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(
|
public init(
|
||||||
_ from: From<Object>,
|
_ from: From<Object>,
|
||||||
_ sectionBy: SectionBy<Object>,
|
_ sectionBy: SectionBy<Object>,
|
||||||
@@ -92,18 +308,10 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
|||||||
self.init(dataStack.publishList(from, sectionBy, fetchClauses))
|
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
|
// MARK: @propertyWrapper
|
||||||
|
|
||||||
public var wrappedValue: Items {
|
public var wrappedValue: ListSnapshot<Object> {
|
||||||
|
|
||||||
return self.observer.items
|
return self.observer.items
|
||||||
}
|
}
|
||||||
@@ -133,7 +341,7 @@ public struct LiveList<Object: DynamicObject>: DynamicProperty {
|
|||||||
private final class Observer: ObservableObject {
|
private final class Observer: ObservableObject {
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var items: Items
|
var items: ListSnapshot<Object>
|
||||||
|
|
||||||
let listPublisher: ListPublisher<Object>
|
let listPublisher: ListPublisher<Object>
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,38 @@ import SwiftUI
|
|||||||
|
|
||||||
// MARK: - LiveObject
|
// MARK: - LiveObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
A property wrapper type that can read `ObjectPublisher` changes.
|
||||||
|
*/
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
public struct LiveObject<O: DynamicObject>: DynamicProperty {
|
public struct LiveObject<O: DynamicObject>: DynamicProperty {
|
||||||
|
|
||||||
// MARK: Public
|
// 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>?) {
|
public init(_ objectPublisher: ObjectPublisher<O>?) {
|
||||||
|
|
||||||
self.observer = .init(objectPublisher: objectPublisher)
|
self.observer = .init(objectPublisher: objectPublisher)
|
||||||
@@ -47,7 +71,7 @@ public struct LiveObject<O: DynamicObject>: DynamicProperty {
|
|||||||
|
|
||||||
// MARK: @propertyWrapper
|
// MARK: @propertyWrapper
|
||||||
|
|
||||||
public var wrappedValue: Item? {
|
public var wrappedValue: ObjectSnapshot<O>? {
|
||||||
|
|
||||||
return self.observer.item
|
return self.observer.item
|
||||||
}
|
}
|
||||||
@@ -72,7 +96,7 @@ public struct LiveObject<O: DynamicObject>: DynamicProperty {
|
|||||||
private final class Observer: ObservableObject {
|
private final class Observer: ObservableObject {
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var item: Item?
|
var item: ObjectSnapshot<O>?
|
||||||
|
|
||||||
let objectPublisher: ObjectPublisher<O>?
|
let objectPublisher: ObjectPublisher<O>?
|
||||||
|
|
||||||
|
|||||||
@@ -31,24 +31,40 @@ import SwiftUI
|
|||||||
|
|
||||||
// MARK: - ObjectReader
|
// MARK: - ObjectReader
|
||||||
|
|
||||||
|
/**
|
||||||
|
A container view that reads changes to an `ObjectPublisher`
|
||||||
|
*/
|
||||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||||
public struct ObjectReader<Object: DynamicObject, Content: View, Value>: View {
|
public struct ObjectReader<Object: DynamicObject, Content: View, Value>: View {
|
||||||
|
|
||||||
// MARK: Internal
|
// 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(
|
public init(
|
||||||
_ objectPublisher: ObjectPublisher<Object>?,
|
_ objectPublisher: ObjectPublisher<Object>?,
|
||||||
@ViewBuilder content: @escaping (Value) -> Content
|
@ViewBuilder content: @escaping (ObjectSnapshot<Object>) -> Content
|
||||||
) where Value == LiveObject<Object>.Item {
|
) where Value == ObjectSnapshot<Object> {
|
||||||
|
|
||||||
self._object = .init(objectPublisher)
|
self._object = .init(objectPublisher)
|
||||||
self.content = content
|
self.content = content
|
||||||
self.keyPath = \.self
|
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(
|
public init(
|
||||||
_ objectPublisher: ObjectPublisher<Object>?,
|
_ objectPublisher: ObjectPublisher<Object>?,
|
||||||
keyPath: KeyPath<LiveObject<Object>.Item, Value>,
|
keyPath: KeyPath<ObjectSnapshot<Object>, Value>,
|
||||||
@ViewBuilder content: @escaping (Value) -> Content
|
@ViewBuilder content: @escaping (Value) -> Content
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -72,10 +88,10 @@ public struct ObjectReader<Object: DynamicObject, Content: View, Value>: View {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
@LiveObject
|
@LiveObject
|
||||||
private var object: LiveObject<Object>.Item?
|
private var object: ObjectSnapshot<Object>?
|
||||||
|
|
||||||
private let content: (Value) -> Content
|
private let content: (Value) -> Content
|
||||||
private let keyPath: KeyPath<LiveObject<Object>.Item, Value>
|
private let keyPath: KeyPath<ObjectSnapshot<Object>, Value>
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#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