diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b60b00..93a7ae9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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:
The signatures of `TableRow` and `TableRowAction` classes were changed from
```swift
let action = TableRowAction(.click) { (data) in
diff --git a/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift b/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift
index 9d9d3ad..a84e42d 100644
--- a/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift
+++ b/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift
@@ -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(item: ())
+ let row = TableRow(item: randomString(length: randomInt(min: 20, max: 100)))
section += row
}
diff --git a/Demo/Classes/Presentation/Controllers/MainController.swift b/Demo/Classes/Presentation/Controllers/MainController.swift
index 7c0afdd..50a244a 100644
--- a/Demo/Classes/Presentation/Controllers/MainController.swift
+++ b/Demo/Classes/Presentation/Controllers/MainController.swift
@@ -23,9 +23,9 @@ class MainController: UIViewController {
title = "TableKit"
- let clickAction = TableRowAction(.click) { [weak self] (data) in
+ let clickAction = TableRowAction(.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(.click) { (options) in
+
+ print("click", options.indexPath)
+ }
let rows = [
- TableRow(item: "Autolayout cells", actions: [clickAction]),
- TableRow(item: "Nib cells", actions: [clickAction])
+ TableRow(item: "Autolayout cells", actions: [clickAction, printClickAction]),
+ TableRow(item: "Nib cells", actions: [clickAction, printClickAction])
]
// automatically creates a section, also could be used like tableDirector.append(rows: rows)
diff --git a/Demo/Classes/Presentation/Controllers/NibCellsController.swift b/Demo/Classes/Presentation/Controllers/NibCellsController.swift
index 32736a6..544a095 100644
--- a/Demo/Classes/Presentation/Controllers/NibCellsController.swift
+++ b/Demo/Classes/Presentation/Controllers/NibCellsController.swift
@@ -21,12 +21,13 @@ class NibCellsController: UITableViewController {
tableDirector = TableDirector(tableView: tableView)
let numbers = [1000, 2000, 3000, 4000, 5000]
-
- let shouldHighlightAction = TableRowAction(.shouldHighlight) { (_) -> Bool in
- return false
- }
- let rows = numbers.map { TableRow(item: $0, actions: [shouldHighlightAction]) }
+ let rows = numbers.map {
+ TableRow(item: $0)
+ .on(.shouldHighlight) { (_) -> Bool in
+ return false
+ }
+ }
tableDirector.append(rows: rows)
}
diff --git a/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift b/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift
index ff4219e..489b590 100644
--- a/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift
+++ b/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift
@@ -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
}
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index dfad30d..de7592a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
@@ -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(.click) { (data) in
+let action = TableRowAction(.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(item: "some", actions: [action])
@@ -98,10 +99,10 @@ let row = TableRow(item: "some", actions: [action])
Or, using nice chaining approach:
```swift
let row = TableRow(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(.custom(MyActions.ButtonClicked)) { (data) in
+let myAction = TableRowAction(.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(.click) { (options) in }
+click1.id = "click1" // optional
+
+let click2 = TableRowAction(.click) { (options) in }
+click2.id = "click2" // optional
+
+let row = TableRow(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 {
diff --git a/Sources/ConfigurableCell.swift b/Sources/ConfigurableCell.swift
index 353e099..cc96430 100644
--- a/Sources/ConfigurableCell.swift
+++ b/Sources/ConfigurableCell.swift
@@ -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
}
}
diff --git a/Sources/TableCellAction.swift b/Sources/TableCellAction.swift
index 0e30413..b962dab 100644
--- a/Sources/TableCellAction.swift
+++ b/Sources/TableCellAction.swift
@@ -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.
diff --git a/Sources/TableDirector.swift b/Sources/TableDirector.swift
index ecf798a..c251dc7 100644
--- a/Sources/TableDirector.swift
+++ b/Sources/TableDirector.swift
@@ -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
}
}
diff --git a/Sources/TableKit.swift b/Sources/TableKit.swift
new file mode 100644
index 0000000..bff0ef4
--- /dev/null
+++ b/Sources/TableKit.swift
@@ -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()
+}
diff --git a/Sources/HeightStrategy.swift b/Sources/TablePrototypeCellHeightCalculator.swift
similarity index 85%
rename from Sources/HeightStrategy.swift
rename to Sources/TablePrototypeCellHeightCalculator.swift
index 850dde9..462d7a7 100644
--- a/Sources/HeightStrategy.swift
+++ b/Sources/TablePrototypeCellHeightCalculator.swift
@@ -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 }
diff --git a/Sources/TableRow.swift b/Sources/TableRow.swift
index 6cf3738..edfca6e 100644
--- a/Sources/TableRow.swift
+++ b/Sources/TableRow.swift
@@ -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: Row where CellType: UITableViewCell {
open let item: CellType.T
- private lazy var actions = [String: TableRowAction]()
+ private lazy var actions = [String: [TableRowAction]]()
private(set) open var editingActions: [UITableViewRowAction]?
open var hashValue: Int {
@@ -78,29 +50,32 @@ open class TableRow: 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: Row where CellType: UITableView
// MARK: - actions -
@discardableResult
- open func action(_ action: TableRowAction) -> Self {
+ open func on(_ action: TableRowAction) -> Self {
+
+ if actions[action.type.key] == nil {
+ actions[action.type.key] = [TableRowAction]()
+ }
+ actions[action.type.key]?.append(action)
- actions[action.type.key] = action
return self
}
+
+ @discardableResult
+ open func on(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) -> Self {
+
+ return on(TableRowAction(type, handler: handler))
+ }
@discardableResult
- open func action(_ type: TableRowActionType, handler: @escaping (_ data: TableRowActionData) -> T) -> Self {
+ open func on(_ key: String, handler: @escaping (_ options: TableRowActionOptions) -> ()) -> Self {
- actions[type.key] = TableRowAction(type, handler: handler)
- return self
+ return on(TableRowAction(.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) -> Self {
+
+ return on(action)
+ }
+
+ @available(*, deprecated, message: "Use 'on' method instead")
+ @discardableResult
+ open func action(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) -> Self {
+
+ return on(TableRowAction(type, handler: handler))
}
}
diff --git a/Sources/TableRowAction.swift b/Sources/TableRowAction.swift
index 2533b6a..73c7419 100644
--- a/Sources/TableRowAction.swift
+++ b/Sources/TableRowAction.swift
@@ -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 where CellType: UITableViewCell {
+open class TableRowActionOptions where CellType: UITableViewCell {
open let item: CellType.T
open let cell: CellType?
@@ -63,38 +38,46 @@ open class TableRowActionData where CellType: UITabl
private enum TableRowActionHandler where CellType: UITableViewCell {
- case voidAction((TableRowActionData) -> Void)
- case action((TableRowActionData) -> Any?)
+ case voidAction((TableRowActionOptions) -> Void)
+ case action((TableRowActionOptions) -> Any?)
- func invoke(item: CellType.T, cell: UITableViewCell?, path: IndexPath) -> Any? {
+ func invoke(withOptions options: TableRowActionOptions) -> 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 where CellType: UITableViewCell {
+ open var id: String?
open let type: TableRowActionType
private let handler: TableRowActionHandler
- public init(_ type: TableRowActionType, handler: @escaping (_ data: TableRowActionData) -> Void) {
+ public init(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> Void) {
self.type = type
self.handler = .voidAction(handler)
}
- public init(_ type: TableRowActionType, handler: @escaping (_ data: TableRowActionData) -> T) {
+ public init(_ key: String, handler: @escaping (_ options: TableRowActionOptions) -> Void) {
+
+ self.type = .custom(key)
+ self.handler = .voidAction(handler)
+ }
+
+ public init(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> 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))
}
}
diff --git a/Sources/TableSection.swift b/Sources/TableSection.swift
index 127ee0e..ddc93e7 100644
--- a/Sources/TableSection.swift
+++ b/Sources/TableSection.swift
@@ -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)
}
diff --git a/TableKit.podspec b/TableKit.podspec
index 1af280b..260e2cc 100644
--- a/TableKit.podspec
+++ b/TableKit.podspec
@@ -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
\ No newline at end of file
+end
diff --git a/TableKit.xcodeproj/project.pbxproj b/TableKit.xcodeproj/project.pbxproj
index 458862d..c2ace6d 100644
--- a/TableKit.xcodeproj/project.pbxproj
+++ b/TableKit.xcodeproj/project.pbxproj
@@ -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 = ""; };
+ 50E858571DB153F500A9AA55 /* TableKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableKit.swift; sourceTree = ""; };
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 = ""; };
- DA9EA7A71D0EC2C90021F650 /* HeightStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeightStrategy.swift; sourceTree = ""; };
+ DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TablePrototypeCellHeightCalculator.swift; sourceTree = ""; };
DA9EA7A81D0EC2C90021F650 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; };
DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellAction.swift; sourceTree = ""; };
DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableDirector.swift; sourceTree = ""; };
@@ -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 = "";
@@ -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;
};
diff --git a/Tests/TableKitTests.swift b/Tests/TableKitTests.swift
index 014cf4d..5c1725f 100644
--- a/Tests/TableKitTests.swift
+++ b/Tests/TableKitTests.swift
@@ -171,7 +171,7 @@ class TabletTests: XCTestCase {
let expectation = self.expectation(description: "cell action")
let row = TableRow(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(item: TestData(title: "title1"))
+ let row2 = TableRow(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(item: TestData(title: "title1"))
+ let row2 = TableRow(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")
+ }
+
}