diff --git a/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift b/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift index 522ea56..618e02a 100644 --- a/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift +++ b/Demo/Classes/Presentation/Controllers/AutolayoutCellsController.swift @@ -14,7 +14,6 @@ class AutolayoutCellsController: UIViewController { @IBOutlet weak var tableView: UITableView! { didSet { tableDirector = TableDirector(tableView: tableView) - tableDirector.register(ConfigurableTableViewCell.self) } } var tableDirector: TableDirector! diff --git a/Demo/Classes/Presentation/Controllers/MainController.swift b/Demo/Classes/Presentation/Controllers/MainController.swift index 5a286a1..cea1b05 100644 --- a/Demo/Classes/Presentation/Controllers/MainController.swift +++ b/Demo/Classes/Presentation/Controllers/MainController.swift @@ -14,7 +14,6 @@ class MainController: UIViewController { @IBOutlet weak var tableView: UITableView! { didSet { tableDirector = TableDirector(tableView: tableView) - tableDirector.register(ConfigurableTableViewCell.self) } } var tableDirector: TableDirector! diff --git a/Demo/Classes/Presentation/Controllers/NibCellsController.swift b/Demo/Classes/Presentation/Controllers/NibCellsController.swift index c6d69f5..5309ebf 100644 --- a/Demo/Classes/Presentation/Controllers/NibCellsController.swift +++ b/Demo/Classes/Presentation/Controllers/NibCellsController.swift @@ -19,7 +19,6 @@ class NibCellsController: UITableViewController { title = "Nib cells" tableDirector = TableDirector(tableView: tableView) - tableDirector.register(NibTableViewCell.self) let numbers = [1000, 2000, 3000, 4000, 5000] diff --git a/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift b/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift index 455f618..4e626f1 100644 --- a/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift +++ b/Demo/Classes/Presentation/Views/AutolayoutTableViewCell.swift @@ -25,7 +25,7 @@ class AutolayoutTableViewCell: UITableViewCell, ConfigurableCell { subtitleLabel.text = LoremIpsumBody } - static func estimatedHeight() -> CGFloat { + static func estimatedHeight() -> CGFloat? { return 500 } } \ No newline at end of file diff --git a/README.md b/README.md index 60a5329..327826e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Build Status Swift 2.2 compatible Carthage compatible - CocoaPods compatible + CocoaPods compatible Platform iOS License: MIT

@@ -17,6 +17,7 @@ It hides a complexity of `UITableViewDataSource` and `UITableViewDelegate` metho - [x] Type-safe generic cells - [x] Functional programming style friendly - [x] The easiest way to map your models or view models to cells +- [x] Automatic cell registration - [x] Correctly handles autolayout cells with multiline labels - [x] Chainable cell actions (select/deselect etc.) - [x] Support cells created from code, xib, or storyboard @@ -44,7 +45,6 @@ let section = TableSection(rows: [row1, row2, row3]) And setup your table: ```swift let tableDirector = TableDirector(tableView: tableView) -tableDirector.register(StringTableViewCell.self, IntTableViewCell.self, FloatTableViewCell.self) tableDirector += section ``` Done. Your table is ready. You may want to look at your cell. It has to conform to `ConfigurableCell` protocol: diff --git a/Sources/ConfigurableCell.swift b/Sources/ConfigurableCell.swift index 473f7cc..efa3fcb 100644 --- a/Sources/ConfigurableCell.swift +++ b/Sources/ConfigurableCell.swift @@ -27,10 +27,10 @@ public protocol ReusableCell { } public protocol ConfigurableCell: ReusableCell { - + associatedtype T - - static func estimatedHeight() -> CGFloat + + static func estimatedHeight() -> CGFloat? static func defaultHeight() -> CGFloat? func configure(_: T, isPrototype: Bool) } @@ -47,11 +47,11 @@ public extension ReusableCell where Self: UITableViewCell { } public extension ConfigurableCell where Self: UITableViewCell { - - static func estimatedHeight() -> CGFloat { + + static func estimatedHeight() -> CGFloat? { return UITableViewAutomaticDimension } - + static func defaultHeight() -> CGFloat? { return nil } diff --git a/Sources/TableCellManager.swift b/Sources/TableCellManager.swift new file mode 100644 index 0000000..5cc29e8 --- /dev/null +++ b/Sources/TableCellManager.swift @@ -0,0 +1,58 @@ +// +// 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 + +public class TableCellManager { + + private var registeredIds = Set() + private weak var tableView: UITableView? + + public init(tableView: UITableView?) { + self.tableView = tableView + } + + public func register(cellType cellType: AnyClass, forReusableCellIdentifier reusableIdentifier: String) { + + if registeredIds.contains(reusableIdentifier) { + return + } + + // check if cell is already registered, probably cell has been registered by storyboard + if tableView?.dequeueReusableCellWithIdentifier(reusableIdentifier) != nil { + + registeredIds.insert(reusableIdentifier) + return + } + + let bundle = NSBundle(forClass: cellType) + + // we hope that cell's xib file has name that equals to cell's class name + // in that case we could register nib + if let _ = bundle.pathForResource(reusableIdentifier, ofType: "nib") { + tableView?.registerNib(UINib(nibName: reusableIdentifier, bundle: bundle), forCellReuseIdentifier: reusableIdentifier) + // otherwise, register cell class + } else { + tableView?.registerClass(cellType, forCellReuseIdentifier: reusableIdentifier) + } + + registeredIds.insert(reusableIdentifier) + } +} \ No newline at end of file diff --git a/Sources/TableDirector.swift b/Sources/TableDirector.swift index 710c77a..b34d62c 100644 --- a/Sources/TableDirector.swift +++ b/Sources/TableDirector.swift @@ -24,13 +24,14 @@ import UIKit Responsible for table view's datasource and delegate. */ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate { - + public private(set) weak var tableView: UITableView? public private(set) var sections = [TableSection]() private weak var scrollDelegate: UIScrollViewDelegate? private var heightStrategy: CellHeightCalculatable? - + private var cellManager: TableCellManager? + public var shouldUsePrototypeCellHeightCalculation: Bool = false { didSet { if shouldUsePrototypeCellHeightCalculation { @@ -38,15 +39,23 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate } } } - - public init(tableView: UITableView, scrollDelegate: UIScrollViewDelegate? = nil) { + + public var isEmpty: Bool { + return sections.isEmpty + } + + public init(tableView: UITableView, scrollDelegate: UIScrollViewDelegate? = nil, shouldUseAutomaticCellRegistration: Bool = true) { super.init() - + + if shouldUseAutomaticCellRegistration { + self.cellManager = TableCellManager(tableView: tableView) + } + self.scrollDelegate = scrollDelegate self.tableView = tableView self.tableView?.delegate = self self.tableView?.dataSource = self - + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didReceiveAction), name: TableKitNotifications.CellAction, object: nil) } @@ -58,40 +67,16 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate tableView?.reloadData() } - public func register(cells: T.Type...) { - - for cell in cells { - - if let nib = cell.nib() { - - tableView?.registerNib(nib, forCellReuseIdentifier: cell.reusableIdentifier()) - return - - } else { - - let resource = cell.reusableIdentifier() - let bundle = NSBundle(forClass: cell) - - if let _ = bundle.pathForResource(resource, ofType: "nib") { - tableView?.registerNib(UINib(nibName: resource, bundle: bundle), forCellReuseIdentifier: cell.reusableIdentifier()) - return - } - } - - tableView?.registerClass(cell, forCellReuseIdentifier: cell.reusableIdentifier()) - } - } - // MARK: Public public func invoke(action action: TableRowActionType, cell: UITableViewCell?, indexPath: NSIndexPath) -> Any? { return sections[indexPath.section].items[indexPath.row].invoke(action, cell: cell, path: indexPath) } - + public override func respondsToSelector(selector: Selector) -> Bool { return super.respondsToSelector(selector) || scrollDelegate?.respondsToSelector(selector) == true } - + public override func forwardingTargetForSelector(selector: Selector) -> AnyObject? { return scrollDelegate?.respondsToSelector(selector) == true ? scrollDelegate : super.forwardingTargetForSelector(selector) } @@ -103,7 +88,7 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate } func didReceiveAction(notification: NSNotification) { - + guard let action = notification.object as? TableCellAction, indexPath = tableView?.indexPathForCell(action.cell) else { return } invoke(action: .custom(action.key), cell: action.cell, indexPath: indexPath) } @@ -111,17 +96,17 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate // MARK: - Height public func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { - + let row = sections[indexPath.section].items[indexPath.row] - return heightStrategy?.estimatedHeight(row, path: indexPath) ?? row.estimatedHeight + return row.estimatedHeight ?? heightStrategy?.estimatedHeight(row, path: indexPath) ?? UITableViewAutomaticDimension } public func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { - + let row = sections[indexPath.section].items[indexPath.row] let rowHeight = invoke(action: .height, cell: nil, indexPath: indexPath) as? CGFloat - return rowHeight ?? heightStrategy?.height(row, path: indexPath) ?? row.defaultHeight + return rowHeight ?? row.defaultHeight ?? heightStrategy?.height(row, path: indexPath) ?? UITableViewAutomaticDimension } // MARK: UITableViewDataSource - configuration @@ -135,17 +120,21 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate } public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - + let row = sections[indexPath.section].items[indexPath.row] + + cellManager?.register(cellType: row.cellType, forReusableCellIdentifier: row.reusableIdentifier) + let cell = tableView.dequeueReusableCellWithIdentifier(row.reusableIdentifier, forIndexPath: indexPath) if cell.frame.size.width != tableView.frame.size.width { cell.frame = CGRectMake(0, 0, tableView.frame.size.width, cell.frame.size.height) cell.layoutIfNeeded() } - + row.configure(cell, isPrototype: false) - + invoke(action: .configure, cell: cell, indexPath: indexPath) + return cell } @@ -170,11 +159,15 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate } public func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return sections[section].headerView?.frame.size.height ?? UITableViewAutomaticDimension + + let section = sections[section] + return section.headerHeight ?? section.headerView?.frame.size.height ?? 0 } public func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return sections[section].footerView?.frame.size.height ?? UITableViewAutomaticDimension + + let section = sections[section] + return section.footerHeight ?? section.footerView?.frame.size.height ?? 0 } // MARK: UITableViewDelegate - actions @@ -201,9 +194,9 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate public func tableView(tableView: UITableView, shouldHighlightRowAtIndexPath indexPath: NSIndexPath) -> Bool { return invoke(action: .shouldHighlight, cell: tableView.cellForRowAtIndexPath(indexPath), indexPath: indexPath) as? Bool ?? true } - + public func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? { - + if hasAction(.willSelect, atIndexPath: indexPath) { return invoke(action: .willSelect, cell: tableView.cellForRowAtIndexPath(indexPath), indexPath: indexPath) as? NSIndexPath } @@ -213,7 +206,7 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate // MARK: - Sections manipulation - public func append(section section: TableSection) -> Self { - + append(sections: [section]) return self } @@ -238,13 +231,13 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate } public func delete(index index: Int) -> Self { - + sections.removeAtIndex(index) return self } public func clear() -> Self { - + sections.removeAll() return self } diff --git a/Sources/TableRow.swift b/Sources/TableRow.swift index 7c1e70e..db139b5 100644 --- a/Sources/TableRow.swift +++ b/Sources/TableRow.swift @@ -21,12 +21,12 @@ import UIKit public protocol RowConfigurable { - + func configure(cell: UITableViewCell, isPrototype: Bool) } public protocol RowActionable { - + func invoke(action: TableRowActionType, cell: UITableViewCell?, path: NSIndexPath) -> Any? func hasAction(action: TableRowActionType) -> Bool } @@ -37,31 +37,37 @@ public protocol RowHashable { } public protocol Row: RowConfigurable, RowActionable, RowHashable { - + var reusableIdentifier: String { get } - var estimatedHeight: CGFloat { get } - var defaultHeight: CGFloat { get } + var cellType: AnyClass { get } + + var estimatedHeight: CGFloat? { get } + var defaultHeight: CGFloat? { get } } public class TableRow: Row { - + public let item: ItemType private lazy var actions = [String: TableRowAction]() public var hashValue: Int { return ObjectIdentifier(self).hashValue } - + public var reusableIdentifier: String { return CellType.reusableIdentifier() } - public var estimatedHeight: CGFloat { + public var estimatedHeight: CGFloat? { return CellType.estimatedHeight() } - - public var defaultHeight: CGFloat { - return CellType.defaultHeight() ?? UITableViewAutomaticDimension + + public var defaultHeight: CGFloat? { + return CellType.defaultHeight() + } + + public var cellType: AnyClass { + return CellType.self } public init(item: ItemType, actions: [TableRowAction]? = nil) { @@ -69,7 +75,7 @@ public class TableRow Any? { return actions[action.key]?.invoke(item: item, cell: cell, path: path) } - + public func hasAction(action: TableRowActionType) -> Bool { return actions[action.key] != nil } @@ -89,7 +95,7 @@ public class TableRow) -> Self { - + actions[action.type.key] = action return self } diff --git a/Sources/TableRowAction.swift b/Sources/TableRowAction.swift index cd3f0c6..5c639a8 100644 --- a/Sources/TableRowAction.swift +++ b/Sources/TableRowAction.swift @@ -29,8 +29,9 @@ public enum TableRowActionType { case willDisplay case shouldHighlight case height + case configure case custom(String) - + var key: String { switch (self) { diff --git a/Sources/TableSection.swift b/Sources/TableSection.swift index e09078e..e42b306 100644 --- a/Sources/TableSection.swift +++ b/Sources/TableSection.swift @@ -21,44 +21,51 @@ import UIKit public class TableSection { - + weak var tableDirector: TableDirector? - + public private(set) var items = [Row]() - + public var headerTitle: String? public var footerTitle: String? - - public private(set) var headerView: UIView? - public private(set) var footerView: UIView? - + + public var headerView: UIView? + public var footerView: UIView? + + public var headerHeight: CGFloat? = nil + public var footerHeight: CGFloat? = nil + public var numberOfRows: Int { return items.count } + public var isEmpty: Bool { + return items.isEmpty + } + public init(rows: [Row]? = nil) { - + if let initialRows = rows { items.appendContentsOf(initialRows) } } - + public convenience init(headerTitle: String?, footerTitle: String?, rows: [Row]? = nil) { self.init(rows: rows) self.headerTitle = headerTitle self.footerTitle = footerTitle } - + public convenience init(headerView: UIView?, footerView: UIView?, rows: [Row]? = nil) { self.init(rows: rows) self.headerView = headerView self.footerView = footerView } - + // MARK: - Public - - + public func clear() { items.removeAll() } @@ -70,12 +77,12 @@ public class TableSection { public func append(rows rows: [Row]) { items.appendContentsOf(rows) } - + public func insert(row row: Row, atIndex index: Int) { items.insert(row, atIndex: index) } - - public func delete(index: Int) { + + public func delete(index index: Int) { items.removeAtIndex(index) } } \ No newline at end of file diff --git a/TableKit.podspec b/TableKit.podspec index 6835141..85230b7 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 = '0.9.0' + s.version = '0.9.1' s.homepage = 'https://github.com/maxsokolov/TableKit' s.summary = 'Type-safe declarative table views. Swift 2.2 is required.' diff --git a/TableKit.xcodeproj/project.pbxproj b/TableKit.xcodeproj/project.pbxproj index b72ffe3..32e8324 100644 --- a/TableKit.xcodeproj/project.pbxproj +++ b/TableKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 50CF6E6B1D6704FE004746FF /* TableCellManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF6E6A1D6704FE004746FF /* TableCellManager.swift */; }; DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */; }; DA9EA7B01D0EC2C90021F650 /* HeightStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A71D0EC2C90021F650 /* HeightStrategy.swift */; }; DA9EA7B11D0EC2C90021F650 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A81D0EC2C90021F650 /* Operators.swift */; }; @@ -30,6 +31,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 50CF6E6A1D6704FE004746FF /* TableCellManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellManager.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 = ""; }; @@ -87,6 +89,7 @@ isa = PBXGroup; children = ( DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */, + 50CF6E6A1D6704FE004746FF /* TableCellManager.swift */, DA9EA7AB1D0EC2C90021F650 /* TableRow.swift */, DA9EA7AC1D0EC2C90021F650 /* TableRowAction.swift */, DA9EA7AE1D0EC2C90021F650 /* TableSection.swift */, @@ -222,6 +225,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 50CF6E6B1D6704FE004746FF /* TableCellManager.swift in Sources */, DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */, DA9EA7B31D0EC2C90021F650 /* TableDirector.swift in Sources */, DA9EA7B71D0EC2C90021F650 /* TableSection.swift in Sources */, diff --git a/Tests/TableKitTests.swift b/Tests/TableKitTests.swift index 7a601e1..767f9d0 100644 --- a/Tests/TableKitTests.swift +++ b/Tests/TableKitTests.swift @@ -28,7 +28,6 @@ class TestController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableDirector = TableDirector(tableView: tableView) - tableDirector.register(TestTableViewCell.self) } }