Swiftpack.co - Package -

Build Status codecov.io CocoaPods compatible Carthage compatible Packagist

DTModelStorage

This project is used as a dependency by DTTableViewManager and DTCollectionViewManager - great tools for UITableView and UICollectionView management.

  • ☑ Support for any data structure - class, struct, enum, tuple
  • ☑ Support for automatic diffing in section
  • ☑ Support for diffable datasources in iOS 13
  • ☑ Protocol-oriented implementation with generic and associated types
  • ☑ Powerful events system for storage consumers.
  • ☑ High test coverage
  • Complete documentation

What this is all about?

The goal of the project is to provide storage classes for datasource based controls. Let's take UITableView, for example. It's datasource methods mostly relates on following:

  • sections
  • items in sections
  • section headers and footers

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 5 storage classes:

  • Single section storage
  • Memory storage
  • CoreData storage
  • Realm storage
  • Storage for diffable datasources

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.

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.

Requirements

  • Xcode 10 and higher
  • Swift 4.1 and higher
  • iOS 8 and higher / tvOS 9.0 and higher

Installation

Swift Package Manager (requires Xcode 11)

  • Add package into Project settings -> Swift Packages

CocoaPods:

pod 'DTModelStorage', '~> 8.0'

Carthage

github "DenTelezhkin/DTModelStorage" "8.0"

MemoryStorage

MemoryStorage encapsulates storage of data models in memory. It's basically Array of SectionModel items, which contain array of items for current section.

let storage = MemoryStorage()

Adding items

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

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

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

storage.reloadItem(model1)

Managing sections

storage.deleteSections(NSIndexSet(index: 1))

Retrieving items

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:

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:

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

Supplementary model providers

All 5 implemented storages have a single supplementary model provider API, that consists of three closures:

  • headerModelProvider
  • footerModelProvider
  • supplementaryModelProvider

supplementaryModelProvider closure setter has been overridden to allow calling headerModelProvider and footerModelProvider. So, for example, if closures are setup in the following way:

storage.headerModelProvider = { index in [1,2,3][index] }
storage.supplementaryModelProvider = { kind, index in [4,5,6][index.item] }
storage.supplementaryHeaderKind = "Foo"

Then supplementary providers will work as shown below:

storage.supplementaryModel(ofKind: "Foo", forSectionAt: IndexPath(item: 0, section:0)) // 1
storage.supplementaryModel(ofKind: "Bar", forSectionAt: IndexPath(item: 0, section:0)) // 4

ProxyDiffableDataSourceStorage

ProxyDiffableDataSourceStorage is a proxy class used by DTTableViewManager/DTCollectionViewManager frameworks to support diffable datasources in iOS 13.

All it does, it contain wrapper closures, that redirect datasource questions to diffable datasource object. Additionally, It has the same supplementary model provider logic that all storages have.

SingleSectionStorage

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.

But before showing any usage examples, let's talk about diffing a little bit. There are a lot of great collection differs outhere, and DTModelStorage is not aimed to providing another one. Instead, it provides an API to work with currently available differs.

Algorithms

There are a lot of community-built algorithms to compute diff between two collections, for example here's list of frameworks that are built using Paul Heckel's A Technique for Isolating Differences Between Files:

There are other algorithms and implementations available, for example:

  • Dwifft - Longest common subsequence algorithm
  • Differ - Longest common subsequence algorithm
  • Changeset - Wagner-Fischer algorithm (specific implementation of Levenstein algorithm).

Because algorithms are built differently, but have some common traits, SingleSectionStorage implements two concrete subclasses, that work with algorithms with Equatable elements and algorithms that work with Hashable elements - SingleSectionEquatableStorage and SingleSectionHashableStorage.

Algorithm adapter

To work with specific algorithm, you would need to create a thin adapter, that converts results of algorithm work to models, compatible with DTModelStorage. Here are some examples of how this can be done:

Example

After adapter has been built, you need to implement EntityIdentifiable protocol on your data models to provide way to identify models:

extension Post : EntityIdentifiable {
    public var identifier: AnyHashable { return id }
}

Create storage:

let storage = SingleSectionEquatableStorage(items: arrayOfPosts, differ: ChangesetDiffer())

Set new array of items and automatically calculate all diffs:

storage.setItems(newPosts)

Full example of automatically animating items in UITableView can be seen in DTTableViewManager repo

Adding items

When you show list of items, common task is to add new loaded items to this list (for example load more content). Doing that is really simple:

storage.addItems(newItems)

Sometimes you may want to customize how items are accumulated in resulting collection of items. For example when content changed in time between first page request and second page request in load-more scenario. If back-end does not handle this for iOS side, you may want to build handling of such cases on client side. To do that, DTModelStorage provides AccumulationStrategy protocol, that consists of single method:

