CocoaPods trunk is moving to be read-only. Read more on the blog, there are 18 months to go.

DiffTableDirector 1.0.4

DiffTableDirector 1.0.4

Maintained by Lavrinenko Aleksandr.



 
Depends on:
SwiftLint>= 0
DeepDiff= 2.3.1
 

  • By
  • aleksiosdev

DiffTableDirector

CI Status Version License Platform

DiffTableDirector - light abstraction above UITableViewDataSource & Delegate. It allows you to work with table in declarative type-safe manner. Provide a lot of little features that you miss in original table view.

Under the hood library use native Diff API for iOS 13 and DeppDiff for iOS 11 & 12.

Features

  • Type-safe generic cells, headers and footers
  • Autoregister table elements
  • Support actions from cell/header/footer via delegate
  • Placeholder view for empty table
  • Observe table bounds cross
  • Easy pagination with your loader and animation
  • Update table via diff on main or background queue
  • Easy to extend

Example

To run the example project, clone the repo, and run pod install from the Example directory first.

Usage

Create TableDirector

import DiffTableKit

let tableDirector = TableDirector(tableView: awesomeTableView)
    
// or

let tableDirector = TableDirector()
// Later 
tableDirector.connect(to: awesomeTableView)

Support Configurable protocol for cell

import DiffTableKit

// MARK: - ConfigurableCell
extension SuperCell: ConfigurableCell  {
	struct ViewModel: Hashable {
    let ID: String 
		let name: String
		let icon: UIImage
	}

	func configure(_ item: ViewModel) {
		_nameLabel.text = item.name
		_iconImageView.set(image: item.icon)
	}
}

// MARK: - ConfigurableHeaderFooter
extension SuperHeader: ConfigurableHeaderFooter {
	typealias ViewModel = String

	func configure(_ item: ViewModel) {
		_titleLabel.text = item
	}
}

Fill your table

import DiffTableKit

let row = TableRow<SuperCell>(item: .init(ID: "uniqIdentifier", title: "Ttile", icon: UIImage(named: "icon"))
let header = TableHeader(item: "Header title")
let section = TableSection(rows: [row], headerConfigurator: header)
tableDirector.reload(with: [section])

And you are best. Code work, table filled. Easy, safe and all items autoregistred. But wait... what about other pressing on cell and ect?

Actions

Actions made by delegates. You can achive same effect with callbacks if you wish

extension SuperCell: ActionCell {
	typealias ViewModel = InfoViewModel
	typealias Delegate = CellPressableDelegate

	func configure(_ item: InfoViewModel) {
		// Fill your cell with data here
		_titleLabel.text = item.title
		
		contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didPressedCell)))
	}

	@objc func didPressedCell() {
		delegate?.didPressedCell(self)
	}
	
}

And creation of cell will change a little

let actionRow = TableActionRow<SuperCell>(viewModel: .init(ID: "uniqIdentifier", title: "Ttile", icon: UIImage(named: "icon"), delegate: self)
tableDirector.reload(with: [actionRow])

Same for header)

Placeholder View

Sometimes table is empty and it looks ugly. One of solutions is to add fancy picture for empty state. Let's do it

import DiffTableKit
tableDirector.addEmptyStateView(viewFactory: { [unowned self] in
			// Called on main thread
			return PlaceholderView()
			}, position: .center)

And now you can see your placeholder view align to center when table is empty. When you load some content - it's disappear.

Diff

Table can show a lot of different information. It can come separetly. Иetter update table with animation than reload it for several times. Apple already provide us UIDiffableDataSource but only for iOS 13. We inspired by it and provide same solution with declarative approach. For iOS 11+)

So we need to do 2 steps:

Step 1: Comnform Hashable protocol for all your ViewModels. Better to provide some uniq id like UUID().uuidString.

struct ViewModel: Hashable {
      let id: String
	... 
}

TableDirector will diff model if it's not hashable. But the same model will be different for him. Keep it in mind and better use nonhashable ViewModels for nondiffalbe tables.

For iOS13 each reload will be diffable. You will see animation if you provide animtion true. For false case it will looks like oldschool reloadData.

Also you can manualy trigger diffable reload (for lower iOS for example). On main queue and on your queue.

	func reload(with sections: sections, reloadRule: .calculateSync) // Will calculate diff on main thread. 

	func reload(with sections: sections, reloadRule: .calculateAsync(queue: yourQueue) // Will calculate on queue your provider. Can prevent freeze for big collections
	

Keep in mind: Apple recommend reload table only on main or only on backgroun queue. Missing with queues can lead to undefined behaviour.

Cross bounds

Remember that case when you need to draw shadow on upper view when table if scrolled? Or maybe separator? And hide it in unscrolled state.

_tableDirector?.topCrossObserver = CrossObserver(didCross: {
	// Draw your shadow/separator here
}, didReturn: {
	// Hide shadow/separator here
})

Same can be done for bottom border. We use it along other project and it's really easy

Pagination and pull to refresh

By the way. This aproach looks good for pagination! Pagination is always pain in bussness logic. But if you want some custom behaviour or animation - UI can be tricky too. So we provide component. It can be configurater. Can controll your view and animate it lifecycle. Like this

let loader = Loader(view: viewForPagination, animator: yourAnimator)
let bottomPaginationController = PaginationController(
	settings: .bottom,
	loader: yourLoader) { (handler) in
		network.request(...) {
			// Change state of pagination. Also your can call same method on pagination controller
			handler.finished(isSuccessfull: true, canLoadNext: true)
		}
	}
self._tableDirector?.add(paginationController: bottomPaginationController)

Loader is simple struct

public struct Loader {
	let view: UIView
	let animator: PaginationControllerLoaderAnimator
}

Class that animate your loader should conform PaginationControllerLoaderAnimator.

public protocol PaginationControllerLoaderAnimator {
	/// Animate loader base on state
	/// - Parameter state: loader state
	func animate(state: PaginationController.Loader.State)
}

/// Loader states
public enum State {
	case initial
	case loading
	case error
	case success
}

Enought to deal with most of cases. The same can be applied for pull to refresh. Just provide up direction for PaginationController

Requirements

iOS 11.0+ Xcode 11.0+ Swift 5.0

Installation

DiffTableDirector is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'DiffTableDirector'

Author

aleksiosdev, [email protected]

License

DiffTableDirector is available under the Apache-2.0 License. See the license file for more info.