diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index 629d7f1afcec..c3ca7e875e35 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -11,6 +11,8 @@ #import "DDLogSwift.h" +#import "MediaService.h" + #import "Notification.h" #import "Notification+Internals.h" #import "NotificationsManager.h" diff --git a/WordPress/Classes/Utility/ImmuTable.swift b/WordPress/Classes/Utility/ImmuTable.swift new file mode 100644 index 000000000000..a685d77d194d --- /dev/null +++ b/WordPress/Classes/Utility/ImmuTable.swift @@ -0,0 +1,323 @@ +import Foundation +import UIKit + +/** + ImmuTable represents the view model for a static UITableView. + + ImmuTable consists of zero or more sections, each one containing zero or more rows, + and an optional header and footer text. + + Each row contains the model necessary to configure a specific type of UITableViewCell. + + To use ImmuTable, first you need to create some custom rows. An example row for a cell + that acts as a button which performs a destructive action could look like this: + + struct DestructiveButtonRow: ImmuTableRow { + static let cell = ImmuTableCell.Class(UITableViewCell.self) + let title: String + let action: ImmuTableActionType? + + func configureCell(cell: UITableViewCell) { + cell.textLabel?.text = title + cell.textLabel?.textAlignment = .Center + cell.textLabel?.textColor = UIColor.redColor() + } + } + + The easiest way to use ImmuTable is through ImmuTableViewHandler, which takes a + UITableViewController as an argument, and acts as the table view delegate and data + source. You would then assign an ImmuTable object to the handler's `viewModel` + property. + + - attention: before using any ImmuTableRow type, you need to call `registerRows(_:tableView:)` + passing the row type. This is needed so ImmuTable can register the class or nib with the table view. + If you fail to do this, UIKit will raise an exception when it tries to load the row. + */ +public struct ImmuTable { + /// An array of the sections to be represented in the table view + public let sections: [ImmuTableSection] + + /// Initializes an ImmuTable object with the given sections + public init(sections: [ImmuTableSection]) { + self.sections = sections + } + + /** + Returns the row model for a specific index path. + + - precondition: `indexPath` should represent a valid section and row, + otherwise this method will raise an exception. + */ + public func rowAtIndexPath(indexPath: NSIndexPath) -> ImmuTableRow { + return sections[indexPath.section].rows[indexPath.row] + } + + /** + Registers the row custom class or nib with the table view so it can later be + dequeued with `dequeueReusableCellWithIdentifier(_:forIndexPath:)` + */ + public static func registerRows(rows: [ImmuTableRow.Type], tableView: UITableView) { + registerRows(rows, registrator: tableView) + } + + /// This function exists for testing purposes + /// - seealso: registerRows(_:tableView:) + internal static func registerRows(rows: [ImmuTableRow.Type], registrator: CellRegistrator) { + let registrables = rows.reduce([:]) { + (var classes, row) -> [String: ImmuTableCell] in + + classes[row.cell.reusableIdentifier] = row.cell + return classes + } + for (identifier, registrable) in registrables { + registrator.register(registrable, cellReuseIdentifier: identifier) + } + } +} + + +// MARK: - + + +/** +ImmuTableSection represents the view model for a table view section. + +A section has an optional header and footer text, and zero or more rows. + +- seealso: ImmuTableRow +*/ +public struct ImmuTableSection { + let headerText: String? + let rows: [ImmuTableRow] + let footerText: String? + + /// Initializes a ImmuTableSection with the given rows and no header or footer text + public init(rows: [ImmuTableRow]) { + self.headerText = nil + self.rows = rows + self.footerText = nil + } + + /// Initializes a ImmuTableSection with the given rows and optionally header and footer text + public init(headerText: String?, rows: [ImmuTableRow], footerText: String?) { + self.headerText = headerText + self.rows = rows + self.footerText = footerText + } +} + + +// MARK: - ImmuTableRow + + +/** +ImmuTableRow represents the minimum common elements of a row model. + +You should implement your own types that conform to ImmuTableRow to define your custom rows. +*/ +public protocol ImmuTableRow { + + /** + The closure to call when the row is tapped. The row is passed as an argument to the closure. + + To improve readability, we recommend that you implement the action logic in one of + your view controller methods, instead of including the closure inline. + + Also, be mindful of retain cycles. If your closure needs to reference `self` in + any way, make sure to use `[unowned self]` in the parameter list. + + An example row with its action could look like this: + + class ViewController: UITableViewController { + + func buildViewModel() { + let item1Row = NavigationItemRow(title: "Item 1", action: navigationAction()) + ... + } + + func navigationAction() -> ImmuTableRow -> Void { + return { [unowned self] row in + let controller = self.controllerForRow(row) + self.navigationController?.pushViewController(controller, animated: true) + } + } + + ... + + } + + */ + var action: ImmuTableActionType? { get } + + /// This method is called when an associated cell needs to be configured. + /// - precondition: You can assume that the passed cell is of the type defined + /// by cell.cellClass and force downcast accordingly. + func configureCell(cell: UITableViewCell) + + /// An ImmuTableCell value defining the associated cell type. + /// - seealso: See ImmuTableCell for possible options. + static var cell: ImmuTableCell { get } + + /// The desired row height (Optional) + /// + /// If not defined or nil, the default height will be used. + static var customHeight: Float? { get } +} + +extension ImmuTableRow { + public var reusableIdentifier: String { + return self.dynamicType.cell.reusableIdentifier + } + + public var cellClass: UITableViewCell.Type { + return self.dynamicType.cell.cellClass + } + + public static var customHeight: Float? { + return nil; + } +} + + +// MARK: - ImmuTableCell + + +/** +ImmuTableCell describes cell types so they can be registered with a table view. + +It supports two options: + - Nib for Interface Builder defined cells. + - Class for cells defined in code. +Both cases presume a custom UITableViewCell subclass. If you aren't subclassing, +you can also use UITableViewCell as the type. + +- note: If you need to use any cell style other than .Default we recommend you + subclass UITableViewCell and override init(style:reuseIdentifier:). +*/ +public enum ImmuTableCell { + + /// A cell using a UINib. Values are the UINib object and the custom cell class. + case Nib(UINib, UITableViewCell.Type) + + /// A cell using a custom class. The associated value is the custom cell class. + case Class(UITableViewCell.Type) + + /// A String that uniquely identifies the cell type + public var reusableIdentifier: String { + switch self { + case .Class(let cellClass): + return NSStringFromClass(cellClass) + case .Nib(_, let cellClass): + return NSStringFromClass(cellClass) + } + } + + /// The class of the custom cell + public var cellClass: UITableViewCell.Type { + switch self { + case .Class(let cellClass): + return cellClass + case .Nib(_, let cellClass): + return cellClass + } + } +} + + +// MARK: - + + +/** +ImmuTableViewHandler is a helper to facilitate integration of ImmuTable in your +table view controllers. + +It acts as the table view data source and delegate, and signals the table view to +reload its data when the underlying model changes. + +- note: as it keeps a weak reference to its target, you should keep a strong + reference to the handler from your view controller. +*/ +public class ImmuTableViewHandler: NSObject, UITableViewDataSource, UITableViewDelegate { + unowned let target: UITableViewController + + /// Initializes the handler with a target table view controller. + /// - postcondition: After initialization, it becomse the data source and + /// delegate for the the target's table view. + public init(takeOver target: UITableViewController) { + self.target = target + super.init() + + self.target.tableView.dataSource = self + self.target.tableView.delegate = self + } + + /// An ImmuTable object representing the table structure. + public var viewModel = ImmuTable(sections: []) { + didSet { + if target.isViewLoaded() { + target.tableView.reloadData() + } + } + } + + // MARK: Table View Data Source + + public func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return viewModel.sections.count + } + + public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.sections[section].rows.count + } + + public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let row = viewModel.rowAtIndexPath(indexPath) + let cell = tableView.dequeueReusableCellWithIdentifier(row.reusableIdentifier, forIndexPath: indexPath) + + row.configureCell(cell) + + return cell + } + + // MARK: Table View Delegate + + public func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let row = viewModel.rowAtIndexPath(indexPath) + row.action?(row) + } + + public func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + let row = viewModel.rowAtIndexPath(indexPath) + if let customHeight = row.dynamicType.customHeight { + return CGFloat(customHeight) + } + return tableView.rowHeight + } +} + + +// MARK: - Type aliases + + +public typealias ImmuTableActionType = (ImmuTableRow) -> Void + + +// MARK: - Internal testing helpers + + +protocol CellRegistrator { + func register(cell: ImmuTableCell, cellReuseIdentifier: String) +} + + +extension UITableView: CellRegistrator { + public func register(cell: ImmuTableCell, cellReuseIdentifier: String) { + switch cell { + case .Nib(let nib, _): + registerNib(nib, forCellReuseIdentifier: cell.reusableIdentifier) + case .Class(let cellClass): + registerClass(cellClass, forCellReuseIdentifier: cell.reusableIdentifier) + } + } +} + diff --git a/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift b/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift index d5847421a0f5..591145bffb1c 100644 --- a/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift +++ b/WordPress/Classes/ViewRelated/Cells/MediaSizeSliderCell.swift @@ -17,7 +17,7 @@ class MediaSizeSliderCell: WPTableViewCell { } } - static let height = 108.0 + static let height: Float = 108.0 // MARK: - Public interface var value: Int { diff --git a/WordPress/Classes/ViewRelated/Cells/WPImmuTableCells.swift b/WordPress/Classes/ViewRelated/Cells/WPImmuTableCells.swift new file mode 100644 index 000000000000..b64b5d72b25a --- /dev/null +++ b/WordPress/Classes/ViewRelated/Cells/WPImmuTableCells.swift @@ -0,0 +1,174 @@ +import Foundation +import UIKit +import WordPressShared.WPTableViewCell + +class WPReusableTableViewCell: WPTableViewCell { + override func prepareForReuse() { + super.prepareForReuse() + + textLabel?.text = nil + detailTextLabel?.text = nil + imageView?.image = nil + accessoryType = .None + selectionStyle = .Default + } +} + +class WPTableViewCellDefault: WPReusableTableViewCell { + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .Default, reuseIdentifier: reuseIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} + +class WPTableViewCellSubtitle: WPReusableTableViewCell { + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} + +class WPTableViewCellValue1: WPReusableTableViewCell { + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .Value1, reuseIdentifier: reuseIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} + +class WPTableViewCellValue2: WPReusableTableViewCell { + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .Value2, reuseIdentifier: reuseIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} + +struct NavigationItemRow : ImmuTableRow { + static let cell = ImmuTableCell.Class(WPTableViewCellDefault) + + let title: String + let action: ImmuTableActionType? + + func configureCell(cell: UITableViewCell) { + cell.textLabel?.text = title + cell.accessoryType = .DisclosureIndicator + + WPStyleGuide.configureTableViewCell(cell) + } +} + +struct EditableTextRow : ImmuTableRow { + static let cell = ImmuTableCell.Class(WPTableViewCellValue1) + + let title: String + let value: String + let action: ImmuTableActionType? + + func configureCell(cell: UITableViewCell) { + cell.textLabel?.text = title + cell.detailTextLabel?.text = value + cell.accessoryType = .DisclosureIndicator + + WPStyleGuide.configureTableViewCell(cell) + } +} + +struct TextRow : ImmuTableRow { + static let cell = ImmuTableCell.Class(WPTableViewCellValue1) + + let title: String + let value: String + let action: ImmuTableActionType? = nil + + func configureCell(cell: UITableViewCell) { + cell.textLabel?.text = title + cell.detailTextLabel?.text = value + cell.selectionStyle = .None + + WPStyleGuide.configureTableViewCell(cell) + } +} + +struct LinkRow : ImmuTableRow { + static let cell = ImmuTableCell.Class(WPTableViewCellValue1) + + let title: String + let action: ImmuTableActionType? + + func configureCell(cell: UITableViewCell) { + cell.textLabel?.text = title + + WPStyleGuide.configureTableViewActionCell(cell) + } +} + +struct LinkWithValueRow : ImmuTableRow { + static let cell = ImmuTableCell.Class(WPTableViewCellValue1) + + let title: String + let value: String + let action: ImmuTableActionType? + + func configureCell(cell: UITableViewCell) { + cell.textLabel?.text = title + cell.detailTextLabel?.text = value + + WPStyleGuide.configureTableViewActionCell(cell) + } +} + +struct SwitchRow: ImmuTableRow { + static let cell = ImmuTableCell.Class(SwitchTableViewCell) + + let title: String + let value: Bool + let action: ImmuTableActionType? = nil + let onChange: Bool -> Void + + func configureCell(cell: UITableViewCell) { + let cell = cell as! SwitchTableViewCell + + cell.textLabel?.text = title + cell.selectionStyle = .None + cell.on = value + cell.onChange = onChange + } +} + +struct MediaSizeRow: ImmuTableRow { + typealias CellType = MediaSizeSliderCell + + static let cell: ImmuTableCell = { + let nib = UINib(nibName: "MediaSizeSliderCell", bundle: NSBundle(forClass: CellType.self)) + return ImmuTableCell.Nib(nib, CellType.self) + }() + static let customHeight: Float? = CellType.height + + let title: String + let value: Int + let onChange: Int -> Void + + let action: ImmuTableActionType? = nil + + func configureCell(cell: UITableViewCell) { + let cell = cell as! CellType + + cell.title = title + cell.value = value + cell.onChange = onChange + + cell.minValue = MediaMinImageSizeDimension + cell.maxValue = MediaMaxImageSizeDimension + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 3bc1b54a5251..df2836d7a221 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -569,6 +569,10 @@ E1E4CE0B1773C59B00430844 /* WPAvatarSource.m in Sources */ = {isa = PBXBuildFile; fileRef = E1E4CE0A1773C59B00430844 /* WPAvatarSource.m */; }; E1E4CE0D177439D100430844 /* WPAvatarSourceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E1E4CE0C177439D100430844 /* WPAvatarSourceTest.m */; }; E1E4CE0F1774563F00430844 /* misteryman.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E1E4CE0E1774531500430844 /* misteryman.jpg */; }; + E1EBC36F1C118EA500F638E0 /* ImmuTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBC36E1C118EA500F638E0 /* ImmuTable.swift */; }; + E1EBC3711C118EB200F638E0 /* WPImmuTableCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBC3701C118EB200F638E0 /* WPImmuTableCells.swift */; }; + E1EBC3731C118ED200F638E0 /* ImmuTableTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBC3721C118ED200F638E0 /* ImmuTableTest.swift */; }; + E1EBC3751C118EDE00F638E0 /* ImmuTableTestViewCellWithNib.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1EBC3741C118EDE00F638E0 /* ImmuTableTestViewCellWithNib.xib */; }; E1F5A1BC1771C90A00E0495F /* WPTableImageSource.m in Sources */ = {isa = PBXBuildFile; fileRef = E1F5A1BB1771C90A00E0495F /* WPTableImageSource.m */; }; E1F80825146420B000726BC7 /* UIImageView+Gravatar.m in Sources */ = {isa = PBXBuildFile; fileRef = E1F80824146420B000726BC7 /* UIImageView+Gravatar.m */; }; E1F8E1231B0B411E0073E628 /* JetpackService.m in Sources */ = {isa = PBXBuildFile; fileRef = E1F8E1221B0B411E0073E628 /* JetpackService.m */; }; @@ -1664,6 +1668,10 @@ E1E4CE0C177439D100430844 /* WPAvatarSourceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WPAvatarSourceTest.m; sourceTree = ""; }; E1E4CE0E1774531500430844 /* misteryman.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = misteryman.jpg; sourceTree = ""; }; E1E977BC17B0FA9A00AFB867 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + E1EBC36E1C118EA500F638E0 /* ImmuTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImmuTable.swift; sourceTree = ""; }; + E1EBC3701C118EB200F638E0 /* WPImmuTableCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WPImmuTableCells.swift; sourceTree = ""; }; + E1EBC3721C118ED200F638E0 /* ImmuTableTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImmuTableTest.swift; sourceTree = ""; }; + E1EBC3741C118EDE00F638E0 /* ImmuTableTestViewCellWithNib.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ImmuTableTestViewCellWithNib.xib; sourceTree = ""; }; E1F5A1BA1771C90A00E0495F /* WPTableImageSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WPTableImageSource.h; sourceTree = ""; }; E1F5A1BB1771C90A00E0495F /* WPTableImageSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WPTableImageSource.m; sourceTree = ""; }; E1F80823146420B000726BC7 /* UIImageView+Gravatar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImageView+Gravatar.h"; sourceTree = ""; }; @@ -2555,6 +2563,7 @@ 5D839AA7187F0D6B00811F4A /* PostFeaturedImageCell.m */, 5D839AA9187F0D8000811F4A /* PostGeolocationCell.h */, 5D839AAA187F0D8000811F4A /* PostGeolocationCell.m */, + E1EBC3701C118EB200F638E0 /* WPImmuTableCells.swift */, FF0AAE081A1509C50089841D /* WPProgressTableViewCell.h */, FF0AAE091A150A560089841D /* WPProgressTableViewCell.m */, ); @@ -2661,6 +2670,7 @@ 852416D11A12ED690030700C /* AppRatingUtilityTests.m */, 5DA988051AEEA594002AFB12 /* DisplayableImageHelperTest.m */, E1266D2E1BBEC37B00FCB6B6 /* GravatarTest.swift */, + E1EBC3721C118ED200F638E0 /* ImmuTableTest.swift */, 93A379EB19FFBF7900415023 /* KeychainTest.m */, 5948AD101AB73D19006E8882 /* WPAppAnalyticsTests.m */, E1E4CE0C177439D100430844 /* WPAvatarSourceTest.m */, @@ -2723,6 +2733,7 @@ 313692771A5D6F7900EBE645 /* HelpshiftUtils.h */, E1266D2C1BBE8B9A00FCB6B6 /* Gravatar.swift */, 313692781A5D6F7900EBE645 /* HelpshiftUtils.m */, + E1EBC36E1C118EA500F638E0 /* ImmuTable.swift */, 5DB4683918A2E718004A89A9 /* LocationService.h */, 5DB4683A18A2E718004A89A9 /* LocationService.m */, 5D3E334C15EEBB6B005FC6F2 /* ReachabilityUtils.h */, @@ -3670,6 +3681,7 @@ E16AB94414D9A13A0047A2E5 /* Mock Data */ = { isa = PBXGroup; children = ( + E1EBC3741C118EDE00F638E0 /* ImmuTableTestViewCellWithNib.xib */, B5AEEC741ACACFDA008BF2A4 /* notifications-badge.json */, B5AEEC751ACACFDA008BF2A4 /* notifications-like.json */, B5AEEC771ACACFDA008BF2A4 /* notifications-new-follower.json */, @@ -4139,6 +4151,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + E1EBC3751C118EDE00F638E0 /* ImmuTableTestViewCellWithNib.xib in Resources */, B5A6BB8C1BF4DF38002F6A96 /* rest-site-settings.json in Resources */, E16AB93414D978240047A2E5 /* InfoPlist.strings in Resources */, 93594BD5191D2F5A0079E6B2 /* stats-batch.json in Resources */, @@ -4690,6 +4703,7 @@ E240859C183D82AE002EB0EF /* WPAnimatedBox.m in Sources */, 852416CF1A12EBDD0030700C /* AppRatingUtility.m in Sources */, B5CC05F91962186D00975CAC /* Meta.m in Sources */, + E1EBC3711C118EB200F638E0 /* WPImmuTableCells.swift in Sources */, 5D20A6531982D56600463A91 /* FollowedSitesViewController.m in Sources */, 5D8D53F119250412003C8859 /* BlogSelectorViewController.m in Sources */, 5D3D559718F88C3500782892 /* ReaderPostService.m in Sources */, @@ -4874,6 +4888,7 @@ 857610D618C0377300EDF406 /* StatsWebViewController.m in Sources */, 5DBFC8A71A9BC34F00E00DE4 /* PostListViewController.m in Sources */, 5D08B90419648C3400D5B381 /* ReaderSubscriptionViewController.m in Sources */, + E1EBC36F1C118EA500F638E0 /* ImmuTable.swift in Sources */, 5DDC44671A72BB07007F538E /* ReaderViewController.m in Sources */, E1D086E2194214C600F0CC19 /* NSDate+WordPressJSON.m in Sources */, 5D839AAB187F0D8000811F4A /* PostGeolocationCell.m in Sources */, @@ -4914,6 +4929,7 @@ E66969CD1B9E2EBF00EC9C00 /* SafeReaderTopicToReaderTopic.m in Sources */, E66969C81B9E0A6800EC9C00 /* ReaderTopicServiceTest.swift in Sources */, 931D26F519ED7E6D00114F17 /* BlogJetpackTest.m in Sources */, + E1EBC3731C118ED200F638E0 /* ImmuTableTest.swift in Sources */, 93B853231B4416A30064FE72 /* WPAnalyticsTrackerAutomatticTracksTests.m in Sources */, E66969CA1B9E0C4F00EC9C00 /* ReaderTopicServiceRemoteTests.m in Sources */, 5DFA7EBC1AF7B8D30072023B /* NSDateStringFormattingTest.m in Sources */, diff --git a/WordPress/WordPressTest/ImmuTableTest.swift b/WordPress/WordPressTest/ImmuTableTest.swift new file mode 100644 index 000000000000..083842213508 --- /dev/null +++ b/WordPress/WordPressTest/ImmuTableTest.swift @@ -0,0 +1,84 @@ +import XCTest +@testable import WordPress + +class ImmuTableTest: XCTestCase { + + func testRegisterRowsWorksWithNibs() { + let mockTable = MockTableView() + let rowsToRegister: [ImmuTableRow.Type] = [ + TestWithNibImmuTableRow.self + ] + + ImmuTable.registerRows(rowsToRegister, registrator: mockTable) + XCTAssertEqual(mockTable.registeredNibs.count, 1, "The table should have registered a nib for TestWithNibImmuTableRow") + XCTAssertEqual(mockTable.registeredClasses.count, 0, "The table shouldn't have registered any classes for TestWithNibImmuTableRow") + } + + func testRegisterRowsDoesntRegisterSameCellTwice() { + let mockTable = MockTableView() + let rowsToRegister: [ImmuTableRow.Type] = [ + BasicImmuTableRow.self, + ImageImmuTableRow.self, + TestImmuTableRow.self + ] + + ImmuTable.registerRows(rowsToRegister, registrator: mockTable) + XCTAssertEqual(2, mockTable.registeredClasses.count, "Each cell class shouldn't be registered more than once") + } + +} + +class TestTableViewCell: UITableViewCell {} +class ImmuTableTestViewCellWithNib: UITableViewCell {} + +struct BasicImmuTableRow: ImmuTableRow { + static let cell = ImmuTableCell.Class(UITableViewCell) + let title: String + var action: ImmuTableActionType? = nil + func configureCell(cell: UITableViewCell) { + } +} + +struct ImageImmuTableRow: ImmuTableRow { + static let cell = ImmuTableCell.Class(UITableViewCell) + let title: String + let image: UIImage + var action: ImmuTableActionType? = nil + func configureCell(cell: UITableViewCell) { + } +} + +struct TestImmuTableRow: ImmuTableRow { + static let cell = ImmuTableCell.Class(TestTableViewCell) + let title: String + var action: ImmuTableActionType? = nil + func configureCell(cell: UITableViewCell) { + } +} + +struct TestWithNibImmuTableRow: ImmuTableRow { + typealias CellType = ImmuTableTestViewCellWithNib + static let cell: ImmuTableCell = { + let nib = UINib(nibName: "ImmuTableTestViewCellWithNib", bundle: NSBundle(forClass: ImmuTableTestViewCellWithNib.self)) + return ImmuTableCell.Nib(nib, CellType.self) + }() + var action: ImmuTableActionType? = nil + func configureCell(cell: UITableViewCell) { + } +} + +class MockTableView: CellRegistrator { + var registeredClasses = [(String, AnyClass)]() + var registeredNibs = [(String, UINib)]() + func register(cell: ImmuTableCell, cellReuseIdentifier identifier: String) { + switch cell { + case .Class(let cellClass): + registeredClasses.append((identifier, cellClass)) + case .Nib(let nib, _): + registeredNibs.append((identifier, nib)) + } + } + func registerClass(cellClass: AnyClass?, forCellReuseIdentifier identifier: String) { + registeredClasses.append((identifier, cellClass!)) + } +} diff --git a/WordPress/WordPressTest/Test Data/ImmuTableTestViewCellWithNib.xib b/WordPress/WordPressTest/Test Data/ImmuTableTestViewCellWithNib.xib new file mode 100644 index 000000000000..a45d9bd547f3 --- /dev/null +++ b/WordPress/WordPressTest/Test Data/ImmuTableTestViewCellWithNib.xib @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + +