From 48f8e51361e787bd9c119a6c1220e105ec821d26 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 2 Oct 2018 18:09:29 +0200 Subject: [PATCH] Add `CaseIterable` table/collectionview helpers (#107) * Add a bunch of convenience methods on `CaseIterable` + tests * Add demo VC for CaseIterable stuff * Document CaseIterable helpers * Bump podspec * `in: indexPath -> `at: indexPath` * clarify caseNames so they don't look like array methods --- Demo.xcodeproj/project.pbxproj | 30 ++- README.md | 44 ++++ Sources/CaseIterable+Sweet.swift | 117 ++++++++++ SweetUIKit.podspec | 2 +- Tests/CaseIterableTests.swift | 166 ++++++++++++++ .../AppIcon.appiconset/Contents.json | 5 + .../CaseIterableViewController.swift | 209 ++++++++++++++++++ .../ViewControllers/DemoViewController.swift | 21 +- 8 files changed, 575 insertions(+), 19 deletions(-) create mode 100644 Sources/CaseIterable+Sweet.swift create mode 100644 Tests/CaseIterableTests.swift create mode 100644 iOSDemo/ViewControllers/CaseIterableViewController.swift diff --git a/Demo.xcodeproj/project.pbxproj b/Demo.xcodeproj/project.pbxproj index e5b9780..757d00d 100755 --- a/Demo.xcodeproj/project.pbxproj +++ b/Demo.xcodeproj/project.pbxproj @@ -88,6 +88,11 @@ 2B3FEEE91EAA04BB00EF8D20 /* DemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3FEEE81EAA04BB00EF8D20 /* DemoViewController.swift */; }; 2B440A6B1EA9EB0A00AC33F8 /* SearchableCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B440A6A1EA9EB0A00AC33F8 /* SearchableCollectionViewController.swift */; }; 334F248E20889A330048EC4F /* SweetUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 149D816C1DBD2AFB00A9EB1A /* SweetUIKit.framework */; }; + 33BFD2A42155538500963663 /* CaseIterable+Sweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BFD2A32155538500963663 /* CaseIterable+Sweet.swift */; }; + 33BFD2A52155538500963663 /* CaseIterable+Sweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BFD2A32155538500963663 /* CaseIterable+Sweet.swift */; }; + 33BFD2A92155540300963663 /* CaseIterableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BFD2A6215553F800963663 /* CaseIterableTests.swift */; }; + 33BFD2AA2155540400963663 /* CaseIterableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BFD2A6215553F800963663 /* CaseIterableTests.swift */; }; + 33BFD2AC215564F900963663 /* CaseIterableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BFD2AB215564F900963663 /* CaseIterableViewController.swift */; }; 440998811DC0B1C600C11852 /* UICollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440998801DC0B1C600C11852 /* UICollectionViewTests.swift */; }; 440998821DC0B1C600C11852 /* UICollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440998801DC0B1C600C11852 /* UICollectionViewTests.swift */; }; 4479760F1DC0A42100A1F577 /* UITableView+Sweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4479760E1DC0A42100A1F577 /* UITableView+Sweet.swift */; }; @@ -221,6 +226,9 @@ 14F393961CC6517E00616696 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 2B3FEEE81EAA04BB00EF8D20 /* DemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoViewController.swift; sourceTree = ""; }; 2B440A6A1EA9EB0A00AC33F8 /* SearchableCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchableCollectionViewController.swift; sourceTree = ""; }; + 33BFD2A32155538500963663 /* CaseIterable+Sweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Sweet.swift"; sourceTree = ""; }; + 33BFD2A6215553F800963663 /* CaseIterableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseIterableTests.swift; sourceTree = ""; }; + 33BFD2AB215564F900963663 /* CaseIterableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseIterableViewController.swift; sourceTree = ""; }; 33CCF0F8215521CB0015417E /* SweetUIKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = SweetUIKit.podspec; sourceTree = ""; }; 33CCF0F9215522530015417E /* config.yml */ = {isa = PBXFileReference; lastKnownFileType = text; name = config.yml; path = .circleci/config.yml; sourceTree = ""; }; 440998801DC0B1C600C11852 /* UICollectionViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionViewTests.swift; sourceTree = ""; }; @@ -368,6 +376,7 @@ 146D72AF1AB782920058798C /* Tests */ = { isa = PBXGroup; children = ( + 33BFD2A6215553F800963663 /* CaseIterableTests.swift */, 9F4C5B6C1DC0DE1A00F2A77B /* IndexPathTests.swift */, 140124B41DBF57FC00EA92FB /* StringTests.swift */, 440998801DC0B1C600C11852 /* UICollectionViewTests.swift */, @@ -415,24 +424,25 @@ isa = PBXGroup; children = ( 146436361FB1122500E2E8AD /* iOS */, - 149D81811DBD2E8000A9EB1A /* SweetUIKit.h */, + 33BFD2A32155538500963663 /* CaseIterable+Sweet.swift */, 140124621DBF57BA00EA92FB /* Identifiable.swift */, 140124631DBF57BA00EA92FB /* IndexPath+Sweet.swift */, + 9FC8B9E81DC74D0500A68185 /* Jiggly.swift */, + 9F2BDE191E09286500E32CAD /* KeyboardAwareInputViewProtocol.swift */, + 9F6BE7111EC1B88700A954A1 /* SearchBarContainerView.swift */, 140124651DBF57BA00EA92FB /* String+Sweet.swift */, 140124661DBF57BA00EA92FB /* SweetCollectionController.swift */, 140124671DBF57BA00EA92FB /* SweetTableController.swift */, + 149D81811DBD2E8000A9EB1A /* SweetUIKit.h */, 140124681DBF57BA00EA92FB /* UIAlertController+Sweet.swift */, + 447976131DC0A42C00A1F577 /* UICollectionView+Sweet.swift */, + 142565771E09641400184D47 /* UIColor+Sweet.swift */, 140124691DBF57BA00EA92FB /* UIImage+Sweet.swift */, 1401246A1DBF57BA00EA92FB /* UILabel+Sweet.swift */, - 9F6BE7111EC1B88700A954A1 /* SearchBarContainerView.swift */, 1401246B1DBF57BA00EA92FB /* UIScrollView+Sweet.swift */, + 4479760E1DC0A42100A1F577 /* UITableView+Sweet.swift */, 1401246C1DBF57BA00EA92FB /* UIView+Sweet.swift */, 1401246D1DBF57BA00EA92FB /* UIViewController+Sweet.swift */, - 4479760E1DC0A42100A1F577 /* UITableView+Sweet.swift */, - 447976131DC0A42C00A1F577 /* UICollectionView+Sweet.swift */, - 9FC8B9E81DC74D0500A68185 /* Jiggly.swift */, - 142565771E09641400184D47 /* UIColor+Sweet.swift */, - 9F2BDE191E09286500E32CAD /* KeyboardAwareInputViewProtocol.swift */, ); path = Sources; sourceTree = ""; @@ -452,6 +462,7 @@ 9F2BDE1C1E092B0C00E32CAD /* EditViewController.swift */, 2B440A6A1EA9EB0A00AC33F8 /* SearchableCollectionViewController.swift */, 14D986661DBF58A900D7842C /* TableController.swift */, + 33BFD2AB215564F900963663 /* CaseIterableViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -874,6 +885,7 @@ 2B3FEEE91EAA04BB00EF8D20 /* DemoViewController.swift in Sources */, 14D986691DBF58A900D7842C /* CollectionViewCell.swift in Sources */, 2B440A6B1EA9EB0A00AC33F8 /* SearchableCollectionViewController.swift in Sources */, + 33BFD2AC215564F900963663 /* CaseIterableViewController.swift in Sources */, 9F2BDE1D1E092B0C00E32CAD /* EditViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -904,6 +916,7 @@ 9F4C5B6D1DC0DE1A00F2A77B /* IndexPathTests.swift in Sources */, 140124B71DBF57FC00EA92FB /* StringTests.swift in Sources */, 140124BB1DBF57FC00EA92FB /* UIScrollViewTests.swift in Sources */, + 33BFD2A92155540300963663 /* CaseIterableTests.swift in Sources */, 440998811DC0B1C600C11852 /* UICollectionViewTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -917,6 +930,7 @@ 140124B81DBF57FC00EA92FB /* StringTests.swift in Sources */, 140124BA1DBF57FC00EA92FB /* UILabelTests.swift in Sources */, 440998821DC0B1C600C11852 /* UICollectionViewTests.swift in Sources */, + 33BFD2AA2155540400963663 /* CaseIterableTests.swift in Sources */, 142565801E09643100184D47 /* UIColorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -933,6 +947,7 @@ 140124981DBF57BA00EA92FB /* UILabel+Sweet.swift in Sources */, 1401247F1DBF57BA00EA92FB /* String+Sweet.swift in Sources */, 447976141DC0A42C00A1F577 /* UICollectionView+Sweet.swift in Sources */, + 33BFD2A42155538500963663 /* CaseIterable+Sweet.swift in Sources */, 140124841DBF57BA00EA92FB /* SweetCollectionController.swift in Sources */, 1464363A1FB1122500E2E8AD /* OpenInSafariActivity.swift in Sources */, 1425657A1E09641400184D47 /* UIColor+Sweet.swift in Sources */, @@ -960,6 +975,7 @@ 140124811DBF57BA00EA92FB /* String+Sweet.swift in Sources */, 447976161DC0A42C00A1F577 /* UICollectionView+Sweet.swift in Sources */, 146436351FB1111D00E2E8AD /* SearchBarContainerView.swift in Sources */, + 33BFD2A52155538500963663 /* CaseIterable+Sweet.swift in Sources */, 140124861DBF57BA00EA92FB /* SweetCollectionController.swift in Sources */, 1425657C1E09641400184D47 /* UIColor+Sweet.swift in Sources */, 140124901DBF57BA00EA92FB /* UIAlertController+Sweet.swift in Sources */, diff --git a/README.md b/README.md index 9dd4880..d19d248 100755 --- a/README.md +++ b/README.md @@ -237,6 +237,50 @@ inputView(_:shouldUpdatePosition:) ``` +## `CaseIterable` helpers + +A bunch of syntactic sugar to help with using `CaseIterable` enums to manage sections and rows in static tableviews. + +This allows you to use something like + +```swift +let item = Item.forRow(at: indexPath) +``` + +in order to get a known item from a list. Please see the sample app's [`CaseIterableViewController`](iOSDemo/ViewControllers/CaseIterableViewController.swift) for an example. + + +### Array Converter + +```swift +/// Returns the `allCases` static var as an array so it can be accessed based on index. +public static var allCasesArray: [Self] +``` + +### Non-optional helpers + +Should be used when the number of of cases in `allCases` is predictable - usually when automatically generated by the compiler. +These methods will `fatalError` if you try to access a case which does not exist. + +```swift +public static func forIndex(_ index: Int) -> Self +public static func forSection(at indexPath: IndexPath) -> Self +public static func forRow(at indexPath: IndexPath) -> Self +public static func forItem(at indexPath: IndexPath) -> Self +``` + +### Optional Helpers + +Should be used when the number of cases in `allCases` is variable - usually when this var is manually overridden to allow showing/hiding of a section or row. + +```swift +public static func optionalForIndex(_ index: Int) -> Self? +public static func optionalForSection(at indexPath: IndexPath) -> Self? +public static func optionalForRow(at indexPath: IndexPath) -> Self? +public static func optionalForItem(at indexPath: IndexPath) -> Self? +``` + + ## Installation **SweetUIKit** is available through [CocoaPods](http://cocoapods.org). To install diff --git a/Sources/CaseIterable+Sweet.swift b/Sources/CaseIterable+Sweet.swift new file mode 100644 index 0000000..14e4467 --- /dev/null +++ b/Sources/CaseIterable+Sweet.swift @@ -0,0 +1,117 @@ +import Foundation + +public extension CaseIterable { + + /// Returns the `allCases` static var as an array so it can be accessed based on index. + public static var allCasesArray: [Self] { + if let asArray = self.allCases as? [Self] { + return asArray + } else { + // The above cast works a lot of the time, but is not guaranteed since it's considered + // an implementation detail despite the fact that the collection is guaranteed to be in + // the order of declaration. If it doesn't work, fall back to mapping `allCases` into an array. + return allCases.map { $0 } + } + } + + // MARK: - Non-optional helpers + + /// Gets the enum value at the given index in `allCases` + /// + /// NOTE: This method will puke up a fatal error if the given `CaseIterable` + /// enum does not contain the given index. use `optionalForIndex(_:)` + /// to avoid crashes if the number of items in `allCases` may not be + /// known at runtime due to overriding the `allCases` property. + /// + /// - Parameter index: The index in the list of cases for which an enum value is desired. + /// - Returns: The enum value at the given index. + public static func forIndex(_ index: Int) -> Self { + guard let item = self.optionalForIndex(index) else { + fatalError("SweetUIKit: Enum \(Self.self) does not contain index \(index)") + } + + return item + } + + /// Gets the enum at the index in `allCases` of the `section` of the passed-in `IndexPath` + /// + /// NOTE: This method will puke up a fatal error if the given `CaseIterable` + /// enum does not contain the given index. use `optionalForSection(at:)` to + /// avoid crashes if the number of items in `allCases` may not be + /// known at runtime due to overriding the `allCases` property. + /// + /// - Parameter index: The indexPath for which you want the `section` property to be used to retrieve the enum value from `allCases`. + /// - Returns: The enum value at the given index. + public static func forSection(at indexPath: IndexPath) -> Self { + return forIndex(indexPath.section) + } + + /// Gets the enum at the index in `allCases` of the `row` of the passed-in `IndexPath` + /// + /// NOTE: This method will puke up a fatal error if the given `CaseIterable` + /// enum does not contain the given index. use `optionalForItem(at:)` to + /// avoid crashes if the number of items in `allCases` may not be + /// known at runtime due to overriding the `allCases` property. + /// + /// - Parameter index: The indexPath for which you want the `section` property to be used to retrieve the enum value from `allCases`. + /// - Returns: The enum value at the given index. + public static func forRow(at indexPath: IndexPath) -> Self { + return forIndex(indexPath.row) + } + + /// Gets the enum at the index in `allCases` of the `item` of the passed-in `IndexPath` + /// + /// NOTE: This method will puke up a fatal error if the given `CaseIterable` + /// enum does not contain the given index. use `optionalForItem(at:)` to + /// avoid crashes if the number of items in `allCases` may not be + /// known at runtime due to overriding the `allCases` property. + /// + /// - Parameter index: The indexPath for which you want the `section` property to be used to retrieve the enum value from `allCases`. + /// - Returns: The enum value at the given index. + public static func forItem(in indexPath: IndexPath) -> Self { + return forIndex(indexPath.item) + } + + // MARK: - Optional Helpers + + /// Gets the enum value at the given index in `allCases` + /// + /// - Parameter index: The index in the list of cases for which an enum value is desired. + /// - Returns: The enum value at the given index, or nil if `allCases` does not contain the given index. + public static func optionalForIndex(_ index: Int) -> Self? { + guard allCasesArray.indices.contains(index) else { + return nil + } + + return allCasesArray[index] + } + + /// Gets the enum at the index in `allCases` of the `section` of the passed-in `IndexPath` + /// + /// NOTE: This method will puke up a fatal error if the given `CaseIterable` + /// enum does not contain the given index. use `optionalForSection(at:)` to + /// avoid crashes if the number of items in `allCases` may not be + /// known at runtime due to overriding the `allCases` property. + /// + /// - Parameter index: The indexPath for which you want the `section` property to be used to retrieve the enum value from `allCases`. + /// - Returns: The enum value at the given index, or nil if `allCases` does not contain the given index. + public static func optionalForSection(at indexPath: IndexPath) -> Self? { + return optionalForIndex(indexPath.section) + } + + /// Gets the enum at the index in `allCases` of the `row` of the passed-in `IndexPath` + /// + /// - Parameter index: The indexPath for which you want the `section` property to be used to retrieve the enum value from `allCases`. + /// - Returns: The enum value at the given index, or nil if `allCases` does not contain the given index. + public static func optionalForRow(at indexPath: IndexPath) -> Self? { + return optionalForIndex(indexPath.row) + } + + /// Gets the enum at the index in `allCases` of the `item` of the passed-in `IndexPath` + /// + /// - Parameter index: The indexPath for which you want the `section` property to be used to retrieve the enum value from `allCases`. + /// - Returns: The enum value at the given index, or nil if `allCases` does not contain the given index. + public static func optionalForItem(at indexPath: IndexPath) -> Self? { + return optionalForIndex(indexPath.item) + } +} diff --git a/SweetUIKit.podspec b/SweetUIKit.podspec index 14a9fd5..87ed659 100755 --- a/SweetUIKit.podspec +++ b/SweetUIKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SweetUIKit" s.summary = "Helpers and sugar for the UIKit framework." - s.version = "1.15.0" + s.version = "1.16.0" s.homepage = "https://github.com/UseSweet/SweetUIKit" s.license = 'MIT' s.author = { "Use Sweet" => "usesweet.contact@gmail.com" } diff --git a/Tests/CaseIterableTests.swift b/Tests/CaseIterableTests.swift new file mode 100644 index 0000000..e73421c --- /dev/null +++ b/Tests/CaseIterableTests.swift @@ -0,0 +1,166 @@ +import XCTest +import SweetUIKit + +class CaseIterableTests: XCTestCase { + + // MARK: - Test Enums + + enum ZeroOptions: CaseIterable {} + + enum OneOption: CaseIterable { + case optionOne + } + + enum TwoOptions: CaseIterable { + case optionOne + case optionTwo + } + + enum ThreeOptions: CaseIterable { + case optionOne + case optionTwo + case optionThree + } + + // MARK: - Tests + + func testAllCasesArrayHasCorrectItemsInOrder() { + let zeroRaw = ZeroOptions.allCases + let zeroArray = ZeroOptions.allCasesArray + + XCTAssertEqual(zeroRaw.count, zeroArray.count) + XCTAssertEqual(zeroArray.count, 0) + + let oneRaw = OneOption.allCases + let oneArray = OneOption.allCasesArray + + XCTAssertEqual(oneRaw.count, oneArray.count) + XCTAssertEqual(oneArray.count, 1) + XCTAssertEqual(oneArray[0], OneOption.optionOne) + + let twoRaw = TwoOptions.allCases + let twoArray = TwoOptions.allCasesArray + + XCTAssertEqual(twoRaw.count, twoArray.count) + XCTAssertEqual(twoArray.count, 2) + XCTAssertEqual(twoArray[0], TwoOptions.optionOne) + XCTAssertEqual(twoArray[1], TwoOptions.optionTwo) + + let threeRaw = ThreeOptions.allCases + let threeArray = ThreeOptions.allCasesArray + + XCTAssertEqual(threeRaw.count, threeArray.count) + XCTAssertEqual(threeArray.count, 3) + XCTAssertEqual(threeArray[0], ThreeOptions.optionOne) + XCTAssertEqual(threeArray[1], ThreeOptions.optionTwo) + XCTAssertEqual(threeArray[2], ThreeOptions.optionThree) + } + + func testRequiredValueForIndex() { + XCTAssertEqual(OneOption.forIndex(0), OneOption.optionOne) + + XCTAssertEqual(TwoOptions.forIndex(0), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.forIndex(1), TwoOptions.optionTwo) + + XCTAssertEqual(ThreeOptions.forIndex(0), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.forIndex(1), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.forIndex(2), ThreeOptions.optionThree) + } + + func testRequiredValueForSection() { + XCTAssertEqual(OneOption.forSection(at: IndexPath(row: 0, section: 0)), OneOption.optionOne) + + XCTAssertEqual(TwoOptions.forSection(at: IndexPath(row: 0, section: 0)), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.forSection(at: IndexPath(row: 0, section: 1)), TwoOptions.optionTwo) + + XCTAssertEqual(ThreeOptions.forSection(at: IndexPath(row: 0, section: 0)), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.forSection(at: IndexPath(row: 0, section: 1)), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.forSection(at: IndexPath(row: 0, section: 2)), ThreeOptions.optionThree) + } + + func testRequiredValueForRow() { + XCTAssertEqual(OneOption.forRow(at: IndexPath(row: 0, section: 0)), OneOption.optionOne) + + XCTAssertEqual(TwoOptions.forRow(at: IndexPath(row: 0, section: 0)), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.forRow(at: IndexPath(row: 1, section: 0)), TwoOptions.optionTwo) + + XCTAssertEqual(ThreeOptions.forRow(at: IndexPath(row: 0, section: 0)), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.forRow(at: IndexPath(row: 1, section: 0)), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.forRow(at: IndexPath(row: 2, section: 0)), ThreeOptions.optionThree) + } + + func testRequiredValueForItem() { + XCTAssertEqual(OneOption.forItem(in: IndexPath(item: 0, section: 0)), OneOption.optionOne) + + XCTAssertEqual(TwoOptions.forItem(in: IndexPath(item: 0, section: 0)), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.forItem(in: IndexPath(item: 1, section: 0)), TwoOptions.optionTwo) + + XCTAssertEqual(ThreeOptions.forItem(in: IndexPath(item: 0, section: 0)), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.forItem(in: IndexPath(item: 1, section: 0)), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.forItem(in: IndexPath(item: 2, section: 0)), ThreeOptions.optionThree) + } + + func testOptionalValueForIndex() { + XCTAssertNil(ZeroOptions.optionalForIndex(0)) + + XCTAssertEqual(OneOption.optionalForIndex(0), OneOption.optionOne) + XCTAssertNil(OneOption.optionalForIndex(1)) + + XCTAssertEqual(TwoOptions.optionalForIndex(0), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.optionalForIndex(1), TwoOptions.optionTwo) + XCTAssertNil(TwoOptions.optionalForIndex(2)) + + XCTAssertEqual(ThreeOptions.optionalForIndex(0), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.optionalForIndex(1), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.optionalForIndex(2), ThreeOptions.optionThree) + XCTAssertNil(ThreeOptions.optionalForIndex(3)) + } + + func testOptionalValueForSection() { + XCTAssertNil(ZeroOptions.optionalForSection(at: IndexPath(row: 0, section: 0))) + + XCTAssertEqual(OneOption.optionalForSection(at: IndexPath(row: 0, section: 0)), OneOption.optionOne) + XCTAssertNil(OneOption.optionalForSection(at: IndexPath(row: 0, section: 1))) + + XCTAssertEqual(TwoOptions.optionalForSection(at: IndexPath(row: 0, section: 0)), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.optionalForSection(at: IndexPath(row: 0, section: 1)), TwoOptions.optionTwo) + XCTAssertNil(TwoOptions.optionalForSection(at: IndexPath(row: 0, section: 2))) + + XCTAssertEqual(ThreeOptions.optionalForSection(at: IndexPath(row: 0, section: 0)), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.optionalForSection(at: IndexPath(row: 0, section: 1)), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.optionalForSection(at: IndexPath(row: 0, section: 2)), ThreeOptions.optionThree) + XCTAssertNil(ThreeOptions.optionalForSection(at: IndexPath(row: 0, section: 3))) + } + + func testOptionalValueForRow() { + XCTAssertNil(ZeroOptions.optionalForRow(at: IndexPath(row: 0, section: 0))) + + XCTAssertEqual(OneOption.optionalForRow(at: IndexPath(row: 0, section: 0)), OneOption.optionOne) + XCTAssertNil(OneOption.optionalForRow(at: IndexPath(row: 1, section: 0))) + + XCTAssertEqual(TwoOptions.optionalForRow(at: IndexPath(row: 0, section: 0)), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.optionalForRow(at: IndexPath(row: 1, section: 0)), TwoOptions.optionTwo) + XCTAssertNil(TwoOptions.optionalForRow(at: IndexPath(row: 2, section: 0))) + + XCTAssertEqual(ThreeOptions.optionalForRow(at: IndexPath(row: 0, section: 0)), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.optionalForRow(at: IndexPath(row: 1, section: 0)), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.optionalForRow(at: IndexPath(row: 2, section: 0)), ThreeOptions.optionThree) + XCTAssertNil(ThreeOptions.optionalForRow(at: IndexPath(row: 3, section: 0))) + } + + func testOptionalValueForItem() { + XCTAssertNil(ZeroOptions.optionalForItem(at: IndexPath(item: 0, section: 0))) + + XCTAssertEqual(OneOption.optionalForItem(at: IndexPath(item: 0, section: 0)), OneOption.optionOne) + XCTAssertNil(OneOption.optionalForItem(at: IndexPath(item: 1, section: 0))) + + XCTAssertEqual(TwoOptions.optionalForItem(at: IndexPath(item: 0, section: 0)), TwoOptions.optionOne) + XCTAssertEqual(TwoOptions.optionalForItem(at: IndexPath(item: 1, section: 0)), TwoOptions.optionTwo) + XCTAssertNil(TwoOptions.optionalForItem(at: IndexPath(item: 2, section: 0))) + + XCTAssertEqual(ThreeOptions.optionalForItem(at: IndexPath(item: 0, section: 0)), ThreeOptions.optionOne) + XCTAssertEqual(ThreeOptions.optionalForItem(at: IndexPath(item: 1, section: 0)), ThreeOptions.optionTwo) + XCTAssertEqual(ThreeOptions.optionalForItem(at: IndexPath(item: 2, section: 0)), ThreeOptions.optionThree) + XCTAssertNil(ThreeOptions.optionalForItem(at: IndexPath(item: 3, section: 0))) + } +} diff --git a/iOSDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOSDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 1d060ed..d8db8d6 100644 --- a/iOSDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/iOSDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -84,6 +84,11 @@ "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/iOSDemo/ViewControllers/CaseIterableViewController.swift b/iOSDemo/ViewControllers/CaseIterableViewController.swift new file mode 100644 index 0000000..c4f5de4 --- /dev/null +++ b/iOSDemo/ViewControllers/CaseIterableViewController.swift @@ -0,0 +1,209 @@ +// +// CaseIterableViewController.swift +// iOSDemo +// +// Created by Ellen Shapiro (Work) on 9/21/18. +// + +import UIKit +import SweetUIKit + +enum Section: String, CaseIterable { + case user + case app + case logout + + var title: String { + return rawValue.uppercased() + } +} + +enum UserRow: CaseIterable { + case username + case emailAddress + + var title: String { + switch self { + case .username: + return "User name" + case .emailAddress: + return "Email address" + } + } + + var value: String { + switch self { + case .username: + return "foobar" + case .emailAddress: + return "foo@bar.baz" + } + } +} + +enum AppRow: CaseIterable { + case currentVersion + case contactDeveloper + case leaveReview + + var title: String { + switch self { + case .currentVersion: + return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "(Version unknown)" + case .contactDeveloper: + return "Contact the developer" + case .leaveReview: + return "Leave a review" + } + } + + var accessoryType: UITableViewCell.AccessoryType { + switch self { + case .currentVersion: + return .none + case .contactDeveloper, + .leaveReview: + return .disclosureIndicator + } + } +} + +enum LogoutRow: String, CaseIterable { + case reset + case logout + + var title: String { + return rawValue.capitalized + } + + var textColor: UIColor { + switch self { + case .reset: + return .darkText + case .logout: + return .red + } + } +} + +class CaseIterableViewController: UIViewController { + + private lazy var cellIdentitfier = String(describing: self) + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .grouped) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.dataSource = self + tableView.delegate = self + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: self.cellIdentitfier) + + return tableView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + title = "CaseIterable Example" + + view.addSubview(tableView) + + NSLayoutConstraint.activate([ + self.tableView.topAnchor.constraint(equalTo: self.view.topAnchor), + self.tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor), + self.tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), + self.tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor), + ]) + } +} + +extension CaseIterableViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section.forIndex(section) { + case .user: + return UserRow.allCases.count + case .app: + return AppRow.allCases.count + case .logout: + return LogoutRow.allCases.count + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentitfier, for: indexPath) + cell.accessoryType = .none + cell.textLabel?.textColor = .darkText + + switch Section.forSection(at: indexPath) { + case .user: + let row = UserRow.forRow(at: indexPath) + cell.textLabel?.text = row.title + cell.detailTextLabel?.text = row.value + case .app: + let row = AppRow.forRow(at: indexPath) + cell.textLabel?.text = row.title + cell.accessoryType = row.accessoryType + case .logout: + let row = LogoutRow.forRow(at: indexPath) + cell.textLabel?.text = row.title + cell.textLabel?.textColor = row.textColor + } + + return cell + } +} + +extension CaseIterableViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + switch Section.forSection(at: indexPath) { + case .user: + switch UserRow.forRow(at: indexPath) { + case .username: + showAlert(with: "Show username edit") + case .emailAddress: + showAlert(with: "Show email address edit") + } + case .app: + switch AppRow.forRow(at: indexPath) { + case .currentVersion: + showAlert(with: "Tapped Current Version") + case .contactDeveloper: + showAlert(with: "Start an email to the developer") + case .leaveReview: + showAlert(with: "Send user to app store to leave a review") + } + case .logout: + switch LogoutRow.forRow(at: indexPath) { + case .reset: + showAlert(with: "Reset user settings but don't log them out") + case .logout: + showAlert(with: "Log out the user!") + } + } + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return Section.forIndex(section).title + } + + private func showAlert(with title: String) { + let alert = UIAlertController(title: title, + message: nil, + preferredStyle: .alert) + + let okAction = UIAlertAction(title: "OK", + style: .default, + handler: nil) + + alert.addAction(okAction) + + present(alert, animated: true) + } +} diff --git a/iOSDemo/ViewControllers/DemoViewController.swift b/iOSDemo/ViewControllers/DemoViewController.swift index d86de8a..ae32aa2 100644 --- a/iOSDemo/ViewControllers/DemoViewController.swift +++ b/iOSDemo/ViewControllers/DemoViewController.swift @@ -2,20 +2,18 @@ import Foundation import UIKit import SweetUIKit -enum DemoItem: Int { - case edit, collection, searchableCollection - - static var titles: [String] { - let allItems: [DemoItem] = [.edit, .collection, .searchableCollection] - - return allItems.map { $0.title } - } +enum DemoItem: CaseIterable { + case edit + case collection + case searchableCollection + case caseIterable var title: String { switch self { case .edit: return "Edit Controller" case .collection: return "Collection Controller" case .searchableCollection: return "Searchable Collection Controller" + case .caseIterable: return "CaseIterable Controller" } } @@ -24,6 +22,7 @@ enum DemoItem: Int { case .edit: return EditViewController() case .collection: return CollectionController() case .searchableCollection: return SearchableCollectionViewController() + case .caseIterable: return CaseIterableViewController() } } } @@ -42,13 +41,13 @@ class DemoViewController: SweetTableController { extension DemoViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return DemoItem.titles.count + return DemoItem.allCases.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeue(TableViewCell.self, for: indexPath) cell.accessoryType = .disclosureIndicator - let itemTitle = DemoItem.titles[indexPath.row] + let itemTitle = DemoItem.forRow(at: indexPath).title cell.titleLabel.text = itemTitle return cell @@ -60,7 +59,7 @@ extension DemoViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - if let item = DemoItem(rawValue: indexPath.row) as DemoItem? { + if let item = DemoItem.optionalForRow(at: indexPath) { navigationController?.pushViewController(item.viewController, animated: true) } }