Skip to content

Commit

Permalink
Document all new API's. Add Changelog entry, restructure readme.
Browse files Browse the repository at this point in the history
  • Loading branch information
DenTelezhkin committed Sep 24, 2018
1 parent ed96bb9 commit e651ee3
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 101 deletions.
2 changes: 1 addition & 1 deletion .swift-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.0
4.2
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.

### Added

* Single section storage classes that encapsulate single section of models with automatic diffing to animate changes. For a lot of use cases this approach is more suitable than `MemoryStorage` and is now a recommended way of handling items in single section.

Read more about it [in README](https://github.com/DenHeadless/DTModelStorage#singlesectionstorage).

* Convenience method to create `MappingCondition` from ModelTransfer objects, for example, if used with `DTTableViewManager`:

```swift
Expand All @@ -31,6 +35,8 @@ memoryStorage.anomalyHandler.silenceAnomaly { anomaly in
}
```

* Support for Swift 4.2 and Xcode 10.

## [7.1.0](https://github.com/DenHeadless/DTModelStorage/releases/tag/7.1.0)

### Added
Expand Down
195 changes: 98 additions & 97 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ The goal of the project is to provide storage classes for datasource based contr
Now, if we look on `UICollectionView`, that stuff does not change. And probably any kind of datasource based control can be adapted to use the same terminology. So, instead of reinventing the wheel every time, let's try to implement universal storage classes, that would fit any control.

`DTModelStorage` supports 4 storage classes:
* Memory storage
* Single section storage
* Memory storage
* CoreData storage
* Realm storage

Expand All @@ -41,101 +41,10 @@ Memory storage classes will provide convenience methods to update storage, CoreD

`DTModelStorage` provides convenience methods to be used with `UITableView` or `UICollectionView`, but does not force any specific use, and does not imply, which UI components are compatible with it. However, storage classes are designed to work with "sections" and "items", which generally means some kind of table or collection of items.

CoreDataStorage
================

`CoreDataStorage` is meant to be used with `NSFetchedResultsController`. It automatically monitors all `NSFetchedResultsControllerDelegate` methods and and calls delegate with appropriate updates.

```swift
let storage = CoreDataStorage(fetchedResultsController: controller)
```

Any section in `CoreDataStorage` conform to `NSFetchedResultsSectionInfo` protocol, however `DTModelStorage` extends them to be `Section` protocol compatible. This way CoreData sections and memory sections have the same interface.

For perfomance reasons, you should not retrieve items via `items` property, if you don't need to. Items may not be fetched from CoreData database, and if you need to retrieve only one specific item, it's better to call -`item(at:)` method instead. This way only one item will be actually fetched from database.

MemoryStorage
SingleSectionStorage
================
`MemoryStorage` encapsulates storage of data models in memory. It's basically Array of `SectionModel` items, which contain array of items for current section, and supplementary models of any kind, that add additional information for section. Good example would be UITableView headers and footers, or UICollectionView with UICollectionViewFlowLayout.

```swift
let storage = MemoryStorage()
```

#### Adding items

```swift
storage.addItem(model)
storage.addItem(model, toSection: 0)

storage.addItems([model1,model2])
storage.addItems([model1,model2], toSection:0)

try? storage.insertItem(model, to: indexPath)
```

#### Remove / replace / Reload

```swift
try? storage.removeItem(model)
storage.removeItems([model1,model2])
storage.removeItems(at:indexPaths)

try? storage.replaceItem(model1, with: model2)

storage.reloadItem(model1)
```

#### Managing sections

```swift
storage.deleteSections(NSIndexSet(index: 1))
```

#### Retrieving items

```swift
let item = storage.item(at:NSIndexPath(forItem:1, inSection:0)

let indexPath = storage.indexPath(forItem:model)

let itemsInSection = storage.items(inSection:0)

let section = storage.section(atIndex:0)
```

#### Updating manually

Sometimes you may need to update batch of sections, remove all items, and add new ones. For those massive updates you don't actually need to update interface until update is finished. Wrap your updates in single block and pass it to updateWithoutAnimations method:

```swift
storage.updateWithoutAnimations {
// Add multiple rows, or another batch of edits
}
// Calling reloadData is mandatory after calling this method. or you will get crash runtime
```

For reordering of items, when animation is not needed, you can call `moveItemWithoutAnimation(from:to:)` method:

```swift
storage.moveItemWithoutAnimation(from: sourceIndexPath, to: destinationIndexPath)
```

#### Supplementary models

```swift
let section = storage.section(atIndex:0)
section.setSupplementaryModel("foo", forKind: UICollectionElementKindSectionHeader)
let model = section.supplementaryModelOfKind(UICollectionElementKindSectionHeader)
```

#### Transferring model

`DTModelStorage` defines `ModelTransfer` protocol, that allows transferring your data model to interested parties. This can be used for example for updating `UITableViewCell`. Thanks to associated `ModelType` of the protocol it is possible to transfer your model without any type casts.

## SingleSectionStorage

While sometimes you need such fine-grained control, that `MemoryStorage` provides, the most often use case is just showing a collection of items, for example array of posts from social network, or search results with a single entity.
While sometimes you need such fine-grained control, that `MemoryStorage` provides, the most often use case for this library is just showing a collection of items, for example array of posts from social network, or search results with a single entity.

In this case, mostly used methods from `MemoryStorage` are `setItems` and `addItems`, because in this case you probably don't need any other methods. What you may want, however, is an ability to automatically calculate diffs between old and new state to be able to animate UI without the need to call `reloadData`. That's where `SingleSectionStorage` comes in.

Expand Down Expand Up @@ -178,13 +87,13 @@ extension Post : Identifiable {
Create storage:

```swift
let storage = SingleSectionEquatableStorage(items: [<array of items>], differ: ChangesetDiffer())
let storage = SingleSectionEquatableStorage(items: arrayOfPosts, differ: ChangesetDiffer())
```

Set new array of items and automatically calculate all diffs:

```swift
storage.setItems(newItems)
storage.setItems(newPosts)
```

Full example of automatically animating items in `UITableView` can be seen in [DTTableViewManager repo](https://github.com/DenHeadless/DTTableViewManager/blob/master/Example/Example%20controllers/AutoDiffSearchViewController.swift)
Expand Down Expand Up @@ -257,6 +166,98 @@ Quite ugly, I know. But that seems like the only option that is possible today.

`SingleSectionStorage` has support for supplementaries to support `UICollectionView` supplementary views, as well as `UITableView` headers and footers. The only difference from `MemoryStorage` is that `SingleSectionStorage` contains, you guessed it, only one section, and therefore one set of supplementaries.

MemoryStorage
================
`MemoryStorage` encapsulates storage of data models in memory. It's basically Array of `SectionModel` items, which contain array of items for current section, and supplementary models of any kind, that add additional information for section. Good example would be UITableView headers and footers, or UICollectionView with UICollectionViewFlowLayout.

```swift
let storage = MemoryStorage()
```

#### Adding items

```swift
storage.addItem(model)
storage.addItem(model, toSection: 0)

storage.addItems([model1,model2])
storage.addItems([model1,model2], toSection:0)

try? storage.insertItem(model, to: indexPath)
```

#### Remove / replace / Reload

```swift
try? storage.removeItem(model)
storage.removeItems([model1,model2])
storage.removeItems(at:indexPaths)

try? storage.replaceItem(model1, with: model2)

storage.reloadItem(model1)
```

#### Managing sections

```swift
storage.deleteSections(NSIndexSet(index: 1))
```

#### Retrieving items

```swift
let item = storage.item(at:NSIndexPath(forItem:1, inSection:0)

let indexPath = storage.indexPath(forItem:model)

let itemsInSection = storage.items(inSection:0)

let section = storage.section(atIndex:0)
```

#### Updating manually

Sometimes you may need to update batch of sections, remove all items, and add new ones. For those massive updates you don't actually need to update interface until update is finished. Wrap your updates in single block and pass it to updateWithoutAnimations method:

```swift
storage.updateWithoutAnimations {
// Add multiple rows, or another batch of edits
}
// Calling reloadData is mandatory after calling this method. or you will get crash runtime
```

For reordering of items, when animation is not needed, you can call `moveItemWithoutAnimation(from:to:)` method:

```swift
storage.moveItemWithoutAnimation(from: sourceIndexPath, to: destinationIndexPath)
```

#### Supplementary models

```swift
let section = storage.section(atIndex:0)
section.setSupplementaryModel("foo", forKind: UICollectionElementKindSectionHeader)
let model = section.supplementaryModelOfKind(UICollectionElementKindSectionHeader)
```

#### Transferring model

`DTModelStorage` defines `ModelTransfer` protocol, that allows transferring your data model to interested parties. This can be used for example for updating `UITableViewCell`. Thanks to associated `ModelType` of the protocol it is possible to transfer your model without any type casts.

CoreDataStorage
================

`CoreDataStorage` is meant to be used with `NSFetchedResultsController`. It automatically monitors all `NSFetchedResultsControllerDelegate` methods and and calls delegate with appropriate updates.

```swift
let storage = CoreDataStorage(fetchedResultsController: controller)
```

Any section in `CoreDataStorage` conform to `NSFetchedResultsSectionInfo` protocol, however `DTModelStorage` extends them to be `Section` protocol compatible. This way CoreData sections and memory sections have the same interface.

For perfomance reasons, you should not retrieve items via `items` property, if you don't need to. Items may not be fetched from CoreData database, and if you need to retrieve only one specific item, it's better to call -`item(at:)` method instead. This way only one item will be actually fetched from database.

## RealmStorage

`RealmStorage` class is made to work with [realm.io](https://realm.io) databases. It works with sections, that contain Realm.Results object.
Expand Down Expand Up @@ -286,7 +287,7 @@ Installation
Requirements
============

* Xcode 8 and higher
* Xcode 9 and higher
* Swift 3 - Swift 4.2
* iOS 8 and higher / tvOS 9.0 and higher

Expand Down
38 changes: 38 additions & 0 deletions Source/Core/AccumulationStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,49 @@

import Foundation

/// Strategy to accumulate `oldItems` and `newItems` into resulting array.
public protocol AccumulationStrategy {

/// Accumulate `oldItems` and `newItems` into resulting array.
///
/// - Parameters:
/// - oldItems: array of items, already present in storage
/// - newItems: array of items, that will be accumulated
/// - Returns: Accumulated items array.
func accumulate<T:Identifiable>(oldItems: [T], newItems: [T]) -> [T]
}

/// Strategy, that adds new items to old items, without comparing their identifiers.
/// This strategy is used by default by `addItems` method of `SingleSectionStorage`.
public struct AdditiveAccumulationStrategy: AccumulationStrategy {

/// Creates additive accumulation strategy
public init() {}

/// Accumulate `oldItems` and `newItems` into resulting array by appending `newItems` to `oldItems`.
///
/// - Parameters:
/// - oldItems: array of items, already present in storage
/// - newItems: array of items, that will be accumulated
/// - Returns: Array, that consists of old items and new items.
public func accumulate<T>(oldItems: [T], newItems: [T]) -> [T] where T : Identifiable {
return oldItems + newItems
}
}

/// Strategy to update old values with a new ones, using old items position.
public struct UpdateOldValuesAccumulationStrategy: AccumulationStrategy {

/// Creates update old values accumulation strategy
public init() {}

/// Accumulate `oldItems` and `newItems` into resulting array by updating old items with new values, using old item positions in collection.
/// Identity of item is determined by `identifier` property.
///
/// - Parameters:
/// - oldItems: array of items, already present in storage
/// - newItems: array of items, that will be accumulated
/// - Returns: Accumulated items array, that contains old items updated with new values and new unique values.
public func accumulate<T>(oldItems: [T], newItems: [T]) -> [T] where T : Identifiable {
var newArray = oldItems
var existingIdentifiers = [AnyHashable:Int]()
Expand All @@ -60,9 +88,19 @@ public struct UpdateOldValuesAccumulationStrategy: AccumulationStrategy {
}
}

/// Strategy to delete old values when accumulating newItems
public struct DeleteOldValuesAccumulationStrategy: AccumulationStrategy {

/// Creates strategy
public init() {}

/// Accumulate `oldItems` and `newItems` into resulting array by deleting old items, that have new values in `newItems`, from `oldItems`.
/// This way old duplicated values are basically moved to their location in `newItems` and updated with new data.
///
/// - Parameters:
/// - oldItems: array of items, already present in storage
/// - newItems: array of items, that will be accumulated
/// - Returns: Accumulated items array.
public func accumulate<T>(oldItems: [T], newItems: [T]) -> [T] where T : Identifiable {
var newArray = oldItems
var existingIdentifiers = [AnyHashable:Int]()
Expand Down
11 changes: 11 additions & 0 deletions Source/Core/SingleSectionDiffing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,32 @@

import Foundation

/// A type that can be identified by `identifier`.
public protocol Identifiable {

/// Unique identifier of object. It must never change for this specific object.
var identifier: AnyHashable { get }
}

/// Edit operation in single section.
///
/// - delete: item is deleted at index
/// - insert: item is inserted at index
/// - move: item is moved `from` index `to` index.
/// - update: item is updated at index.
public enum SingleSectionOperation {
case delete(Int)
case insert(Int)
case move(from: Int, to: Int)
case update(Int)
}

/// Algorithm that requires elements in collection to be `Hashable`
public protocol HashableDiffingAlgorithm {
func diff<T: Identifiable & Hashable>(from: [T], to: [T]) -> [SingleSectionOperation]
}

/// Algorithm that requires elements in collection to be `Equatable`
public protocol EquatableDiffingAlgorithm {
func diff<T: Identifiable & Equatable>(from: [T], to: [T]) -> [SingleSectionOperation]
}
Loading

0 comments on commit e651ee3

Please sign in to comment.