Merge pull request #19 from maxsokolov/develop
Heights Calculation Strategy Support
This commit is contained in:
commit
1539f39e54
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ class StoryboardTableViewCell: UITableViewCell, ConfigurableCell {
|
|||
|
||||
typealias T = String
|
||||
|
||||
func configure(value: T) {
|
||||
func configure(value: T, isPrototype: Bool) {
|
||||
textLabel?.text = value
|
||||
}
|
||||
}
|
||||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
101
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ public enum TableRowActionType {
|
|||
case select
|
||||
case deselect
|
||||
case willSelect
|
||||
case configure
|
||||
case willDisplay
|
||||
case shouldHighlight
|
||||
case height
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
Loading…
Reference in New Issue