Merge pull request #19 from maxsokolov/develop

Heights Calculation Strategy Support
This commit is contained in:
Max Sokolov 2016-06-18 07:51:15 +04:00 committed by GitHub
commit 1539f39e54
18 changed files with 313 additions and 171 deletions

View File

@ -6,6 +6,5 @@ branches:
before_install:
- brew update
- brew reinstall --HEAD xctool
- cd Tablet
script:
- xctool clean build test -project TableKit.xcodeproj -scheme TableKit -sdk iphonesimulator

View File

@ -14,14 +14,14 @@ class MainController: UIViewController {
@IBOutlet weak var tableView: UITableView! {
didSet {
tableDirector = TableDirector(tableView: tableView)
tableDirector.shouldUsePrototypeCellHeightCalculation = true
}
}
var tableDirector: TableDirector!
override func viewDidLoad() {
super.viewDidLoad()
let a = TableRowAction<String, StoryboardImageTableViewCell>(.click) {
(data) in
@ -35,65 +35,22 @@ class MainController: UIViewController {
let row1 = TableRow<String, StoryboardImageTableViewCell>(item: "1")
let row2 = TableRow<String, StoryboardImageTableViewCell>(item: "2")
let row3 = TableRow<String, StoryboardImageTableViewCell>(item: "3", actions: [a])
row1
.addAction(TableRowAction(.shouldHighlight) { (data) -> Bool in
.action(TableRowAction(.shouldHighlight) { (data) -> Bool in
print("1")
return false
})
.addAction(TableRowAction(.click) { (data) in
.action(TableRowAction(.click) { (data) in
print("2")
})
let section = TableSection()
section.append(builder: b)
//let section = TableSection(headerTitle: "", footerTitle: "", rows: [row1, row2, row3])
tableDirector += [section]
//tableDirector.append(section: section)
/*rowBuilder
.addAction(TableRowAction(type: .Click) { (data) in
})
rowBuilder
.delete(indexes: [0], animated: .None)
.insert(["2"], atIndex: 0, animated: .None)
.update(index: 0, item: "", animated: .None)
.move([1, 2], toIndexes: [3, 4])
//tableView.moveRowAtIndexPath(<#T##indexPath: NSIndexPath##NSIndexPath#>, toIndexPath: <#T##NSIndexPath#>)
//tableView.deleteRowsAtIndexPaths(<#T##indexPaths: [NSIndexPath]##[NSIndexPath]#>, withRowAnimation: <#T##UITableViewRowAnimation#>)
//tableView.insertRowsAtIndexPaths(<#T##indexPaths: [NSIndexPath]##[NSIndexPath]#>, withRowAnimation: <#T##UITableViewRowAnimation#>)
//tableView.reloadRowsAtIndexPaths(<#T##indexPaths: [NSIndexPath]##[NSIndexPath]#>, withRowAnimation: <#T##UITableViewRowAnimation#>)
//tableView.moveSection(0, toSection: 0)
//tableView.reloadSections([], withRowAnimation: .None)
//tableView.deleteSections([], withRowAnimation: .None)
//tableView.insertSections([], withRowAnimation: .None)
//tableDirector.performBatchUpdates {
//}*/
//tableDirector.append(section: section)
}
}

View File

@ -17,7 +17,7 @@ class StoryboardImageTableViewCell: UITableViewCell, ConfigurableCell {
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var customImageView: UIImageView!
func configure(string: T) {
func configure(string: T, isPrototype: Bool) {
titleLabel.text = string
subtitleLabel.text = "Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.Copyright © 2016 Tablet. All rights reserved.1"
@ -30,7 +30,7 @@ class StoryboardImageTableViewCell: UITableViewCell, ConfigurableCell {
override func layoutSubviews() {
super.layoutSubviews()
//contentView.layoutIfNeeded()
//subtitleLabel.preferredMaxLayoutWidth = subtitleLabel.bounds.size.width
contentView.layoutIfNeeded()
subtitleLabel.preferredMaxLayoutWidth = subtitleLabel.bounds.size.width
}
}

View File

@ -13,7 +13,7 @@ class StoryboardTableViewCell: UITableViewCell, ConfigurableCell {
typealias T = String
func configure(value: T) {
func configure(value: T, isPrototype: Bool) {
textLabel?.text = value
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10116" systemVersion="15B42" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10116" systemVersion="15G7b" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10116" systemVersion="15B42" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="dOU-ON-YYD">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10116" systemVersion="15G7b" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="dOU-ON-YYD">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
@ -26,7 +26,7 @@
<!--Main Controller-->
<scene sceneID="bgC-Xq-OSw">
<objects>
<viewController id="grv-aL-Qbb" customClass="MainController" customModule="TabletDemo" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="grv-aL-Qbb" customClass="MainController" customModule="TableKitDemo" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="COn-EH-LKP"/>
<viewControllerLayoutGuide type="bottom" id="iga-ib-rj1"/>
@ -39,7 +39,7 @@
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="StoryboardImageTableViewCell" rowHeight="141" id="IBY-tW-SgU" customClass="StoryboardImageTableViewCell" customModule="TabletDemo" customModuleProvider="target">
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="StoryboardImageTableViewCell" rowHeight="141" id="IBY-tW-SgU" customClass="StoryboardImageTableViewCell" customModule="TableKitDemo" customModuleProvider="target">
<rect key="frame" x="0.0" y="92" width="600" height="141"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="IBY-tW-SgU" id="UNZ-nz-200">
@ -85,7 +85,7 @@
<outlet property="titleLabel" destination="YnK-4H-0SS" id="ilA-7H-pq7"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="StoryboardTableViewCell" id="nE5-Y5-OFf" customClass="StoryboardTableViewCell" customModule="TabletDemo" customModuleProvider="target">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="StoryboardTableViewCell" id="nE5-Y5-OFf" customClass="StoryboardTableViewCell" customModule="TableKitDemo" customModuleProvider="target">
<rect key="frame" x="0.0" y="233" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="nE5-Y5-OFf" id="3yF-sl-yNq">
@ -117,7 +117,7 @@
<!--Header Footer Controller-->
<scene sceneID="Jd0-of-RF9">
<objects>
<viewController id="sSs-TX-Ch0" customClass="HeaderFooterController" customModule="TabletDemo" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="sSs-TX-Ch0" customClass="HeaderFooterController" customModule="TableKitDemo" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="s9s-Gu-3M6"/>
<viewControllerLayoutGuide type="bottom" id="Tux-up-dnH"/>
@ -130,7 +130,7 @@
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="StoryboardTableViewCell" id="lqW-2N-6mf" customClass="StoryboardTableViewCell" customModule="TabletDemo" customModuleProvider="target">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="StoryboardTableViewCell" id="lqW-2N-6mf" customClass="StoryboardTableViewCell" customModule="TableKitDemo" customModuleProvider="target">
<rect key="frame" x="0.0" y="92" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="lqW-2N-6mf" id="tjN-ow-437">

101
README.md
View File

@ -1,11 +1,12 @@
#TableKit
<p align="left">
<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_2.2-compatible-4BC51D.svg?style=flat" alt="Swift 2.2 compatible" /></a>
<img src="https://img.shields.io/badge/platform-iOS-blue.svg?style=flat" alt="Platform iOS" />
<a href="https://cocoapods.org/pods/tablekit"><img src="https://img.shields.io/badge/pod-0.7.0-blue.svg" alt="CocoaPods compatible" /></a>
<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>
<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_2.2-compatible-4BC51D.svg?style=flat" alt="Swift 2.2 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-0.8.0-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>
TableKit is a super lightweight yet powerful generic library that allows you to build complex table views in a declarative type-safe manner.
@ -13,23 +14,28 @@ It hides a complexity of `UITableViewDataSource` and `UITableViewDelegate` metho
## Features
- [x] Type-safe cells based on generics
- [x] Type-safe generic cells
- [x] Functional programming style friendly
- [x] The easiest way to map your models or view models to cells
- [x] Correctly handles autolayout cells with multiline labels
- [x] Chainable cell actions (select/deselect etc.)
- [x] Support cells created from code, xib, or storyboard
- [x] Automatic xib/classes registration
- [x] Support different cells height calculation strategies
- [x] Support portrait and landscape orientations
- [x] No need to subclass
- [x] Extensibility
- [x] Tests
## Usage
## Getting Started
An example app is included demonstrating TableKit's functionality.
#### Basic usage
Create your rows:
```swift
let row1 = TableRow<String, StringTableViewCell>(item: "1")
let row2 = TableRow<String, IntTableViewCell>(item: 2)
let row3 = TableRow<String, FloatTableViewCell>(item: 3.0)
let row2 = TableRow<Int, IntTableViewCell>(item: 2)
let row3 = TableRow<Float, FloatTableViewCell>(item: 3.0)
```
Put rows into section:
```swift
@ -38,6 +44,7 @@ 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:
@ -46,7 +53,7 @@ class StringTableViewCell: UITableViewCell, ConfigurableCell {
typealias T = String
func configure(string: T) {
func configure(string: T, isPrototype: Bool) {
titleLabel.text = string
}
@ -57,12 +64,17 @@ class StringTableViewCell: UITableViewCell, ConfigurableCell {
```
You could have as many rows and sections as you need.
## Row actions
#### Row actions
It nice to have some actions that related to your cells:
```swift
let action = TableRowAction<String, StringTableViewCell>(.click) { (data) in
// you could access any useful information that relates to the action
// data.cell - StringTableViewCell?
// data.item - String
// data.path - NSIndexPath
}
let row = TableRow<String, StringTableViewCell>(item: "some", actions: [action])
@ -70,17 +82,16 @@ let row = TableRow<String, StringTableViewCell>(item: "some", actions: [action])
Or, using nice chaining approach:
```swift
let row = TableRow<String, StringTableViewCell>(item: "some")
row
.addAction(TableRowAction(.click) { (data) in
.action(TableRowAction(.click) { (data) in
})
.addAction(TableRowAction(.shouldHighlight) { (data) -> Bool in
.action(TableRowAction(.shouldHighlight) { (data) -> Bool in
return false
})
```
You could find all available actions <a href="https://github.com/maxsokolov/TableKit/blob/master/Sources/TableRowAction.swift" target="_blank">here</a>.
## Batch rows
#### Batch rows
You could have a situation when you need a lot of cells with the same type. In that case it's better to use `TableRowBuilder`:
```swift
let builder = TableRowBuilder<String, StringTableViewCell> {
@ -99,6 +110,56 @@ let builder = TableRowBuilder<String, StringTableViewCell>(items: ["1", "2", "3"
section.append(builder: builder)
```
## Advanced
#### Cell height calculating strategy
By default TableKit relies on <a href="https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html" target="_blank">self-sizing cells</a>. In that case you have to provide an estimated height for your cells:
```swift
class StringTableViewCell: UITableViewCell, ConfigurableCell {
// ...
static func estimatedHeight() -> CGFloat {
return 44
}
}
```
It's enough for most cases. But you may be not happy with this. So you could use a prototype cell to calculate cell's heights. To enable this feature simply use this property:
```swift
tableDirector.shouldUsePrototypeCellHeightCalculation = true
```
It does all dirty work with prototypes for you <a href="https://github.com/maxsokolov/TableKit/blob/master/Sources/HeightStrategy.swift" target="_blank">behind the scene</a>, so you don't have to worry about anything except of your cell configuration:
```swift
class ImageTableViewCell: UITableViewCell, ConfigurableCell {
func configure(url: NSURL, isPrototype: Bool) {
if !isPrototype {
loadImageAsync(url: url, imageView: imageView)
}
}
override func layoutSubviews() {
super.layoutSubviews()
contentView.layoutIfNeeded()
multilineLabel.preferredMaxLayoutWidth = multilineLabel.bounds.size.width
}
}
```
First of all you have to set `preferredMaxLayoutWidth` for all your multiline labels. And check if a configuring cell is a prototype cell. If it is, you don't have to do any additional work that not actually affect cell's height. For example you don't have to load remote image for a prototype cell.
#### Functional programming
It's never been so easy to deal with table views.
```swift
let users = /* some users array */
let rows: [Row] = users.filter({ $0.state == .active }).map({ TableRow<String, UserTableViewCell>(item: $0.username) })
tableDirector += rows
```
Done, your table is ready. It's just awesome!
## Installation
#### CocoaPods
@ -112,7 +173,9 @@ use_frameworks!
pod 'TableKit'
```
#### Carthage
Add the line `github "maxsokolov/tablekit"` to your `Cartfile`
Add the line `github "maxsokolov/tablekit"` to your `Cartfile`.
#### Manual
Clone the repo and drag files from `Sources` folder into your Xcode project.
## Requirements

View File

@ -20,21 +20,33 @@
import UIKit
public protocol ConfigurableCell {
public protocol ReusableCell {
static func reusableIdentifier() -> String
static func nib() -> UINib?
}
public protocol ConfigurableCell: ReusableCell {
associatedtype T
static func reusableIdentifier() -> String
static func estimatedHeight() -> CGFloat
static func defaultHeight() -> CGFloat?
func configure(_: T)
func configure(_: T, isPrototype: Bool)
}
public extension ConfigurableCell where Self: UITableViewCell {
public extension ReusableCell where Self: UITableViewCell {
static func reusableIdentifier() -> String {
return String(self)
}
static func nib() -> UINib? {
return nil
}
}
public extension ConfigurableCell where Self: UITableViewCell {
static func estimatedHeight() -> CGFloat {
return UITableViewAutomaticDimension

View File

@ -20,29 +20,58 @@
import UIKit
public protocol HeightStrategy {
public protocol CellHeightCalculatable {
var tableView: UITableView? { get set }
func height<Item, Cell: ConfigurableCell where Cell.T == Item, Cell: UITableViewCell>(item: Item, indexPath: NSIndexPath, cell: Cell.Type) -> CGFloat
func height(row: Row, path: NSIndexPath) -> CGFloat
func estimatedHeight(row: Row, path: NSIndexPath) -> CGFloat
func invalidate()
}
public class PrototypeHeightStrategy: HeightStrategy {
public class PrototypeHeightStrategy: CellHeightCalculatable {
public weak var tableView: UITableView?
private var prototypes = [String: UITableViewCell]()
private var cachedHeights = [Int: CGFloat]()
private var separatorHeight = 1 / UIScreen.mainScreen().scale
public func height<Item, Cell: ConfigurableCell where Cell.T == Item, Cell: UITableViewCell>(item: Item, indexPath: NSIndexPath, cell: Cell.Type) -> CGFloat {
guard let cell = tableView?.dequeueReusableCellWithIdentifier(Cell.reusableIdentifier()) as? Cell else { return 0 }
cell.bounds = CGRectMake(0, 0, tableView?.bounds.size.width ?? 0, cell.bounds.height)
cell.configure(item)
init(tableView: UITableView?) {
self.tableView = tableView
}
public func height(row: Row, path: NSIndexPath) -> CGFloat {
guard let tableView = tableView else { return 0 }
let hash = row.hashValue ^ Int(tableView.bounds.size.width).hashValue
if let height = cachedHeights[hash] {
return height
}
guard let cell = tableView.dequeueReusableCellWithIdentifier(row.reusableIdentifier) else { return 0 }
cell.bounds = CGRectMake(0, 0, tableView.bounds.size.width, cell.bounds.height)
row.configure(cell, isPrototype: true)
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + 1
let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + (tableView.separatorStyle != .None ? separatorHeight : 0)
cachedHeights[hash] = height
return height
}
public func estimatedHeight(row: Row, path: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
public func invalidate() {
cachedHeights.removeAll()
}
}

View File

@ -27,6 +27,15 @@ public func +=(left: TableDirector, right: [TableSection]) {
left.append(sections: right)
}
// --
public func +=(left: TableDirector, right: Row) {
left.append(sections: [TableSection(rows: [right])])
}
public func +=(left: TableDirector, right: [Row]) {
left.append(sections: [TableSection(rows: right)])
}
// --
public func +=(left: TableSection, right: Row) {
left.append(row: right)

View File

@ -27,7 +27,17 @@ 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?
public var shouldUsePrototypeCellHeightCalculation: Bool = false {
didSet {
if shouldUsePrototypeCellHeightCalculation {
heightStrategy = PrototypeHeightStrategy(tableView: tableView)
}
}
}
public init(tableView: UITableView, scrollDelegate: UIScrollViewDelegate? = nil) {
super.init()
@ -47,6 +57,30 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate
public func reload() {
tableView?.reloadData()
}
public func register<T: UITableViewCell where T: ReusableCell>(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
@ -77,11 +111,15 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate
// MARK: - Height
public func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return sections[indexPath.section].items[indexPath.row].estimatedHeight
let row = sections[indexPath.section].items[indexPath.row]
return heightStrategy?.estimatedHeight(row, path: indexPath) ?? row.estimatedHeight
}
public func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return sections[indexPath.section].items[indexPath.row].defaultHeight
let row = sections[indexPath.section].items[indexPath.row]
return heightStrategy?.height(row, path: indexPath) ?? row.defaultHeight
}
// MARK: UITableViewDataSource - configuration
@ -104,7 +142,7 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate
cell.layoutIfNeeded()
}
row.configure(cell)
row.configure(cell, isPrototype: false)
return cell
}
@ -185,6 +223,12 @@ public class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate
return self
}
public func append(rows rows: [Row]) -> Self {
append(section: TableSection(rows: rows))
return self
}
public func clear() -> Self {
sections.removeAll()

View File

@ -22,7 +22,7 @@ import UIKit
public protocol RowConfigurable {
func configure(cell: UITableViewCell)
func configure(cell: UITableViewCell, isPrototype: Bool)
}
public protocol RowActionable {
@ -31,7 +31,12 @@ public protocol RowActionable {
func hasAction(action: TableRowActionType) -> Bool
}
public protocol Row: RowConfigurable, RowActionable {
public protocol RowHashable {
var hashValue: Int { get }
}
public protocol Row: RowConfigurable, RowActionable, RowHashable {
var reusableIdentifier: String { get }
var estimatedHeight: CGFloat { get }
@ -42,6 +47,10 @@ public class TableRow<ItemType, CellType: ConfigurableCell where CellType.T == I
public let item: ItemType
private lazy var actions = [String: TableRowAction<ItemType, CellType>]()
public var hashValue: Int {
return ObjectIdentifier(self).hashValue
}
public var reusableIdentifier: String {
return CellType.reusableIdentifier()
@ -60,11 +69,11 @@ public class TableRow<ItemType, CellType: ConfigurableCell where CellType.T == I
self.item = item
actions?.forEach { self.actions[$0.type.key] = $0 }
}
// MARK: - RowConfigurable -
public func configure(cell: UITableViewCell) {
(cell as? CellType)?.configure(item)
public func configure(cell: UITableViewCell, isPrototype: Bool) {
(cell as? CellType)?.configure(item, isPrototype: isPrototype)
}
// MARK: - RowActionable -
@ -79,7 +88,7 @@ public class TableRow<ItemType, CellType: ConfigurableCell where CellType.T == I
// MARK: - actions -
public func addAction(action: TableRowAction<ItemType, CellType>) -> Self {
public func action(action: TableRowAction<ItemType, CellType>) -> Self {
actions[action.type.key] = action
return self

View File

@ -26,7 +26,6 @@ public enum TableRowActionType {
case select
case deselect
case willSelect
case configure
case willDisplay
case shouldHighlight
case height

View File

@ -22,7 +22,7 @@ import UIKit
public protocol RowBuilder {
func rowItems() -> [Row]?
func rows() -> [Row]?
}
public class TableRowBuilder<ItemType, CellType: ConfigurableCell where CellType.T == ItemType, CellType: UITableViewCell>: RowBuilder {
@ -42,7 +42,7 @@ public class TableRowBuilder<ItemType, CellType: ConfigurableCell where CellType
// MARK: - RowBuilder -
public func rowItems() -> [Row]? {
public func rows() -> [Row]? {
return items?.map { TableRow<ItemType, CellType>(item: $0, actions: actions) }
}
}
@ -51,7 +51,7 @@ public extension TableSection {
public func append(builder builder: RowBuilder) {
if let rows = builder.rowItems() {
if let rows = builder.rows() {
append(rows: rows)
}
}

View File

@ -66,8 +66,16 @@ public class TableSection {
public func append(row row: Row) {
append(rows: [row])
}
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) {
items.removeAtIndex(index)
}
}

View File

@ -2,7 +2,7 @@ Pod::Spec.new do |s|
s.name = 'TableKit'
s.module_name = 'TableKit'
s.version = '0.7.0'
s.version = '0.8.0'
s.homepage = 'https://github.com/maxsokolov/TableKit'
s.summary = 'Type-safe declarative table views. Swift 2.2 is required.'

View File

@ -28,7 +28,26 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA9EA7C31D0EC45F0021F650"
BuildableName = "TableKitTests.xctest"
BlueprintName = "TableKitTests"
ReferencedContainer = "container:TableKit.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA9EA7551D0B679A0021F650"
BuildableName = "TableKit.framework"
BlueprintName = "TableKit"
ReferencedContainer = "container:TableKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>

View File

@ -28,6 +28,7 @@ class TestController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableDirector = TableDirector(tableView: tableView)
tableDirector.register(TestTableViewCell.self)
}
}
@ -45,7 +46,7 @@ struct TestTableViewCellOptions {
static let EstimatedHeight: Float = 255
}
/*class TestTableViewCell: UITableViewCell, ConfigurableCell {
class TestTableViewCell: UITableViewCell, ConfigurableCell {
typealias T = TestData
@ -57,12 +58,12 @@ struct TestTableViewCellOptions {
return TestTableViewCellOptions.EstimatedHeight
}
func configure(item: T) {
func configure(item: T, isPrototype: Bool) {
textLabel?.text = item.title
}
func raiseAction() {
Action(key: TestTableViewCellOptions.CellAction, sender: self, userInfo: [TestTableViewCellOptions.CellActionUserInfoKey: TestTableViewCellOptions.CellActionUserInfoValue]).invoke()
TableCellAction(key: TestTableViewCellOptions.CellAction, sender: self, userInfo: nil).invoke()
}
}
@ -74,6 +75,7 @@ class TabletTests: XCTestCase {
super.setUp()
testController = TestController()
testController.view.hidden = false
}
override func tearDown() {
@ -89,60 +91,55 @@ class TabletTests: XCTestCase {
XCTAssertNotNil(testController.tableDirector.tableView, "TableDirector should have table view")
}
func testSimpleRowBuilderCreatesRowsAndSection() {
let source = ["1", "2", "3"]
let rows = TableBaseRowBuilder<String, UITableViewCell>(items: source)
.action(.configure) { data -> Void in
XCTAssertNotNil(data.cell, "Action should have a cell")
data.cell?.textLabel?.text = "\(data.item)"
}
func testRowInSection() {
let data = TestData(title: "title")
let row = TableRow<TestData, TestTableViewCell>(item: data)
testController.tableDirector += row
testController.tableView.reloadData()
XCTAssertTrue(testController.tableView.dataSource?.numberOfSectionsInTableView?(testController.tableView) == 1, "Table view should have a section")
XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == 1, "Table view should have certain number of rows in a section")
let cell = testController.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0)) as? TestTableViewCell
XCTAssertNotNil(cell)
XCTAssertTrue(cell?.textLabel?.text == data.title)
}
func testManyRowsInSection() {
let data = [TestData(title: "1"), TestData(title: "2"), TestData(title: "3")]
let rows: [Row] = data.map({ TableRow<TestData, TestTableViewCell>(item: $0) })
testController.view.hidden = false
testController.tableDirector += rows
testController.tableView.reloadData()
XCTAssertTrue(testController.tableView.dataSource?.numberOfSectionsInTableView?(testController.tableView) == 1, "Table view should have a section")
XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == source.count, "Table view should have certain number of rows in a section")
for (index, element) in source.enumerate() {
let cell = testController.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0))
XCTAssertTrue(testController.tableView.dataSource?.numberOfSectionsInTableView?(testController.tableView) == 1, "Table view should have a section")
XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == data.count, "Table view should have certain number of rows in a section")
for (index, element) in data.enumerate() {
let cell = testController.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as? TestTableViewCell
XCTAssertNotNil(cell)
XCTAssertTrue(cell?.textLabel?.text == element)
XCTAssertTrue(cell?.textLabel?.text == element.title)
}
}
func testConfigurableRowBuilderCreatesRowsAndSection() {
let testData = TestData(title: "title")
func testTableSectionCreatesSectionWithHeaderAndFooterTitles() {
testController.view.hidden = false
testController.tableDirector += TableRowBuilder<TestData, TestTableViewCell>(item: testData)
testController.tableView.reloadData()
let cell = testController.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0)) as? TestTableViewCell
let row = TableRow<TestData, TestTableViewCell>(item: TestData(title: "title"))
XCTAssertNotNil(cell, "Cell should exists and should be TestTableViewCell")
XCTAssertTrue(cell?.textLabel?.text == testData.title, "Cell's textLabel.text should equal to testData's title")
}
func testSectionBuilderCreatesSectionWithHeaderAndFooterTitles() {
let row = TableRowBuilder<TestData, TestTableViewCell>(items: [TestData(title: "title")])
let sectionHeaderTitle = "Header Title"
let sectionFooterTitle = "Footer Title"
let section = TableSectionBuilder(headerTitle: sectionHeaderTitle, footerTitle: sectionFooterTitle, rows: [row])
testController.view.hidden = false
let section = TableSection(headerTitle: sectionHeaderTitle, footerTitle: sectionFooterTitle, rows: [row])
testController.tableDirector += section
testController.tableView.reloadData()
XCTAssertTrue(testController.tableView.dataSource?.numberOfSectionsInTableView?(testController.tableView) == 1, "Table view should have a section")
XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == 1, "Table view should have certain number of rows in a section")
@ -150,17 +147,16 @@ class TabletTests: XCTestCase {
XCTAssertTrue(testController.tableView.dataSource?.tableView?(testController.tableView, titleForFooterInSection: 0) == sectionFooterTitle)
}
func testSectionBuilderCreatesSectionWithHeaderAndFooterViews() {
let row = TableRowBuilder<TestData, TestTableViewCell>(items: [TestData(title: "title")])
func testTableSectionCreatesSectionWithHeaderAndFooterViews() {
let row = TableRow<TestData, TestTableViewCell>(item: TestData(title: "title"))
let sectionHeaderView = UIView()
let sectionFooterView = UIView()
let section = TableSectionBuilder(headerView: sectionHeaderView, footerView: sectionFooterView, rows: nil)
let section = TableSection(headerView: sectionHeaderView, footerView: sectionFooterView, rows: nil)
section += row
testController.view.hidden = false
testController.tableDirector += section
testController.tableView.reloadData()
@ -170,20 +166,18 @@ class TabletTests: XCTestCase {
XCTAssertTrue(testController.tableView.delegate?.tableView?(testController.tableView, viewForHeaderInSection: 0) == sectionHeaderView)
XCTAssertTrue(testController.tableView.delegate?.tableView?(testController.tableView, viewForFooterInSection: 0) == sectionFooterView)
}
func testRowBuilderCustomActionInvokedAndSentUserInfo() {
let expectation = expectationWithDescription("cell action")
let row = TableRowBuilder<TestData, TestTableViewCell>(items: [TestData(title: "title")])
.action(TestTableViewCellOptions.CellAction) { data -> Void in
let row = TableRow<TestData, TestTableViewCell>(item: TestData(title: "title"))
.action(TableRowAction(.custom(TestTableViewCellOptions.CellAction)) { (data) in
XCTAssertNotNil(data.cell, "Action data should have a cell")
XCTAssertNotNil(data.userInfo, "Action data should have a user info dictionary")
XCTAssertTrue(data.userInfo?[TestTableViewCellOptions.CellActionUserInfoKey] as? String == TestTableViewCellOptions.CellActionUserInfoValue, "UserInfo should have correct value for key")
expectation.fulfill()
}
})
testController.view.hidden = false
testController.tableDirector += row
@ -197,4 +191,4 @@ class TabletTests: XCTestCase {
waitForExpectationsWithTimeout(1.0, handler: nil)
}
}*/
}