mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-04-19 23:41:20 +02:00
WIP: README
This commit is contained in:
@@ -1476,7 +1476,7 @@ class MyViewController: UIViewController, ObjectObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
We then need to keep a `ObjectMonitor` instance and register our `ObjectObserver` as an observer:
|
We then need to keep an `ObjectMonitor` instance and register our `ObjectObserver` as an observer:
|
||||||
```swift
|
```swift
|
||||||
let person: MyPersonEntity = // ...
|
let person: MyPersonEntity = // ...
|
||||||
self.monitor = CoreStore.monitorObject(person)
|
self.monitor = CoreStore.monitorObject(person)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ extension CSDataStack {
|
|||||||
Creates a `CSObjectMonitor` for the specified `NSManagedObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `NSManagedObject`.
|
Creates a `CSObjectMonitor` for the specified `NSManagedObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `NSManagedObject`.
|
||||||
|
|
||||||
- parameter object: the `NSManagedObject` to observe changes from
|
- parameter object: the `NSManagedObject` to observe changes from
|
||||||
- returns: a `ObjectMonitor` that monitors changes to `object`
|
- returns: an `ObjectMonitor` that monitors changes to `object`
|
||||||
*/
|
*/
|
||||||
@objc
|
@objc
|
||||||
public func monitorObject(_ object: NSManagedObject) -> CSObjectMonitor {
|
public func monitorObject(_ object: NSManagedObject) -> CSObjectMonitor {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ extension CoreStore {
|
|||||||
Using the `defaultStack`, creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`.
|
Using the `defaultStack`, creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`.
|
||||||
|
|
||||||
- parameter object: the `DynamicObject` to observe changes from
|
- parameter object: the `DynamicObject` to observe changes from
|
||||||
- returns: a `ObjectMonitor` that monitors changes to `object`
|
- returns: an `ObjectMonitor` that monitors changes to `object`
|
||||||
*/
|
*/
|
||||||
public static func monitorObject<O: DynamicObject>(_ object: O) -> ObjectMonitor<O> {
|
public static func monitorObject<O: DynamicObject>(_ object: O) -> ObjectMonitor<O> {
|
||||||
|
|
||||||
|
|||||||
@@ -34,21 +34,35 @@ import CoreData
|
|||||||
extension DataStack {
|
extension DataStack {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates a `ObjectPublisher` for the specified `DynamicObject`. Multiple objects may then register themselves to be notified when changes are made to the `DynamicObject`.
|
Creates an `ObjectPublisher` for the specified `DynamicObject`. Multiple objects may then register themselves to be notified when changes are made to the `DynamicObject`.
|
||||||
|
|
||||||
- parameter object: the `DynamicObject` to observe changes from
|
- parameter object: the `DynamicObject` to observe changes from
|
||||||
- returns: a `ObjectPublisher` that broadcasts changes to `object`
|
- returns: an `ObjectPublisher` that broadcasts changes to `object`
|
||||||
*/
|
*/
|
||||||
public func objectPublisher<O: DynamicObject>(_ object: O) -> ObjectPublisher<O> {
|
public func objectPublisher<O: DynamicObject>(_ object: O) -> ObjectPublisher<O> {
|
||||||
|
|
||||||
return ObjectPublisher<O>(objectID: object.cs_id(), context: self.unsafeContext())
|
return ObjectPublisher<O>(objectID: object.cs_id(), context: self.unsafeContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- returns: a `ListPublisher` that broadcasts changes to the fetched results
|
||||||
|
*/
|
||||||
public func listPublisher<D>(_ from: From<D>, _ fetchClauses: FetchClause...) -> ListPublisher<D> {
|
public func listPublisher<D>(_ from: From<D>, _ fetchClauses: FetchClause...) -> ListPublisher<D> {
|
||||||
|
|
||||||
return self.listPublisher(from, fetchClauses)
|
return self.listPublisher(from, 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.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- returns: a `ListPublisher` that broadcasts changes to the fetched results
|
||||||
|
*/
|
||||||
public func listPublisher<D>(_ from: From<D>, _ fetchClauses: [FetchClause]) -> ListPublisher<D> {
|
public func listPublisher<D>(_ from: From<D>, _ fetchClauses: [FetchClause]) -> ListPublisher<D> {
|
||||||
|
|
||||||
return ListPublisher(
|
return ListPublisher(
|
||||||
@@ -66,7 +80,25 @@ 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 listPublisher<B: FetchChainableBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
|
public func listPublisher<B: FetchChainableBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
|
||||||
|
|
||||||
return self.listPublisher(
|
return self.listPublisher(
|
||||||
@@ -74,34 +106,15 @@ extension DataStack {
|
|||||||
clauseChain.fetchClauses
|
clauseChain.fetchClauses
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func listPublisher<D>(createAsynchronously: @escaping (ListPublisher<D>) -> Void, _ from: From<D>, _ fetchClauses: FetchClause...) {
|
/**
|
||||||
|
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.
|
||||||
self.listPublisher(
|
|
||||||
createAsynchronously: createAsynchronously,
|
- parameter from: a `From` clause indicating the entity type
|
||||||
from, fetchClauses
|
- 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.
|
||||||
}
|
- returns: a `ListPublisher` that broadcasts changes to the fetched results
|
||||||
|
*/
|
||||||
public func listPublisher<D>(createAsynchronously: @escaping (ListPublisher<D>) -> Void, _ from: From<D>, _ fetchClauses: [FetchClause]) {
|
|
||||||
|
|
||||||
_ = ListPublisher(
|
|
||||||
dataStack: self,
|
|
||||||
from: from,
|
|
||||||
sectionBy: nil,
|
|
||||||
applyFetchClauses: { fetchRequest in
|
|
||||||
|
|
||||||
fetchClauses.forEach { $0.applyToFetchRequest(fetchRequest) }
|
|
||||||
|
|
||||||
Internals.assert(
|
|
||||||
fetchRequest.sortDescriptors?.isEmpty == false,
|
|
||||||
"An \(Internals.typeName(ListPublisher<D>.self)) requires a sort information. Specify from a \(Internals.typeName(OrderBy<D>.self)) clause or any custom \(Internals.typeName(FetchClause.self)) that provides a sort descriptor."
|
|
||||||
)
|
|
||||||
},
|
|
||||||
createAsynchronously: createAsynchronously
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func listPublisher<D>(_ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: FetchClause...) -> ListPublisher<D> {
|
public func listPublisher<D>(_ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: FetchClause...) -> ListPublisher<D> {
|
||||||
|
|
||||||
return self.listPublisher(
|
return self.listPublisher(
|
||||||
@@ -110,7 +123,15 @@ extension DataStack {
|
|||||||
fetchClauses
|
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.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- returns: a `ListPublisher` that broadcasts changes to the fetched results
|
||||||
|
*/
|
||||||
public func listPublisher<D>(_ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: [FetchClause]) -> ListPublisher<D> {
|
public func listPublisher<D>(_ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: [FetchClause]) -> ListPublisher<D> {
|
||||||
|
|
||||||
return ListPublisher(
|
return ListPublisher(
|
||||||
@@ -128,7 +149,26 @@ 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 listPublisher<B: SectionMonitorBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
|
public func listPublisher<B: SectionMonitorBuilderType>(_ clauseChain: B) -> ListPublisher<B.ObjectType> {
|
||||||
|
|
||||||
return self.listPublisher(
|
return self.listPublisher(
|
||||||
@@ -137,45 +177,6 @@ extension DataStack {
|
|||||||
clauseChain.fetchClauses
|
clauseChain.fetchClauses
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func listPublisher<D>(createAsynchronously: @escaping (ListPublisher<D>) -> Void, _ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: FetchClause...) {
|
|
||||||
|
|
||||||
self.listPublisher(
|
|
||||||
createAsynchronously: createAsynchronously,
|
|
||||||
from,
|
|
||||||
sectionBy,
|
|
||||||
fetchClauses
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func listPublisher<D>(createAsynchronously: @escaping (ListPublisher<D>) -> Void, _ from: From<D>, _ sectionBy: SectionBy<D>, _ fetchClauses: [FetchClause]) {
|
|
||||||
|
|
||||||
_ = ListPublisher(
|
|
||||||
dataStack: self,
|
|
||||||
from: from,
|
|
||||||
sectionBy: sectionBy,
|
|
||||||
applyFetchClauses: { fetchRequest in
|
|
||||||
|
|
||||||
fetchClauses.forEach { $0.applyToFetchRequest(fetchRequest) }
|
|
||||||
|
|
||||||
Internals.assert(
|
|
||||||
fetchRequest.sortDescriptors?.isEmpty == false,
|
|
||||||
"An \(Internals.typeName(ListPublisher<D>.self)) requires a sort information. Specify from a \(Internals.typeName(OrderBy<D>.self)) clause or any custom \(Internals.typeName(FetchClause.self)) that provides a sort descriptor."
|
|
||||||
)
|
|
||||||
},
|
|
||||||
createAsynchronously: createAsynchronously
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func listPublisher<B: SectionMonitorBuilderType>(createAsynchronously: @escaping (ListPublisher<B.ObjectType>) -> Void, _ clauseChain: B) {
|
|
||||||
|
|
||||||
self.listPublisher(
|
|
||||||
createAsynchronously: createAsynchronously,
|
|
||||||
clauseChain.from,
|
|
||||||
clauseChain.sectionBy,
|
|
||||||
clauseChain.fetchClauses
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ extension DataStack {
|
|||||||
Creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`.
|
Creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`.
|
||||||
|
|
||||||
- parameter object: the `DynamicObject` to observe changes from
|
- parameter object: the `DynamicObject` to observe changes from
|
||||||
- returns: a `ObjectMonitor` that monitors changes to `object`
|
- returns: an `ObjectMonitor` that monitors changes to `object`
|
||||||
*/
|
*/
|
||||||
public func monitorObject<O: DynamicObject>(_ object: O) -> ObjectMonitor<O> {
|
public func monitorObject<O: DynamicObject>(_ object: O) -> ObjectMonitor<O> {
|
||||||
|
|
||||||
|
|||||||
@@ -34,13 +34,59 @@ import CoreData
|
|||||||
extension DiffableDataSource {
|
extension DiffableDataSource {
|
||||||
|
|
||||||
// MARK: - CollectionView
|
// MARK: - CollectionView
|
||||||
|
|
||||||
|
/**
|
||||||
|
The `DiffableDataSource.CollectionView` serves as a `UICollectionViewDataSource` that handles `ListPublisher` snapshots for a `UICollectionView`. Subclasses of `DiffableDataSource.CollectionView` may override some `UICollectionViewDataSource` methods as needed.
|
||||||
|
The `DiffableDataSource.CollectionView` instance needs to be held on (retained) for as long as the `UICollectionView`'s lifecycle.
|
||||||
|
```
|
||||||
|
self.dataSource = DiffableDataSource.CollectionView<Person>(
|
||||||
|
collectionView: self.collectionView,
|
||||||
|
dataStack: Shared.defaultStack,
|
||||||
|
cellProvider: { (collectionView, indexPath, person) in
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
|
||||||
|
cell.setPerson(person)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
The dataSource can then apply changes from a `ListPublisher` as shown:
|
||||||
|
```
|
||||||
|
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||||
|
self?.dataSource?.apply(
|
||||||
|
listPublisher.snapshot,
|
||||||
|
animatingDifferences: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`DiffableDataSource.CollectionView` fully handles the reload animations.
|
||||||
|
*/
|
||||||
open class CollectionView<O: DynamicObject>: NSObject, UICollectionViewDataSource {
|
open class CollectionView<O: DynamicObject>: NSObject, UICollectionViewDataSource {
|
||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
|
|
||||||
|
/**
|
||||||
|
The object type represented by this dataSource
|
||||||
|
*/
|
||||||
public typealias ObjectType = O
|
public typealias ObjectType = O
|
||||||
|
|
||||||
|
/**
|
||||||
|
Initializes the `DiffableDataSource.CollectionView`. This instance needs to be held on (retained) for as long as the `UICollectionView`'s lifecycle.
|
||||||
|
```
|
||||||
|
self.dataSource = DiffableDataSource.CollectionView<Person>(
|
||||||
|
collectionView: self.collectionView,
|
||||||
|
dataStack: Shared.defaultStack,
|
||||||
|
cellProvider: { (collectionView, indexPath, person) in
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
|
||||||
|
cell.setPerson(person)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- parameter collectionView: the `UICollectionView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.CollectionView`.
|
||||||
|
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
|
||||||
|
- parameter cellProvider: a closure that configures and returns the `UICollectionViewCell` for the object
|
||||||
|
- parameter supplementaryViewProvider: an optional closure for providing `UICollectionReusableView` supplementary views. If not set, defaults to returning `nil`
|
||||||
|
*/
|
||||||
@nonobjc
|
@nonobjc
|
||||||
public init(collectionView: UICollectionView, dataStack: DataStack, cellProvider: @escaping (UICollectionView, IndexPath, O) -> UICollectionViewCell?, supplementaryViewProvider: @escaping (UICollectionView, String, IndexPath) -> UICollectionReusableView? = { _, _, _ in nil }) {
|
public init(collectionView: UICollectionView, dataStack: DataStack, cellProvider: @escaping (UICollectionView, IndexPath, O) -> UICollectionViewCell?, supplementaryViewProvider: @escaping (UICollectionView, String, IndexPath) -> UICollectionReusableView? = { _, _, _ in nil }) {
|
||||||
|
|
||||||
@@ -76,7 +122,21 @@ extension DiffableDataSource {
|
|||||||
|
|
||||||
collectionView.dataSource = self
|
collectionView.dataSource = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Reloads the `UICollectionView` using a `ListSnapshot`. This is typically from the `snapshot` property of a `ListPublisher`:
|
||||||
|
```
|
||||||
|
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||||
|
self?.dataSource?.apply(
|
||||||
|
listPublisher.snapshot,
|
||||||
|
animatingDifferences: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- parameter snapshot: the `ListSnapshot` used to reload the `UITableView` with. This is typically from the `snapshot` property of a `ListPublisher`.
|
||||||
|
- parameter animatingDifferences: if `true`, animations will be applied as configured by the `defaultRowAnimation` value. Defaults to `true`.
|
||||||
|
*/
|
||||||
public func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true) {
|
public func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true) {
|
||||||
|
|
||||||
let diffableSnapshot = snapshot.diffableSnapshot
|
let diffableSnapshot = snapshot.diffableSnapshot
|
||||||
@@ -104,8 +164,15 @@ extension DiffableDataSource {
|
|||||||
)
|
)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func itemIdentifier(for indexPath: IndexPath) -> O.ObjectID? {
|
/**
|
||||||
|
Returns the object identifier for the item at the specified `IndexPath`, or `nil` if not found
|
||||||
|
|
||||||
|
- parameter indexPath: the `IndexPath` to search for
|
||||||
|
- returns: the object identifier for the item at the specified `IndexPath`, or `nil` if not found
|
||||||
|
*/
|
||||||
|
@nonobjc
|
||||||
|
public func itemID(for indexPath: IndexPath) -> O.ObjectID? {
|
||||||
|
|
||||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||||
//
|
//
|
||||||
@@ -116,8 +183,15 @@ extension DiffableDataSource {
|
|||||||
return self.legacyDataSource.itemIdentifier(for: indexPath)
|
return self.legacyDataSource.itemIdentifier(for: indexPath)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func indexPath(for itemIdentifier: O.ObjectID) -> IndexPath? {
|
/**
|
||||||
|
Returns the `IndexPath` for the item with the specified object identifier, or `nil` if not found
|
||||||
|
|
||||||
|
- parameter itemID: the object identifier to search for
|
||||||
|
- returns: the `IndexPath` for the item with the specified object identifier, or `nil` if not found
|
||||||
|
*/
|
||||||
|
@nonobjc
|
||||||
|
public func indexPath(for itemID: O.ObjectID) -> IndexPath? {
|
||||||
|
|
||||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||||
//
|
//
|
||||||
@@ -125,7 +199,7 @@ extension DiffableDataSource {
|
|||||||
// }
|
// }
|
||||||
// else {
|
// else {
|
||||||
|
|
||||||
return self.legacyDataSource.indexPath(for: itemIdentifier)
|
return self.legacyDataSource.indexPath(for: itemID)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,18 +35,66 @@ extension DiffableDataSource {
|
|||||||
|
|
||||||
// MARK: - TableView
|
// MARK: - TableView
|
||||||
|
|
||||||
|
/**
|
||||||
|
The `DiffableDataSource.TableView` serves as a `UITableViewDataSource` that handles `ListPublisher` snapshots for a `UITableView`. Subclasses of `DiffableDataSource.TableView` may override some `UITableViewDataSource` methods as needed.
|
||||||
|
The `DiffableDataSource.TableView` instance needs to be held on (retained) for as long as the `UITableView`'s lifecycle.
|
||||||
|
```
|
||||||
|
self.dataSource = DiffableDataSource.TableView<Person>(
|
||||||
|
tableView: self.tableView,
|
||||||
|
dataStack: Shared.defaultStack,
|
||||||
|
cellProvider: { (tableView, indexPath, person) in
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
|
||||||
|
cell.setPerson(person)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
The dataSource can then apply changes from a `ListPublisher` as shown:
|
||||||
|
```
|
||||||
|
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||||
|
self?.dataSource?.apply(
|
||||||
|
listPublisher.snapshot,
|
||||||
|
animatingDifferences: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`DiffableDataSource.TableView` fully handles the reload animations. To turn change the default animation, set the `defaultRowAnimation`.
|
||||||
|
*/
|
||||||
open class TableView<O: DynamicObject>: NSObject, UITableViewDataSource {
|
open class TableView<O: DynamicObject>: NSObject, UITableViewDataSource {
|
||||||
|
|
||||||
// MARK: Open
|
// MARK: Open
|
||||||
|
|
||||||
|
/**
|
||||||
|
The animation style for row changes
|
||||||
|
*/
|
||||||
@nonobjc
|
@nonobjc
|
||||||
open var defaultRowAnimation: UITableView.RowAnimation = .automatic
|
open var defaultRowAnimation: UITableView.RowAnimation = .automatic
|
||||||
|
|
||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
|
|
||||||
|
/**
|
||||||
|
The object type represented by this dataSource
|
||||||
|
*/
|
||||||
public typealias ObjectType = O
|
public typealias ObjectType = O
|
||||||
|
|
||||||
|
/**
|
||||||
|
Initializes the `DiffableDataSource.TableView`. This instance needs to be held on (retained) for as long as the `UITableView`'s lifecycle.
|
||||||
|
```
|
||||||
|
self.dataSource = DiffableDataSource.TableView<Person>(
|
||||||
|
tableView: self.tableView,
|
||||||
|
dataStack: Shared.defaultStack,
|
||||||
|
cellProvider: { (tableView, indexPath, person) in
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
|
||||||
|
cell.setPerson(person)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- parameter tableView: the `UITableView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.TableView`.
|
||||||
|
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
|
||||||
|
- parameter cellProvider: a closure that configures and returns the `UITableViewCell` for the object
|
||||||
|
*/
|
||||||
@nonobjc
|
@nonobjc
|
||||||
public init(tableView: UITableView, dataStack: DataStack, cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell?) {
|
public init(tableView: UITableView, dataStack: DataStack, cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell?) {
|
||||||
|
|
||||||
@@ -81,7 +129,23 @@ extension DiffableDataSource {
|
|||||||
|
|
||||||
tableView.dataSource = self
|
tableView.dataSource = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Reloads the `UITableView` using a `ListSnapshot`. This is typically from the `snapshot` property of a `ListPublisher`:
|
||||||
|
```
|
||||||
|
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||||
|
self?.dataSource?.apply(
|
||||||
|
listPublisher.snapshot,
|
||||||
|
animatingDifferences: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
If the `defaultRowAnimation` is configured to, animations are also applied accordingly.
|
||||||
|
|
||||||
|
- parameter snapshot: the `ListSnapshot` used to reload the `UITableView` with. This is typically from the `snapshot` property of a `ListPublisher`.
|
||||||
|
- parameter animatingDifferences: if `true`, animations will be applied as configured by the `defaultRowAnimation` value. Defaults to `true`.
|
||||||
|
*/
|
||||||
|
@nonobjc
|
||||||
public func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true) {
|
public func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true) {
|
||||||
|
|
||||||
let diffableSnapshot = snapshot.diffableSnapshot
|
let diffableSnapshot = snapshot.diffableSnapshot
|
||||||
@@ -110,8 +174,15 @@ extension DiffableDataSource {
|
|||||||
)
|
)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func itemIdentifier(for indexPath: IndexPath) -> O.ObjectID? {
|
/**
|
||||||
|
Returns the object identifier for the item at the specified `IndexPath`, or `nil` if not found
|
||||||
|
|
||||||
|
- parameter indexPath: the `IndexPath` to search for
|
||||||
|
- returns: the object identifier for the item at the specified `IndexPath`, or `nil` if not found
|
||||||
|
*/
|
||||||
|
@nonobjc
|
||||||
|
public func itemID(for indexPath: IndexPath) -> O.ObjectID? {
|
||||||
|
|
||||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||||
//
|
//
|
||||||
@@ -122,8 +193,15 @@ extension DiffableDataSource {
|
|||||||
return self.legacyDataSource.itemIdentifier(for: indexPath)
|
return self.legacyDataSource.itemIdentifier(for: indexPath)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func indexPath(for itemIdentifier: O.ObjectID) -> IndexPath? {
|
/**
|
||||||
|
Returns the `IndexPath` for the item with the specified object identifier, or `nil` if not found
|
||||||
|
|
||||||
|
- parameter itemID: the object identifier to search for
|
||||||
|
- returns: the `IndexPath` for the item with the specified object identifier, or `nil` if not found
|
||||||
|
*/
|
||||||
|
@nonobjc
|
||||||
|
public func indexPath(for itemID: O.ObjectID) -> IndexPath? {
|
||||||
|
|
||||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||||
//
|
//
|
||||||
@@ -131,7 +209,7 @@ extension DiffableDataSource {
|
|||||||
// }
|
// }
|
||||||
// else {
|
// else {
|
||||||
|
|
||||||
return self.legacyDataSource.indexPath(for: itemIdentifier)
|
return self.legacyDataSource.indexPath(for: itemID)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,11 +310,17 @@ extension DiffableDataSource {
|
|||||||
|
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
|
@nonobjc
|
||||||
private weak var tableView: UITableView?
|
private weak var tableView: UITableView?
|
||||||
|
|
||||||
|
@nonobjc
|
||||||
private let dataStack: DataStack
|
private let dataStack: DataStack
|
||||||
|
|
||||||
|
@nonobjc
|
||||||
private let cellProvider: (UITableView, IndexPath, O) -> UITableViewCell?
|
private let cellProvider: (UITableView, IndexPath, O) -> UITableViewCell?
|
||||||
|
|
||||||
|
@nonobjc
|
||||||
private var rawDataSource: Any!
|
private var rawDataSource: Any!
|
||||||
|
|
||||||
// @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, *)
|
||||||
@@ -245,6 +329,7 @@ extension DiffableDataSource {
|
|||||||
// return self.rawDataSource as! UITableViewDiffableDataSource<String, O.ObjectID>
|
// return self.rawDataSource as! UITableViewDiffableDataSource<String, O.ObjectID>
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
@nonobjc
|
||||||
private var legacyDataSource: Internals.DiffableDataUIDispatcher<O> {
|
private var legacyDataSource: Internals.DiffableDataUIDispatcher<O> {
|
||||||
|
|
||||||
return self.rawDataSource as! Internals.DiffableDataUIDispatcher<O>
|
return self.rawDataSource as! Internals.DiffableDataUIDispatcher<O>
|
||||||
|
|||||||
@@ -26,4 +26,7 @@
|
|||||||
|
|
||||||
// MARK: - DiffableDataSource
|
// MARK: - DiffableDataSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
Namespace for diffable data source types. See `DiffableDataSource.TableView` and `DiffableDataSource.CollectionView` for actual implementations
|
||||||
|
*/
|
||||||
public enum DiffableDataSource {}
|
public enum DiffableDataSource {}
|
||||||
|
|||||||
@@ -431,77 +431,159 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
|
|||||||
|
|
||||||
|
|
||||||
// MARK: Public (Mutators)
|
// MARK: Public (Mutators)
|
||||||
|
|
||||||
|
/**
|
||||||
|
Appends extra items to the specified section
|
||||||
|
|
||||||
|
- parameter itemIDs: the object identifiers for the objects to append
|
||||||
|
- parameter sectionID: the section to append the items to
|
||||||
|
*/
|
||||||
public mutating func appendItems(withIDs itemIDs: [ItemID], toSectionWithID sectionID: SectionID? = nil) {
|
public mutating func appendItems(withIDs itemIDs: [ItemID], toSectionWithID sectionID: SectionID? = nil) {
|
||||||
|
|
||||||
self.diffableSnapshot.appendItems(itemIDs, toSection: sectionID)
|
self.diffableSnapshot.appendItems(itemIDs, toSection: sectionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Inserts extra items before a specified item
|
||||||
|
|
||||||
|
- parameter itemIDs: the object identifiers for the objects to insert
|
||||||
|
- parameter beforeItemID: an existing identifier to insert items before of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func insertItems(withIDs itemIDs: [ItemID], beforeItemID: ItemID) {
|
public mutating func insertItems(withIDs itemIDs: [ItemID], beforeItemID: ItemID) {
|
||||||
|
|
||||||
self.diffableSnapshot.insertItems(itemIDs, beforeItem: beforeItemID)
|
self.diffableSnapshot.insertItems(itemIDs, beforeItem: beforeItemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Inserts extra items after a specified item
|
||||||
|
|
||||||
|
- parameter itemIDs: the object identifiers for the objects to insert
|
||||||
|
- parameter beforeItemID: an existing identifier to insert items after of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func insertItems(withIDs itemIDs: [ItemID], afterItemID: ItemID) {
|
public mutating func insertItems(withIDs itemIDs: [ItemID], afterItemID: ItemID) {
|
||||||
|
|
||||||
self.diffableSnapshot.insertItems(itemIDs, afterItem: afterItemID)
|
self.diffableSnapshot.insertItems(itemIDs, afterItem: afterItemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Deletes the specified items
|
||||||
|
|
||||||
|
- parameter itemIDs: the object identifiers for the objects to delete
|
||||||
|
*/
|
||||||
public mutating func deleteItems(withIDs itemIDs: [ItemID]) {
|
public mutating func deleteItems(withIDs itemIDs: [ItemID]) {
|
||||||
|
|
||||||
self.diffableSnapshot.deleteItems(itemIDs)
|
self.diffableSnapshot.deleteItems(itemIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Deletes all items
|
||||||
|
*/
|
||||||
public mutating func deleteAllItems() {
|
public mutating func deleteAllItems() {
|
||||||
|
|
||||||
self.diffableSnapshot.deleteAllItems()
|
self.diffableSnapshot.deleteAllItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Moves an item before another specified item
|
||||||
|
|
||||||
|
- parameter itemID: an object identifier in the list to move. Specifying an invalid value will raise an exception.
|
||||||
|
- parameter beforeItemID: another identifier to move the item before of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func moveItem(withID itemID: ItemID, beforeItemID: ItemID) {
|
public mutating func moveItem(withID itemID: ItemID, beforeItemID: ItemID) {
|
||||||
|
|
||||||
self.diffableSnapshot.moveItem(itemID, beforeItem: beforeItemID)
|
self.diffableSnapshot.moveItem(itemID, beforeItem: beforeItemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Moves an item after another specified item
|
||||||
|
|
||||||
|
- parameter itemID: an object identifier in the list to move. Specifying an invalid value will raise an exception.
|
||||||
|
- parameter beforeItemID: another identifier to move the item after of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func moveItem(withID itemID: ItemID, afterItemID: ItemID) {
|
public mutating func moveItem(withID itemID: ItemID, afterItemID: ItemID) {
|
||||||
|
|
||||||
self.diffableSnapshot.moveItem(itemID, afterItem: afterItemID)
|
self.diffableSnapshot.moveItem(itemID, afterItem: afterItemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Marks the specified items as reloaded
|
||||||
|
|
||||||
|
- parameter itemIDs: the object identifiers to reload
|
||||||
|
*/
|
||||||
public mutating func reloadItems(withIDs itemIDs: [ItemID]) {
|
public mutating func reloadItems(withIDs itemIDs: [ItemID]) {
|
||||||
|
|
||||||
self.diffableSnapshot.reloadItems(itemIDs)
|
self.diffableSnapshot.reloadItems(itemIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Appends new section identifiers to the end of the list
|
||||||
|
|
||||||
|
- parameter sectionIDs: the sections to append
|
||||||
|
*/
|
||||||
public mutating func appendSections(withIDs sectionIDs: [SectionID]) {
|
public mutating func appendSections(withIDs sectionIDs: [SectionID]) {
|
||||||
|
|
||||||
self.diffableSnapshot.appendSections(sectionIDs)
|
self.diffableSnapshot.appendSections(sectionIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Inserts new sections before an existing section
|
||||||
|
|
||||||
|
- parameter sectionIDs: the section identifiers for the sections to insert
|
||||||
|
- parameter beforeSectionID: an existing identifier to insert items before of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func insertSections(withIDs sectionIDs: [SectionID], beforeSectionID: SectionID) {
|
public mutating func insertSections(withIDs sectionIDs: [SectionID], beforeSectionID: SectionID) {
|
||||||
|
|
||||||
self.diffableSnapshot.insertSections(sectionIDs, beforeSection: beforeSectionID)
|
self.diffableSnapshot.insertSections(sectionIDs, beforeSection: beforeSectionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Inserts new sections after an existing section
|
||||||
|
|
||||||
|
- parameter sectionIDs: the section identifiers for the sections to insert
|
||||||
|
- parameter beforeSectionID: an existing identifier to insert items after of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func insertSections(withIDs sectionIDs: [SectionID], afterSectionID: SectionID) {
|
public mutating func insertSections(withIDs sectionIDs: [SectionID], afterSectionID: SectionID) {
|
||||||
|
|
||||||
self.diffableSnapshot.insertSections(sectionIDs, afterSection: afterSectionID)
|
self.diffableSnapshot.insertSections(sectionIDs, afterSection: afterSectionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Deletes the specified sections
|
||||||
|
|
||||||
|
- parameter sectionIDs: the section identifiers for the sections to delete
|
||||||
|
*/
|
||||||
public mutating func deleteSections(withIDs sectionIDs: [SectionID]) {
|
public mutating func deleteSections(withIDs sectionIDs: [SectionID]) {
|
||||||
|
|
||||||
self.diffableSnapshot.deleteSections(sectionIDs)
|
self.diffableSnapshot.deleteSections(sectionIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Moves a section before another specified section
|
||||||
|
|
||||||
|
- parameter sectionID: a section identifier in the list to move. Specifying an invalid value will raise an exception.
|
||||||
|
- parameter beforeSectionID: another identifier to move the section before of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func moveSection(withID sectionID: SectionID, beforeSectionID: SectionID) {
|
public mutating func moveSection(withID sectionID: SectionID, beforeSectionID: SectionID) {
|
||||||
|
|
||||||
self.diffableSnapshot.moveSection(sectionID, beforeSection: beforeSectionID)
|
self.diffableSnapshot.moveSection(sectionID, beforeSection: beforeSectionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Moves a section after another specified section
|
||||||
|
|
||||||
|
- parameter sectionID: a section identifier in the list to move. Specifying an invalid value will raise an exception.
|
||||||
|
- parameter afterSectionID: another identifier to move the section after of. Specifying an invalid value will raise an exception.
|
||||||
|
*/
|
||||||
public mutating func moveSection(withID sectionID: SectionID, afterSectionID: SectionID) {
|
public mutating func moveSection(withID sectionID: SectionID, afterSectionID: SectionID) {
|
||||||
|
|
||||||
self.diffableSnapshot.moveSection(sectionID, afterSection: afterSectionID)
|
self.diffableSnapshot.moveSection(sectionID, afterSection: afterSectionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Marks the specified sections as reloaded
|
||||||
|
|
||||||
|
- parameter sectionIDs: the section identifiers to reload
|
||||||
|
*/
|
||||||
public mutating func reloadSections(withIDs sectionIDs: [SectionID]) {
|
public mutating func reloadSections(withIDs sectionIDs: [SectionID]) {
|
||||||
|
|
||||||
self.diffableSnapshot.reloadSections(sectionIDs)
|
self.diffableSnapshot.reloadSections(sectionIDs)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import CoreData
|
|||||||
// MARK: - ObjectObserver
|
// MARK: - ObjectObserver
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Implement the `ObjectObserver` protocol to observe changes to a single `DynamicObject` instance. `ObjectObserver`s may register themselves to a `ObjectMonitor`'s `addObserver(_:)` method:
|
Implement the `ObjectObserver` protocol to observe changes to a single `DynamicObject` instance. `ObjectObserver`s may register themselves to an `ObjectMonitor`'s `addObserver(_:)` method:
|
||||||
```
|
```
|
||||||
let monitor = CoreStore.monitorObject(object)
|
let monitor = CoreStore.monitorObject(object)
|
||||||
monitor.addObserver(self)
|
monitor.addObserver(self)
|
||||||
|
|||||||
@@ -116,9 +116,6 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
|
|||||||
|
|
||||||
// MARK: ObjectRepresentation
|
// MARK: ObjectRepresentation
|
||||||
|
|
||||||
/**
|
|
||||||
The `DynamicObject` type associated with this list
|
|
||||||
*/
|
|
||||||
public typealias ObjectType = O
|
public typealias ObjectType = O
|
||||||
|
|
||||||
public func objectID() -> O.ObjectID {
|
public func objectID() -> O.ObjectID {
|
||||||
|
|||||||
@@ -36,13 +36,15 @@ import AppKit
|
|||||||
|
|
||||||
// MARK: - ObjectSnapshot
|
// MARK: - ObjectSnapshot
|
||||||
|
|
||||||
|
/**
|
||||||
|
The `ObjectSnapshot` is a full copy of a `DynamicObject`'s properties at a given point in time. This is useful especially when keeping thread-safe state values, in ViewModels for example. Since this is a value type, any changes in this `struct` does not affect the actual object.
|
||||||
|
*/
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
public struct ObjectSnapshot<O: DynamicObject>: ObjectRepresentation, Hashable {
|
public struct ObjectSnapshot<O: DynamicObject>: ObjectRepresentation, Hashable {
|
||||||
|
|
||||||
public typealias ObjectType = O
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: ObjectRepresentation
|
// MARK: ObjectRepresentation
|
||||||
|
|
||||||
|
public typealias ObjectType = O
|
||||||
|
|
||||||
public func objectID() -> O.ObjectID {
|
public func objectID() -> O.ObjectID {
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ extension UnsafeDataTransaction {
|
|||||||
Creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`.
|
Creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`.
|
||||||
|
|
||||||
- parameter object: the `DynamicObject` to observe changes from
|
- parameter object: the `DynamicObject` to observe changes from
|
||||||
- returns: a `ObjectMonitor` that monitors changes to `object`
|
- returns: an `ObjectMonitor` that monitors changes to `object`
|
||||||
*/
|
*/
|
||||||
public func monitorObject<O: DynamicObject>(_ object: O) -> ObjectMonitor<O> {
|
public func monitorObject<O: DynamicObject>(_ object: O) -> ObjectMonitor<O> {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user