Merge pull request #1 from maxsokolov/master

Merge from source repo
This commit is contained in:
motylevm 2017-06-07 17:20:59 +03:00 committed by GitHub
commit c9bd52797d
17 changed files with 423 additions and 162 deletions

View File

@ -2,8 +2,20 @@
All notable changes to this project will be documented in this file.
## [2.0.0](https://github.com/maxsokolov/TableKit/releases/tag/1.4.0)
Released on 2016-09-06. Breaking changes in 2.0.0:
## [2.3.0](https://github.com/maxsokolov/TableKit/releases/tag/2.3.0)
Released on 2016-11-16.
- `shouldUsePrototypeCellHeightCalculation` moved to `TableDirector(tableView: tableView, shouldUsePrototypeCellHeightCalculation: true)`
- Prototype cell height calculation bugfixes
## [2.1.0](https://github.com/maxsokolov/TableKit/releases/tag/2.1.0)
Released on 2016-10-19.
- `action` method was deprecated on TableRow. Use `on` instead.
- Support multiple actions with same type on row.
- You could now build your own cell height calculating strategy. See [TablePrototypeCellHeightCalculator](Sources/TablePrototypeCellHeightCalculator.swift).
- Default distance between sections changed to `UITableViewAutomaticDimension`. You can customize it, see [TableSection](Sources/TableSection.swift)
## [2.0.0](https://github.com/maxsokolov/TableKit/releases/tag/2.0.0)
Released on 2016-10-06. Breaking changes in 2.0.0:
<br/>The signatures of `TableRow` and `TableRowAction` classes were changed from
```swift
let action = TableRowAction<String, StringTableViewCell>(.click) { (data) in

View File

@ -13,12 +13,31 @@ class AutolayoutCellsController: UIViewController {
@IBOutlet weak var tableView: UITableView! {
didSet {
tableDirector = TableDirector(tableView: tableView)
tableDirector.shouldUsePrototypeCellHeightCalculation = true
tableDirector = TableDirector(tableView: tableView, shouldUsePrototypeCellHeightCalculation: true)
}
}
var tableDirector: TableDirector!
func randomString(length: Int) -> String {
let letters : NSString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let len = UInt32(letters.length)
var randomString = ""
for _ in 0 ..< length {
let rand = arc4random_uniform(len)
var nextChar = letters.character(at: Int(rand))
randomString += NSString(characters: &nextChar, length: 1) as String
}
return randomString
}
func randomInt(min: Int, max:Int) -> Int {
return min + Int(arc4random_uniform(UInt32(max - min + 1)))
}
override func viewDidLoad() {
super.viewDidLoad()
@ -27,10 +46,10 @@ class AutolayoutCellsController: UIViewController {
let section = TableSection()
var rows = 0
while rows <= 1000 {
while rows <= 20 {
rows += 1
let row = TableRow<AutolayoutTableViewCell>(item: ())
let row = TableRow<AutolayoutTableViewCell>(item: randomString(length: randomInt(min: 20, max: 100)))
section += row
}

View File

@ -23,9 +23,9 @@ class MainController: UIViewController {
title = "TableKit"
let clickAction = TableRowAction<ConfigurableTableViewCell>(.click) { [weak self] (data) in
let clickAction = TableRowAction<ConfigurableTableViewCell>(.click) { [weak self] (options) in
switch (data.indexPath as NSIndexPath).row {
switch options.indexPath.row {
case 0:
self?.performSegue(withIdentifier: "autolayoutcells", sender: nil)
case 1:
@ -34,11 +34,16 @@ class MainController: UIViewController {
break
}
}
let printClickAction = TableRowAction<ConfigurableTableViewCell>(.click) { (options) in
print("click", options.indexPath)
}
let rows = [
TableRow<ConfigurableTableViewCell>(item: "Autolayout cells", actions: [clickAction]),
TableRow<ConfigurableTableViewCell>(item: "Nib cells", actions: [clickAction])
TableRow<ConfigurableTableViewCell>(item: "Autolayout cells", actions: [clickAction, printClickAction]),
TableRow<ConfigurableTableViewCell>(item: "Nib cells", actions: [clickAction, printClickAction])
]
// automatically creates a section, also could be used like tableDirector.append(rows: rows)

View File

@ -21,12 +21,13 @@ class NibCellsController: UITableViewController {
tableDirector = TableDirector(tableView: tableView)
let numbers = [1000, 2000, 3000, 4000, 5000]
let shouldHighlightAction = TableRowAction<NibTableViewCell>(.shouldHighlight) { (_) -> Bool in
return false
}
let rows = numbers.map { TableRow<NibTableViewCell>(item: $0, actions: [shouldHighlightAction]) }
let rows = numbers.map {
TableRow<NibTableViewCell>(item: $0)
.on(.shouldHighlight) { (_) -> Bool in
return false
}
}
tableDirector.append(rows: rows)
}

View File

@ -10,23 +10,22 @@ import UIKit
import TableKit
private let LoremIpsumTitle = "Lorem ipsum dolor sit amet, consectetur adipisicing elit"
private let LoremIpsumBody = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius adipisci, sed libero. Iste asperiores suscipit, consequatur debitis animi impedit numquam facilis iusto porro labore dolorem, maxime magni incidunt. Delectus, est! Totam at eius excepturi deleniti sed, error repellat itaque omnis maiores tempora ratione dolor velit minus porro aspernatur repudiandae labore quas adipisci esse, nulla tempore voluptatibus cupiditate. Ab provident, atque. Possimus deserunt nisi perferendis, consequuntur odio et aperiam, est, dicta dolor itaque sunt laborum, magni qui optio illum dolore laudantium similique harum. Eveniet quis, libero eligendi delectus repellendus repudiandae ipsum? Vel nam odio dolorem, voluptas sequi minus quo tempore, animi est quia earum maxime. Reiciendis quae repellat, modi non, veniam natus soluta at optio vitae in excepturi minima eveniet dolor."
class AutolayoutTableViewCell: UITableViewCell, ConfigurableCell {
typealias T = Void
typealias T = String
@IBOutlet var titleLabel: UILabel!
@IBOutlet var subtitleLabel: UILabel!
static var estimatedHeight: CGFloat? {
return 700
return 150
}
func configure(with string: T) {
titleLabel.text = LoremIpsumTitle
subtitleLabel.text = LoremIpsumBody
subtitleLabel.text = string
}
override func layoutSubviews() {
@ -37,4 +36,4 @@ class AutolayoutTableViewCell: UITableViewCell, ConfigurableCell {
titleLabel.preferredMaxLayoutWidth = titleLabel.bounds.size.width
subtitleLabel.preferredMaxLayoutWidth = subtitleLabel.bounds.size.width
}
}
}

View File

@ -4,7 +4,7 @@
<a href="https://travis-ci.org/maxsokolov/TableKit"><img src="https://api.travis-ci.org/maxsokolov/TableKit.svg" alt="Build Status" /></a>
<a href="https://developer.apple.com/swift"><img src="https://img.shields.io/badge/Swift_3.0-compatible-4BC51D.svg?style=flat" alt="Swift 3.0 compatible" /></a>
<a href="https://github.com/Carthage/Carthage"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat" alt="Carthage compatible" /></a>
<a href="https://cocoapods.org/pods/tablekit"><img src="https://img.shields.io/badge/pod-2.0.0-blue.svg" alt="CocoaPods compatible" /></a>
<a href="https://cocoapods.org/pods/tablekit"><img src="https://img.shields.io/badge/pod-2.3.1-blue.svg" alt="CocoaPods compatible" /></a>
<img src="https://img.shields.io/badge/platform-iOS-blue.svg?style=flat" alt="Platform iOS" />
<a href="https://raw.githubusercontent.com/maxsokolov/tablekit/master/LICENSE"><img src="http://img.shields.io/badge/license-MIT-blue.svg?style=flat" alt="License: MIT" /></a>
</p>
@ -84,13 +84,14 @@ You could have as many rows and sections as you need.
It nice to have some actions that related to your cells:
```swift
let action = TableRowAction<StringTableViewCell>(.click) { (data) in
let action = TableRowAction<StringTableViewCell>(.click) { (options) in
// you could access any useful information that relates to the action
// data.cell - StringTableViewCell?
// data.item - String
// data.indexPath - IndexPath
// options.cell - StringTableViewCell?
// options.item - String
// options.indexPath - IndexPath
// options.userInfo - [AnyHashable: Any]?
}
let row = TableRow<StringTableViewCell>(item: "some", actions: [action])
@ -98,10 +99,10 @@ let row = TableRow<StringTableViewCell>(item: "some", actions: [action])
Or, using nice chaining approach:
```swift
let row = TableRow<StringTableViewCell>(item: "some")
.action(.click) { (data) in
.on(.click) { (options) in
}
.action(.shouldHighlight) { (data) -> Bool in
.on(.shouldHighlight) { (options) -> Bool in
return false
}
```
@ -126,10 +127,29 @@ class MyTableViewCell: UITableViewCell, ConfigurableCell {
```
And handle them accordingly:
```swift
let myAction = TableRowAction<MyTableViewCell>(.custom(MyActions.ButtonClicked)) { (data) in
let myAction = TableRowAction<MyTableViewCell>(.custom(MyActions.ButtonClicked)) { (options) in
}
```
## Multiple actions with same type
It's also possible to use multiple actions with same type:
```swift
let click1 = TableRowAction<StringTableViewCell>(.click) { (options) in }
click1.id = "click1" // optional
let click2 = TableRowAction<StringTableViewCell>(.click) { (options) in }
click2.id = "click2" // optional
let row = TableRow<StringTableViewCell>(item: "some", actions: [click1, click2])
```
Could be useful in case if you want to separate your logic somehow. Actions will be invoked in order which they were attached.
> If you define multiple actions with same type which also return a value, only last return value will be used for table view.
You could also remove any action by id:
```swift
row.removeAction(forActionId: "action_id")
```
# Advanced
@ -147,9 +167,9 @@ class StringTableViewCell: UITableViewCell, ConfigurableCell {
```
It's enough for most cases. But you may be not happy with this. So you could use a prototype cell to calculate cells heights. To enable this feature simply use this property:
```swift
tableDirector.shouldUsePrototypeCellHeightCalculation = true
let tableDirector = TableDirector(tableView: tableView, shouldUsePrototypeCellHeightCalculation: true)
```
It does all dirty work with prototypes for you [behind the scene](Sources/HeightStrategy.swift), so you don't have to worry about anything except of your cell configuration:
It does all dirty work with prototypes for you [behind the scene](Sources/TablePrototypeCellHeightCalculator.swift), so you don't have to worry about anything except of your cell configuration:
```swift
class ImageTableViewCell: UITableViewCell, ConfigurableCell {

View File

@ -34,22 +34,14 @@ public protocol ConfigurableCell {
public extension ConfigurableCell where Self: UITableViewCell {
static var reuseIdentifier: String {
get {
return String(describing: self)
}
return String(describing: self)
}
static var estimatedHeight: CGFloat? {
get {
return nil
}
return nil
}
static var defaultHeight: CGFloat? {
get {
return nil
}
return nil
}
}

View File

@ -20,10 +20,6 @@
import UIKit
struct TableKitNotifications {
static let CellAction = "TableKitNotificationsCellAction"
}
/**
A custom action that you can trigger from your cell.
You can easily catch actions using a chaining manner with your row.

View File

@ -26,16 +26,20 @@ import UIKit
open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
open private(set) weak var tableView: UITableView?
open private(set) var sections = [TableSection]()
open fileprivate(set) var sections = [TableSection]()
private weak var scrollDelegate: UIScrollViewDelegate?
private var heightStrategy: CellHeightCalculatable?
private var cellRegisterer: TableCellRegisterer?
public private(set) var rowHeightCalculator: RowHeightCalculator?
private var sectionsIndexTitlesIndexes: [Int]?
@available(*, deprecated, message: "Produced incorrect behaviour")
open var shouldUsePrototypeCellHeightCalculation: Bool = false {
didSet {
if shouldUsePrototypeCellHeightCalculation {
heightStrategy = PrototypeHeightStrategy(tableView: tableView)
rowHeightCalculator = TablePrototypeCellHeightCalculator(tableView: tableView)
} else {
rowHeightCalculator = nil
}
}
}
@ -44,21 +48,29 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
return sections.isEmpty
}
public init(tableView: UITableView, scrollDelegate: UIScrollViewDelegate? = nil, shouldUseAutomaticCellRegistration: Bool = true) {
public init(tableView: UITableView, scrollDelegate: UIScrollViewDelegate? = nil, shouldUseAutomaticCellRegistration: Bool = true, cellHeightCalculator: RowHeightCalculator?) {
super.init()
if shouldUseAutomaticCellRegistration {
self.cellRegisterer = TableCellRegisterer(tableView: tableView)
}
self.rowHeightCalculator = cellHeightCalculator
self.scrollDelegate = scrollDelegate
self.tableView = tableView
self.tableView?.delegate = self
self.tableView?.dataSource = self
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveAction), name: NSNotification.Name(rawValue: TableKitNotifications.CellAction), object: nil)
}
public convenience init(tableView: UITableView, scrollDelegate: UIScrollViewDelegate? = nil, shouldUseAutomaticCellRegistration: Bool = true, shouldUsePrototypeCellHeightCalculation: Bool = false) {
let heightCalculator: TablePrototypeCellHeightCalculator? = shouldUsePrototypeCellHeightCalculation ? TablePrototypeCellHeightCalculator(tableView: tableView) : nil
self.init(tableView: tableView, scrollDelegate: scrollDelegate, shouldUseAutomaticCellRegistration: shouldUseAutomaticCellRegistration, cellHeightCalculator: heightCalculator)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@ -70,8 +82,8 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
// MARK: Public
@discardableResult
open func invoke(action: TableRowActionType, cell: UITableViewCell?, indexPath: IndexPath) -> Any? {
return sections[indexPath.section].rows[indexPath.row].invoke(action, cell: cell, path: indexPath)
open func invoke(action: TableRowActionType, cell: UITableViewCell?, indexPath: IndexPath, userInfo: [AnyHashable: Any]? = nil) -> Any? {
return sections[indexPath.section].rows[indexPath.row].invoke(action: action, cell: cell, path: indexPath, userInfo: userInfo)
}
open override func responds(to selector: Selector) -> Bool {
@ -82,36 +94,42 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
return scrollDelegate?.responds(to: selector) == true ? scrollDelegate : super.forwardingTarget(for: selector)
}
// MARK: - Internal -
// MARK: - Internal
func hasAction(_ action: TableRowActionType, atIndexPath indexPath: IndexPath) -> Bool {
return sections[indexPath.section].rows[indexPath.row].hasAction(action)
return sections[indexPath.section].rows[indexPath.row].has(action: action)
}
func didReceiveAction(_ notification: Notification) {
guard let action = notification.object as? TableCellAction, let indexPath = tableView?.indexPath(for: action.cell) else { return }
invoke(action: .custom(action.key), cell: action.cell, indexPath: indexPath)
invoke(action: .custom(action.key), cell: action.cell, indexPath: indexPath, userInfo: notification.userInfo)
}
// MARK: - Height
open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let row = sections[indexPath.section].rows[indexPath.row]
cellRegisterer?.register(cellType: row.cellType, forCellReuseIdentifier: row.reuseIdentifier)
return row.estimatedHeight ?? heightStrategy?.estimatedHeight(row, path: indexPath) ?? UITableViewAutomaticDimension
if rowHeightCalculator != nil {
cellRegisterer?.register(cellType: row.cellType, forCellReuseIdentifier: row.reuseIdentifier)
}
return row.defaultHeight ?? row.estimatedHeight ?? rowHeightCalculator?.estimatedHeight(forRow: row, at: indexPath) ?? UITableViewAutomaticDimension
}
open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let row = sections[indexPath.section].rows[indexPath.row]
if rowHeightCalculator != nil {
cellRegisterer?.register(cellType: row.cellType, forCellReuseIdentifier: row.reuseIdentifier)
}
let rowHeight = invoke(action: .height, cell: nil, indexPath: indexPath) as? CGFloat
return rowHeight ?? row.defaultHeight ?? heightStrategy?.height(row, path: indexPath) ?? UITableViewAutomaticDimension
return rowHeight ?? row.defaultHeight ?? rowHeightCalculator?.height(forRow: row, at: indexPath) ?? UITableViewAutomaticDimension
}
// MARK: UITableViewDataSource - configuration
@ -127,6 +145,9 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = sections[indexPath.section].rows[indexPath.row]
cellRegisterer?.register(cellType: row.cellType, forCellReuseIdentifier: row.reuseIdentifier)
let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath)
if cell.frame.size.width != tableView.frame.size.width {
@ -163,13 +184,39 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let section = sections[section]
return section.headerHeight ?? section.headerView?.frame.size.height ?? 0
return section.headerHeight ?? section.headerView?.frame.size.height ?? UITableViewAutomaticDimension
}
open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
let section = sections[section]
return section.footerHeight ?? section.footerView?.frame.size.height ?? 0
return section.footerHeight ?? section.footerView?.frame.size.height ?? UITableViewAutomaticDimension
}
// MARK: UITableViewDataSource - Index
public func sectionIndexTitles(for tableView: UITableView) -> [String]? {
var indexTitles = [String]()
var indexTitlesIndexes = [Int]()
sections.enumerated().forEach { index, section in
if let title = section.indexTitle {
indexTitles.append(title)
indexTitlesIndexes.append(index)
}
}
if !indexTitles.isEmpty {
sectionsIndexTitlesIndexes = indexTitlesIndexes
return indexTitles
}
sectionsIndexTitlesIndexes = nil
return nil
}
public func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return sectionsIndexTitlesIndexes?[index] ?? 0
}
// MARK: UITableViewDelegate - actions
@ -205,7 +252,7 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
return indexPath
}
// MARK: - Row editing -
// MARK: - Row editing
open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return sections[indexPath.section].rows[indexPath.row].isEditingAllowed(forIndexPath: indexPath)
@ -221,8 +268,10 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
invoke(action: .clickDelete, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath)
}
}
// MARK: - Sections manipulation -
}
// MARK: - Sections manipulation
extension TableDirector {
@discardableResult
open func append(section: TableSection) -> Self {
@ -253,16 +302,42 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
}
@discardableResult
open func delete(index: Int) -> Self {
open func replaceSection(at index: Int, with section: TableSection) -> Self {
if index < sections.count {
sections[index] = section
}
return self
}
@discardableResult
open func delete(sectionAt index: Int) -> Self {
sections.remove(at: index)
return self
}
@discardableResult
open func remove(sectionAt index: Int) -> Self {
return delete(sectionAt: index)
}
@discardableResult
open func clear() -> Self {
rowHeightCalculator?.invalidate()
sections.removeAll()
return self
}
// MARK: - deprecated methods
@available(*, deprecated, message: "Use 'delete(sectionAt:)' method instead")
@discardableResult
open func delete(index: Int) -> Self {
sections.remove(at: index)
return self
}
}

86
Sources/TableKit.swift Normal file
View File

@ -0,0 +1,86 @@
//
// Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import UIKit
struct TableKitNotifications {
static let CellAction = "TableKitNotificationsCellAction"
}
public protocol RowConfigurable {
func configure(_ cell: UITableViewCell)
}
public protocol RowActionable {
var editingActions: [UITableViewRowAction]? { get }
func isEditingAllowed(forIndexPath indexPath: IndexPath) -> Bool
func invoke(action: TableRowActionType, cell: UITableViewCell?, path: IndexPath, userInfo: [AnyHashable: Any]?) -> Any?
func has(action: TableRowActionType) -> Bool
}
public protocol RowHashable {
var hashValue: Int { get }
}
public protocol Row: RowConfigurable, RowActionable, RowHashable {
var reuseIdentifier: String { get }
var cellType: AnyClass { get }
var estimatedHeight: CGFloat? { get }
var defaultHeight: CGFloat? { get }
}
public enum TableRowActionType {
case click
case clickDelete
case select
case deselect
case willSelect
case willDisplay
case shouldHighlight
case height
case canEdit
case configure
case custom(String)
var key: String {
switch (self) {
case .custom(let key):
return key
default:
return "_\(self)"
}
}
}
public protocol RowHeightCalculator {
func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat
func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat
func invalidate()
}

View File

@ -20,26 +20,18 @@
import UIKit
public protocol CellHeightCalculatable {
open class TablePrototypeCellHeightCalculator: RowHeightCalculator {
func height(_ row: Row, path: IndexPath) -> CGFloat
func estimatedHeight(_ row: Row, path: IndexPath) -> CGFloat
func invalidate()
}
open class PrototypeHeightStrategy: CellHeightCalculatable {
private weak var tableView: UITableView?
private(set) weak var tableView: UITableView?
private var prototypes = [String: UITableViewCell]()
private var cachedHeights = [Int: CGFloat]()
private var separatorHeight = 1 / UIScreen.main.scale
init(tableView: UITableView?) {
public init(tableView: UITableView?) {
self.tableView = tableView
}
open func height(_ row: Row, path: IndexPath) -> CGFloat {
open func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat {
guard let tableView = tableView else { return 0 }
@ -58,6 +50,7 @@ open class PrototypeHeightStrategy: CellHeightCalculatable {
guard let cell = prototypeCell else { return 0 }
cell.prepareForReuse()
row.configure(cell)
cell.bounds = CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: cell.bounds.height)
@ -71,7 +64,7 @@ open class PrototypeHeightStrategy: CellHeightCalculatable {
return height
}
open func estimatedHeight(_ row: Row, path: IndexPath) -> CGFloat {
open func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat {
guard let tableView = tableView else { return 0 }

View File

@ -20,38 +20,10 @@
import UIKit
public protocol RowConfigurable {
func configure(_ cell: UITableViewCell)
}
public protocol RowActionable {
var editingActions: [UITableViewRowAction]? { get }
func isEditingAllowed(forIndexPath indexPath: IndexPath) -> Bool
func invoke(_ action: TableRowActionType, cell: UITableViewCell?, path: IndexPath) -> Any?
func hasAction(_ action: TableRowActionType) -> Bool
}
public protocol RowHashable {
var hashValue: Int { get }
}
public protocol Row: RowConfigurable, RowActionable, RowHashable {
var reuseIdentifier: String { get }
var cellType: AnyClass { get }
var estimatedHeight: CGFloat? { get }
var defaultHeight: CGFloat? { get }
}
open class TableRow<CellType: ConfigurableCell>: Row where CellType: UITableViewCell {
open let item: CellType.T
private lazy var actions = [String: TableRowAction<CellType>]()
private lazy var actions = [String: [TableRowAction<CellType>]]()
private(set) open var editingActions: [UITableViewRowAction]?
open var hashValue: Int {
@ -78,29 +50,32 @@ open class TableRow<CellType: ConfigurableCell>: Row where CellType: UITableView
self.item = item
self.editingActions = editingActions
actions?.forEach { self.actions[$0.type.key] = $0 }
actions?.forEach { on($0) }
}
// MARK: - RowConfigurable -
open func configure(_ cell: UITableViewCell) {
(cell as? CellType)?.configure(with: item)
}
// MARK: - RowActionable -
open func invoke(_ action: TableRowActionType, cell: UITableViewCell?, path: IndexPath) -> Any? {
return actions[action.key]?.invoke(item: item, cell: cell, path: path)
open func invoke(action: TableRowActionType, cell: UITableViewCell?, path: IndexPath, userInfo: [AnyHashable: Any]? = nil) -> Any? {
return actions[action.key]?.flatMap({ $0.invokeActionOn(cell: cell, item: item, path: path, userInfo: userInfo) }).last
}
open func hasAction(_ action: TableRowActionType) -> Bool {
open func has(action: TableRowActionType) -> Bool {
return actions[action.key] != nil
}
open func isEditingAllowed(forIndexPath indexPath: IndexPath) -> Bool {
if actions[TableRowActionType.canEdit.key] != nil {
return invoke(.canEdit, cell: nil, path: indexPath) as? Bool ?? false
return invoke(action: .canEdit, cell: nil, path: indexPath) as? Bool ?? false
}
return editingActions?.isEmpty == false || actions[TableRowActionType.clickDelete.key] != nil
}
@ -108,16 +83,55 @@ open class TableRow<CellType: ConfigurableCell>: Row where CellType: UITableView
// MARK: - actions -
@discardableResult
open func action(_ action: TableRowAction<CellType>) -> Self {
open func on(_ action: TableRowAction<CellType>) -> Self {
if actions[action.type.key] == nil {
actions[action.type.key] = [TableRowAction<CellType>]()
}
actions[action.type.key]?.append(action)
actions[action.type.key] = action
return self
}
@discardableResult
open func on<T>(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions<CellType>) -> T) -> Self {
return on(TableRowAction<CellType>(type, handler: handler))
}
@discardableResult
open func action<T>(_ type: TableRowActionType, handler: @escaping (_ data: TableRowActionData<CellType>) -> T) -> Self {
open func on(_ key: String, handler: @escaping (_ options: TableRowActionOptions<CellType>) -> ()) -> Self {
actions[type.key] = TableRowAction<CellType>(type, handler: handler)
return self
return on(TableRowAction<CellType>(.custom(key), handler: handler))
}
open func removeAllActions() {
actions.removeAll()
}
open func removeAction(forActionId actionId: String) {
for (key, value) in actions {
if let actionIndex = value.index(where: { $0.id == actionId }) {
actions[key]?.remove(at: actionIndex)
}
}
}
// MARK: - deprecated actions -
@available(*, deprecated, message: "Use 'on' method instead")
@discardableResult
open func action(_ action: TableRowAction<CellType>) -> Self {
return on(action)
}
@available(*, deprecated, message: "Use 'on' method instead")
@discardableResult
open func action<T>(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions<CellType>) -> T) -> Self {
return on(TableRowAction<CellType>(type, handler: handler))
}
}

View File

@ -20,32 +20,7 @@
import UIKit
public enum TableRowActionType {
case click
case clickDelete
case select
case deselect
case willSelect
case willDisplay
case shouldHighlight
case height
case canEdit
case configure
case custom(String)
var key: String {
switch (self) {
case .custom(let key):
return key
default:
return "_\(self)"
}
}
}
open class TableRowActionData<CellType: ConfigurableCell> where CellType: UITableViewCell {
open class TableRowActionOptions<CellType: ConfigurableCell> where CellType: UITableViewCell {
open let item: CellType.T
open let cell: CellType?
@ -63,38 +38,46 @@ open class TableRowActionData<CellType: ConfigurableCell> where CellType: UITabl
private enum TableRowActionHandler<CellType: ConfigurableCell> where CellType: UITableViewCell {
case voidAction((TableRowActionData<CellType>) -> Void)
case action((TableRowActionData<CellType>) -> Any?)
case voidAction((TableRowActionOptions<CellType>) -> Void)
case action((TableRowActionOptions<CellType>) -> Any?)
func invoke(item: CellType.T, cell: UITableViewCell?, path: IndexPath) -> Any? {
func invoke(withOptions options: TableRowActionOptions<CellType>) -> Any? {
switch self {
case .voidAction(let handler):
return handler(TableRowActionData(item: item, cell: cell as? CellType, path: path, userInfo: nil))
return handler(options)
case .action(let handler):
return handler(TableRowActionData(item: item, cell: cell as? CellType, path: path, userInfo: nil))
return handler(options)
}
}
}
open class TableRowAction<CellType: ConfigurableCell> where CellType: UITableViewCell {
open var id: String?
open let type: TableRowActionType
private let handler: TableRowActionHandler<CellType>
public init(_ type: TableRowActionType, handler: @escaping (_ data: TableRowActionData<CellType>) -> Void) {
public init(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions<CellType>) -> Void) {
self.type = type
self.handler = .voidAction(handler)
}
public init<T>(_ type: TableRowActionType, handler: @escaping (_ data: TableRowActionData<CellType>) -> T) {
public init(_ key: String, handler: @escaping (_ options: TableRowActionOptions<CellType>) -> Void) {
self.type = .custom(key)
self.handler = .voidAction(handler)
}
public init<T>(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions<CellType>) -> T) {
self.type = type
self.handler = .action(handler)
}
func invoke(item: CellType.T, cell: UITableViewCell?, path: IndexPath) -> Any? {
return handler.invoke(item: item, cell: cell, path: path)
public func invokeActionOn(cell: UITableViewCell?, item: CellType.T, path: IndexPath, userInfo: [AnyHashable: Any]?) -> Any? {
return handler.invoke(withOptions: TableRowActionOptions(item: item, cell: cell as? CellType, path: path, userInfo: userInfo))
}
}

View File

@ -26,6 +26,7 @@ open class TableSection {
open var headerTitle: String?
open var footerTitle: String?
open var indexTitle: String?
open var headerView: UIView?
open var footerView: UIView?
@ -88,6 +89,17 @@ open class TableSection {
rows[index] = row
}
open func delete(rowAt index: Int) {
rows.remove(at: index)
}
open func remove(rowAt index: Int) {
rows.remove(at: index)
}
// MARK: - deprecated methods -
@available(*, deprecated, message: "Use 'delete(rowAt:)' method instead")
open func delete(index: Int) {
rows.remove(at: index)
}

View File

@ -2,7 +2,7 @@ Pod::Spec.new do |s|
s.name = 'TableKit'
s.module_name = 'TableKit'
s.version = '2.0.0'
s.version = '2.4.0'
s.homepage = 'https://github.com/maxsokolov/TableKit'
s.summary = 'Type-safe declarative table views with Swift.'
@ -14,4 +14,4 @@ Pod::Spec.new do |s|
s.source_files = 'Sources/*.swift'
s.source = { :git => 'https://github.com/maxsokolov/TableKit.git', :tag => s.version }
end
end

View File

@ -8,8 +8,9 @@
/* Begin PBXBuildFile section */
50CF6E6B1D6704FE004746FF /* TableCellRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */; };
50E858581DB153F500A9AA55 /* TableKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E858571DB153F500A9AA55 /* TableKit.swift */; };
DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */; };
DA9EA7B01D0EC2C90021F650 /* HeightStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A71D0EC2C90021F650 /* HeightStrategy.swift */; };
DA9EA7B01D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */; };
DA9EA7B11D0EC2C90021F650 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A81D0EC2C90021F650 /* Operators.swift */; };
DA9EA7B21D0EC2C90021F650 /* TableCellAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */; };
DA9EA7B31D0EC2C90021F650 /* TableDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */; };
@ -32,9 +33,10 @@
/* Begin PBXFileReference section */
50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellRegisterer.swift; sourceTree = "<group>"; };
50E858571DB153F500A9AA55 /* TableKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableKit.swift; sourceTree = "<group>"; };
DA9EA7561D0B679A0021F650 /* TableKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TableKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurableCell.swift; sourceTree = "<group>"; };
DA9EA7A71D0EC2C90021F650 /* HeightStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeightStrategy.swift; sourceTree = "<group>"; };
DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TablePrototypeCellHeightCalculator.swift; sourceTree = "<group>"; };
DA9EA7A81D0EC2C90021F650 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = "<group>"; };
DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellAction.swift; sourceTree = "<group>"; };
DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableDirector.swift; sourceTree = "<group>"; };
@ -88,15 +90,16 @@
DA9EA7A51D0EC2B90021F650 /* Sources */ = {
isa = PBXGroup;
children = (
50E858571DB153F500A9AA55 /* TableKit.swift */,
DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */,
50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */,
DA9EA7AB1D0EC2C90021F650 /* TableRow.swift */,
DA9EA7AC1D0EC2C90021F650 /* TableRowAction.swift */,
DA9EA7AE1D0EC2C90021F650 /* TableSection.swift */,
DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */,
DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */,
DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */,
DA9EA7A81D0EC2C90021F650 /* Operators.swift */,
DA9EA7A71D0EC2C90021F650 /* HeightStrategy.swift */,
);
path = Sources;
sourceTree = "<group>";
@ -231,11 +234,12 @@
DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */,
DA9EA7B31D0EC2C90021F650 /* TableDirector.swift in Sources */,
DA9EA7B71D0EC2C90021F650 /* TableSection.swift in Sources */,
DA9EA7B01D0EC2C90021F650 /* HeightStrategy.swift in Sources */,
DA9EA7B01D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift in Sources */,
DA9EA7B51D0EC2C90021F650 /* TableRowAction.swift in Sources */,
DA9EA7B21D0EC2C90021F650 /* TableCellAction.swift in Sources */,
DA9EA7B11D0EC2C90021F650 /* Operators.swift in Sources */,
DA9EA7B41D0EC2C90021F650 /* TableRow.swift in Sources */,
50E858581DB153F500A9AA55 /* TableKit.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -171,7 +171,7 @@ class TabletTests: XCTestCase {
let expectation = self.expectation(description: "cell action")
let row = TableRow<TestTableViewCell>(item: TestData(title: "title"))
.action(TableRowAction(.custom(TestTableViewCellOptions.CellAction)) { (data) in
.on(TableRowAction(.custom(TestTableViewCellOptions.CellAction)) { (data) in
XCTAssertNotNil(data.cell, "Action data should have a cell")
@ -190,4 +190,54 @@ class TabletTests: XCTestCase {
waitForExpectations(timeout: 1.0, handler: nil)
}
func testReplaceSectionOnExistingIndex() {
let row1 = TableRow<TestTableViewCell>(item: TestData(title: "title1"))
let row2 = TableRow<TestTableViewCell>(item: TestData(title: "title2"))
let section1 = TableSection(headerView: nil, footerView: nil, rows: nil)
section1 += row1
let section2 = TableSection(headerView: nil, footerView: nil, rows: nil)
section2 += row2
testController.tableDirector += section1
testController.tableView.reloadData()
let cell = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
XCTAssertTrue(cell?.textLabel?.text == "title1")
testController.tableDirector.replaceSection(at: 0, with: section2)
testController.tableView.reloadData()
let cell1 = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
XCTAssertTrue(cell1?.textLabel?.text == "title2")
}
func testReplaceSectionOnWrongIndex() {
let row1 = TableRow<TestTableViewCell>(item: TestData(title: "title1"))
let row2 = TableRow<TestTableViewCell>(item: TestData(title: "title2"))
let section1 = TableSection(headerView: nil, footerView: nil, rows: nil)
section1 += row1
let section2 = TableSection(headerView: nil, footerView: nil, rows: nil)
section2 += row2
testController.tableDirector += section1
testController.tableView.reloadData()
let cell = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
XCTAssertTrue(cell?.textLabel?.text == "title1")
testController.tableDirector.replaceSection(at: 33, with: section2)
testController.tableView.reloadData()
let cell1 = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
XCTAssertTrue(cell1?.textLabel?.text == "title1")
}
}