protocol AccumulationStrategy {
    func accumulate<T:Identifiable>(oldItems: [T], newItems: [T]) -> [T]
}

This strategy determines how new collection of items will be formed. DTModelStorage provides three concrete implementations of this protocol:

  • AdditiveAccumulationStrategy - default strategy, that simply adds oldItems to newItemsArray.
  • UpdateOldValuesAccumulationStrategy - replaces old values with new values from newItems array - uniqueness is determined by Identifiable identifier property
  • DeleteOldValuesAccumulationStrategy - deletes old items, new values remain in new location as returned by newArray - uniqueness is determined by EntityIdentifiable identifier property.

To use any of the strategies, just call addItems method with additional parameter:

storage.addItems(newItems, UpdateOldValuesAccumulationStrategy())

Several model types in SingleSectionStorage

SingleSectionStorage class uses generics to determine it's item type. While it provides compile-time guarantees for item type, it unfortunately prevents using several model types in SingleSectionStorage using Any type or a protocol. To do that, Swift needs to implement feature called Generalized existentials. Unfortunately, at the moment of writing (Xcode 10, Swift 4.2) this feature is not implemented.

Therefore, to support several data models type in SingleSection we can use technique called type-erasing. We can build generic wrapper class, that implements all protocols that are required, but actually accepts Any value:

struct AnyIdentifiableEquatable: Identifiable, Equatable {
    let value : Any
    let equals: (Any) -> Bool
    let identifier: AnyHashable

    init<T:Identifiable & Equatable>(_ value: T) {
        self.value = value
        equals = {
            guard let instance = $0 as? T else { return false }
            return instance == value
        }
        identifier = value.identifier
    }

    static func == (lhs: AnyIdentifiableEquatable, rhs: AnyIdentifiableEquatable) -> Bool {
        return lhs.equals(rhs.value) || rhs.equals(lhs.value)
    }
}

This way you can create a storage, that accepts any number of data models:

let typeErasedInstances = [AnyIdentifiableEquatable(Foo()), AnyIdentifiableEquatable(Bar())]
let storage = SingleSectionEquatableStorage(items: typeErasedInstances, differ: DwifftDiffer())

Quite ugly, I know. But that seems like the only option that is possible today.

CoreDataStorage

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

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.

RealmStorage

RealmStorage class is made to work with realm.io databases. It works with sections, that contain Realm.Results object.

Creating storage and filling it with results is very easy:

let results = try! Realm().objects(Dog)

let storage = RealmStorage()
storage.addSection(with:results)

That's it! Results are automatically monitored, and refreshed, if Realm objects change.

