diff --git a/Example/MYTableViewManager.xcodeproj/project.pbxproj b/Example/MYTableViewManager.xcodeproj/project.pbxproj index 44f2146..13b37d8 100644 --- a/Example/MYTableViewManager.xcodeproj/project.pbxproj +++ b/Example/MYTableViewManager.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 86398C851A6569AC0014C50F /* MYBaseClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86398C841A6569AC0014C50F /* MYBaseClasses.swift */; }; + 86398C851A6569AC0014C50F /* MYCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86398C841A6569AC0014C50F /* MYCommon.swift */; }; 86398C871A6569BC0014C50F /* MYTableViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86398C861A6569BC0014C50F /* MYTableViewManager.swift */; }; 86398C891A6569CC0014C50F /* MYTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86398C881A6569CC0014C50F /* MYTableViewCell.swift */; }; 86398C8B1A6569D90014C50F /* MYHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86398C8A1A6569D90014C50F /* MYHeaderFooterView.swift */; }; @@ -22,6 +22,9 @@ 86B1FFE51A6568F1000E3772 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 86B1FFE41A6568F1000E3772 /* Images.xcassets */; }; 86B1FFE81A6568F1000E3772 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 86B1FFE61A6568F1000E3772 /* LaunchScreen.xib */; }; 86B1FFF41A6568F1000E3772 /* MYTableViewManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86B1FFF31A6568F1000E3772 /* MYTableViewManagerTests.swift */; }; + 8C94F4411A907B9F00B2B2D0 /* MYReloadTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C94F4401A907B9F00B2B2D0 /* MYReloadTracker.swift */; }; + 8C9CD6FC1A8F4EBB004802C7 /* MYSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9CD6FB1A8F4EBB004802C7 /* MYSection.swift */; }; + 8C9CD6FE1A8F4F27004802C7 /* MYViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9CD6FD1A8F4F27004802C7 /* MYViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,7 +38,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 86398C841A6569AC0014C50F /* MYBaseClasses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYBaseClasses.swift; sourceTree = ""; }; + 86398C841A6569AC0014C50F /* MYCommon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYCommon.swift; sourceTree = ""; }; 86398C861A6569BC0014C50F /* MYTableViewManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYTableViewManager.swift; sourceTree = ""; }; 86398C881A6569CC0014C50F /* MYTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYTableViewCell.swift; sourceTree = ""; }; 86398C8A1A6569D90014C50F /* MYHeaderFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYHeaderFooterView.swift; sourceTree = ""; }; @@ -54,6 +57,9 @@ 86B1FFED1A6568F1000E3772 /* MYTableViewManagerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MYTableViewManagerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 86B1FFF21A6568F1000E3772 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 86B1FFF31A6568F1000E3772 /* MYTableViewManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MYTableViewManagerTests.swift; sourceTree = ""; }; + 8C94F4401A907B9F00B2B2D0 /* MYReloadTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYReloadTracker.swift; sourceTree = ""; }; + 8C9CD6FB1A8F4EBB004802C7 /* MYSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYSection.swift; sourceTree = ""; }; + 8C9CD6FD1A8F4F27004802C7 /* MYViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MYViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -77,7 +83,10 @@ 86398C831A6569810014C50F /* Source */ = { isa = PBXGroup; children = ( - 86398C841A6569AC0014C50F /* MYBaseClasses.swift */, + 86398C841A6569AC0014C50F /* MYCommon.swift */, + 8C94F4401A907B9F00B2B2D0 /* MYReloadTracker.swift */, + 8C9CD6FD1A8F4F27004802C7 /* MYViewModel.swift */, + 8C9CD6FB1A8F4EBB004802C7 /* MYSection.swift */, 86398C861A6569BC0014C50F /* MYTableViewManager.swift */, 86398C881A6569CC0014C50F /* MYTableViewCell.swift */, 86398C8A1A6569D90014C50F /* MYHeaderFooterView.swift */, @@ -249,15 +258,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8C94F4411A907B9F00B2B2D0 /* MYReloadTracker.swift in Sources */, 86398C8D1A6569EB0014C50F /* MYLabel.swift in Sources */, 86398C931A656AD40014C50F /* ChildViewController.swift in Sources */, 86398C891A6569CC0014C50F /* MYTableViewCell.swift in Sources */, 86B1FFE01A6568F1000E3772 /* ViewController.swift in Sources */, - 86398C851A6569AC0014C50F /* MYBaseClasses.swift in Sources */, + 86398C851A6569AC0014C50F /* MYCommon.swift in Sources */, 86398C911A656AAC0014C50F /* CustomCell.swift in Sources */, 86398C8F1A6569F70014C50F /* MYExtension.swift in Sources */, 86B1FFDE1A6568F1000E3772 /* AppDelegate.swift in Sources */, 86398C8B1A6569D90014C50F /* MYHeaderFooterView.swift in Sources */, + 8C9CD6FC1A8F4EBB004802C7 /* MYSection.swift in Sources */, + 8C9CD6FE1A8F4F27004802C7 /* MYViewModel.swift in Sources */, 86398C871A6569BC0014C50F /* MYTableViewManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Example/MYTableViewManager/CustomCell.swift b/Example/MYTableViewManager/CustomCell.swift index 7e5ad2e..5184ca9 100644 --- a/Example/MYTableViewManager/CustomCell.swift +++ b/Example/MYTableViewManager/CustomCell.swift @@ -14,7 +14,7 @@ class CustomCell : MYTableViewCell { override func configureCell(data: MYCellViewModel) { super.configureCell(data) if let title = data.userData as? String { - titleLabel.text = title + titleLabel.text = title + "(\(data.section),\(data.row))" } } } \ No newline at end of file diff --git a/Example/MYTableViewManager/ViewController.swift b/Example/MYTableViewManager/ViewController.swift index 25bf864..fc91973 100644 --- a/Example/MYTableViewManager/ViewController.swift +++ b/Example/MYTableViewManager/ViewController.swift @@ -10,19 +10,60 @@ import UIKit class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! - private var tableViewManager: MYTableViewManager! + private var tvm: MYTableViewManager! override func viewWillAppear(animated: Bool) { - tableViewManager?.deselectAllCells() + tvm?.deselectAllCells() } override func viewDidLoad() { super.viewDidLoad() - tableViewManager = MYTableViewManager(tableView: tableView) - tableViewManager.delegate = self + tvm = MYTableViewManager(tableView: tableView) + tvm.delegate = self - tableViewManager.registerCellNib(CustomCell) + tvm.registerCellNib(CustomCell) + delay(1) { + _ = self.start() + } + } + + func start() { + + for index in 0...4 { + let cvm = (0..<2).map { [weak self] i -> MYCellViewModel in + return MYCellViewModel(cellClass: CustomCell.self, userData: "index \(index*2 + i)") { _ in + println("Did select new cell : \(index + i)") + self?.pushChildViewController() + } + } + tvm[0].insert(cvm[0], atIndex: 0) + //.fire(.Left) + tvm[0].append(cvm[1]) + + //tvm[0][index*2]?.userData = "new title: \(index)" + //tvm[0][index + 1]?.fire() + } + //tvm[0].remove(0) + /* + delay(2) { + for index in 0...4 { + self.tvm[0].remove(index+1) + //.fire(.Right) + } + } + */ + + println("finish setting") + delay(2) { + self.tvm[0].fire() + return + } + + delay(7) { + //self.tvm[0].fire() + return + } /* let longTitle1 = "Don't have to write the code for UITableViewDelegate and UITableViewDataSource protocols" let longTitle2 = "Support dynamic cell height from ios7" @@ -37,17 +78,11 @@ class ViewController: UIViewController { data.dynamicHeightEnabled = true return data } - tableViewManager.resetWithData(cellData, inSection: 0) - - tableViewManager.loadmoreHandler = { [weak self] in - println("Loadmore") - self?.delay(1) { - self?.tableViewManager.loadmoreEnabled = true - return - } - } - delay(1.0) { + self.tvm[0].reset(cellData) + .fire() + + delay(2) { let titles = ["new cell 1", "new cell 2"] let newCellData = titles.map { [weak self] title -> MYCellViewModel in return MYCellViewModel(cellClass: CustomCell.self, userData: title) { _ in @@ -55,16 +90,29 @@ class ViewController: UIViewController { self?.pushChildViewController() } } - self.tableViewManager.resetWithData(newCellData, inSection: 5) - //self.tableViewManager.insertData(newCellData, inSection: 0, atRow: 1, reloadType: .InsertRows(.Middle)) + self.tvm[0].insert(newCellData, atIndex: 2) + .fire(.Middle) } - - delay(2.0) { - self.tableViewManager.removeDataInSection(0, atRow: 2) + + delay(5) { + self.tvm[0].remove(1) + .fire(.Left) + return } + */ + /* + tvm.loadmoreHandler = { [weak self] in + println("Loadmore") + self?.delay(1) { + self?.tvm.loadmoreEnabled = true + return + } + } + + delay(3.0) { - self.tableViewManager.updateUserData("Last cell", inSection: 5, atRow: 1) + self.tvm.updateUserData("Last cell", inSection: 5, atRow: 1) } delay(5.0) { @@ -75,18 +123,29 @@ class ViewController: UIViewController { self?.pushChildViewController() } } - self.tableViewManager.insertDataBeforeLastRow(newCellData, inSection: 0, reloadType: .InsertRows(.Middle)) + self.tvm.insertDataBeforeLastRow(newCellData, inSection: 0, reloadType: .InsertRows(.Middle)) } delay(6.0) { - self.tableViewManager.removeLastDataInSection(0) - //self.tableViewManager.removeDataInSection(0, inRange: (7..<9), reloadType: .DeleteRows(.Middle)) + self.tvm.removeLastDataInSection(0) + //self.tvm.removeDataInSection(0, inRange: (7..<9), reloadType: .DeleteRows(.Middle)) } - tableViewManager.loadmoreEnabled = true + tvm.loadmoreEnabled = true */ } + func appendItems(index: Int, num: Int) { + let cvm = (0.. MYCellViewModel in + return MYCellViewModel(cellClass: CustomCell.self, userData: "index \(index + i)") { _ in + println("Did select new cell : \(index + i)") + self?.pushChildViewController() + } + } + self.tvm[0].append(cvm) + .fire(.Left) + } + func pushChildViewController() { let storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = storyboard.instantiateViewControllerWithIdentifier("ChildViewController") as ChildViewController @@ -105,7 +164,6 @@ class ViewController: UIViewController { extension ViewController : MYTableViewManagerDelegate { func scrollViewDidScroll(scrollView: UIScrollView) { - println("OK") } } diff --git a/Source/MYCommon.swift b/Source/MYCommon.swift new file mode 100644 index 0000000..61618cf --- /dev/null +++ b/Source/MYCommon.swift @@ -0,0 +1,22 @@ +// +// MYCommon.swift +// MYTableViewManager +// +// Created by Le Van Nghia on 1/13/15. +// Copyright (c) 2015 Le Van Nghia. All rights reserved. +// + +import UIKit + +public typealias MYSelectionHandler = (MYBaseViewProtocol) -> () +public typealias MYAnimation = UITableViewRowAnimation + +public protocol MYBaseViewProtocol { + func highlight(Bool) + func unhighlight(Bool) + func emitSelectedEvent(MYBaseViewProtocol) +} + +public protocol MYBaseViewDelegate : class { + func didSelect(view: MYBaseViewProtocol) +} diff --git a/Source/MYExtension.swift b/Source/MYExtension.swift index af474be..cc45b29 100644 --- a/Source/MYExtension.swift +++ b/Source/MYExtension.swift @@ -14,14 +14,78 @@ extension String { } } +extension NSRange { + init(range: Range) { + self.location = range.startIndex + self.length = range.endIndex - range.startIndex + } +} + extension Array { - mutating func insert(newArray: Array, atIndex index: Int) { - let left = self[0.. count ? [] : self[index.. Bool { + return index >= 0 && index < count + } + + func getSafeIndex(index: Int) -> Int { + return min(count, max(0, index)) + } + + func indexOf(item: T) -> Int? { + if item is Element { + return find(unsafeBitCast(self, [T].self), item) + } + return nil + } + + func getSafeRange(range: Range) -> Range? { + let start = max(0, range.startIndex) + let end = min(count, range.endIndex) + return start <= end ? Range(start: start, end: end) : nil } func get(index: Int) -> T? { - return 0 <= index && index < count ? self[index] : nil + return hasIndex(index) ? self[index] : nil + } + + mutating func append(newArray: Array) -> Range { + let range = Range(start: count, end: count + newArray.count) + self += newArray + return range + } + + mutating func insert(newArray: Array, atIndex index: Int) -> Range { + let start = min(count, max(0, index)) + let end = start + newArray.count + + let left = self[0..(start: start, end: end) + } + + mutating func remove(index: Int) -> Range? { + if !hasIndex(index) { + return nil + } + self.removeAtIndex(index) + return Range(start: index, end: index + 1) + } + + mutating func remove(range: Range) -> Range? { + if let sr = getSafeRange(range) { + self.removeRange(sr) + return sr + } + return nil + } + + mutating func removeLast() -> Range? { + return self.remove(count - 1) + } + + func each(exe: (Int, Element) -> ()) { + for (index, item) in enumerate(self) { + exe(index, item) + } } } \ No newline at end of file diff --git a/Source/MYHeaderFooterView.swift b/Source/MYHeaderFooterView.swift index d9365fa..430dc0f 100644 --- a/Source/MYHeaderFooterView.swift +++ b/Source/MYHeaderFooterView.swift @@ -10,13 +10,24 @@ import UIKit public class MYHeaderFooterViewModel : MYViewModel { let identifier: String - var viewHeight: CGFloat = 44 - var isEnabled = true + internal(set) var section: Int = 0 + internal(set) var isHeader = true + public var viewHeight: CGFloat = 44 + public var isEnabled = true public init(viewClass: AnyClass, userData: AnyObject?, selectionHandler: MYSelectionHandler? = nil) { self.identifier = String.className(viewClass) super.init(userData: userData, selectionHandler: selectionHandler) } + + func fire() -> Self { + if isHeader { + delegate?.reloadHeader(section) + } else { + delegate?.reloadFooter(section) + } + return self + } } public class MYHeaderFooterView : UITableViewHeaderFooterView, MYBaseViewProtocol { diff --git a/Source/MYReloadTracker.swift b/Source/MYReloadTracker.swift new file mode 100644 index 0000000..aefc29e --- /dev/null +++ b/Source/MYReloadTracker.swift @@ -0,0 +1,78 @@ +// +// MYReloadTracker.swift +// MYTableViewManager +// +// Created by Le VanNghia on 2/15/15. +// Copyright (c) 2015 Le Van Nghia. All rights reserved. +// + +import Foundation + +enum ReloadState { + case Reset, Add, Remove, Begin +} + +class MYReloadTracker { + var state: ReloadState = .Begin + private var originalIndexes: [Int] = [] + private var removedIndexes: [Int] = [] + + init() { + didFire(0) + } + + func didReset() { + state = .Reset + } + + func didAdd(range: Range) { + if state == .Reset || state == .Remove { + state = .Reset + return + } + let ind = range.map { _ -> Int in -1 } + originalIndexes.insert(ind, atIndex: range.startIndex) + state = .Add + } + + func didRemove(range: Range) { + if state == .Reset || state == .Add { + state = .Reset + return + } + let ri = originalIndexes[range] + originalIndexes.remove(range) + for i in ri { + var newIndexes: [Int] = [] + if removedIndexes.indexOf(i) == nil { + newIndexes.append(i) + } + removedIndexes += newIndexes + } + state = .Remove + } + + func didFire(count: Int) { + state = .Begin + originalIndexes = (0.. [NSIndexPath] { + switch state { + case .Add: + var addedIndexes: [Int] = [] + originalIndexes.each { i, value in + if value == -1 { + addedIndexes.append(i) + } + } + return addedIndexes.map { NSIndexPath(forRow: $0, inSection: section) } + case .Remove: + return removedIndexes.map { NSIndexPath(forRow: $0, inSection: section) } + default: + break + } + return [] + } +} \ No newline at end of file diff --git a/Source/MYSection.swift b/Source/MYSection.swift new file mode 100644 index 0000000..194c9ef --- /dev/null +++ b/Source/MYSection.swift @@ -0,0 +1,170 @@ +// +// MYSection.swift +// MYTableViewManager +// +// Created by Le VanNghia on 2/14/15. +// Copyright (c) 2015 Le Van Nghia. All rights reserved. +// + +import Foundation + +protocol MYSectionDelegate : class { + func reloadTableView() + func reloadSections(indexSet: NSIndexSet, animation: MYAnimation) + func insertRows(indexPaths: [NSIndexPath], animation: MYAnimation) + func deleteRows(indexPaths: [NSIndexPath], animation: MYAnimation) + + func willAddCellViewModels(viewmodels: [MYCellViewModel]) +} + +public class MYSection { + internal(set) var index: Int = 0 { + didSet { + header?.section = index + footer?.section = index + for item in items { + item.section = index + } + } + } + weak var delegate: MYSectionDelegate? + private var items: [MYCellViewModel] = [] + private let reloadTracker = MYReloadTracker() + + public var header: MYHeaderFooterViewModel? { + didSet { + header?.section = index + header?.isHeader = true + } + } + public var footer: MYHeaderFooterViewModel? { + didSet { + footer?.section = index + footer?.isHeader = false + } + } + + public subscript(index: Int) -> MYCellViewModel? { + get { + return items.hasIndex(index) ? items[index] : nil + } + } + + public init() { + } +} + +public extension MYSection { + // MARK - reset + func reset() -> Self { + items = [] + reloadTracker.didReset() + return self + } + + func reset(viewmodel: MYCellViewModel) -> Self { + return reset([viewmodel]) + } + + func reset(viewmodels: [MYCellViewModel]) -> Self { + delegate?.willAddCellViewModels(viewmodels) + items = viewmodels + resetIndex(0, end: self.count-1) + reloadTracker.didReset() + return self + } + + // MARK - apppend + func append(viewmodel: MYCellViewModel) -> Self { + return append([viewmodel]) + } + + func append(viewmodels: [MYCellViewModel]) -> Self { + delegate?.willAddCellViewModels(viewmodels) + let r = items.append(viewmodels) + resetIndex(r.startIndex, end: self.count-1) + reloadTracker.didAdd(r) + return self + } + + // MARK - insert + func insert(viewmodel: MYCellViewModel, atIndex index: Int) -> Self { + return insert([viewmodel], atIndex: index) + } + + func insert(viewmodels: [MYCellViewModel], atIndex index: Int) -> Self { + delegate?.willAddCellViewModels(viewmodels) + let r = items.insert(viewmodels, atIndex: index) + resetIndex(r.startIndex, end: self.count-1) + reloadTracker.didAdd(r) + return self + } + + // MARK - remove + func remove(index: Int) -> Self { + if let r = items.remove(index) { + resetIndex(r.startIndex, end: self.count-1) + reloadTracker.didRemove(r) + } + return self + } + + func removeLast() -> Self { + self.remove(items.count - 1) + return self + } + + func remove(range: Range) -> Self { + if let r = items.remove(range) { + resetIndex(r.startIndex, end: self.count-1) + reloadTracker.didRemove(r) + } + return self + } + + // MARK - fire + func fire(_ animation: MYAnimation = .None) -> Self { + switch reloadTracker.state { + case .Add: + println("ADD") + delegate?.insertRows(reloadTracker.getIndexPaths(index), animation: animation) + case .Remove: + println("REMOVE") + delegate?.deleteRows(reloadTracker.getIndexPaths(index), animation: animation) + default: + println("RESET") + delegate?.reloadSections(NSIndexSet(index: index), animation: animation) + break + } + reloadTracker.didFire(self.count) + return self + } + + private func resetIndex(begin: Int, end: Int) { + if begin > end { + return + } + for i in (begin...end) { + items[i].row = i + items[i].section = index + } + } +} + +public extension MYSection { + var count: Int { + return items.count + } + + var isEmpty: Bool { + return items.count == 0 + } + + var first: MYCellViewModel? { + return items.first + } + + var last: MYCellViewModel? { + return items.last + } +} \ No newline at end of file diff --git a/Source/MYTableViewCell.swift b/Source/MYTableViewCell.swift index 7763f32..8b16535 100644 --- a/Source/MYTableViewCell.swift +++ b/Source/MYTableViewCell.swift @@ -10,10 +10,12 @@ import UIKit public class MYCellViewModel : MYViewModel { let identifier: String - var cellHeight: CGFloat = 44 - var cellSelectionEnabled = true - var calculatedHeight: CGFloat? - var dynamicHeightEnabled: Bool = false { + internal(set) var row: Int = 0 + internal(set) var section: Int = 0 + public var cellHeight: CGFloat = 44 + public var cellSelectionEnabled = true + public var calculatedHeight: CGFloat? + public var dynamicHeightEnabled: Bool = false { didSet { calculatedHeight = nil } @@ -24,6 +26,11 @@ public class MYCellViewModel : MYViewModel { self.cellHeight = height super.init(userData: userData, selectionHandler: selectionHandler) } + + public func fire(_ animation: MYAnimation = .None) -> Self { + delegate?.reloadView(row, section: section, animation: animation) + return self + } } public class MYTableViewCell : UITableViewCell, MYBaseViewProtocol { diff --git a/Source/MYTableViewManager.swift b/Source/MYTableViewManager.swift index c67964b..cbefc7e 100644 --- a/Source/MYTableViewManager.swift +++ b/Source/MYTableViewManager.swift @@ -9,15 +9,6 @@ import UIKit -public enum MYReloadType { - case InsertRows(UITableViewRowAnimation) - case DeleteRows(UITableViewRowAnimation) - case ReloadRows(UITableViewRowAnimation) - case ReloadSection(UITableViewRowAnimation) - case ReloadTableView - case None -} - @objc public protocol MYTableViewManagerDelegate : class { optional func scrollViewDidScroll(scrollView: UIScrollView) optional func scrollViewWillBeginDecelerating(scrollView: UIScrollView) @@ -25,21 +16,22 @@ public enum MYReloadType { } public class MYTableViewManager : NSObject { - typealias MYCellViewModelList = [MYCellViewModel] public weak var delegate: MYTableViewManagerDelegate? + public var loadmoreHandler: (() -> ())? + public var loadmoreEnabled = false + public var loadmoreThreshold: CGFloat = 25 + public var sectionCount: Int { + return sections.count + } private weak var tableView: UITableView? - private var dataSource: [Int: MYCellViewModelList] = [:] - private var headerViewData: [Int: MYHeaderFooterViewModel] = [:] - private var footerViewData: [Int: MYHeaderFooterViewModel] = [:] - private var numberOfSections: Int = 0 + private var sections: [MYSection] = [] + + private let reloadTracker = MYReloadTracker() private var selectedCells = [MYBaseViewProtocol]() private var heightCalculationCells: [String: MYTableViewCell] = [:] private var currentTopSection = 0 private var willFloatingSection = -1 - public var loadmoreHandler: (() -> ())? - public var loadmoreEnabled = false - public var loadmoreThreshold: CGFloat = 25 public init(tableView: UITableView) { super.init() @@ -55,305 +47,109 @@ public class MYTableViewManager : NSObject { selectedCells.removeAll(keepCapacity: false) } - public func resetAllData() { - dataSource = [:] - headerViewData = [:] - footerViewData = [:] - numberOfSections = 0 + public func resetAll() -> Self { + sections = [] selectedCells = [] heightCalculationCells = [:] currentTopSection = 0 willFloatingSection = -1 - } -} - -// MARK - register cell and header/footer -public extension MYTableViewManager { - func registerCellClass(cellClass: AnyClass) { - let identifier = String.className(cellClass) - tableView?.registerClass(cellClass, forCellReuseIdentifier: identifier) + return self } - func registerCellNib(cellClass: AnyClass) { - let identifier = String.className(cellClass) - let nib = UINib(nibName: identifier, bundle: nil) - tableView?.registerNib(nib, forCellReuseIdentifier: identifier) - } - - func registerHeaderFooterViewClass(viewClass: AnyClass) { - let identifier = String.className(viewClass) - tableView?.registerClass(viewClass, forHeaderFooterViewReuseIdentifier: identifier) - } - - func registerHeaderFooterViewNib(viewClass: AnyClass) { - let identifier = String.className(viewClass) - let nib = UINib(nibName: identifier, bundle: nil) - tableView?.registerNib(nib, forHeaderFooterViewReuseIdentifier: identifier) - } -} - -// MARK - Append -public extension MYTableViewManager { - func appendData(data: MYCellViewModel, inSection section: Int, reloadType: MYReloadType = .InsertRows(.None)) { - let cellData = [data] - self.appendData(cellData, inSection: section, reloadType: reloadType) - } - - func appendData(data: [MYCellViewModel], inSection section: Int, reloadType: MYReloadType = .InsertRows(.None)) { - if self.dataSource.indexForKey(section) != nil { - self.setBaseViewDataDelegate(data) - self.dataSource[section]! += data - - switch reloadType { - case .InsertRows(let animation): - let startRowIndex = self.dataSource[section]!.count - data.count - let endRowIndex = startRowIndex + data.count - let indexPaths = (startRowIndex.. NSIndexPath in - return NSIndexPath(forRow: index, inSection: section) - } - self.tableView?.insertRowsAtIndexPaths(indexPaths, withRowAnimation: animation) - - case .ReloadSection(let animation): - let indexSet = NSIndexSet(index: section) - self.tableView?.reloadSections(indexSet, withRowAnimation: animation) - - case .None: - break - - default: - self.tableView?.reloadData() + public subscript(index: Int) -> MYSection { + get { + if let s = sections.get(index) { + return s } - return - } - self.resetWithData(data, inSection: section, reloadType: reloadType) - } -} - -// MARK - Reset -public extension MYTableViewManager { - func resetWithData(data: MYCellViewModel, inSection section: Int, reloadType: MYReloadType = .ReloadSection(.None)) { - resetWithData([data], inSection: section, reloadType: reloadType) - } - - func resetWithData(data: [MYCellViewModel], inSection section: Int, reloadType: MYReloadType = .ReloadSection(.None)) { - self.setBaseViewDataDelegate(data) - - let length = section + 1 - self.numberOfSections - let insertSections: NSIndexSet? = length > 0 ? NSIndexSet(indexesInRange: NSMakeRange(self.numberOfSections, length)) : nil - self.numberOfSections = max(self.numberOfSections, section + 1) - self.dataSource[section] = data - - switch reloadType { - case .ReloadSection(let animation): - if insertSections != nil { - self.tableView?.insertSections(insertSections!, withRowAnimation: animation) - } else { - let indexSet = NSIndexSet(index: section) - self.tableView?.reloadSections(indexSet, withRowAnimation: animation) - } - - case .None: - break - - default: - self.tableView?.reloadData() - } - } -} - -// MARK - Insert -public extension MYTableViewManager { - func insertData(data: MYCellViewModel, inSection section: Int, atRow row: Int, reloadType: MYReloadType = .InsertRows(.None)) { - self.insertData([data], inSection: section, atRow: row) - } - - func insertData(data: [MYCellViewModel], inSection section: Int, atRow row: Int, reloadType: MYReloadType = .InsertRows(.None)) { - self.setBaseViewDataDelegate(data) - - if self.dataSource[section] == nil { - var rt: MYReloadType = .None - switch reloadType { - case .None: - rt = .None - default: - rt = .ReloadSection(.None) - } - return self.resetWithData(data, inSection: section, reloadType: rt) - } - if row < 0 || row > self.dataSource[section]!.count { - return - } - self.dataSource[section]?.insert(data, atIndex: row) + let length = index + 1 - sectionCount + let insertSet: NSIndexSet = NSIndexSet(indexesInRange: NSMakeRange(sectionCount, length)) - switch reloadType { - case .InsertRows(let animation): - let startRowIndex = row - let endRowIndex = startRowIndex + data.count - let indexPaths = (startRowIndex.. NSIndexPath in - return NSIndexPath(forRow: index, inSection: section) + let newSections = (sectionCount...index).map { i -> MYSection in + let ns = MYSection() + ns.delegate = self + ns.index = i + return ns } - self.tableView?.insertRowsAtIndexPaths(indexPaths, withRowAnimation: animation) - - case .ReloadSection(let animation): - let indexSet = NSIndexSet(index: section) - self.tableView?.reloadSections(indexSet, withRowAnimation: animation) - - case .None: - break - - default: - self.tableView?.reloadData() + sections += newSections + tableView?.insertSections(insertSet, withRowAnimation: .None) + return sections[index] } } - - func insertDataBeforeLastRow(data: [MYCellViewModel], inSection section: Int, reloadType: MYReloadType = .InsertRows(.None)) { - let lastRow = max((self.dataSource[section]?.count ?? 0) - 1, 0) - self.insertData(data, inSection: section, atRow: lastRow, reloadType: reloadType) - } } -// MARK - Remove public extension MYTableViewManager { - func removeDataInSection(section: Int, atRow row: Int, reloadType: MYReloadType = .DeleteRows(.None)) { - removeDataInSection(section, inRange: (row...row), reloadType: reloadType) + func insertSection(section: MYSection, atIndex index: Int) -> Self { + // TODO : implementation + return self } - - func removeLastDataInSection(section: Int, reloadType: MYReloadType = .DeleteRows(.None)) { - let lastIndex = (dataSource[section]?.count ?? 0) - 1 - removeDataInSection(section, atRow: lastIndex, reloadType: reloadType) - } - - func removeDataInSection(section: Int, inRange range: Range, reloadType: MYReloadType = .DeleteRows(.None)) { - if self.dataSource[section] != nil { - let start = max(0, range.startIndex) - let end = min(self.dataSource[section]!.count, range.endIndex) - let safeRange = Range(start: start, end: end) - self.dataSource[section]!.removeRange(safeRange) - switch reloadType { - case .DeleteRows(let animation): - let indexPaths = safeRange.map { NSIndexPath(forRow: $0, inSection: section) } - self.tableView?.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: animation) - - case .ReloadSection(let animation): - let indexSet = NSIndexSet(index: section) - self.tableView?.reloadSections(indexSet, withRowAnimation: animation) - - case .None: - break - - default: - self.tableView?.reloadData() - } - } + func removeSectionAtIndex(index: Int) -> Self { + // TODO : implementation + return self } -} - -// MARK - Update user info -public extension MYTableViewManager { - func updateUserData(userData: AnyObject?, inSection section: Int, atRow row: Int, reloadType: MYReloadType = .ReloadRows(.None)) { - if self.dataSource[section] != nil { - if let data = self.dataSource[section]?.get(row) { - data.userData = userData - data.calculatedHeight = nil - - switch reloadType { - case .ReloadRows(let animation): - let indexPath = NSIndexPath(forRow: row, inSection: section) - self.tableView?.reloadRowsAtIndexPaths([indexPath], withRowAnimation: animation) - - case .ReloadSection(let animation): - let indexSet = NSIndexSet(index: section) - self.tableView?.reloadSections(indexSet, withRowAnimation: animation) - - case .None: - break - - default: - self.tableView?.reloadData() - } - } - } + + func fire(_ animation: MYAnimation = .None) -> Self { + // TODO : implementation + tableView?.reloadData() + return self } } -// MARK - tableView methods -public extension MYTableViewManager { +// MARK - MYSectionDelegate +extension MYTableViewManager : MYSectionDelegate { func reloadTableView() { tableView?.reloadData() } - - func reloadSection(section: Int, animation: UITableViewRowAnimation) { - tableView?.reloadSections(NSIndexSet(index: section), withRowAnimation: animation) - } - - func numberRowsInSection(section: Int) -> Int { - return dataSource[section]?.count ?? 0 - } - - func cellForRowAtSection(section: Int, row: Int) -> MYTableViewCell? { - let indexPath = NSIndexPath(forRow: row, inSection: section) - return tableView?.cellForRowAtIndexPath(indexPath) as? MYTableViewCell - } -} -// MARK - header/footer -public extension MYTableViewManager { - func setHeaderData(data: MYHeaderFooterViewModel, inSection section: Int) { - headerViewData[section] = data + func reloadSections(indexSet: NSIndexSet, animation: MYAnimation) { + tableView?.reloadSections(indexSet, withRowAnimation: animation) } - func setFooterData(data: MYHeaderFooterViewModel, inSection section: Int) { - footerViewData[section] = data + func insertRows(indexPaths: [NSIndexPath], animation: MYAnimation) { + tableView?.insertRowsAtIndexPaths(indexPaths, withRowAnimation: animation) } - func setHeaderViewInSection(section: Int, hidden: Bool) { - if let data = headerViewData[section] { - data.isEnabled = !hidden - } + func deleteRows(indexPaths: [NSIndexPath], animation: MYAnimation) { + tableView?.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: animation) } - func setFooterViewInSection(section: Int, hidden: Bool) { - if let data = footerViewData[section] { - data.isEnabled = !hidden - } + func willAddCellViewModels(viewmodels: [MYCellViewModel]) { + setBaseViewDataDelegate(viewmodels) } } -// MARK - private methods -private extension MYTableViewManager { - func addSelectedView(view: MYBaseViewProtocol) { - deselectAllCells() - selectedCells = [view] +// MARK - MYBaseViewDataDelegate +extension MYTableViewManager : MYBaseViewDataDelegate { + public func didCallSelectionHandler(view: MYBaseViewProtocol) { + addSelectedView(view) + } + + public func reloadView(index: Int, section: Int, animation: MYAnimation) { + let indexPath = NSIndexPath(forRow: index, inSection: section) + tableView?.reloadRowsAtIndexPaths([indexPath], withRowAnimation: animation) } - func setBaseViewDataDelegate(dataList: [MYViewModel]) { - for data in dataList { - data.delegate = self + public func reloadHeader(section: Int) { + if let headerView = tableView?.headerViewForSection(section) as? MYHeaderFooterView { + if let viewmodel = sections[section].header { + headerView.configureView(viewmodel) + } } } - func calculateHeightForConfiguredSizingCell(cell: MYTableViewCell) -> CGFloat { - cell.bounds = CGRectMake(0, 0, tableView?.bounds.width ?? UIScreen.mainScreen().bounds.width, cell.bounds.height) - cell.setNeedsLayout() - cell.layoutIfNeeded() - - let size = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) - return size.height + 1.0 - } -} - -// MARK - MYBaseViewDataDelegate -extension MYTableViewManager : MYBaseViewDataDelegate { - public func didSelectView(view: MYBaseViewProtocol) { - addSelectedView(view) + public func reloadFooter(section: Int) { + if let footerView = tableView?.footerViewForSection(section) as? MYHeaderFooterView { + if let viewmodel = sections[section].footer { + footerView.configureView(viewmodel) + } + } } } // MARK - UITableViewDelegate extension MYTableViewManager : UITableViewDelegate { public func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { - if let cellData = dataSource[indexPath.section]?.get(indexPath.row) { + if let cellData = self.cellViewModelAtIndexPath(indexPath) { if !cellData.dynamicHeightEnabled { return cellData.cellHeight } @@ -373,52 +169,52 @@ extension MYTableViewManager : UITableViewDelegate { } public func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { - if let cellData = dataSource[indexPath.section]?.get(indexPath.row) { + if let cellData = self.cellViewModelAtIndexPath(indexPath) { return cellData.cellHeight } return 0 } public func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if let data = headerViewData[section] { - return data.isEnabled ? data.viewHeight : 0 + if let header = self.sections.get(section)?.header { + return header.isEnabled ? header.viewHeight : 0 } return 0 } public func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if let data = headerViewData[section] { - if !data.isEnabled { + if let header = self.sections.get(section)?.header { + if !header.isEnabled { return nil } - let headerView = tableView.dequeueReusableHeaderFooterViewWithIdentifier(data.identifier) as MYHeaderFooterView - headerView.configureView(data) + let headerView = tableView.dequeueReusableHeaderFooterViewWithIdentifier(header.identifier) as MYHeaderFooterView + headerView.configureView(header) return headerView } return nil } public func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - if let data = footerViewData[section] { - return data.isEnabled ? data.viewHeight : 0 + if let footer = self.sections.get(section)?.footer { + return footer.isEnabled ? footer.viewHeight : 0 } return 0 } public func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - if let data = footerViewData[section] { - if !data.isEnabled { + if let footer = self.sections.get(section)?.footer { + if !footer.isEnabled { return nil } - let footerView = tableView.dequeueReusableHeaderFooterViewWithIdentifier(data.identifier) as MYHeaderFooterView - footerView.configureView(data) + let footerView = tableView.dequeueReusableHeaderFooterViewWithIdentifier(footer.identifier) as MYHeaderFooterView + footerView.configureView(footer) return footerView } return nil } public func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { - if let cellData = dataSource[indexPath.section]?.get(indexPath.row) { + if let cellData = self.cellViewModelAtIndexPath(indexPath) { if let myCell = cell as? MYTableViewCell { myCell.willAppear(cellData) } @@ -426,7 +222,7 @@ extension MYTableViewManager : UITableViewDelegate { } public func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { - if let cellData = dataSource[indexPath.section]?.get(indexPath.row) { + if let cellData = self.cellViewModelAtIndexPath(indexPath) { if let myCell = cell as? MYTableViewCell { myCell.didDisappear(cellData) } @@ -434,25 +230,52 @@ extension MYTableViewManager : UITableViewDelegate { } } -// MARK - MYTableViewManager +// MARK - UITableViewDataSource extension MYTableViewManager : UITableViewDataSource { public func numberOfSectionsInTableView(tableView: UITableView) -> Int { - return numberOfSections + return sectionCount } public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return dataSource[section]?.count ?? 0 + return self.sections.get(section)?.count ?? 0 } public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - let cellData = dataSource[indexPath.section]![indexPath.row] - let cell = tableView.dequeueReusableCellWithIdentifier(cellData.identifier, forIndexPath: indexPath) as MYTableViewCell - cell.configureCell(cellData) - return cell + if let cellData = self.cellViewModelAtIndexPath(indexPath) { + let cell = tableView.dequeueReusableCellWithIdentifier(cellData.identifier, forIndexPath: indexPath) as MYTableViewCell + cell.configureCell(cellData) + return cell + } + return UITableViewCell() + } +} + +// MARK - register cell and header/footer view +public extension MYTableViewManager { + func registerCellClass(cellClass: AnyClass) { + let identifier = String.className(cellClass) + tableView?.registerClass(cellClass, forCellReuseIdentifier: identifier) + } + + func registerCellNib(cellClass: AnyClass) { + let identifier = String.className(cellClass) + let nib = UINib(nibName: identifier, bundle: nil) + tableView?.registerNib(nib, forCellReuseIdentifier: identifier) + } + + func registerHeaderFooterViewClass(viewClass: AnyClass) { + let identifier = String.className(viewClass) + tableView?.registerClass(viewClass, forHeaderFooterViewReuseIdentifier: identifier) + } + + func registerHeaderFooterViewNib(viewClass: AnyClass) { + let identifier = String.className(viewClass) + let nib = UINib(nibName: identifier, bundle: nil) + tableView?.registerNib(nib, forHeaderFooterViewReuseIdentifier: identifier) } } -// MARK - loadmore +// MARK - UIScrollViewDelegate extension MYTableViewManager { public func scrollViewDidScroll(scrollView: UIScrollView) { delegate?.scrollViewDidScroll?(scrollView) @@ -493,10 +316,7 @@ extension MYTableViewManager { } } } -} -// MARK - UIScrollViewDelegate -extension MYTableViewManager { public func scrollViewWillBeginDecelerating(scrollView: UIScrollView) { delegate?.scrollViewWillBeginDecelerating?(scrollView) } @@ -504,4 +324,31 @@ extension MYTableViewManager { public func scrollViewWillBeginDragging(scrollView: UIScrollView) { delegate?.scrollViewWillBeginDragging?(scrollView) } +} + +// MARK - private methods +private extension MYTableViewManager { + func cellViewModelAtIndexPath(indexPath: NSIndexPath) -> MYCellViewModel? { + return self.sections.get(indexPath.section)?[indexPath.row] + } + + func addSelectedView(view: MYBaseViewProtocol) { + deselectAllCells() + selectedCells = [view] + } + + func setBaseViewDataDelegate(dataList: [MYViewModel]) { + for data in dataList { + data.delegate = self + } + } + + func calculateHeightForConfiguredSizingCell(cell: MYTableViewCell) -> CGFloat { + cell.bounds = CGRectMake(0, 0, tableView?.bounds.width ?? UIScreen.mainScreen().bounds.width, cell.bounds.height) + cell.setNeedsLayout() + cell.layoutIfNeeded() + + let size = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) + return size.height + 1.0 + } } \ No newline at end of file diff --git a/Source/MYBaseClasses.swift b/Source/MYViewModel.swift similarity index 55% rename from Source/MYBaseClasses.swift rename to Source/MYViewModel.swift index 5689a67..2ff678d 100644 --- a/Source/MYBaseClasses.swift +++ b/Source/MYViewModel.swift @@ -1,27 +1,18 @@ // -// BaseClasses.swift +// MYViewModel.swift // MYTableViewManager // -// Created by Le Van Nghia on 1/13/15. +// Created by Le VanNghia on 2/14/15. // Copyright (c) 2015 Le Van Nghia. All rights reserved. // -import UIKit - -public typealias MYSelectionHandler = (MYBaseViewProtocol) -> () - -public protocol MYBaseViewProtocol { - func highlight(Bool) - func unhighlight(Bool) - func emitSelectedEvent(MYBaseViewProtocol) -} +import Foundation public protocol MYBaseViewDataDelegate : class { - func didSelectView(view: MYBaseViewProtocol) -} - -public protocol MYBaseViewDelegate : class { - func didSelect(view: MYBaseViewProtocol) + func didCallSelectionHandler(view: MYBaseViewProtocol) + func reloadView(index: Int, section: Int, animation: MYAnimation) + func reloadHeader(section: Int) + func reloadFooter(section: Int) } public class MYViewModel : NSObject, MYBaseViewDelegate { @@ -36,6 +27,6 @@ public class MYViewModel : NSObject, MYBaseViewDelegate { public func didSelect(view: MYBaseViewProtocol) { action?(view) - delegate?.didSelectView(view) + delegate?.didCallSelectionHandler(view) } } \ No newline at end of file