Skip to content

πŸ’Ύ πŸ”œπŸ“± Type-safe data-driven CollectionView, TableView Framework. (We can also use ASCollectionNode)

License

Notifications You must be signed in to change notification settings

tatsuya-yamamoto-nm/DataSources

Β 
Β 

Repository files navigation

⚠️ The latest updates is this PR. It changes the difference algorithm to DifferenceKit.

DataSources

πŸ’Ύ πŸ”œπŸ“± Type-safe data-driven List-UI Framework. (We can also use ASCollectionNode)

Partial updates(insert, delete, move) of UICollectionView/UITableView is important things for fancy UI.
But, It's hard that synchronous of data and UI.
DataSources will solve this problem.

CI Status Version License Platform Carthage compatible FOSSA Status

Thanks

Diff-algorithm

Features

  • Data driven update
    • Data did change, then will display.
  • Partial updates, no more calling reloadData
    • Smooth and Faster.
    • if the count of changes larger than 300, update with non-animation.
  • Simplified usage
  • We can use different type each section.
  • Type-safe
    • We can take clearly typed object by IndexPath.
  • Using Adapter-pattern for List-UI
    • For example, We can also use this for ASCollectionNode of Texture. (Demo app includes it)
  • Reorder by UI operation
  • This library is not supported moving between section.

Requirements

  • Swift 4
  • iOS 9+

Usage (Example)

Conform protocol Diffable

public protocol Diffable {
  associatedtype Identifier : Hashable
  var diffIdentifier: Identifier { get }
}
struct Model : Diffable {

  var diffIdentifier: String {
    return id
  }
  
  let id: String
}

🀠 Most Simplified Usage

  1. Define SectionDataController in ViewController
let collectionView: UICollectionView

let sectionDataController = SectionDataController<Model, CollectionViewAdapter>(
  adapter: CollectionViewAdapter(collectionView: self.collectionView),
  isEqual: { $0.id == $1.id } // If Model has Equatable, you can omit this closure.
)

var models: [Model] = [] {
  didSet {
    sectionDataController.update(items: items, updateMode: .partial(animated: true), completion: {
      // Completed update
    })
  }
}

let dataSource = CollectionViewDataSource(sectionDataController: sectionDataController)

dataSource.cellFactory = { _, collectionView, indexPath, model in
   let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
   cell.label.text = model.title
   return cell
 }

collectionView.dataSource = dataSource

😎 Semi Manual

Single-Section (UICollectionView)

  1. Define SectionDataController in ViewController
let collectionView: UICollectionView
var models: [Model]

let sectionDataController = SectionDataController<Model, CollectionViewAdapter>(
  adapter: CollectionViewAdapter(collectionView: self.collectionView),
  isEqual: { $0.id == $1.id } // If Model has Equatable, you can omit this closure.
)
  1. Bind Models to SectionDataController in ViewController
var models: [Model] = […] {
  didSet {
    sectionDataController.update(items: items, updateMode: .partial(animated: true), completion: {
      // Completed update
    })
  }
}
  1. Implement UICollectionViewDataSource in ViewController
func numberOfSections(in collectionView: UICollectionView) -> Int {
  return 1
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  return sectionDataController.numberOfItems()
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

  let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
  let m = sectionDataController.item(for: indexPath)
  cell.label.text = m.title
  return cell
}

Multiple-Section (UICollectionView)

  1. Define DataController in ViewController
let collectionView: UICollectionView
var models: [Model]

let dataController = DataController<CollectionViewAdapter>(adapter: CollectionViewAdapter(collectionView: self.collectionView))
  1. Define Section<T> in ViewController
let section0 = Section(ModelA.self, isEqual: { $0.id == $1.id })
let section1 = Section(ModelB.self, isEqual: { $0.id == $1.id })
  1. Add Section to DataController

Order of Section will be decided in the order of addition.

dataController.add(section: section0) // will be 0 of section
dataController.add(section: section1) // will be 1 of section
  1. Bind Models to DataController
var section0Models: [ModelA] = […] {
  didSet {
    dataController.update(
      in: section0,
      items: section0Models,
      updateMode: .partial(animated: true),
      completion: {
        
    })
  }
}

var section1Models: [ModelA] = […] {
  didSet {
    dataController.update(
      in: section1,
      items: section1Models,
      updateMode: .partial(animated: true),
      completion: {
        
    })
  }
}
  1. Implement UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
  return dataController.numberOfSections()
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  return dataController.numberOfItems(in: section)
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

  return dataController.item(
    at: indexPath,    
    handlers: [
    .init(section: section0) { (m: ModelA) in
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
      cell.label.text = m.title
      return cell
    },
    .init(section: section1) { (m: ModelB) in
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
      cell.label.text = m.title
      return cell
      },
    ])

  /* Other way
  switch indexPath.section {
  case section0:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
    let m = _dataController.item(at: indexPath, in: section0)
    cell.label.text = m.title
    return cell
  case section1:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
    let m = _dataController.item(at: indexPath, in: section1)
    cell.label.text = m.title
    return cell
  default:
    fatalError()
  }
   */
}

Reorder by UI operation

SectionDataController has a snapshot for List-UI. It helps that perform batch update List-UI in safety.

But, the snapshots include side-effects. For example, if we did reorder items of List-UI by UI operation. In this time, Items of List-UI is caused differences to the snapshot. It will be caused unnecessary diff.

Therefore when we reorder items, we should operation followings.

  1. Reorder items of UI
  2. Call SectionDataController.reserveMoved(...
  3. Reorder items of Array
  4. Call SectionDataController.update(items: [T]..

Appendix

Combination with RxSwift

We can use DataControllers with RxSwift. The following code is an example.

Add extension

import RxSwift
import DataControllers

extension SectionDataController : ReactiveCompatible {}

extension Reactive where Base : SectionDataControllerType {

  public func partialUpdate<
    T,
    Controller: ObservableType
    >
    (animated: Bool) -> (_ o: Source) -> Disposable where Source.E == [T], T == Base.ItemType {

    weak var t = base.asSectionDataController()

    return { source in

      source
        .observeOn(MainScheduler.instance)
        .concatMap { (newItems: [T]) -> Completable in
          Completable.create { o in
            guard let sectionDataController = t else {
              o(.completed)
              return Disposables.create()
            }
            sectionDataController.update(items: newItems, updateMode: .partial(animated: animated), completion: {
              o(.completed)
            })
            return Disposables.create()
          }
        }
        .subscribe()
    }
  }
}
let models: Variable<[Model]>
let sectionDataController = SectionDataController<Model, CollectionViewAdapter>

models
  .asDriver()
  .drive(sectionDataController.rx.partialUpdate(animated: true))

Demo Application

This repository include Demo-Application. You can touch DataSources.

  1. Clone repository.
$ git clone https://github.com/muukii/DataSources.git
$ cd DataSources
$ pod install
  1. Open xcworkspace
  2. Run DataSourcesDemo on iPhone Simulator.

Installation

CocoaPods

pod 'DataSources'

Carthage

github "muukii/DataSources"

You need to add DataSources.framework and ListDiff.framework to your project.

Author

muukii, [email protected], https://muukii.me/

License

DataSources is available under the MIT license. See the LICENSE file for more info.

FOSSA Status

About

πŸ’Ύ πŸ”œπŸ“± Type-safe data-driven CollectionView, TableView Framework. (We can also use ASCollectionNode)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 94.6%
  • Ruby 3.9%
  • Objective-C 1.5%