Note You should not use RealmStorage with multiple sections, because it may lead to crashes when simultaneous UI updates make UI state inconsistent(https://github.com/DenTelezhkin/DTModelStorage/issues/21).

One possible solution to this are diffable datasources in iOS 13, where you should be able to construct all sections manually thus avoiding crashes.

Github

link
Stars:
Help us keep the lights on

Dependencies

Used By

Total: 0

Releases

8.0.0 - 2019-11-08 11:46:01

8.0.0-beta.2 - 2019-09-06 09:28:51

  • Added support for Xcode versions, that are older than Xcode 11.

8.0.0-beta.1 - 2019-08-20 09:04:30

This is a major release with some breaking changes, please read DTModelStorage 8.0 Migration Guide

Added

  • bundle property on ViewModelMapping, that exposes recommended bundle to be used when searching for resources of given mapping.
  • Setter for SingleSectionStorage.items property.
  • Section.item(at:) method.
  • ProxyDiffableDataSourceStorage that serves as a bridge between DTTableViewManager/DTCollectionViewManager and diffable datasource classes(UITableViewDiffableDataSource`UICollectionViewDiffableDataSource`).

Changed

  • configureForTableViewUsage, configureForCollectionViewUsage, headerModel(forSection:), footerModel(forSection:), have been moved to protocol extensions instead of being implemented in BaseStorage class. As a consequence, BaseStorage no longer confirms to HeaderFooterStorage protocol.

Breaking

Identifiable protocol has been renamed to EntityIdentifiable protocol to avoid unwanted clashes with Foundation.Identifiable protocol, that is available on iOS 13 and higher.

Complete rewrite of header/footer/supplementary model handling. Instead of several implementations and model storages, the API now consists of three closure based properties on SupplementaryStorage protocol : headerModelProvider, footerModelProvider and supplementaryModelProvider. All storage classes implement this protocol (MemoryStorage, CoreDataStorage, RealmStorage, SingleSectionStorage, ProxyDiffableDataSourceStorage).

Storage protocols and classes have been restructured:

  • SupplementaryAccessible renamed to SectionLocatable
  • HeaderFooterStorage and HeaderFooterSettable have been removed
  • HeaderFooterStorage functionality mostly has been merged into new protocol SupplementaryStorage
  • BaseStorage has been split into BaseSupplementaryStorage and BaseUpdateDeliveringStorage that inherits from it.

Several methods continue to work, but are now bridging to new closure-based API:

  • setSectionHeaderModels
  • setSectionFooterModels
  • headerModel(forSection:)
  • footerModel(forSection:)
  • supplementaryModel(ofKind:forSectionAt:)

setSectionHeaderModels and setSectionFooterModels, as well as new closure-based API do not call reloadData method, as they were doing before. If you need to reset section headers/footers/supplementaries, consider calling StorageUpdating.storageNeedsReloading() method manually.

All methods that allowed to set header/footer/supplementary models partially, for a specific section or specific supplementary kind, have been made unavailable or removed.

CoreDataStorage now sets headerModelProvider closure to allow using FetchedResultsController section name as header instead of having arbitrary logic that compared supplementaryKind to displaySectionNameForSupplementaryKinds property, which is also made unavailable.

Removed

  • Deprecated MemoryStorageError.BatchInsertionReason enum.
  • Deprecated ViewModelMappingCustomizing protocol.
  • sections method on Storage protocol. It is replaced by more perfomant numberOfSections() and numberOfItems(inSection:) methods.
  • sections method on CoreDataStorage and SingleSectionStorage
  • items property on Section protocol. It is replaced by item(at:) method.

7.4.1 - 2019-07-16 14:44:46

Added

  • bundle property on ViewModelMapping, that exposes recommended bundle to be used when searching for resources of given mapping.

Fixed

  • setItemsForAllSections method now properly removes all sections prior to setting new ones. This prevents a bug, where old sections could stay, if this method was called with fewer number of sections.

7.4.0 - 2019-06-17 19:43:25

Added

  • Support for Swift Package Manager in Xcode 11

Changed

  • Slightly improved RealmStorage item(at:) method perfomance.
  • Improved StorageUpdate description.
  • ViewModelMapping.xibName can now be changed inside of mapping blocks to allow changing xibName per mapping.

7.3.0 - 2019-04-07 11:30:18

Added

  • Support for Swift 5 and Xcode 10.2

Deprecated

  • ViewModelMappingCustomizing protocol. Please switch to using mapping conditions instead.

Removed

  • Support for Xcode 9 and Swift 3

7.2.1 - 2019-03-01 08:18:23

  • Make enqueueDatasourceUpdate method and enqueuedDatasourceUpdates property public to allow building custom storages that defer datasource updates.

7.2.0 - 2018-09-25 14:10:11

Added

  • Single section storage classes with support for automatic diffing.

    SingleSectionStorage classes are now a recommended way of handling items in single section in more powerful way than MemoryStorage class.

    Read more about it in README.

  • Support for Swift 4.2 and Xcode 10.

  • Convenience methods for conditional mappings and anomalies

    • Method to create MappingCondition from ModelTransfer objects
    • Ability to silence anomalies using enum or closure

7.1.0 - 2018-06-09 12:39:15

Added

Changed

  • Support for Xcode 10 (beta 1) with Swift 4.1 and Swift 4.2.

Fixed

  • Crash that could happen if move operation for MemoryStorage happened from and to the same section with not enough items in section.

7.0.3 - 2018-04-02 14:45:56

  • Updates for Xcode 9.3 and Swift 4.1

7.0.2 - 2018-02-01 12:07:02

  • Properly collect all updates from updateWithoutAnimations block.

7.0.1 - 2018-02-01 11:58:04

  • Fixed a bug, that prevented datasource from being updated when updateWithoutAnimations method on MemoryStorage was used.

7.0.0 - 2018-01-19 15:50:03

7.0.0-beta.1 - 2017-12-04 12:07:02

  • Implemented mechanism for deferring datasource updates of MemoryStorage. When turned on, updates are no longer applied automatically, but can be applied calling StorageUpdate.applyDeferredDatasourceUpdates() method. Keep in mind, that not only datasource updates are not applied, but object and section changes are also empty until updates are applied. This behaviour is turned on by default, to disable it, call
MemoryStorage.defersDatasourceUpdates = false
  • Ream podspec now requires Realm version 3.x

6.0.0 - 2017-11-01 12:08:58

  • Fixed warnings for Xcode 9.1 / Swift 4.0.2

6.0.0-beta.2 - 2017-09-27 14:48:20

  • Build with Xcode 9.0 release.

6.0.0-beta.1 - 2017-09-10 17:43:32

This is major release, containing breaking API changes, please read DTTableViewManager 6.0 Migration Guide

  • MemoryStorage now has a convenience method for moving item without animations: moveItemWithoutAnimation(from:to:).
  • EventReaction class now has 4 and 5 argument reactions
  • All storage protocols are now class-bound.
  • Implemented mapping conditions. ViewModelMapping was changed to be able to work with mapping blocks.

Breaking

  • RealmStorage is not included in Carthage releases.
  • setItems method, that accepted array of arrays of items to set items for all sections, has been renamed to setItemsForAllSections to provide more clarity and not to clash with setItems(_:forSection:) method.

5.1.0 - 2017-06-19 15:12:38

  • Swift 3.2 support(Xcode 9 beta 1).
  • RealmStorage now accepts RealmCollection objects into section, thus allowing List and LinkingObjects to be used(previously only Results objects could be used in section).

5.0.1 - 2017-05-29 15:48:31

  • Improved handling of NSFetchedResultsControllerDelegate NSFetchedResultsChangeType.update change type in cases, where object inserts/removal/moves is used simultaneously with object updates(#17).

5.0.0 - 2017-04-06 15:01:29

  • Reworked EventReaction class to use ViewModelMapping to properly identify not only model and ViewType, but also viewClass. This allows event reactions to run for cases where two view subclasses, conforming to ModelTransfer, use the same model, and have similar events.

4.1.0 - 2017-01-29 09:18:47

  • Adds setItems(_:) method, that allows to set items for all sections in MemoryStorage.

4.0.0 - 2016-10-28 14:29:43

  • StorageUpdate properties, that tracked changes independently of each other, have been replaced with objectChanges, sectionChanges arrays, that track all changes in order they occured
  • StorageUpdate now has updatedObjects dictionary, that allow tracking objects, that have been updated, along with corresponding indexPath. It is done because UITableView and UICollectionView defer cell updates after insertions and deletions are completed, and therefore shift indexPaths. For example, if you were to insert 0 item and update it, UITableView would think that you are updating 1 item instead of 0, because it happens in single animation block and 0 item becomes 1.

3.0.0 - 2016-10-23 13:28:04

No changes from previous betas.

3.0.0-beta.3 - 2016-10-13 08:49:37

  • Requires Realm 2.0 and higher.
  • Fixes crash, that happens, when subscribing to Realm notifications and Realm is read-only(thanks, @augmentedworks!)

3.0.0-beta.2 - 2016-09-24 09:25:57

  • Enables RealmStorage with RealmSwift dependency

3.0.0-beta.1 - 2016-09-17 09:37:50

Swift 3.0 and higher is required for this version of framework.

Note. Beta 1 Does not include RealmStorage subspec due to RealmSwift.framework podspec issues

Reworked

  • UIReaction class has been replaced with new EventReaction class, that allows more flexible and powerful events usage
  • Supplementary models are now stored in [String:[Int:Any]] instead of [String:Any] to support supplementary models, whose position is determined by indexPath in UICollectionView. SupplementaryStorageProtocol, SupplementaryAccessible protocols have been reworked to reflect those changes.
  • MemoryStorageErrors have been made an Error type following conventions from SE-0112.

Added

  • MemoryStorage and RealmStorage now implement SectionLocationIdentifyable protocol, allowing any section to find out, what it's index is.
  • SectionModel and RealmSection gained currentSectionIndex property, that shows index of section in sections array.
  • displaySectionNameForSupplementaryKinds property on CoreDataStorage, that defines, for which supplementary kinds NSFetchedResultsController sectionName should be used as a data model.
  • removeItemsFromSection method on MemoryStorage, that allows to remove all items from specific section

Removed

  • itemForCellClass:atIndexPath:, itemForHeaderClass:atSectionIndex:, itemsForFooterClass:atSectionIndex:
  • makeNSIndexSet method, because Swift 3 allows to directly create IndexSet from both Array and Set.

2.6.2 - 2016-06-30 13:30:16

Fixed

  • CoreDataStorage now properly updates new indexPath after Move event on iOS 9.

2.6.1 - 2016-05-30 15:44:42

Fixed

  • Now properly handles case, when deleteSections method was called with index, that is not present in MemoryStorage or RealmStorage

Added

  • setSectionWithResults(_:forSectionIndex:) for RealmStorage

Updated

  • Realm dependency to 1.0.0

2.6.0 - 2016-05-24 15:31:42

Changed

  • Support for fine-grained notifications in Realm
  • Update to Realm 0.103.1 and higher.

Fixed

  • Fixed https://github.com/DenHeadless/DTTableViewManager/issues/34, thanks @orkenstein, @andrewSvsg

2.5.1 - 2016-05-24 13:59:45

Changed

  • Realm dependency updated to 0.102 version.