├── .hound.yml ├── Tests ├── Assets │ ├── icon.png │ └── icon-highlighted.png ├── CustomTypes.swift ├── Model │ ├── SectionSpec.swift │ ├── IconSpec.swift │ ├── DetailTextSpec.swift │ └── SubtitleSpec.swift ├── ViewController │ └── QuickTableViewControllerSpec.swift ├── View │ ├── ReusableSpec.swift │ └── ConfigurableSpec.swift └── Row │ ├── TapActionRowSpec.swift │ ├── SwitchRowSpec.swift │ └── OptionRowSpec.swift ├── Example-iOS ├── Assets.xcassets │ ├── Contents.json │ ├── iconmonstr-gear.imageset │ │ ├── iconmonstr-gear.png │ │ ├── iconmonstr-gear@2x.png │ │ ├── iconmonstr-gear@3x.png │ │ └── Contents.json │ ├── iconmonstr-globe.imageset │ │ ├── iconmonstr-globe.png │ │ ├── iconmonstr-globe@2x.png │ │ ├── iconmonstr-globe@3x.png │ │ └── Contents.json │ ├── iconmonstr-time.imageset │ │ ├── iconmonstr-time.png │ │ ├── iconmonstr-time@2x.png │ │ ├── iconmonstr-time@3x.png │ │ └── Contents.json │ ├── iconmonstr-x-mark.imageset │ │ ├── iconmonstr-x-mark.png │ │ ├── iconmonstr-x-mark@2x.png │ │ ├── iconmonstr-x-mark@3x.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── AppDelegate.swift ├── UINibs │ ├── OptionCell.xib │ ├── SwitchCell.xib │ ├── TapActionCell.xib │ └── UITableViewCell.xib ├── ViewControllers │ ├── AppearanceViewController.swift │ ├── RootViewController.swift │ ├── DynamicTableViewController.swift │ ├── DynamicTableView.swift │ ├── ExampleViewController.swift │ └── CustomizationViewController.swift └── Base.lproj │ └── LaunchScreen.xib ├── Example-tvOS ├── Assets.xcassets │ ├── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Top Shelf Image.imageset │ │ │ └── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Contents.json │ └── Launch Image.launchimage │ │ └── Contents.json ├── Info.plist ├── AppDelegate.swift └── ExampleViewController.swift ├── Gemfile ├── .codecov.yml ├── scripts ├── swiftlint.rb └── push-docs.sh ├── QuickTableViewController.xcworkspace ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings └── contents.xcworkspacedata ├── Podfile ├── .jazzy.yml ├── Podfile.lock ├── .gitignore ├── Example-iOSUITests ├── Info.plist └── ExampleUITests.swift ├── Example-tvOSUITests ├── Info.plist └── ExampleUITests.swift ├── QuickTableViewController ├── Info-tvOSTests.plist ├── Info-iOSTests.plist ├── Info-tvOS.plist ├── Info-iOS.plist └── QuickTableViewController.h ├── QuickTableViewController.podspec ├── .swiftlint.yml ├── Dangerfile ├── LICENSE ├── .travis.yml ├── Source ├── Protocol │ ├── Row.swift │ ├── Configurable.swift │ ├── RowStyle.swift │ ├── RowCompatible.swift │ └── Reusable.swift ├── Model │ ├── Section.swift │ ├── DetailText.swift │ ├── Icon.swift │ ├── Subtitle.swift │ ├── Deprecated.swift │ └── RadioSection.swift ├── Views │ ├── TapActionCell.swift │ └── SwitchCell.swift └── Rows │ ├── TapActionRow.swift │ ├── OptionRow.swift │ ├── SwitchRow.swift │ └── NavigationRow.swift ├── Package.swift ├── Makefile ├── Rakefile ├── QuickTableViewController.xcodeproj └── xcshareddata │ └── xcschemes │ ├── Example-iOS.xcscheme │ ├── Example-tvOS.xcscheme │ ├── QuickTableViewController-tvOS.xcscheme │ └── QuickTableViewController-iOS.xcscheme └── Gemfile.lock /.hound.yml: -------------------------------------------------------------------------------- 1 | rubocop: 2 | enabled: false 3 | swiftlint: 4 | enabled: true 5 | config_file: .swiftlint.yml 6 | -------------------------------------------------------------------------------- /Tests/Assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Tests/Assets/icon.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Tests/Assets/icon-highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Tests/Assets/icon-highlighted.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "cocoapods" 4 | gem "danger" 5 | gem "jazzy", ">= 0.9.0" 6 | gem "rake" 7 | gem "xcpretty" 8 | -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | comment: 5 | layout: "diff, files" 6 | ignore: 7 | - "Carthage/**/*" 8 | - "Example*/**/*" 9 | - "Tests/**/*" 10 | - "Pods/**/*" 11 | -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-gear.imageset/iconmonstr-gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-gear.imageset/iconmonstr-gear.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-globe.imageset/iconmonstr-globe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-globe.imageset/iconmonstr-globe.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-time.imageset/iconmonstr-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-time.imageset/iconmonstr-time.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-gear.imageset/iconmonstr-gear@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-gear.imageset/iconmonstr-gear@2x.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-gear.imageset/iconmonstr-gear@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-gear.imageset/iconmonstr-gear@3x.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-time.imageset/iconmonstr-time@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-time.imageset/iconmonstr-time@2x.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-time.imageset/iconmonstr-time@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-time.imageset/iconmonstr-time@3x.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-x-mark.imageset/iconmonstr-x-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-x-mark.imageset/iconmonstr-x-mark.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-globe.imageset/iconmonstr-globe@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-globe.imageset/iconmonstr-globe@2x.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-globe.imageset/iconmonstr-globe@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-globe.imageset/iconmonstr-globe@3x.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-x-mark.imageset/iconmonstr-x-mark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-x-mark.imageset/iconmonstr-x-mark@2x.png -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-x-mark.imageset/iconmonstr-x-mark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/QuickTableViewController/develop/Example-iOS/Assets.xcassets/iconmonstr-x-mark.imageset/iconmonstr-x-mark@3x.png -------------------------------------------------------------------------------- /scripts/swiftlint.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/env ruby 2 | 3 | not_ci = !ENV["TRAVIS"] || ENV["TRAVIS"]&.empty? 4 | 5 | if not_ci 6 | system "${PODS_ROOT}/SwiftLint/swiftlint --config .swiftlint.yml" 7 | else 8 | puts "Skip SwiftLint" 9 | end 10 | -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /QuickTableViewController.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /QuickTableViewController.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /QuickTableViewController.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /scripts/push-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd docs && pwd 4 | 5 | if [ "${TRAVIS_BRANCH}" = "master" ] && [ -n "$DANGER_GITHUB_API_TOKEN" ]; then 6 | echo "Updating gh-pages" 7 | git remote add upstream "https://${GH_PAGES_GITHUB_API_TOKEN}@github.com/bcylin/QuickTableViewController.git" 8 | git push --quiet upstream HEAD:gh-pages 9 | git remote remove upstream 10 | else 11 | echo "Skip gh-pages updates on ${TRAVIS_BRANCH}" 12 | fi 13 | 14 | cd - 15 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | install! "cocoapods", generate_multiple_pod_projects: true 2 | inhibit_all_warnings! 3 | use_frameworks! 4 | 5 | workspace "QuickTableViewController" 6 | project "QuickTableViewController" 7 | 8 | def linter 9 | pod "SwiftLint", podspec: "https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/4/0/1/SwiftLint/0.32.0/SwiftLint.podspec.json" 10 | end 11 | 12 | target "QuickTableViewController-iOSTests" do 13 | platform :ios, "8.0" 14 | linter 15 | end 16 | -------------------------------------------------------------------------------- /.jazzy.yml: -------------------------------------------------------------------------------- 1 | clean: true 2 | author: bcylin 3 | author_url: https://github.com/bcylin 4 | github_url: https://github.com/bcylin/QuickTableViewController 5 | github_file_prefix: https://github.com/bcylin/QuickTableViewController/blob/v1.2.1 6 | xcodebuild_arguments: [-project, QuickTableViewController.xcodeproj, -scheme, QuickTableViewController-iOS] 7 | module: QuickTableViewController 8 | module_version: 1.2.1 9 | output: docs/output 10 | theme: fullwidth 11 | skip_undocumented: true 12 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftLint (0.32.0) 3 | 4 | DEPENDENCIES: 5 | - SwiftLint (from `https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/4/0/1/SwiftLint/0.32.0/SwiftLint.podspec.json`) 6 | 7 | EXTERNAL SOURCES: 8 | SwiftLint: 9 | :podspec: https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/4/0/1/SwiftLint/0.32.0/SwiftLint.podspec.json 10 | 11 | SPEC CHECKSUMS: 12 | SwiftLint: 009a898ef2a1c851f45e1b59349bf6ff2ddc990d 13 | 14 | PODFILE CHECKSUM: 96aa123a793662bd2268447e11f25f81eafaaaef 15 | 16 | COCOAPODS: 1.8.4 17 | -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-gear.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iconmonstr-gear.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iconmonstr-gear@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iconmonstr-gear@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-globe.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iconmonstr-globe.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iconmonstr-globe@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iconmonstr-globe@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-time.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iconmonstr-time.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iconmonstr-time@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iconmonstr-time@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "11.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "landscape", 12 | "idiom" : "tv", 13 | "extent" : "full-screen", 14 | "minimum-system-version" : "9.0", 15 | "scale" : "1x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/iconmonstr-x-mark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iconmonstr-x-mark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iconmonstr-x-mark@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iconmonstr-x-mark@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # https://github.com/github/gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | test_output 26 | docs 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | 32 | ## CocoaPods 33 | Pods/ 34 | 35 | ## Carthage 36 | Carthage/ 37 | -------------------------------------------------------------------------------- /Example-iOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example-tvOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /QuickTableViewController/Info-tvOSTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "size" : "1280x768", 5 | "idiom" : "tv", 6 | "filename" : "App Icon - App Store.imagestack", 7 | "role" : "primary-app-icon" 8 | }, 9 | { 10 | "size" : "400x240", 11 | "idiom" : "tv", 12 | "filename" : "App Icon.imagestack", 13 | "role" : "primary-app-icon" 14 | }, 15 | { 16 | "size" : "2320x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image Wide.imageset", 19 | "role" : "top-shelf-image-wide" 20 | }, 21 | { 22 | "size" : "1920x720", 23 | "idiom" : "tv", 24 | "filename" : "Top Shelf Image.imageset", 25 | "role" : "top-shelf-image" 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } -------------------------------------------------------------------------------- /QuickTableViewController/Info-iOSTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /QuickTableViewController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "QuickTableViewController" 3 | s.version = "1.2.1" 4 | s.summary = "A simple way to create a UITableView for settings." 5 | s.screenshots = "https://bcylin.github.io/QuickTableViewController/img/screenshot-1.png", 6 | "https://bcylin.github.io/QuickTableViewController/img/screenshot-2.png" 7 | s.homepage = "https://github.com/bcylin/QuickTableViewController" 8 | s.license = { type: "MIT", file: "LICENSE" } 9 | s.author = "bcylin" 10 | 11 | s.swift_version = "4.2" 12 | s.ios.deployment_target = "8.0" 13 | s.tvos.deployment_target = "9.0" 14 | 15 | s.source = { git: "https://github.com/bcylin/QuickTableViewController.git", tag: "v#{s.version}" } 16 | s.source_files = "Source/**/*.swift" 17 | s.requires_arc = true 18 | end 19 | -------------------------------------------------------------------------------- /QuickTableViewController/Info-tvOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /QuickTableViewController/Info-iOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | 101 21 | LSRequiresIPhoneOS 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | UIUserInterfaceStyle 28 | Automatic 29 | 30 | 31 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - cyclomatic_complexity 3 | - force_cast 4 | - function_body_length 5 | - identifier_name 6 | - line_length 7 | - vertical_whitespace 8 | opt_in_rules: 9 | - closure_end_indentation 10 | - closure_spacing 11 | - conditional_returns_on_newline 12 | - empty_count 13 | - explicit_init 14 | - explicit_top_level_acl 15 | - fatal_error_message 16 | - first_where 17 | - implicit_return 18 | - implicitly_unwrapped_optional 19 | - let_var_whitespace 20 | - multiline_parameters 21 | - nimble_operator 22 | - number_separator 23 | - object_literal 24 | - operator_usage_whitespace 25 | - overridden_super_call 26 | - private_outlet 27 | - prohibited_super_call 28 | - redundant_nil_coalescing 29 | - unneeded_parentheses_in_closure_argument 30 | - vertical_parameter_alignment_on_call 31 | included: 32 | - "Example-iOS" 33 | - "Example-iOSUITests" 34 | - "Example-tvOS" 35 | - "Example-tvOSUITests" 36 | - "Source" 37 | - "Tests" 38 | excluded: 39 | - Carthage 40 | - Pods 41 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Sometimes it's a README fix, or something like that - which isn't relevant for 4 | # including in a project's CHANGELOG for example 5 | declared_trivial = (github.pr_title + github.pr_body).include? "#trivial" 6 | 7 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet 8 | warn "PR is classed as Work in Progress" if github.pr_title.include? "[WIP]" 9 | 10 | # Warn when there is a big PR 11 | warn "Big PR" if git.lines_of_code > 500 12 | 13 | # Ensure there is a summary for a PR 14 | fail "Please provide a summary in the Pull Request description" if github.pr_body.length < 5 15 | 16 | # Add a CHANGELOG entry for app changes 17 | if git.lines_of_code > 50 && !git.modified_files.include?("CHANGELOG.md") && !declared_trivial 18 | fail "Please update [CHANGELOG.md](https://github.com/bcylin/QuickTableViewController/blob/develop/CHANGELOG.md).", sticky: true 19 | end 20 | 21 | # Ensure a clean commits history 22 | if git.commits.any? { |c| c.message =~ /^Merge branch/ } 23 | fail "Please rebase to get rid of the merge commits in this PR", sticky: true 24 | end 25 | -------------------------------------------------------------------------------- /Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 bcylin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Example-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | QuickTableViewControllerExample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.2.1 21 | CFBundleVersion 22 | 101 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.1 3 | cache: 4 | - bundler 5 | - cocoapods 6 | before_install: 7 | - export LANG=en_US.UTF-8 8 | - xcrun instruments -s devices 9 | install: 10 | - bundle install --without development --deployment --jobs=3 --retry=3 11 | - bundle exec rake launch:simulators 12 | - bundle exec pod install 13 | before_script: 14 | - if [ -n "$DANGER_GITHUB_API_TOKEN" ]; then bundle exec danger; fi 15 | script: 16 | - set -e 17 | - bundle exec rake "test:ios[QuickTableViewController-iOS]" 18 | - bash <(curl -s https://codecov.io/bash) -cF ios -J "QuickTableViewController" 19 | - bundle exec rake "test:ios[Example-iOS]" 20 | - make -B carthage 21 | - make -B docs 22 | after_success: 23 | - sh scripts/push-docs.sh 24 | notifications: 25 | email: false 26 | slack: 27 | rooms: 28 | secure: OBqhJVYItWtvqHFhgpGm0aJBzfiBQpCbP/Ak0/2OH9HZNA0jAWGEFcipdzpsHCSNpDHlsSC2lq8CD+Py1rgQDfG/pgoVSY9Da+OLwXHoSaEA61qQMkAmXFEv5KfMcCXG+L4L5vatIVo3BvKbTWifGfNY4ZgcqGRK6n+emiJLGhkdDvdj8TX+jJ3TbRE7uUCpwjh6tuj0icvz65ve7kRSBTKrMOynxZGpjZ+Ur0gUnilJIV8k8/jdTg0oxzk1BueYsOw3aV33R59Jn8Dwm4IhLFlzkdW8LxfXGbOSV9bvxYSV8ZzD+PecwpSc3j92eZZomLgLLCyOT/ato2X454asMhyE+drtu3dlY6NTiKXuVQYAZ0Zm1kreHwSdUyupO1L8LplHNzF2BIDWqOdr+EtsAsHbQpilA2O/KjDnLiD6EQsWbzi0/Y3Nqeo3adAd+q2LWJAHvdRPcrYJuuC2lE/KW+lnzL4NhCOht95vTnnbCzTsmnPBLzTIYen3DRcu4/jQ/jzhy/qCgdexUIwmam73iGPeH4CVdnyfTV0YIQuJOi2bbllSWm0qIxAJ7P9UGIKCI9nCE5TfmmUP60SW6hm7o4UE1xOfyLaPEf9v9/CZ92EHecStftBbK+wsTrJqy4NmZlRVSw9nsR26mqOm2R9wf5oWxKaMMQTD8dSQ0F293n8= 29 | -------------------------------------------------------------------------------- /Source/Protocol/Row.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Row.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// Any type that conforms to this protocol is capable of representing a row in a table view. 30 | public protocol Row: class { 31 | 32 | /// The text of the row. 33 | var text: String { get } 34 | 35 | /// The detail text of the row. 36 | var detailText: DetailText? { get } 37 | 38 | /// A closure related to the action of the row. 39 | var action: ((Row) -> Void)? { get } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // 3 | // Package.swift 4 | // 5 | // Copyright © 2016 bcylin. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import PackageDescription 27 | 28 | let package = Package( 29 | name: "QuickTableViewController", 30 | platforms: [ 31 | .iOS(.v13), 32 | .tvOS(.v13), 33 | ], 34 | products: [ 35 | .library( 36 | name: "QuickTableViewController", 37 | targets: ["QuickTableViewController"]), 38 | ], 39 | targets: [ 40 | .target( 41 | name: "QuickTableViewController", 42 | path: "Source"), 43 | ], 44 | swiftLanguageVersions: [.v5] 45 | ) 46 | -------------------------------------------------------------------------------- /QuickTableViewController/QuickTableViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // QuickTableViewController.h 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 25/08/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | #import 28 | 29 | //! Project version number for QuickTableViewController. 30 | FOUNDATION_EXPORT double QuickTableViewControllerVersionNumber; 31 | 32 | //! Project version string for QuickTableViewController. 33 | FOUNDATION_EXPORT const unsigned char QuickTableViewControllerVersionString[]; 34 | 35 | // In this header, you should import all the public headers of your framework using statements like #import 36 | -------------------------------------------------------------------------------- /Example-tvOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example-tvOS 4 | // 5 | // Created by Ben on 19/04/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | @UIApplicationMain 30 | internal final class AppDelegate: UIResponder, UIApplicationDelegate { 31 | 32 | var window: UIWindow? 33 | 34 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 35 | window = UIWindow(frame: UIScreen.main.bounds) 36 | window?.rootViewController = UINavigationController(rootViewController: ExampleViewController()) 37 | window?.makeKeyAndVisible() 38 | return true 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Tests/CustomTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomTypes.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 30/12/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | @testable import QuickTableViewController 29 | 30 | internal final class CustomCell: UITableViewCell {} 31 | internal final class CustomSwitchCell: SwitchCell {} 32 | internal final class CustomTapActionCell: TapActionCell {} 33 | 34 | internal final class CustomNavigationRow: NavigationRow {} 35 | internal final class CustomSwitchRow: SwitchRow {} 36 | internal final class CustomTapActionRow: TapActionRow {} 37 | internal final class CustomOptionRow: OptionRow {} 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | test: unit-test ui-test 4 | 5 | unit-test: 6 | bundle exec rake \ 7 | "test:ios[QuickTableViewController-iOS]" \ 8 | "test:tvos[QuickTableViewController-tvOS]" \ 9 | 10 | ui-test: 11 | bundle exec rake \ 12 | "test:ios[Example-iOS]" \ 13 | "test:tvos[Example-tvOS]" 14 | 15 | ci-test: unit-test ui-test 16 | make -B carthage 17 | make -B docs 18 | 19 | bump: 20 | ifeq (,$(strip $(version))) 21 | # Usage: make bump version= 22 | else 23 | ruby -pi -e "gsub(%r{\d+\.\d+\.\d+}, %{"$(version)"})" QuickTableViewController.podspec 24 | ruby -pi -e "gsub(%r{\d+\.\d+\.\d+}, %{"$(version)"})" .jazzy.yml 25 | xcrun agvtool new-marketing-version $(version) 26 | endif 27 | 28 | carthage: 29 | set -o pipefail && carthage build --no-skip-current --verbose | bundle exec xcpretty -c 30 | 31 | coverage: 32 | slather coverage -s --input-format profdata --workspace QuickTableViewController.xcworkspace --scheme QuickTableViewController-iOS QuickTableViewController.xcodeproj 33 | 34 | docs: 35 | test -d docs || git clone -b gh-pages --single-branch https://github.com/bcylin/QuickTableViewController.git docs 36 | cd docs && git fetch origin gh-pages && git clean -f -d 37 | cd docs && git checkout gh-pages && git reset --hard origin/gh-pages 38 | bundle exec jazzy --config .jazzy.yml 39 | 40 | for file in "html" "css" "js" "json"; do \ 41 | echo "Cleaning whitespace in *."$$file ; \ 42 | find docs/output -name "*."$$file -exec sed -E -i "" -e "s/[[:blank:]]*$$//" {} \; ; \ 43 | done 44 | find docs -type f -execdir chmod 644 {} \; 45 | 46 | cp -rfv docs/output/* docs 47 | cd docs && \ 48 | git add . && \ 49 | git diff-index --quiet HEAD || \ 50 | git commit -m "[CI] Update documentation at $(shell date +'%Y-%m-%d %H:%M:%S %z')" 51 | 52 | preview-docs: 53 | make -B docs 54 | open docs/index.html 55 | 56 | update-docs: 57 | make -B docs 58 | cd docs && git push origin HEAD:gh-pages 59 | -------------------------------------------------------------------------------- /Tests/Model/SectionSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionTests.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 18/01/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class SectionTests: XCTestCase { 31 | 32 | func testInitialiation() { 33 | // Given 34 | let row = NavigationRow(text: "", detailText: .none) 35 | 36 | // When 37 | let section = Section(title: "title", rows: [row], footer: "footer") 38 | 39 | // Then it should have the given parameters 40 | XCTAssertEqual(section.title, "title") 41 | XCTAssertEqual(section.rows.count, 1) 42 | XCTAssertEqual(section.rows.first as? NavigationRow, row) 43 | XCTAssertEqual(section.footer, "footer") 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Source/Model/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Section.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// A class that represents a section in a table view. 30 | open class Section { 31 | 32 | // MARK: - Initializer 33 | 34 | /// Initializes a section with a nullable title, containing rows and an optional footer. 35 | public init(title: String?, rows: [Row & RowStyle], footer: String? = nil) { 36 | self.title = title 37 | self.rows = rows 38 | self.footer = footer 39 | } 40 | 41 | // MARK: - Properties 42 | 43 | /// The text of the section title. 44 | public let title: String? 45 | 46 | /// The array of rows in the section. 47 | open var rows: [Row & RowStyle] 48 | 49 | /// The text of the section footer. 50 | open var footer: String? 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Source/Protocol/Configurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configurable.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 30/07/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// Any type that conforms to this protocol is able to take `Row & RowStyle` as the configuration. 30 | public protocol Configurable { 31 | /// Configure the receiver with an instance that conforms to `Row & RowStyle`. 32 | func configure(with row: Row & RowStyle) 33 | } 34 | 35 | 36 | extension UITableViewCell { 37 | 38 | internal func defaultSetUp(with row: Row & RowStyle) { 39 | textLabel?.text = row.text 40 | detailTextLabel?.text = row.detailText?.text 41 | 42 | // Reset the accessory view in case the cell is reused. 43 | accessoryView = nil 44 | accessoryType = row.accessoryType 45 | 46 | imageView?.image = row.icon?.image 47 | imageView?.highlightedImage = row.icon?.highlightedImage 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Example-iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example-iOS 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | @UIApplicationMain 30 | internal final class AppDelegate: UIResponder, UIApplicationDelegate { 31 | 32 | var window: UIWindow? 33 | 34 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 35 | 36 | if #available(iOS 9.0, *) { 37 | // See AppearanceViewController for the setups. 38 | UILabel.appearance(whenContainedInInstancesOf: [UITableViewCell.self]).textColor = .blue 39 | } 40 | 41 | window = UIWindow(frame: UIScreen.main.bounds) 42 | window?.backgroundColor = UIColor.white 43 | window?.rootViewController = UINavigationController(rootViewController: RootViewController()) 44 | window?.makeKeyAndVisible() 45 | return true 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Source/Protocol/RowStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RowStyle.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 30/07/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// Any type that conforms to this protocol carries the info for the UI. 30 | public protocol RowStyle { 31 | 32 | /// The type of the table view cell to display the row. 33 | var cellType: UITableViewCell.Type { get } 34 | 35 | /// The reuse identifier of the table view cell to display the row. 36 | var cellReuseIdentifier: String { get } 37 | 38 | /// The style of the table view cell to display the row. 39 | var cellStyle: UITableViewCell.CellStyle { get } 40 | 41 | /// The icon of the row. 42 | var icon: Icon? { get } 43 | 44 | /// The type of standard accessory view the cell should use. 45 | var accessoryType: UITableViewCell.AccessoryType { get } 46 | 47 | /// The flag that indicates whether the table view cell should trigger the action when selected. 48 | var isSelectable: Bool { get } 49 | 50 | /// The additional customization during cell configuration. 51 | var customize: ((UITableViewCell, Row & RowStyle) -> Void)? { get } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Source/Protocol/RowCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RowCompatible.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 10/12/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// This protocol defines the compatible interface of a `NavigationRow` regardless of its associated cell type. 30 | public protocol NavigationRowCompatible: Row, RowStyle { 31 | #if os(iOS) 32 | /// A closure that will be invoked when the accessory button is selected. 33 | var accessoryButtonAction: ((Row) -> Void)? { get } 34 | #endif 35 | } 36 | 37 | 38 | /// This protocol defines the compatible interface of a `TapActionRow` regardless of its associated cell type. 39 | public protocol TapActionRowCompatible: Row, RowStyle {} 40 | 41 | 42 | /// This protocol defines the compatible interface of an `OptionRow` regardless of its associated cell type. 43 | public protocol OptionRowCompatible: Row, RowStyle { 44 | /// The state of selection. 45 | var isSelected: Bool { get set } 46 | } 47 | 48 | 49 | /// This protocol defines the compatible interface of a `SwitchRow` regardless of its associated cell type. 50 | public protocol SwitchRowCompatible: Row, RowStyle { 51 | /// The state of the switch. 52 | var switchValue: Bool { get set } 53 | } 54 | -------------------------------------------------------------------------------- /Source/Model/DetailText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailText.swift 3 | // QuickTableViewController 4 | // 5 | // Created by bcylin on 31/12/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// An enum that represents a detail text with `UITableViewCell.CellStyle`. 30 | public enum DetailText: Equatable { 31 | 32 | /// Does not show a detail text in `UITableViewCell.CellStyle.default`. 33 | case none 34 | /// Shows the detail text in `UITableViewCell.CellStyle.subtitle`. 35 | case subtitle(String) 36 | /// Shows the detail text in `UITableViewCell.CellStyle.value1`. 37 | case value1(String) 38 | /// Shows the detail text in `UITableViewCell.CellStyle.value2`. 39 | case value2(String) 40 | 41 | /// Returns the corresponding table view cell style. 42 | public var style: UITableViewCell.CellStyle { 43 | switch self { 44 | case .none: return .default 45 | case .subtitle: return .subtitle 46 | case .value1: return .value1 47 | case .value2: return .value2 48 | } 49 | } 50 | 51 | /// Returns the associated text of the case. 52 | public var text: String? { 53 | switch self { 54 | case .none: 55 | return nil 56 | case let .subtitle(text), let .value1(text), let .value2(text): 57 | return text 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Source/Model/Icon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icon.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A struct that represents the image used in a row. 30 | public enum Icon: Equatable { 31 | 32 | /// Icon with an image of the given name for the normal state. 33 | /// The "-highlighted" suffix is appended to the name for the highlighted image. 34 | case named(String) 35 | /// Icon with an image for the normal state. 36 | case image(UIImage) 37 | /// Icon with images for the normal and highlighted states. 38 | case images(normal: UIImage, highlighted: UIImage) 39 | 40 | /// The image for the normal state. 41 | public var image: UIImage? { 42 | switch self { 43 | case let .named(name): 44 | return UIImage(named: name) 45 | case let .image(image): 46 | return image 47 | case let .images(normal: image, highlighted: _): 48 | return image 49 | } 50 | } 51 | 52 | /// The image for the highlighted state. 53 | public var highlightedImage: UIImage? { 54 | switch self { 55 | case let .named(name): 56 | return UIImage(named: name + "-highlighted") 57 | case .image: 58 | return nil 59 | case let .images(normal: _, highlighted: image): 60 | return image 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Tests/ViewController/QuickTableViewControllerSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickTableViewControllerTests.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 21/01/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class QuickTableViewControllerTests: XCTestCase { 31 | 32 | func testInitialization() { 33 | // When 34 | let controller = QuickTableViewController() 35 | 36 | // Then it should set up table view with style 37 | XCTAssertEqual(controller.tableView.style, .grouped) 38 | } 39 | 40 | func testInitialization_withStyle() { 41 | // When 42 | let controller = QuickTableViewController(style: .plain) 43 | 44 | // Then it should set up table view with style 45 | XCTAssertEqual(controller.tableView.style, .plain) 46 | } 47 | 48 | func testViewConfiguration() { 49 | // Given 50 | let controller = QuickTableViewController(style: .grouped) 51 | 52 | // When 53 | let view = controller.view 54 | let tableView = controller.tableView 55 | 56 | // Than it should set up table view 57 | XCTAssert(try XCTUnwrap(view?.subviews.contains(tableView))) 58 | XCTAssertEqual(tableView.dataSource as? QuickTableViewController, controller) 59 | XCTAssertEqual(tableView.delegate as? QuickTableViewController, controller) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Example-iOS/UINibs/OptionCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Example-iOS/UINibs/SwitchCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Example-iOS/UINibs/TapActionCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Source/Protocol/Reusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reusable.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 21/08/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | extension UITableViewCell: Reusable {} 30 | 31 | 32 | internal protocol Reusable { 33 | static var reuseIdentifier: String { get } 34 | } 35 | 36 | 37 | internal extension Reusable { 38 | 39 | static var reuseIdentifier: String { 40 | let type = String(describing: self) 41 | return type.matches(of: String.typeDescriptionPattern).last ?? type 42 | } 43 | 44 | } 45 | 46 | 47 | internal extension String { 48 | 49 | static var typeDescriptionPattern: String { 50 | // For the types in the format of "(CustomCell in _B5334F301B8CC6AA00C64A6D)" 51 | return "^\\(([\\w\\d]+)\\sin\\s_[0-9A-F]+\\)$" 52 | } 53 | 54 | func matches(of pattern: String) -> [String] { 55 | let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) 56 | #if swift(>=3.2) 57 | let fullText = NSRange(location: 0, length: count) 58 | #else 59 | let fullText = NSRange(location: 0, length: characters.count) 60 | #endif 61 | 62 | guard let matches = regex?.matches(in: self, options: [], range: fullText) else { 63 | return [] 64 | } 65 | 66 | return matches.reduce([]) { accumulator, match in 67 | accumulator + (0..=4) 69 | return (self as NSString).substring(with: match.range(at: $0)) 70 | #else 71 | return (self as NSString).substring(with: match.rangeAt($0)) 72 | #endif 73 | } 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Example-iOS/ViewControllers/AppearanceViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppearanceViewController.swift 3 | // Example-iOS 4 | // 5 | // Created by Ben on 30/01/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import QuickTableViewController 29 | 30 | internal final class AppearanceViewController: QuickTableViewController { 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | title = "UIAppearance" 35 | 36 | // Register the nib files to the table view for UIAppearance customization (in AppDelegate) to work. 37 | tableView.register(UINib(nibName: "SwitchCell", bundle: .main), forCellReuseIdentifier: "SwitchCell") 38 | tableView.register(UINib(nibName: "TapActionCell", bundle: .main), forCellReuseIdentifier: "TapActionCell") 39 | tableView.register(UINib(nibName: "UITableViewCell", bundle: .main), forCellReuseIdentifier: "UITableViewCell.subtitle") 40 | tableView.register(UINib(nibName: "OptionCell", bundle: .main), forCellReuseIdentifier: "UITableViewCell") 41 | 42 | tableContents = [ 43 | Section(title: "Switch", rows: [ 44 | SwitchRow(text: "SwitchCell", switchValue: true, action: { _ in }) 45 | ]), 46 | 47 | Section(title: "Tap Action", rows: [ 48 | TapActionRow(text: "TapActionCell", action: { _ in }) 49 | ]), 50 | 51 | Section(title: "Navigation", rows: [ 52 | NavigationRow(text: "UITableViewCell", detailText: .subtitle(".subtitle"), action: { _ in }) 53 | ]), 54 | 55 | RadioSection(title: "Radio Buttons", options: [ 56 | OptionRow(text: "UITableViewCell", isSelected: true, action: { _ in }) 57 | ]) 58 | ] 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Tests/View/ReusableSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReusableTests.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 21/08/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class ReusableTests: XCTestCase { 31 | 32 | private class CustomCell: UITableViewCell {} 33 | 34 | private let pattern = String.typeDescriptionPattern 35 | 36 | func testTypeString_withInvalidPattern() { 37 | // Given 38 | let typeString = String(describing: type(of: self)) 39 | 40 | // When 41 | let matches = typeString.matches(of: "\\") 42 | 43 | // Then it should return an empty array 44 | XCTAssert(matches.isEmpty) 45 | } 46 | 47 | func testTypeString_withSpecialFormat() { 48 | // Given 49 | let typeString = "(CustomCell in _B5334F301B8CC6AA00C64A6D)" 50 | 51 | // When 52 | let matches = typeString.matches(of: pattern) 53 | 54 | // Then it should match the pattern 55 | XCTAssertEqual(matches.count, 2) 56 | } 57 | 58 | func testTypeString_withNameOnly() { 59 | // Given 60 | let typeString = "CustomCell" 61 | 62 | // When 63 | let matches = typeString.matches(of: pattern) 64 | 65 | // Then it should not match the pattern 66 | XCTAssert(matches.isEmpty) 67 | } 68 | 69 | func testReuseIdentifier_withCustomType() { 70 | // When 71 | let identifier = CustomCell.reuseIdentifier 72 | 73 | // Then 74 | XCTAssertEqual(identifier, "CustomCell") 75 | } 76 | 77 | func testReuseIdentifier_withTypeInModule() { 78 | // When 79 | let identifier = SwitchCell.reuseIdentifier 80 | 81 | // Then 82 | XCTAssertEqual(identifier, "SwitchCell") 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Tests/Row/TapActionRowSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapActionRowTests.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 17/01/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class TapActionRowTests: XCTestCase { 31 | 32 | // MARK: - Initialization 33 | 34 | func testInitialiation() { 35 | // Given 36 | var actionInvoked = false 37 | 38 | // When 39 | let row = TapActionRow(text: "title") { _ in actionInvoked = true } 40 | 41 | // Then it should have the given parameters 42 | XCTAssertEqual(row.text, "title") 43 | XCTAssertEqual(row.cellReuseIdentifier, "TapActionCell") 44 | 45 | // When 46 | row.action?(row) 47 | 48 | // Then 49 | XCTAssertEqual(actionInvoked, true) 50 | } 51 | 52 | // MARK: - Equatable 53 | 54 | func testEquatable_withIdenticalParameters() { 55 | // Given 56 | let one = TapActionRow(text: "Same", action: nil) 57 | 58 | // When 59 | let another = TapActionRow(text: "Same", action: nil) 60 | 61 | // Then 62 | XCTAssert(one == another) 63 | } 64 | 65 | func testEquatable_withDifferentTexts() { 66 | // Given 67 | let one = TapActionRow(text: "Same", action: nil) 68 | 69 | // When 70 | let another = TapActionRow(text: "Different", action: nil) 71 | 72 | // Then 73 | XCTAssert(one != another) 74 | } 75 | 76 | func testEquatable_withDifferentActions() { 77 | // Given 78 | let one = TapActionRow(text: "Same", action: nil) 79 | 80 | // When 81 | let another = TapActionRow(text: "Same", action: { _ in }) 82 | 83 | // Then it should be equal regardless of the actions attached 84 | XCTAssert(one == another) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /Example-iOS/ViewControllers/RootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.swift 3 | // Example-iOS 4 | // 5 | // Created by Ben on 30/01/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import QuickTableViewController 29 | 30 | internal final class RootViewController: QuickTableViewController { 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | let titleLabel = UILabel() 36 | titleLabel.text = "QuickTableViewController" 37 | titleLabel.font = UIFont.boldSystemFont(ofSize: 17) 38 | title = " " 39 | navigationItem.titleView = titleLabel 40 | 41 | tableContents = [ 42 | Section(title: "Default", rows: [ 43 | NavigationRow(text: "Use default cell types", detailText: .none, action: { [weak self] _ in 44 | self?.navigationController?.pushViewController(ExampleViewController(), animated: true) 45 | }) 46 | ]), 47 | 48 | Section(title: "Customization", rows: [ 49 | NavigationRow(text: "Use custom cell types", detailText: .none, action: { [weak self] _ in 50 | self?.navigationController?.pushViewController(CustomizationViewController(), animated: true) 51 | }) 52 | ]), 53 | 54 | Section(title: "UIAppearance", rows: [ 55 | NavigationRow(text: "UILabel customization", detailText: .none, action: { [weak self] _ in 56 | self?.navigationController?.pushViewController(AppearanceViewController(), animated: true) 57 | }) 58 | ]), 59 | 60 | Section(title: "Dynamic", rows: [ 61 | NavigationRow(text: "Dynamic Rows", detailText: .none, action: { [weak self] _ in 62 | self?.navigationController?.pushViewController(DynamicViewController(), animated: true) 63 | }) 64 | ]) 65 | ] 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Source/Views/TapActionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapActionCell.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 03/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A `UITableViewCell` subclass with the title text center aligned. 30 | open class TapActionCell: UITableViewCell { 31 | 32 | // TapActionCell on tvOS does not need customization. 33 | #if os(iOS) 34 | 35 | // MARK: - Initializer 36 | 37 | /** 38 | Overrides `UITableViewCell`'s designated initializer. 39 | 40 | - parameter style: Unused. It always uses `UITableViewCellStyle.default`. 41 | - parameter reuseIdentifier: A string used to identify the cell object if it is to be reused for drawing multiple rows of a table view. 42 | 43 | - returns: An initialized `TapActionCell` object. 44 | */ 45 | public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 46 | super.init(style: .default, reuseIdentifier: reuseIdentifier) 47 | setUpAppearance() 48 | } 49 | 50 | /** 51 | Overrides the designated initializer that returns an object initialized from data in a given unarchiver. 52 | 53 | - parameter aDecoder: An unarchiver object. 54 | 55 | - returns: `self`, initialized using the data in decoder. 56 | */ 57 | public required init?(coder aDecoder: NSCoder) { 58 | super.init(coder: aDecoder) 59 | setUpAppearance() 60 | } 61 | 62 | // MARK: UIView 63 | 64 | open override func tintColorDidChange() { 65 | super.tintColorDidChange() 66 | textLabel?.textColor = tintColor 67 | } 68 | 69 | // MARK: Private Methods 70 | 71 | private func setUpAppearance() { 72 | textLabel?.numberOfLines = 0 73 | textLabel?.textAlignment = .center 74 | textLabel?.textColor = tintColor 75 | } 76 | 77 | #endif 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Example-iOS/ViewControllers/DynamicTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicTableViewController.swift 3 | // Example-iOS 4 | // 5 | // Created by Zac on 30/01/2018. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import UIKit 27 | import QuickTableViewController 28 | 29 | internal final class DynamicViewController: QuickTableViewController { 30 | 31 | var dynamicRows: [Row & RowStyle] = [] 32 | 33 | private var cachedTableContents: [Section] = [] 34 | 35 | override var tableContents: [Section] { 36 | get { 37 | return cachedTableContents 38 | } 39 | set {} // swiftlint:disable:this unused_setter_value 40 | } 41 | 42 | private let quickTableView = QuickTableView(frame: .zero, style: .grouped) 43 | 44 | override var tableView: UITableView { 45 | get { 46 | return quickTableView 47 | } 48 | set {} // swiftlint:disable:this unused_setter_value 49 | } 50 | 51 | private func buildContents() -> [Section] { 52 | let rows: [Row & RowStyle] = [ 53 | TapActionRow(text: "AddCell", action: { [unowned self] _ in 54 | self.dynamicRows.append( 55 | NavigationRow(text: "UITableViewCell", detailText: .value1(String(describing: (self.dynamicRows.count + 1))), action: nil) 56 | ) 57 | self.tableView.insertRows(at: [IndexPath(row: self.dynamicRows.count, section: 0)], with: .automatic) 58 | }) 59 | ] + dynamicRows 60 | 61 | return [ 62 | Section(title: "Tap Action", rows: rows) 63 | ] 64 | } 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | title = "Dynamic" 69 | cachedTableContents = buildContents() 70 | quickTableView.quickDelegate = self 71 | } 72 | 73 | } 74 | 75 | extension DynamicViewController: QuickTableViewDelegate { 76 | func quickReload() { 77 | cachedTableContents = buildContents() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Source/Model/Subtitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Subtitle.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | @available(*, deprecated, message: "Use `DetailText` instead.") 30 | public enum Subtitle: Equatable { 31 | 32 | /// Does not show a subtitle as `UITableViewCellStyle.default`. 33 | case none 34 | /// Shows the associated text in `UITableViewCellStyle.subtitle`. 35 | case belowTitle(String) 36 | /// Shows the associated text in `UITableViewCellStyle.value1`. 37 | case rightAligned(String) 38 | /// Shows the associated text in `UITableViewCellStyle.value2`. 39 | case leftAligned(String) 40 | 41 | /// Returns the corresponding table view cell style. 42 | public var style: UITableViewCell.CellStyle { 43 | return detailText.style 44 | } 45 | 46 | /// Returns the associated text of the case. 47 | public var text: String? { 48 | return detailText.text 49 | } 50 | 51 | @available(*, deprecated, message: "The conversion between Subtitle and DetailText.") 52 | internal var detailText: DetailText { 53 | switch self { 54 | case .none: return .none 55 | case let .belowTitle(text): return .subtitle(text) 56 | case let .rightAligned(text): return .value1(text) 57 | case let .leftAligned(text): return .value2(text) 58 | } 59 | } 60 | 61 | } 62 | 63 | 64 | internal extension DetailText { 65 | 66 | @available(*, deprecated, message: "The conversion between DetailText and Subtitle.") 67 | var subtitle: Subtitle { 68 | switch self { 69 | case .none: return .none 70 | case let .subtitle(text): return .belowTitle(text) 71 | case let .value1(text): return .rightAligned(text) 72 | case let .value2(text): return .leftAligned(text) 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Example-iOS/UINibs/UITableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | ios_device = "iPhone 11" 4 | ios_version = "13.1" 5 | tvos_device = "Apple TV" 6 | tvos_version = "13.0" 7 | 8 | 9 | def xcodebuild(params) 10 | return ":" unless params[:scheme] 11 | [ 12 | %(xcodebuild), 13 | %(-workspace QuickTableViewController.xcworkspace), 14 | %(-scheme #{params[:scheme]}), 15 | %(-sdk #{params[:simulator]}), 16 | params[:destination], 17 | params[:action], 18 | %(| xcpretty -c && exit ${PIPESTATUS[0]}) 19 | ].reject(&:nil?).join " " 20 | end 21 | 22 | 23 | namespace :launch do 24 | desc "Launch iOS and tvOS simulators" 25 | task :simulators do 26 | sh %(xcrun instruments -w "#{ios_device} (#{ios_version}) [" || true) 27 | sh %(xcrun instruments -w "#{tvos_device} (#{tvos_version}) [" || true) 28 | end 29 | end 30 | 31 | 32 | namespace :build do 33 | desc "Build an iOS target with the specified scheme" 34 | task :ios, [:scheme] do |t, args| 35 | puts "Usage: rake 'build:ios[scheme]'" unless args[:scheme] 36 | 37 | sh xcodebuild(args.to_hash.merge({ 38 | action: "clean build analyze", 39 | simulator: "iphonesimulator" 40 | })) 41 | exit $?.exitstatus if not $?.success? 42 | end 43 | 44 | desc "Build a tvOS target with the specified scheme" 45 | task :tvos, [:scheme] do |t, args| 46 | puts "Usage: rake 'build:tvos[scheme]'" unless args[:scheme] 47 | 48 | sh xcodebuild(args.to_hash.merge({ 49 | action: "clean build analyze", 50 | simulator: "appletvsimulator" 51 | })) 52 | exit $?.exitstatus if not $?.success? 53 | end 54 | end 55 | 56 | 57 | namespace :test do 58 | desc "Run tests with the specified iOS scheme" 59 | task :ios, [:scheme] do |t, args| 60 | puts "Usage: rake 'test:ios[scheme]'" unless args[:scheme] 61 | 62 | sh xcodebuild(args.to_hash.merge({ 63 | action: "-enableCodeCoverage YES clean build test", 64 | destination: %(-destination "name=#{ios_device},OS=#{ios_version}"), 65 | simulator: "iphonesimulator" 66 | })) 67 | exit $?.exitstatus if not $?.success? 68 | end 69 | 70 | desc "Run tests with the specified tvOS scheme" 71 | task :tvos, [:scheme] do |t, args| 72 | puts "Usage: rake 'test:tvos[scheme]'" unless args[:scheme] 73 | 74 | sh xcodebuild(args.to_hash.merge({ 75 | action: "-enableCodeCoverage YES clean build test", 76 | destination: %(-destination "name=#{tvos_device},OS=#{tvos_version}"), 77 | simulator: "appletvsimulator" 78 | })) 79 | exit $?.exitstatus if not $?.success? 80 | end 81 | end 82 | 83 | 84 | desc "Bump versions" 85 | task :bump, [:version] do |t, args| 86 | version = args[:version] 87 | unless version 88 | puts "Usage: rake bump[version]" 89 | next 90 | end 91 | 92 | sh %(xcrun agvtool new-marketing-version #{version}) 93 | 94 | podspec = "QuickTableViewController.podspec" 95 | text = File.read podspec 96 | File.write podspec, text.gsub(%r(\"\d+\.\d+\.\d+\"), "\"#{version}\"") 97 | puts "Updated #{podspec} to #{version}" 98 | 99 | jazzy = ".jazzy.yml" 100 | text = File.read jazzy 101 | File.write jazzy, text.gsub(%r(:\s\d+\.\d+\.\d+), ": #{version}") 102 | puts "Updated #{jazzy} to #{version}" 103 | end 104 | -------------------------------------------------------------------------------- /Source/Rows/TapActionRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapActionRow.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A class that represents a row that triggers certain action when selected. 30 | open class TapActionRow: TapActionRowCompatible, Equatable { 31 | 32 | // MARK: - Initializer 33 | 34 | /// Initializes a `TapActionRow` with a text, an action closure, 35 | /// and an optional customization closure. 36 | public init( 37 | text: String, 38 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 39 | action: ((Row) -> Void)? 40 | ) { 41 | self.text = text 42 | self.customize = customization 43 | self.action = action 44 | } 45 | 46 | // MARK: - Row 47 | 48 | /// The text of the row. 49 | public let text: String 50 | 51 | /// The detail text is disabled in `TapActionRow`. 52 | public let detailText: DetailText? = nil 53 | 54 | /// A closure that will be invoked when the row is selected. 55 | public let action: ((Row) -> Void)? 56 | 57 | // MARK: - RowStyle 58 | 59 | /// The type of the table view cell to display the row. 60 | public let cellType: UITableViewCell.Type = T.self 61 | 62 | /// The reuse identifier of the table view cell to display the row. The default value is **TapActionCell**. 63 | public let cellReuseIdentifier: String = T.reuseIdentifier 64 | 65 | /// The cell style is `.default`. 66 | public let cellStyle: UITableViewCell.CellStyle = .default 67 | 68 | /// The default icon is nil. 69 | public let icon: Icon? = nil 70 | 71 | /// The default accessory type is `.none`. 72 | public let accessoryType: UITableViewCell.AccessoryType = .none 73 | 74 | /// The `TapActionRow` is selectable when action is not nil. 75 | public var isSelectable: Bool { 76 | return action != nil 77 | } 78 | 79 | /// The additional customization during cell configuration. 80 | public let customize: ((UITableViewCell, Row & RowStyle) -> Void)? 81 | 82 | // MARK: - Equatable 83 | 84 | /// Returns true iff `lhs` and `rhs` have equal titles and detail texts. 85 | public static func == (lhs: TapActionRow, rhs: TapActionRow) -> Bool { 86 | return 87 | lhs.text == rhs.text && 88 | lhs.detailText == rhs.detailText 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Example-iOS/ViewControllers/DynamicTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicTableView.swift 3 | // Example-iOS 4 | // 5 | // Created by Zac on 14/02/2020. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | import UIKit 28 | 29 | internal protocol QuickTableViewDelegate: class { 30 | func quickReload() 31 | } 32 | 33 | open class QuickTableView: UITableView { 34 | internal weak var quickDelegate: QuickTableViewDelegate? 35 | 36 | override open func reloadData() { 37 | self.quickDelegate?.quickReload() 38 | super.reloadData() 39 | } 40 | 41 | // MARK: Rows 42 | 43 | override open func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) { 44 | self.quickDelegate?.quickReload() 45 | super.reloadRows(at: indexPaths, with: animation) 46 | } 47 | 48 | override open func insertRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) { 49 | self.quickDelegate?.quickReload() 50 | super.insertRows(at: indexPaths, with: animation) 51 | } 52 | 53 | override open func deleteRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) { 54 | self.quickDelegate?.quickReload() 55 | super.deleteRows(at: indexPaths, with: animation) 56 | } 57 | 58 | override open func moveRow(at indexPath: IndexPath, to newIndexPath: IndexPath) { 59 | self.quickDelegate?.quickReload() 60 | super.moveRow(at: indexPath, to: newIndexPath) 61 | } 62 | 63 | // MARK: Sections 64 | 65 | override open func reloadSections(_ sections: IndexSet, with animation: UITableView.RowAnimation) { 66 | self.quickDelegate?.quickReload() 67 | super.reloadSections(sections, with: animation) 68 | } 69 | 70 | override open func deleteSections(_ sections: IndexSet, with animation: UITableView.RowAnimation) { 71 | self.quickDelegate?.quickReload() 72 | super.deleteSections(sections, with: animation) 73 | } 74 | 75 | override open func insertSections(_ sections: IndexSet, with animation: UITableView.RowAnimation) { 76 | self.quickDelegate?.quickReload() 77 | super.insertSections(sections, with: animation) 78 | } 79 | 80 | override open func reloadSectionIndexTitles() { 81 | self.quickDelegate?.quickReload() 82 | super.reloadSectionIndexTitles() 83 | } 84 | 85 | override open func moveSection(_ section: Int, toSection newSection: Int) { 86 | self.quickDelegate?.quickReload() 87 | super.moveSection(section, toSection: newSection) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/Model/IconSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconTests.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 17/01/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class IconTests: XCTestCase { 31 | 32 | private let image = UIImage(named: "icon", in: Bundle(for: IconTests.self), compatibleWith: nil)! 33 | private let highlighted = UIImage(named: "icon-highlighted", in: Bundle(for: IconTests.self), compatibleWith: nil)! 34 | 35 | // MARK: - Initialization 36 | 37 | func testInitialiation() { 38 | // Given 39 | let image = self.image 40 | 41 | // When 42 | let icon = Icon.image(image) 43 | 44 | // Then 45 | XCTAssertEqual(icon.image, image) 46 | XCTAssertNil(icon.highlightedImage) 47 | } 48 | 49 | // MARK: - Equatable 50 | 51 | func testEquatable_withIdenticalParameters() { 52 | // Given 53 | let one = Icon.image(image) 54 | 55 | // When 56 | let another = Icon.image(image) 57 | 58 | // Then 59 | XCTAssert(one == another) 60 | } 61 | 62 | func testEquatable_withDifferentImages() { 63 | // Given 64 | let one = Icon.image(image) 65 | 66 | // When 67 | let another = Icon.image(highlighted) 68 | 69 | // Then 70 | XCTAssert(one != another) 71 | } 72 | 73 | func testEquatable_withDifferentHighlightedImages() { 74 | // Given 75 | let one = Icon.images(normal: image, highlighted: highlighted) 76 | 77 | // When 78 | let another = Icon.images(normal: image, highlighted: UIImage()) 79 | 80 | // Then 81 | XCTAssert(one != another) 82 | } 83 | 84 | func testEquatable_withDifferentImageSpecifications() { 85 | // Given 86 | let one = Icon.image(image) 87 | 88 | // When 89 | let another = Icon.named("image") 90 | 91 | // Then 92 | XCTAssert(one != another) 93 | } 94 | 95 | func testEquatable_withIdenticalImageNames() { 96 | // Given 97 | let one = Icon.named("Same") 98 | 99 | // When 100 | let another = Icon.named("Same") 101 | 102 | // Then 103 | XCTAssert(one == another) 104 | } 105 | 106 | func testEquatable_withDifferentImageNames() { 107 | // Given 108 | let one = Icon.named("Same") 109 | 110 | // When 111 | let another = Icon.named("Different") 112 | 113 | // Then 114 | XCTAssert(one != another) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Example-iOS/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Tests/Model/DetailTextSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailTextTests.swift 3 | // QuickTableViewController 4 | // 5 | // Created by bcylin on 31/12/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class DetailTextTests: XCTestCase { 31 | 32 | func testCellStyle() { 33 | // It should return the descriptive name of the style 34 | XCTAssertEqual(DetailText.none.style, .default) 35 | XCTAssertEqual(DetailText.subtitle("text").style, .subtitle) 36 | XCTAssertEqual(DetailText.value1("text").style, .value1) 37 | XCTAssertEqual(DetailText.value2("text").style, .value2) 38 | } 39 | 40 | func testAssocitatedValue() { 41 | // It should return the associated text 42 | XCTAssertNil(DetailText.none.text) 43 | XCTAssertEqual(DetailText.subtitle("3").text, "3") 44 | XCTAssertEqual(DetailText.value1("1").text, "1") 45 | XCTAssertEqual(DetailText.value2("2").text, "2") 46 | } 47 | 48 | // MARK: - Equatable 49 | 50 | func testEquatableNone() { 51 | // Given 52 | let a = DetailText.none 53 | let b = DetailText.none 54 | 55 | // Then it should be equal when both are .none 56 | XCTAssert(a == b) 57 | } 58 | 59 | func testEquatableSubtitle() { 60 | // Given 61 | let a = DetailText.subtitle("Same") 62 | let b = DetailText.subtitle("Same") 63 | let c = DetailText.subtitle("Different") 64 | let d = DetailText.value1("Same") 65 | let e = DetailText.none 66 | 67 | // Then it should be equal only when both type and associated value match 68 | XCTAssert(a == b) 69 | XCTAssert(a != c) 70 | XCTAssert(a != d) 71 | XCTAssert(a != e) 72 | } 73 | 74 | func testEquatableValue1() { 75 | // Given 76 | let a = DetailText.value1("Same") 77 | let b = DetailText.value1("Same") 78 | let c = DetailText.value1("Different") 79 | let d = DetailText.value2("Same") 80 | let e = DetailText.none 81 | 82 | // Then it should be equal only when both type and associated value match 83 | XCTAssert(a == b) 84 | XCTAssert(a != c) 85 | XCTAssert(a != d) 86 | XCTAssert(a != e) 87 | } 88 | 89 | func testEquatableValue2() { 90 | // Given 91 | let a = DetailText.value2("Same") 92 | let b = DetailText.value2("Same") 93 | let c = DetailText.value2("Different") 94 | let d = DetailText.subtitle("Same") 95 | let e = DetailText.none 96 | 97 | // Then it should be equal only when both type and associated value match 98 | XCTAssert(a == b) 99 | XCTAssert(a != c) 100 | XCTAssert(a != d) 101 | XCTAssert(a != e) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Example-tvOS/ExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleViewController.swift 3 | // Example-tvOS 4 | // 5 | // Created by Ben on 19/04/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import QuickTableViewController 29 | 30 | internal final class ExampleViewController: QuickTableViewController { 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | title = "QuickTableViewController" 35 | 36 | tableContents = [ 37 | Section(title: "Switch", rows: [ 38 | SwitchRow(text: "Setting 1", switchValue: true, action: showLog()), 39 | SwitchRow(text: "Setting 2", switchValue: false, action: showLog()) 40 | ]), 41 | 42 | Section(title: "Tap Action", rows: [ 43 | TapActionRow(text: "Tap action", action: showAlert()) 44 | ]), 45 | 46 | Section(title: "Navigation", rows: [ 47 | NavigationRow(text: "CellStyle.default", detailText: .none, action: showDetail()), 48 | NavigationRow(text: "CellStyle", detailText: .subtitle(".subtitle"), action: showDetail()), 49 | NavigationRow(text: "CellStyle", detailText: .value1(".value1")), 50 | NavigationRow(text: "CellStyle", detailText: .value2(".value2")) 51 | ]), 52 | 53 | RadioSection(title: "Radio Buttons", options: [ 54 | OptionRow(text: "Option 1", isSelected: true, action: showLog()), 55 | OptionRow(text: "Option 2", isSelected: false, action: showLog()), 56 | OptionRow(text: "Option 3", isSelected: false, action: showLog()) 57 | ], footer: "See RadioSection for more details.") 58 | ] 59 | } 60 | 61 | // MARK: - Private 62 | 63 | private func showAlert() -> (Row) -> Void { 64 | return { [weak self] row in 65 | let alert = UIAlertController( 66 | title: row.text, 67 | message: row.detailText.flatMap({ $0.text }), 68 | preferredStyle: .alert 69 | ) 70 | alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) 71 | self?.present(alert, animated: true, completion: nil) 72 | } 73 | } 74 | 75 | private func showDetail() -> (Row) -> Void { 76 | return { [weak self] row in 77 | let controller = UIViewController() 78 | controller.title = row.text + (row.detailText?.text ?? "") 79 | self?.navigationController?.pushViewController(controller, animated: true) 80 | } 81 | } 82 | 83 | private func showLog() -> (Row) -> Void { 84 | return { 85 | switch $0 { 86 | case let row as SwitchRowCompatible: 87 | print("\(row.text) = \(row.switchValue)") 88 | case let option as OptionRowCompatible where option.isSelected: 89 | print("\(option.text) is selected") 90 | default: 91 | break 92 | } 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /Source/Rows/OptionRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionRow.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 30/07/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A class that represents a row of selectable option. 30 | open class OptionRow: OptionRowCompatible, Equatable { 31 | 32 | // MARK: - Initializer 33 | 34 | /// Initializes an `OptionRow` with a text, a selection state and an action closure. 35 | /// The detail text, icon, and the customization closure are optional. 36 | public init( 37 | text: String, 38 | detailText: DetailText? = nil, 39 | isSelected: Bool, 40 | icon: Icon? = nil, 41 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 42 | action: ((Row) -> Void)? 43 | ) { 44 | self.text = text 45 | self.detailText = detailText 46 | self.isSelected = isSelected 47 | self.icon = icon 48 | self.customize = customization 49 | self.action = action 50 | } 51 | 52 | // MARK: - OptionRowCompatible 53 | 54 | /// The state of selection. 55 | public var isSelected: Bool = false { 56 | didSet { 57 | guard isSelected != oldValue else { 58 | return 59 | } 60 | DispatchQueue.main.async { 61 | self.action?(self) 62 | } 63 | } 64 | } 65 | 66 | // MARK: - Row 67 | 68 | /// The text of the row. 69 | public let text: String 70 | 71 | /// The detail text of the row. 72 | public let detailText: DetailText? 73 | 74 | /// A closure that will be invoked when the `isSelected` is changed. 75 | public let action: ((Row) -> Void)? 76 | 77 | // MARK: - RowStyle 78 | 79 | /// The type of the table view cell to display the row. 80 | public let cellType: UITableViewCell.Type = T.self 81 | 82 | /// The reuse identifier of the table view cell to display the row. The default value is **UITableViewCell**. 83 | public let cellReuseIdentifier: String = T.reuseIdentifier 84 | 85 | /// The cell style is `.default`. 86 | public let cellStyle: UITableViewCell.CellStyle = .default 87 | 88 | /// The icon of the row. 89 | public let icon: Icon? 90 | 91 | /// Returns `.checkmark` when the row is selected, otherwise returns `.none`. 92 | public var accessoryType: UITableViewCell.AccessoryType { 93 | return isSelected ? .checkmark : .none 94 | } 95 | 96 | /// `OptionRow` is always selectable. 97 | public let isSelectable: Bool = true 98 | 99 | /// Additional customization during cell configuration. 100 | public let customize: ((UITableViewCell, Row & RowStyle) -> Void)? 101 | 102 | // MARK: - Equatable 103 | 104 | /// Returns true iff `lhs` and `rhs` have equal titles, detail texts, selection states, and icons. 105 | public static func == (lhs: OptionRow, rhs: OptionRow) -> Bool { 106 | return 107 | lhs.text == rhs.text && 108 | lhs.detailText == rhs.detailText && 109 | lhs.isSelected == rhs.isSelected && 110 | lhs.icon == rhs.icon 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Source/Model/Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deprecated.swift 3 | // QuickTableViewController 4 | // 5 | // Created by bcylin on 01/01/2019. 6 | // Copyright © 2019 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | public extension Row { 30 | 31 | @available(*, deprecated, message: "Use `text` instead.") 32 | var title: String { 33 | return text 34 | } 35 | 36 | @available(*, deprecated, message: "Use `detailText` instead.") 37 | var subtitle: Subtitle? { 38 | return detailText?.subtitle 39 | } 40 | 41 | } 42 | 43 | //////////////////////////////////////////////////////////////////////////////// 44 | 45 | public extension NavigationRow { 46 | 47 | @available(*, deprecated, message: "Use `init(text:detailText:icon:customization:action:)` instead.") 48 | convenience init( 49 | title: String, 50 | subtitle: Subtitle, 51 | icon: Icon? = nil, 52 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 53 | action: ((Row) -> Void)? = nil 54 | ) { 55 | self.init( 56 | text: title, 57 | detailText: subtitle.detailText, 58 | icon: icon, 59 | customization: customization, 60 | action: action 61 | ) 62 | } 63 | 64 | } 65 | 66 | //////////////////////////////////////////////////////////////////////////////// 67 | 68 | public extension OptionRow { 69 | 70 | @available(*, deprecated, message: "Use `init(text:detailText:isSelected:icon:customization:action:)` instead.") 71 | convenience init( 72 | title: String, 73 | isSelected: Bool, 74 | icon: Icon? = nil, 75 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 76 | action: ((Row) -> Void)? 77 | ) { 78 | self.init( 79 | text: title, 80 | detailText: nil, 81 | isSelected: isSelected, 82 | icon: icon, 83 | customization: customization, 84 | action: action 85 | ) 86 | } 87 | 88 | } 89 | 90 | //////////////////////////////////////////////////////////////////////////////// 91 | 92 | public extension SwitchRow { 93 | 94 | @available(*, deprecated, message: "Use `init(text:detailText:switchValue:icon:customization:action:)` instead.") 95 | convenience init( 96 | title: String, 97 | switchValue: Bool, 98 | icon: Icon? = nil, 99 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 100 | action: ((Row) -> Void)? 101 | ) { 102 | self.init( 103 | text: title, 104 | detailText: nil, 105 | switchValue: switchValue, 106 | icon: icon, 107 | customization: customization, 108 | action: action 109 | ) 110 | } 111 | 112 | } 113 | 114 | //////////////////////////////////////////////////////////////////////////////// 115 | 116 | public extension TapActionRow { 117 | 118 | @available(*, deprecated, message: "Use `init(text:customization:action:)` instead.") 119 | convenience init( 120 | title: String, 121 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 122 | action: ((Row) -> Void)? 123 | ) { 124 | self.init( 125 | text: title, 126 | customization: customization, 127 | action: action 128 | ) 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /QuickTableViewController.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /QuickTableViewController.xcodeproj/xcshareddata/xcschemes/Example-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.1) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | claide (1.0.3) 17 | claide-plugins (0.9.2) 18 | cork 19 | nap 20 | open4 (~> 1.3) 21 | cocoapods (1.8.4) 22 | activesupport (>= 4.0.2, < 5) 23 | claide (>= 1.0.2, < 2.0) 24 | cocoapods-core (= 1.8.4) 25 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 26 | cocoapods-downloader (>= 1.2.2, < 2.0) 27 | cocoapods-plugins (>= 1.0.0, < 2.0) 28 | cocoapods-search (>= 1.0.0, < 2.0) 29 | cocoapods-stats (>= 1.0.0, < 2.0) 30 | cocoapods-trunk (>= 1.4.0, < 2.0) 31 | cocoapods-try (>= 1.1.0, < 2.0) 32 | colored2 (~> 3.1) 33 | escape (~> 0.0.4) 34 | fourflusher (>= 2.3.0, < 3.0) 35 | gh_inspector (~> 1.0) 36 | molinillo (~> 0.6.6) 37 | nap (~> 1.0) 38 | ruby-macho (~> 1.4) 39 | xcodeproj (>= 1.11.1, < 2.0) 40 | cocoapods-core (1.8.4) 41 | activesupport (>= 4.0.2, < 6) 42 | algoliasearch (~> 1.0) 43 | concurrent-ruby (~> 1.1) 44 | fuzzy_match (~> 2.0.4) 45 | nap (~> 1.0) 46 | cocoapods-deintegrate (1.0.4) 47 | cocoapods-downloader (1.3.0) 48 | cocoapods-plugins (1.0.0) 49 | nap 50 | cocoapods-search (1.0.0) 51 | cocoapods-stats (1.1.0) 52 | cocoapods-trunk (1.4.1) 53 | nap (>= 0.8, < 2.0) 54 | netrc (~> 0.11) 55 | cocoapods-try (1.1.0) 56 | colored2 (3.1.2) 57 | concurrent-ruby (1.1.6) 58 | cork (0.3.0) 59 | colored2 (~> 3.1) 60 | danger (6.2.2) 61 | claide (~> 1.0) 62 | claide-plugins (>= 0.9.2) 63 | colored2 (~> 3.1) 64 | cork (~> 0.1) 65 | faraday (~> 0.9) 66 | faraday-http-cache (~> 2.0) 67 | git (~> 1.6) 68 | kramdown (~> 2.0) 69 | kramdown-parser-gfm (~> 1.0) 70 | no_proxy_fix 71 | octokit (~> 4.7) 72 | terminal-table (~> 1) 73 | escape (0.0.4) 74 | faraday (0.17.3) 75 | multipart-post (>= 1.2, < 3) 76 | faraday-http-cache (2.0.0) 77 | faraday (~> 0.8) 78 | ffi (1.12.2) 79 | fourflusher (2.3.1) 80 | fuzzy_match (2.0.4) 81 | gh_inspector (1.1.3) 82 | git (1.6.0) 83 | rchardet (~> 1.8) 84 | httpclient (2.8.3) 85 | i18n (0.9.5) 86 | concurrent-ruby (~> 1.0) 87 | jazzy (0.13.1) 88 | cocoapods (~> 1.5) 89 | mustache (~> 1.1) 90 | open4 91 | redcarpet (~> 3.4) 92 | rouge (>= 2.0.6, < 4.0) 93 | sassc (~> 2.1) 94 | sqlite3 (~> 1.3) 95 | xcinvoke (~> 0.3.0) 96 | json (2.3.0) 97 | kramdown (2.1.0) 98 | kramdown-parser-gfm (1.1.0) 99 | kramdown (~> 2.0) 100 | liferaft (0.0.6) 101 | minitest (5.14.0) 102 | molinillo (0.6.6) 103 | multipart-post (2.1.1) 104 | mustache (1.1.1) 105 | nanaimo (0.2.6) 106 | nap (1.1.0) 107 | netrc (0.11.0) 108 | no_proxy_fix (0.1.2) 109 | octokit (4.16.0) 110 | faraday (>= 0.9) 111 | sawyer (~> 0.8.0, >= 0.5.3) 112 | open4 (1.3.4) 113 | public_suffix (4.0.3) 114 | rake (13.0.1) 115 | rchardet (1.8.0) 116 | redcarpet (3.5.0) 117 | rouge (2.0.7) 118 | ruby-macho (1.4.0) 119 | sassc (2.2.1) 120 | ffi (~> 1.9) 121 | sawyer (0.8.2) 122 | addressable (>= 2.3.5) 123 | faraday (> 0.8, < 2.0) 124 | sqlite3 (1.4.2) 125 | terminal-table (1.8.0) 126 | unicode-display_width (~> 1.1, >= 1.1.1) 127 | thread_safe (0.3.6) 128 | tzinfo (1.2.6) 129 | thread_safe (~> 0.1) 130 | unicode-display_width (1.6.1) 131 | xcinvoke (0.3.0) 132 | liferaft (~> 0.0.6) 133 | xcodeproj (1.15.0) 134 | CFPropertyList (>= 2.3.3, < 4.0) 135 | atomos (~> 0.1.3) 136 | claide (>= 1.0.2, < 2.0) 137 | colored2 (~> 3.1) 138 | nanaimo (~> 0.2.6) 139 | xcpretty (0.3.0) 140 | rouge (~> 2.0.7) 141 | 142 | PLATFORMS 143 | ruby 144 | 145 | DEPENDENCIES 146 | cocoapods 147 | danger 148 | jazzy (>= 0.9.0) 149 | rake 150 | xcpretty 151 | 152 | BUNDLED WITH 153 | 2.0.2 154 | -------------------------------------------------------------------------------- /Source/Model/RadioSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadioSection.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 17/08/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// A section that allows only one option selected in a table view. 30 | open class RadioSection: Section { 31 | 32 | // MARK: - Initializer 33 | 34 | /// Initializes a section with a title, containing rows and an optional footer. 35 | public init(title: String?, options: [OptionRowCompatible], footer: String? = nil) { 36 | self.options = options 37 | super.init(title: title, rows: [], footer: footer) 38 | } 39 | 40 | private override init(title: String?, rows: [Row & RowStyle], footer: String? = nil) { 41 | fatalError("init with title, rows, and footer is not supported") 42 | } 43 | 44 | // MARK: - Section 45 | 46 | /// The array of rows in the section. 47 | open override var rows: [Row & RowStyle] { 48 | get { 49 | return options 50 | } 51 | set { 52 | options = newValue as? [OptionRowCompatible] ?? options 53 | } 54 | } 55 | 56 | // MARK: - RadioSection 57 | 58 | /// A boolean that indicates whether there's always one option selected. 59 | open var alwaysSelectsOneOption: Bool = false { 60 | didSet { 61 | if alwaysSelectsOneOption && selectedOption == nil { 62 | options.first?.isSelected = true 63 | } 64 | } 65 | } 66 | 67 | /// The array of options in the section. It's identical to the `rows`. 68 | open private(set) var options: [OptionRowCompatible] 69 | 70 | /// Returns the selected index, or nil when nothing is selected. 71 | open var indexOfSelectedOption: Int? { 72 | return options.firstIndex { $0.isSelected } 73 | } 74 | 75 | /// Returns the selected option row, or nil when nothing is selected. 76 | open var selectedOption: OptionRowCompatible? { 77 | if let index = indexOfSelectedOption { 78 | return options[index] 79 | } else { 80 | return nil 81 | } 82 | } 83 | 84 | /// Toggle the selection of the given option and keep options mutually exclusive. 85 | /// If `alwaysSelectOneOption` is set to true, it will not deselect the current selection. 86 | /// 87 | /// - Parameter option: The option to flip the `isSelected` state. 88 | /// - Returns: The indexes of changed options. 89 | open func toggle(_ option: OptionRowCompatible) -> IndexSet { 90 | if option.isSelected && alwaysSelectsOneOption { 91 | return [] 92 | } 93 | 94 | defer { 95 | option.isSelected = !option.isSelected 96 | } 97 | 98 | if option.isSelected { 99 | // Deselect the selected option. 100 | return options.firstIndex(where: { $0 === option }).map { [$0] } ?? [] 101 | } 102 | 103 | var toggledIndexes: IndexSet = [] 104 | 105 | for (index, element) in options.enumerated() { 106 | switch element { 107 | case let target where target === option: 108 | toggledIndexes.insert(index) 109 | case _ where element.isSelected: 110 | toggledIndexes.insert(index) 111 | element.isSelected = false 112 | default: 113 | break 114 | } 115 | } 116 | 117 | return toggledIndexes 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /QuickTableViewController.xcodeproj/xcshareddata/xcschemes/QuickTableViewController-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Source/Rows/SwitchRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwitchRow.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A class that represents a row with a switch. 30 | open class SwitchRow: SwitchRowCompatible, Equatable { 31 | 32 | // MARK: - Initializer 33 | 34 | /// Initializes a `SwitchRow` with a title, a switch state and an action closure. 35 | /// The detail text, icon and the customization closure are optional. 36 | public init( 37 | text: String, 38 | detailText: DetailText? = nil, 39 | switchValue: Bool, 40 | icon: Icon? = nil, 41 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 42 | action: ((Row) -> Void)? 43 | ) { 44 | self.text = text 45 | self.detailText = detailText 46 | self.switchValue = switchValue 47 | self.icon = icon 48 | self.customize = customization 49 | self.action = action 50 | } 51 | 52 | // MARK: - SwitchRowCompatible 53 | 54 | /// The state of the switch. 55 | public var switchValue: Bool = false { 56 | didSet { 57 | guard switchValue != oldValue else { 58 | return 59 | } 60 | DispatchQueue.main.async { 61 | self.action?(self) 62 | } 63 | } 64 | } 65 | 66 | // MARK: - Row 67 | 68 | /// The text of the row. 69 | public let text: String 70 | 71 | /// The detail text of the row. 72 | public let detailText: DetailText? 73 | 74 | /// A closure that will be invoked when the `switchValue` is changed. 75 | public let action: ((Row) -> Void)? 76 | 77 | // MARK: - RowStyle 78 | 79 | /// The type of the table view cell to display the row. 80 | public let cellType: UITableViewCell.Type = T.self 81 | 82 | /// The reuse identifier of the table view cell to display the row. The default value is **SwitchCell**. 83 | public let cellReuseIdentifier: String = T.reuseIdentifier 84 | 85 | /// The cell style is `.default`. 86 | public let cellStyle: UITableViewCell.CellStyle = .default 87 | 88 | /// The icon of the row. 89 | public let icon: Icon? 90 | 91 | #if os(iOS) 92 | 93 | /// The default accessory type is `.none`. 94 | public let accessoryType: UITableViewCell.AccessoryType = .none 95 | 96 | /// The `SwitchRow` should not be selectable. 97 | public let isSelectable: Bool = false 98 | 99 | #elseif os(tvOS) 100 | 101 | /// Returns `.checkmark` when the `switchValue` is on, otherwise returns `.none`. 102 | public var accessoryType: UITableViewCell.AccessoryType { 103 | return switchValue ? .checkmark : .none 104 | } 105 | 106 | /// The `SwitchRow` is selectable on tvOS. 107 | public let isSelectable: Bool = true 108 | 109 | #endif 110 | 111 | /// The additional customization during cell configuration. 112 | public let customize: ((UITableViewCell, Row & RowStyle) -> Void)? 113 | 114 | // MARK: - Equatable 115 | 116 | /// Returns true iff `lhs` and `rhs` have equal titles, detail texts, switch values, and icons. 117 | public static func == (lhs: SwitchRow, rhs: SwitchRow) -> Bool { 118 | return 119 | lhs.text == rhs.text && 120 | lhs.detailText == rhs.detailText && 121 | lhs.switchValue == rhs.switchValue && 122 | lhs.icon == rhs.icon 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /Source/Views/SwitchCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwitchCell.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 03/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// The `SwitchCellDelegate` protocol allows the adopting delegate to respond to the UI interaction. Not available on tvOS. 30 | @available(tvOS, unavailable, message: "SwitchCellDelegate is not available on tvOS.") 31 | public protocol SwitchCellDelegate: class { 32 | /// Tells the delegate that the switch control is toggled. 33 | func switchCell(_ cell: SwitchCell, didToggleSwitch isOn: Bool) 34 | } 35 | 36 | 37 | /// A `UITableViewCell` subclass that shows a `UISwitch` as the `accessoryView`. 38 | open class SwitchCell: UITableViewCell, Configurable { 39 | 40 | #if os(iOS) 41 | 42 | /// A `UISwitch` as the `accessoryView`. Not available on tvOS. 43 | @available(tvOS, unavailable, message: "switchControl is not available on tvOS.") 44 | public private(set) lazy var switchControl: UISwitch = { 45 | let control = UISwitch() 46 | control.addTarget(self, action: #selector(SwitchCell.didToggleSwitch(_:)), for: .valueChanged) 47 | return control 48 | }() 49 | 50 | #endif 51 | 52 | /// The switch cell's delegate object, which should conform to `SwitchCellDelegate`. Not available on tvOS. 53 | @available(tvOS, unavailable, message: "SwitchCellDelegate is not available on tvOS.") 54 | open weak var delegate: SwitchCellDelegate? 55 | 56 | // MARK: - Initializer 57 | 58 | /** 59 | Overrides `UITableViewCell`'s designated initializer. 60 | 61 | - parameter style: A constant indicating a cell style. 62 | - parameter reuseIdentifier: A string used to identify the cell object if it is to be reused for drawing multiple rows of a table view. 63 | 64 | - returns: An initialized `SwitchCell` object. 65 | */ 66 | public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 67 | super.init(style: style, reuseIdentifier: reuseIdentifier) 68 | setUpAppearance() 69 | } 70 | 71 | /** 72 | Overrides the designated initializer that returns an object initialized from data in a given unarchiver. 73 | 74 | - parameter aDecoder: An unarchiver object. 75 | 76 | - returns: `self`, initialized using the data in decoder. 77 | */ 78 | public required init?(coder aDecoder: NSCoder) { 79 | super.init(coder: aDecoder) 80 | setUpAppearance() 81 | } 82 | 83 | // MARK: - Configurable 84 | 85 | /// Set up the switch control (iOS) or accessory type (tvOS) with the provided row. 86 | open func configure(with row: Row & RowStyle) { 87 | #if os(iOS) 88 | if let row = row as? SwitchRowCompatible { 89 | switchControl.isOn = row.switchValue 90 | } 91 | accessoryView = switchControl 92 | #elseif os(tvOS) 93 | accessoryView = nil 94 | accessoryType = row.accessoryType 95 | #endif 96 | } 97 | 98 | // MARK: - Private 99 | 100 | @available(tvOS, unavailable, message: "UISwitch is not available on tvOS.") 101 | @objc 102 | private func didToggleSwitch(_ sender: UISwitch) { 103 | delegate?.switchCell(self, didToggleSwitch: sender.isOn) 104 | } 105 | 106 | private func setUpAppearance() { 107 | textLabel?.numberOfLines = 0 108 | detailTextLabel?.numberOfLines = 0 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Tests/View/ConfigurableSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurableTests.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 27/12/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class ConfigurableTests: XCTestCase { 31 | 32 | func testConfiguration_default() { 33 | do { 34 | // Given 35 | let cell = SwitchCell() 36 | let row = SwitchRow(text: "", switchValue: true, action: nil) 37 | 38 | // When 39 | cell.configure(with: row) 40 | 41 | // Then 42 | #if os(iOS) 43 | XCTAssertEqual(cell.accessoryView, cell.switchControl) 44 | XCTAssertEqual(cell.switchControl.isOn, true) 45 | #elseif os(tvOS) 46 | XCTAssertNil(cell.accessoryView) 47 | XCTAssertEqual(cell.accessoryType, .checkmark) 48 | #endif 49 | } 50 | 51 | do { 52 | // Given 53 | let cell = SwitchCell() 54 | let row = SwitchRow(text: "", switchValue: false, action: nil) 55 | 56 | // When 57 | cell.configure(with: row) 58 | 59 | // Then 60 | #if os(iOS) 61 | XCTAssertEqual(cell.accessoryView, cell.switchControl) 62 | XCTAssertEqual(cell.switchControl.isOn, false) 63 | #elseif os(tvOS) 64 | XCTAssertNil(cell.accessoryView) 65 | XCTAssertEqual(cell.accessoryType, .none) 66 | #endif 67 | } 68 | } 69 | 70 | func testConfiguration_customRow() { 71 | do { 72 | // Given 73 | let cell = SwitchCell() 74 | let row = CustomSwitchRow(text: "", switchValue: true, action: nil) 75 | 76 | // When 77 | cell.configure(with: row) 78 | 79 | // Then 80 | #if os(iOS) 81 | XCTAssertEqual(cell.accessoryView, cell.switchControl) 82 | XCTAssertEqual(cell.switchControl.isOn, true) 83 | #elseif os(tvOS) 84 | XCTAssertNil(cell.accessoryView) 85 | XCTAssertEqual(cell.accessoryType, .checkmark) 86 | #endif 87 | } 88 | 89 | do { 90 | // Given 91 | let cell = SwitchCell() 92 | let row = CustomSwitchRow(text: "", switchValue: false, action: nil) 93 | 94 | // When 95 | cell.configure(with: row) 96 | 97 | // Then 98 | #if os(iOS) 99 | XCTAssertEqual(cell.accessoryView, cell.switchControl) 100 | XCTAssertEqual(cell.switchControl.isOn, false) 101 | #elseif os(tvOS) 102 | XCTAssertNil(cell.accessoryView) 103 | XCTAssertEqual(cell.accessoryType, .none) 104 | #endif 105 | } 106 | } 107 | 108 | func testConfiguration_customCell() { 109 | do { 110 | // Given 111 | let cell = CustomSwitchCell() 112 | let row = CustomSwitchRow(text: "", switchValue: true, action: nil) 113 | 114 | // When 115 | cell.configure(with: row) 116 | 117 | // Then 118 | #if os(iOS) 119 | XCTAssertEqual(cell.accessoryView, cell.switchControl) 120 | XCTAssertEqual(cell.switchControl.isOn, true) 121 | #elseif os(tvOS) 122 | XCTAssertNil(cell.accessoryView) 123 | XCTAssertEqual(cell.accessoryType, .checkmark) 124 | #endif 125 | } 126 | 127 | do { 128 | // Given 129 | let cell = CustomSwitchCell() 130 | let row = CustomSwitchRow(text: "", switchValue: false, action: nil) 131 | 132 | // When 133 | cell.configure(with: row) 134 | 135 | // Then 136 | #if os(iOS) 137 | XCTAssertEqual(cell.accessoryView, cell.switchControl) 138 | XCTAssertEqual(cell.switchControl.isOn, false) 139 | #elseif os(tvOS) 140 | XCTAssertNil(cell.accessoryView) 141 | XCTAssertEqual(cell.accessoryType, .none) 142 | #endif 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /QuickTableViewController.xcodeproj/xcshareddata/xcschemes/QuickTableViewController-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /Tests/Model/SubtitleSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubtitleTests.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 28/10/2015. 6 | // Copyright © 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import QuickTableViewController 28 | import XCTest 29 | 30 | @available(*, deprecated, message: "Compatibility tests") 31 | internal final class SubtitleTests: XCTestCase { 32 | 33 | func testCompatibility() { 34 | XCTAssertEqual(Subtitle.none.detailText, .none) 35 | XCTAssertEqual(Subtitle.belowTitle("text").detailText, .subtitle("text")) 36 | XCTAssertEqual(Subtitle.rightAligned("text").detailText, .value1("text")) 37 | XCTAssertEqual(Subtitle.leftAligned("text").detailText, .value2("text")) 38 | } 39 | 40 | func testNavigationRow() { 41 | // Given 42 | let subtitle = Subtitle.belowTitle("detail text") 43 | let detailText = DetailText.subtitle("detail text") 44 | let icon = Icon.named("icon") 45 | var invoked = false 46 | 47 | // When 48 | let row = NavigationRow(title: "text", subtitle: subtitle, icon: icon, action: { _ in invoked = true }) 49 | 50 | // Then it should have the given parameters 51 | XCTAssertEqual(row.text, "text") 52 | XCTAssertEqual(row.detailText, detailText) 53 | XCTAssertEqual(row.icon, icon) 54 | XCTAssertEqual(row.cellReuseIdentifier, "UITableViewCell.subtitle") 55 | 56 | // It should support deprecated properties 57 | XCTAssertEqual(row.title, "text") 58 | XCTAssertEqual(row.subtitle?.text, "detail text") 59 | 60 | // When 61 | row.action?(row) 62 | 63 | // Then 64 | XCTAssertEqual(invoked, true) 65 | } 66 | 67 | func testOptionRow() { 68 | // Given 69 | let icon = Icon.named("icon") 70 | var invoked = false 71 | 72 | // When 73 | let row = OptionRow(title: "text", isSelected: true, icon: icon) { _ in invoked = true } 74 | 75 | // Then it should have the given parameters 76 | XCTAssertEqual(row.text, "text") 77 | XCTAssertNil(row.detailText) 78 | XCTAssertEqual(row.isSelected, true) 79 | 80 | // With RowStyle 81 | XCTAssertEqual(row.cellReuseIdentifier, "UITableViewCell") 82 | XCTAssertEqual(row.cellStyle, .default) 83 | XCTAssertEqual(row.icon, icon) 84 | XCTAssertEqual(row.accessoryType, .checkmark) 85 | XCTAssertEqual(row.isSelectable, true) 86 | XCTAssertNil(row.customize) 87 | 88 | // It should support deprecated properties 89 | XCTAssertEqual(row.title, "text") 90 | XCTAssertNil(row.subtitle?.text) 91 | 92 | // When 93 | row.action?(row) 94 | 95 | // Then 96 | XCTAssertEqual(invoked, true) 97 | } 98 | 99 | func testSwitchRow() { 100 | // Given 101 | var invoked = false 102 | 103 | // When 104 | let row = SwitchRow(title: "text", switchValue: true) { _ in invoked = true } 105 | 106 | // Then it should have the given parameters 107 | XCTAssertEqual(row.text, "text") 108 | XCTAssertNil(row.detailText) 109 | XCTAssertEqual(row.switchValue, true) 110 | XCTAssertEqual(row.cellReuseIdentifier, "SwitchCell") 111 | 112 | // It should support deprecated properties 113 | XCTAssertEqual(row.title, "text") 114 | XCTAssertNil(row.subtitle?.text) 115 | 116 | // When 117 | row.action?(row) 118 | 119 | // Then 120 | XCTAssertEqual(invoked, true) 121 | } 122 | 123 | func testTapActionRow() { 124 | // Given 125 | var invoked = false 126 | 127 | // When 128 | let row = TapActionRow(title: "text") { _ in invoked = true } 129 | 130 | // Then it should initialize with given parameters 131 | XCTAssertEqual(row.text, "text") 132 | XCTAssertNil(row.detailText) 133 | XCTAssertEqual(row.cellReuseIdentifier, "TapActionCell") 134 | 135 | // It should support deprecated properties 136 | XCTAssertEqual(row.title, "text") 137 | XCTAssertNil(row.subtitle?.text) 138 | 139 | // When 140 | row.action?(row) 141 | 142 | // Then 143 | XCTAssertEqual(invoked, true) 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /Tests/Row/SwitchRowSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwitchRowTests.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 17/01/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class SwitchRowTests: XCTestCase { 31 | 32 | // MARK: - Initialization 33 | 34 | func testInitialiation() { 35 | // Given 36 | let detailText = DetailText.subtitle("subtitle") 37 | let icon = Icon.named("icon") 38 | var actionInvoked = false 39 | 40 | // When 41 | let row = SwitchRow(text: "title", detailText: detailText, switchValue: true, icon: icon) { _ in actionInvoked = true } 42 | 43 | // Then it should have the given parameters 44 | XCTAssertEqual(row.text, "title") 45 | XCTAssertEqual(row.detailText, detailText) 46 | XCTAssertEqual(row.switchValue, true) 47 | 48 | // With RowStyle 49 | XCTAssertEqual(row.icon, icon) 50 | XCTAssertEqual(row.cellReuseIdentifier, "SwitchCell") 51 | 52 | // When 53 | row.action?(row) 54 | 55 | // Then 56 | XCTAssertEqual(actionInvoked, true) 57 | } 58 | 59 | // MARK: - Equatable 60 | 61 | func testEquatable_withIdenticalParameters() { 62 | // Given 63 | let one = SwitchRow(text: "Same", switchValue: true, action: nil) 64 | 65 | // When 66 | let another = SwitchRow(text: "Same", switchValue: true, action: nil) 67 | 68 | // Then 69 | XCTAssert(one == another) 70 | } 71 | 72 | func testEquatable_withDifferentTexts() { 73 | // Given 74 | let one = SwitchRow(text: "Same", switchValue: true, action: nil) 75 | 76 | // When 77 | let another = SwitchRow(text: "Different", switchValue: true, action: nil) 78 | 79 | // Then 80 | XCTAssert(one != another) 81 | } 82 | 83 | func testEquatable_withDifferentDetailTexts() { 84 | // Given 85 | let one = SwitchRow(text: "Same", switchValue: true, action: nil) 86 | 87 | // When 88 | let another = SwitchRow(text: "Same", detailText: .subtitle("Different"), switchValue: true, action: nil) 89 | 90 | // Then 91 | XCTAssert(one != another) 92 | } 93 | 94 | func testEquatable_withDifferentSwitchValues() { 95 | // Given 96 | let one = SwitchRow(text: "Same", switchValue: true, action: nil) 97 | 98 | // When 99 | let another = SwitchRow(text: "Same", switchValue: false, action: nil) 100 | 101 | // Then 102 | XCTAssert(one != another) 103 | } 104 | 105 | func testEquatable_withDifferentIcons() { 106 | // Given 107 | let one = SwitchRow(text: "Same", switchValue: true, action: nil) 108 | 109 | // When 110 | let another = SwitchRow(text: "Same", switchValue: true, icon: .image(UIImage()), action: nil) 111 | 112 | // Then 113 | XCTAssert(one != another) 114 | } 115 | 116 | func testEquatable_withDifferentActions() { 117 | // Given 118 | let one = SwitchRow(text: "Same", switchValue: true, action: nil) 119 | 120 | // When 121 | let another = SwitchRow(text: "Same", switchValue: true, action: { _ in }) 122 | 123 | // Then it should be equal regardless of the actions attached 124 | XCTAssert(one == another) 125 | } 126 | 127 | // MARK: - Action Invocation 128 | 129 | func testActionInvocationWhenSwitchValueIsToggled() { 130 | let expectation = self.expectation(description: "it should invoke the action closure") 131 | 132 | // Given a switch row that's off 133 | let row = SwitchRow(text: "", switchValue: false) { _ in expectation.fulfill() } 134 | 135 | // When the switch value is toggled 136 | row.switchValue = true 137 | 138 | // Then 139 | waitForExpectations(timeout: 1) 140 | } 141 | 142 | func testActionInvocationWhenSwitchValueIsNotToggled() { 143 | let expectation = self.expectation(description: "it should not invoke the action closure") 144 | expectation.isInverted = true 145 | 146 | // Given a switch row that's off 147 | let row = SwitchRow(text: "", switchValue: false) { _ in expectation.fulfill() } 148 | 149 | // When the switch value is not toggled 150 | row.switchValue = false 151 | 152 | // Then 153 | waitForExpectations(timeout: 1) 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /Example-iOS/ViewControllers/ExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleViewController.swift 3 | // Example-iOS 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import QuickTableViewController 29 | 30 | internal final class ExampleViewController: QuickTableViewController { 31 | 32 | // MARK: - Properties 33 | 34 | private let debugging = Section(title: nil, rows: [NavigationRow(text: "", detailText: .none)]) 35 | 36 | // MARK: - UIViewController 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | title = "QuickTableViewController" 41 | 42 | let gear = #imageLiteral(resourceName: "iconmonstr-gear") 43 | let globe = #imageLiteral(resourceName: "iconmonstr-globe") 44 | let time = #imageLiteral(resourceName: "iconmonstr-time") 45 | 46 | tableContents = [ 47 | Section(title: "Switch", rows: [ 48 | SwitchRow(text: "Setting 1", switchValue: true, icon: .image(globe), action: didToggleSwitch()), 49 | SwitchRow(text: "Setting 2", switchValue: false, icon: .image(time), action: didToggleSwitch()) 50 | ]), 51 | 52 | Section(title: "Tap Action", rows: [ 53 | TapActionRow(text: "Tap action", action: showAlert()) 54 | ]), 55 | 56 | Section(title: "Navigation", rows: [ 57 | NavigationRow(text: "CellStyle.default", detailText: .none, icon: .image(gear)), 58 | NavigationRow(text: "CellStyle", detailText: .subtitle(".subtitle"), icon: .image(globe), accessoryButtonAction: showDetail()), 59 | NavigationRow(text: "CellStyle", detailText: .value1(".value1"), icon: .image(time), action: showDetail()), 60 | NavigationRow(text: "CellStyle", detailText: .value2(".value2")) 61 | ], footer: "UITableViewCellStyle.Value2 hides the image view."), 62 | 63 | RadioSection(title: "Radio Buttons", options: [ 64 | OptionRow(text: "Option 1", isSelected: true, action: didToggleSelection()), 65 | OptionRow(text: "Option 2", isSelected: false, action: didToggleSelection()), 66 | OptionRow(text: "Option 3", isSelected: false, action: didToggleSelection()) 67 | ], footer: "See RadioSection for more details."), 68 | 69 | debugging 70 | ] 71 | } 72 | 73 | // MARK: - UITableViewDataSource 74 | 75 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 76 | let cell = super.tableView(tableView, cellForRowAt: indexPath) 77 | // Alter the cells created by QuickTableViewController 78 | return cell 79 | } 80 | 81 | // MARK: - Private Methods 82 | 83 | private func didToggleSelection() -> (Row) -> Void { 84 | return { [weak self] in 85 | if let option = $0 as? OptionRowCompatible { 86 | let state = "\(option.text) is " + (option.isSelected ? "selected" : "deselected") 87 | self?.showDebuggingText(state) 88 | } 89 | } 90 | } 91 | 92 | private func didToggleSwitch() -> (Row) -> Void { 93 | return { [weak self] in 94 | if let row = $0 as? SwitchRowCompatible { 95 | let state = "\(row.text) = \(row.switchValue)" 96 | self?.showDebuggingText(state) 97 | } 98 | } 99 | } 100 | 101 | private func showAlert() -> (Row) -> Void { 102 | return { [weak self] _ in 103 | let alert = UIAlertController(title: "Action Triggered", message: nil, preferredStyle: .alert) 104 | alert.addAction(UIAlertAction(title: "OK", style: .cancel) { [weak self] _ in 105 | self?.dismiss(animated: true, completion: nil) 106 | }) 107 | self?.present(alert, animated: true, completion: nil) 108 | } 109 | } 110 | 111 | private func showDetail() -> (Row) -> Void { 112 | return { [weak self] in 113 | let detail = $0.text + ($0.detailText?.text ?? "") 114 | let controller = UIViewController() 115 | controller.view.backgroundColor = .white 116 | controller.title = detail 117 | self?.navigationController?.pushViewController(controller, animated: true) 118 | self?.showDebuggingText(detail + " is selected") 119 | } 120 | } 121 | 122 | private func showDebuggingText(_ text: String) { 123 | print(text) 124 | debugging.rows = [NavigationRow(text: text, detailText: .none)] 125 | if let section = tableContents.firstIndex(where: { $0 === debugging }) { 126 | tableView.reloadSections([section], with: .none) 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Tests/Row/OptionRowSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionRowTests.swift 3 | // QuickTableViewControllerTests 4 | // 5 | // Created by Ben on 17/08/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import QuickTableViewController 28 | import XCTest 29 | 30 | internal final class OptionRowTests: XCTestCase { 31 | 32 | // MARK: - Initialization 33 | 34 | func testInitialiation() { 35 | // Given 36 | let detailText = DetailText.subtitle("subtitle") 37 | let icon = Icon.named("icon") 38 | var actionInvoked = false 39 | 40 | // When 41 | let row = OptionRow(text: "title", detailText: detailText, isSelected: true, icon: icon) { _ in actionInvoked = true } 42 | 43 | // Then it should have the given parameters 44 | XCTAssertEqual(row.text, "title") 45 | XCTAssertEqual(row.detailText, detailText) 46 | XCTAssertEqual(row.isSelected, true) 47 | 48 | // With RowStyle 49 | XCTAssertEqual(row.cellReuseIdentifier, "UITableViewCell") 50 | XCTAssertEqual(row.cellStyle, .default) 51 | XCTAssertEqual(row.icon, icon) 52 | XCTAssertEqual(row.accessoryType, .checkmark) 53 | XCTAssertEqual(row.isSelectable, true) 54 | XCTAssertNil(row.customize) 55 | 56 | // When 57 | row.action?(row) 58 | 59 | // Then 60 | XCTAssertEqual(actionInvoked, true) 61 | } 62 | 63 | // MARK: - Equatable 64 | 65 | func testEquatable_withIdenticalParameters() { 66 | // Given 67 | let one = OptionRow(text: "Same", isSelected: true, action: nil) 68 | 69 | // When 70 | let another = OptionRow(text: "Same", isSelected: true, action: nil) 71 | 72 | // Then 73 | XCTAssert(one == another) 74 | } 75 | 76 | func testEquatable_withDifferentTexts() { 77 | // Given 78 | let one = OptionRow(text: "Same", isSelected: true, action: nil) 79 | 80 | // When 81 | let another = OptionRow(text: "Different", isSelected: true, action: nil) 82 | 83 | // Then 84 | XCTAssert(one != another) 85 | } 86 | 87 | func testEquatable_withDifferentDetailTexts() { 88 | // Given 89 | let one = OptionRow(text: "Same", isSelected: true, action: nil) 90 | 91 | // When 92 | let another = OptionRow(text: "Same", detailText: .subtitle("Different"), isSelected: true, action: nil) 93 | 94 | // Then 95 | XCTAssert(one != another) 96 | } 97 | 98 | func testEquatable_withDifferentSelectionStates() { 99 | // Given 100 | let one = OptionRow(text: "Same", isSelected: true, action: nil) 101 | 102 | // When 103 | let another = OptionRow(text: "Same", isSelected: false, action: nil) 104 | 105 | // Then 106 | XCTAssert(one != another) 107 | } 108 | 109 | func testEquatable_withDifferentIcons() { 110 | // Given 111 | let one = OptionRow(text: "Same", isSelected: true, action: nil) 112 | 113 | // When 114 | let another = OptionRow(text: "Same", isSelected: true, icon: .image(UIImage()), action: nil) 115 | 116 | // Then 117 | XCTAssert(one != another) 118 | } 119 | 120 | func testEquatable_withDifferentActions() { 121 | // Given 122 | let one = OptionRow(text: "Same", isSelected: true, action: nil) 123 | 124 | // When 125 | let another = OptionRow(text: "Same", isSelected: true, action: { _ in }) 126 | 127 | // Then it should be equal regardless of the actions attached 128 | XCTAssert(one == another) 129 | } 130 | 131 | // MARK: - Action Invocation 132 | 133 | func testActionInvocationWhenSelectionIsToggled() { 134 | let expectation = self.expectation(description: "it should invoke the action closure") 135 | 136 | // Given an option row that's not selected 137 | let row = OptionRow(text: "", isSelected: false) { _ in expectation.fulfill() } 138 | 139 | // When the selection is toggled 140 | row.isSelected = true 141 | 142 | // Then 143 | waitForExpectations(timeout: 1, handler: nil) 144 | XCTAssertEqual(row.accessoryType, .checkmark) 145 | } 146 | 147 | func testActionInvocationWhenSelectionIsNotToggled() { 148 | let expectation = self.expectation(description: "it should not invoke the action closure") 149 | expectation.isInverted = true 150 | 151 | // Given an option row that's not selected 152 | let row = OptionRow(text: "", isSelected: false) { _ in expectation.fulfill() } 153 | 154 | // When the selection is not toggled 155 | row.isSelected = false 156 | 157 | // Then 158 | waitForExpectations(timeout: 1, handler: nil) 159 | XCTAssertEqual(row.accessoryType, .none) 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /Source/Rows/NavigationRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRow.swift 3 | // QuickTableViewController 4 | // 5 | // Created by Ben on 01/09/2015. 6 | // Copyright (c) 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A class that represents a row that triggers certain navigation when selected. 30 | open class NavigationRow: NavigationRowCompatible, Equatable { 31 | 32 | // MARK: - Initializer 33 | 34 | #if os(iOS) 35 | 36 | /// Designated initializer on iOS. Returns a `NavigationRow` with a text and a detail text. 37 | /// The icon, customization, action and accessory button action closures are optional. 38 | public init( 39 | text: String, 40 | detailText: DetailText, 41 | icon: Icon? = nil, 42 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 43 | action: ((Row) -> Void)? = nil, 44 | accessoryButtonAction: ((Row) -> Void)? = nil 45 | ) { 46 | self.text = text 47 | self.detailText = detailText 48 | self.icon = icon 49 | self.customize = customization 50 | self.action = action 51 | self.accessoryButtonAction = accessoryButtonAction 52 | } 53 | 54 | #elseif os(tvOS) 55 | 56 | /// Designated initializer on tvOS. Returns a `NavigationRow` with a text and a detail text. 57 | /// The icon, customization and action closures are optional. 58 | public init( 59 | text: String, 60 | detailText: DetailText, 61 | icon: Icon? = nil, 62 | customization: ((UITableViewCell, Row & RowStyle) -> Void)? = nil, 63 | action: ((Row) -> Void)? = nil 64 | ) { 65 | self.text = text 66 | self.detailText = detailText 67 | self.icon = icon 68 | self.customize = customization 69 | self.action = action 70 | } 71 | 72 | #endif 73 | 74 | // MARK: - Row 75 | 76 | /// The text of the row. 77 | public let text: String 78 | 79 | /// The detail text of the row. 80 | public let detailText: DetailText? 81 | 82 | /// A closure that will be invoked when the row is selected. 83 | public let action: ((Row) -> Void)? 84 | 85 | #if os(iOS) 86 | 87 | /// A closure that will be invoked when the accessory button is selected. 88 | public let accessoryButtonAction: ((Row) -> Void)? 89 | 90 | #endif 91 | 92 | // MARK: - RowStyle 93 | 94 | /// The type of the table view cell to display the row. 95 | public let cellType: UITableViewCell.Type = T.self 96 | 97 | /// Returns the reuse identifier of the table view cell to display the row. 98 | public var cellReuseIdentifier: String { 99 | return T.reuseIdentifier + (detailText?.style.stringValue ?? "") 100 | } 101 | 102 | /// Returns the table view cell style for the specified subtitle. 103 | public var cellStyle: UITableViewCell.CellStyle { 104 | return detailText?.style ?? .default 105 | } 106 | 107 | /// The icon of the row. 108 | public let icon: Icon? 109 | 110 | /// Returns the accessory type with the disclosure indicator when `action` is not nil, 111 | /// and with the detail button when `accessoryButtonAction` is not nil. 112 | public var accessoryType: UITableViewCell.AccessoryType { 113 | #if os(iOS) 114 | switch (action, accessoryButtonAction) { 115 | case (nil, nil): return .none 116 | case (.some, nil): return .disclosureIndicator 117 | case (nil, .some): return .detailButton 118 | case (.some, .some): return .detailDisclosureButton 119 | } 120 | #elseif os(tvOS) 121 | return (action == nil) ? .none : .disclosureIndicator 122 | #endif 123 | } 124 | 125 | /// The `NavigationRow` is selectable when action is not nil. 126 | public var isSelectable: Bool { 127 | return action != nil 128 | } 129 | 130 | /// The additional customization during cell configuration. 131 | public let customize: ((UITableViewCell, Row & RowStyle) -> Void)? 132 | 133 | // MARK: Equatable 134 | 135 | /// Returns true iff `lhs` and `rhs` have equal titles, detail texts and icons. 136 | public static func == (lhs: NavigationRow, rhs: NavigationRow) -> Bool { 137 | return 138 | lhs.text == rhs.text && 139 | lhs.detailText == rhs.detailText && 140 | lhs.icon == rhs.icon 141 | } 142 | 143 | } 144 | 145 | 146 | private extension UITableViewCell.CellStyle { 147 | 148 | var stringValue: String { 149 | switch self { 150 | case .default: return ".default" 151 | case .subtitle: return ".subtitle" 152 | case .value1: return ".value1" 153 | case .value2: return ".value2" 154 | @unknown default: return ".default" 155 | } 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /Example-tvOSUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITests.swift 3 | // Example-tvOSUITests 4 | // 5 | // Created by Francesco Deliro on 07/10/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import XCTest 28 | 29 | internal final class ExampleUITests: XCTestCase { 30 | 31 | private lazy var app: XCUIApplication = XCUIApplication() 32 | 33 | override func setUp() { 34 | super.setUp() 35 | continueAfterFailure = false 36 | app.launch() 37 | } 38 | 39 | func testInteractions() { 40 | 41 | let tables = XCUIApplication().tables 42 | let existance = NSPredicate(format: "exists == true") 43 | let hasFocus = NSPredicate(format: "hasFocus == true") 44 | 45 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "Setting 1").element, handler: nil) 46 | waitForExpectations(timeout: 5, handler: nil) 47 | XCUIRemote.shared.press(.select) 48 | 49 | XCUIRemote.shared.press(.down) 50 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "Setting 2").element, handler: nil) 51 | waitForExpectations(timeout: 5, handler: nil) 52 | XCUIRemote.shared.press(.select) 53 | 54 | XCUIRemote.shared.press(.down) 55 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "Tap action").element, handler: nil) 56 | waitForExpectations(timeout: 5, handler: nil) 57 | XCUIRemote.shared.press(.select) 58 | 59 | expectation(for: hasFocus, evaluatedWith: app.alerts["Tap action"].otherElements["OK"], handler: nil) 60 | waitForExpectations(timeout: 5, handler: nil) 61 | XCUIRemote.shared.press(.select) 62 | 63 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "Tap action").element, handler: nil) 64 | waitForExpectations(timeout: 5, handler: nil) 65 | 66 | XCUIRemote.shared.press(.down) 67 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "CellStyle.default").element, handler: nil) 68 | waitForExpectations(timeout: 5, handler: nil) 69 | XCUIRemote.shared.press(.select) 70 | 71 | expectation(for: existance, evaluatedWith: app.otherElements.containing(.staticText, identifier: "CellStyle.default").element, handler: nil) 72 | waitForExpectations(timeout: 5, handler: nil) 73 | XCUIRemote.shared.press(.menu) 74 | 75 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "CellStyle.default").element, handler: nil) 76 | waitForExpectations(timeout: 5, handler: nil) 77 | 78 | XCUIRemote.shared.press(.down) 79 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: ".subtitle").element, handler: nil) 80 | waitForExpectations(timeout: 5, handler: nil) 81 | XCUIRemote.shared.press(.select) 82 | 83 | expectation(for: existance, evaluatedWith: app.otherElements.containing(.staticText, identifier: "CellStyle.subtitle").element, handler: nil) 84 | waitForExpectations(timeout: 5, handler: nil) 85 | XCUIRemote.shared.press(.menu) 86 | 87 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: ".subtitle").element, handler: nil) 88 | waitForExpectations(timeout: 5, handler: nil) 89 | 90 | XCUIRemote.shared.press(.down) 91 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: ".value1").element, handler: nil) 92 | waitForExpectations(timeout: 5, handler: nil) 93 | XCUIRemote.shared.press(.select) 94 | 95 | XCUIRemote.shared.press(.down) 96 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: ".value2").element, handler: nil) 97 | waitForExpectations(timeout: 5, handler: nil) 98 | XCUIRemote.shared.press(.select) 99 | 100 | XCUIRemote.shared.press(.down) 101 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "Option 1").element, handler: nil) 102 | waitForExpectations(timeout: 5, handler: nil) 103 | XCUIRemote.shared.press(.select) 104 | 105 | XCUIRemote.shared.press(.down) 106 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "Option 2").element, handler: nil) 107 | waitForExpectations(timeout: 5, handler: nil) 108 | XCUIRemote.shared.press(.select) 109 | 110 | XCUIRemote.shared.press(.down) 111 | expectation(for: hasFocus, evaluatedWith: tables.cells.containing(.staticText, identifier: "Option 3").element, handler: nil) 112 | waitForExpectations(timeout: 5, handler: nil) 113 | XCUIRemote.shared.press(.select) 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /Example-iOS/ViewControllers/CustomizationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomizationViewController.swift 3 | // Example-iOS 4 | // 5 | // Created by Ben on 30/01/2018. 6 | // Copyright © 2018 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import QuickTableViewController 29 | 30 | private final class CustomCell: UITableViewCell {} 31 | private final class CustomSwitchCell: SwitchCell {} 32 | private final class CustomTapActionCell: TapActionCell {} 33 | private final class CustomOptionCell: UITableViewCell {} 34 | 35 | private final class CustomSwitchRow: SwitchRow {} 36 | private final class CustomTapActionRow: TapActionRow {} 37 | private final class CustomNavigationRow: NavigationRow {} 38 | private final class CustomOptionRow: OptionRow {} 39 | 40 | internal final class CustomizationViewController: QuickTableViewController { 41 | 42 | private let debugging = Section( 43 | title: nil, 44 | rows: [NavigationRow(text: "", detailText: .none)], 45 | footer: "Select or toggle each row to show their cell reuse identifiers." 46 | ) 47 | 48 | // MARK: - UIViewController 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | title = "Customization" 53 | 54 | tableContents = [ 55 | debugging, 56 | 57 | Section(title: "Switch", rows: [ 58 | SwitchRow( 59 | text: "SwitchRow\n", 60 | switchValue: true, 61 | customization: set(label: "0-0"), 62 | action: showLog() 63 | ), 64 | CustomSwitchRow( 65 | text: "CustomSwitchRow\n", 66 | switchValue: false, 67 | customization: set(label: "0-1"), 68 | action: showLog() 69 | ) 70 | ]), 71 | 72 | Section(title: "Tap Action", rows: [ 73 | TapActionRow( 74 | text: "TapActionRow\n", 75 | customization: set(label: "1-0"), 76 | action: showLog() 77 | ), 78 | CustomTapActionRow( 79 | text: "CustomTapActionRow\n", 80 | customization: set(label: "1-1"), 81 | action: showLog() 82 | ) 83 | ]), 84 | 85 | Section(title: "Navigation", rows: [ 86 | NavigationRow( 87 | text: "NavigationRow", 88 | detailText: .none, 89 | customization: set(label: "2-0"), 90 | action: showLog() 91 | ), 92 | NavigationRow( 93 | text: "NavigationRow", 94 | detailText: .subtitle(".subtitle"), 95 | customization: set(label: "2-1"), 96 | action: showLog() 97 | ), 98 | CustomNavigationRow( 99 | text: "CustomNavigationRow", 100 | detailText: .value1(".value1"), 101 | customization: set(label: "2-2"), 102 | action: showLog() 103 | ), 104 | CustomNavigationRow( 105 | text: "CustomNavigationRow", 106 | detailText: .value2(".value2"), 107 | customization: set(label: "2-3"), 108 | action: showLog() 109 | ) 110 | ]), 111 | 112 | RadioSection(title: "Radio Buttons", options: [ 113 | OptionRow( 114 | text: "OptionRow", 115 | isSelected: false, 116 | customization: set(label: "3-0"), 117 | action: showLog() 118 | ), 119 | CustomOptionRow( 120 | text: "CustomOptionRow", 121 | isSelected: false, 122 | customization: set(label: "3-1"), 123 | action: showLog() 124 | ), 125 | CustomOptionRow( 126 | text: "CustomOptionRow", 127 | isSelected: false, 128 | customization: set(label: "3-2"), 129 | action: showLog() 130 | ) 131 | ]), 132 | 133 | Section(title: nil, rows: [ 134 | NavigationRow(text: "Customization closure", detailText: .none, customization: { cell, _ in 135 | cell.accessibilityLabel = "4-0" 136 | cell.accessoryView = UIImageView(image: #imageLiteral(resourceName: "iconmonstr-x-mark")) 137 | }) 138 | ]) 139 | ] 140 | 141 | } 142 | 143 | // MARK: - UITableViewDelegate 144 | 145 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 146 | super.tableView(tableView, didSelectRowAt: indexPath) 147 | tableView.deselectRow(at: indexPath, animated: true) 148 | } 149 | 150 | // MARK: - Private 151 | 152 | private func set(label: String) -> ((UITableViewCell, Row & RowStyle) -> Void) { 153 | return { cell, _ in 154 | cell.accessibilityLabel = label 155 | } 156 | } 157 | 158 | private func showLog() -> (Row) -> Void { 159 | return { [weak self] in 160 | if let option = $0 as? OptionRowCompatible, !option.isSelected { 161 | return 162 | } 163 | let identifier = ($0 as! RowStyle).cellReuseIdentifier 164 | self?.debugging.rows = [ 165 | NavigationRow(text: identifier, detailText: .none, customization: self?.set(label: "debug")) 166 | ] 167 | print(identifier) 168 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in 169 | self?.tableView.reloadData() 170 | } 171 | } 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /Example-iOSUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITests.swift 3 | // Example-iOSUITests 4 | // 5 | // Created by Ben on 14/08/2017. 6 | // Copyright © 2017 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import XCTest 28 | 29 | internal final class ExampleUITests: XCTestCase { 30 | 31 | private lazy var app: XCUIApplication = XCUIApplication() 32 | 33 | override func setUp() { 34 | super.setUp() 35 | continueAfterFailure = false 36 | app.launch() 37 | } 38 | 39 | func testInteractions() { 40 | app.tables.staticTexts["Use default cell types"].tap() 41 | 42 | let tables = XCUIApplication().tables 43 | let existance = NSPredicate(format: "exists == true") 44 | 45 | tables.switches["Setting 1"].tap() 46 | expectation(for: existance, evaluatedWith: tables.staticTexts["Setting 1 = false"], handler: nil) 47 | waitForExpectations(timeout: 5, handler: nil) 48 | 49 | tables.switches["Setting 1"].tap() 50 | expectation(for: existance, evaluatedWith: tables.staticTexts["Setting 1 = true"], handler: nil) 51 | waitForExpectations(timeout: 5, handler: nil) 52 | 53 | tables.switches["Setting 2"].tap() 54 | expectation(for: existance, evaluatedWith: tables.staticTexts["Setting 2 = true"], handler: nil) 55 | waitForExpectations(timeout: 5, handler: nil) 56 | 57 | tables.switches["Setting 2"].tap() 58 | expectation(for: existance, evaluatedWith: tables.staticTexts["Setting 2 = false"], handler: nil) 59 | waitForExpectations(timeout: 5, handler: nil) 60 | 61 | tables.staticTexts["Tap action"].tap() 62 | addUIInterruptionMonitor(withDescription: "Action Triggered") { 63 | let button = $0.buttons["OK"] 64 | if button.exists { 65 | button.tap() 66 | return true 67 | } 68 | return false 69 | } 70 | 71 | tables.cells.containing(.staticText, identifier: ".subtitle").buttons["More Info"].tap() 72 | app.navigationBars.buttons.element(boundBy: 0).tap() 73 | expectation(for: existance, evaluatedWith: tables.staticTexts["CellStyle.subtitle is selected"], handler: nil) 74 | waitForExpectations(timeout: 5, handler: nil) 75 | 76 | tables.staticTexts[".value1"].tap() 77 | app.navigationBars.buttons.element(boundBy: 0).tap() 78 | expectation(for: existance, evaluatedWith: tables.staticTexts["CellStyle.value1 is selected"], handler: nil) 79 | waitForExpectations(timeout: 5, handler: nil) 80 | 81 | tables.staticTexts["Option 1"].tap() 82 | expectation(for: existance, evaluatedWith: tables.staticTexts["Option 1 is deselected"], handler: nil) 83 | waitForExpectations(timeout: 5, handler: nil) 84 | tables.staticTexts["Option 2"].tap() 85 | expectation(for: existance, evaluatedWith: tables.staticTexts["Option 2 is selected"], handler: nil) 86 | waitForExpectations(timeout: 5, handler: nil) 87 | tables.staticTexts["Option 3"].tap() 88 | expectation(for: existance, evaluatedWith: tables.staticTexts["Option 3 is selected"], handler: nil) 89 | waitForExpectations(timeout: 5, handler: nil) 90 | } 91 | 92 | func testCustomization() { 93 | app.tables.staticTexts["Use custom cell types"].tap() 94 | 95 | let tables = XCUIApplication().tables 96 | let existance = NSPredicate(format: "exists == true") 97 | 98 | tables.switches["0-0"].tap() 99 | expectation(for: existance, evaluatedWith: tables.staticTexts["CustomSwitchCell"], handler: nil) 100 | waitForExpectations(timeout: 5, handler: nil) 101 | 102 | tables.switches["0-1"].tap() 103 | expectation(for: existance, evaluatedWith: tables.staticTexts["SwitchCell"], handler: nil) 104 | waitForExpectations(timeout: 5, handler: nil) 105 | 106 | tables.cells["1-0"].tap() 107 | expectation(for: existance, evaluatedWith: tables.staticTexts["CustomTapActionCell"], handler: nil) 108 | waitForExpectations(timeout: 5, handler: nil) 109 | 110 | tables.cells["1-1"].tap() 111 | expectation(for: existance, evaluatedWith: tables.staticTexts["TapActionCell"], handler: nil) 112 | waitForExpectations(timeout: 5, handler: nil) 113 | 114 | tables.cells["2-0"].tap() 115 | expectation(for: existance, evaluatedWith: tables.staticTexts["UITableViewCell.default"], handler: nil) 116 | waitForExpectations(timeout: 5, handler: nil) 117 | 118 | tables.cells["2-1"].tap() 119 | expectation(for: existance, evaluatedWith: tables.staticTexts["CustomCell.subtitle"], handler: nil) 120 | waitForExpectations(timeout: 5, handler: nil) 121 | 122 | tables.cells["2-2"].tap() 123 | expectation(for: existance, evaluatedWith: tables.staticTexts["UITableViewCell.value1"], handler: nil) 124 | waitForExpectations(timeout: 5, handler: nil) 125 | 126 | tables.cells["2-3"].tap() 127 | expectation(for: existance, evaluatedWith: tables.staticTexts["CustomCell.value2"], handler: nil) 128 | waitForExpectations(timeout: 5, handler: nil) 129 | 130 | tables.cells["3-0"].tap() 131 | expectation(for: existance, evaluatedWith: tables.staticTexts["UITableViewCell"], handler: nil) 132 | waitForExpectations(timeout: 5, handler: nil) 133 | 134 | tables.cells["3-1"].tap() 135 | expectation(for: existance, evaluatedWith: tables.staticTexts["UITableViewCell"], handler: nil) 136 | waitForExpectations(timeout: 5, handler: nil) 137 | 138 | tables.cells["3-2"].tap() 139 | expectation(for: existance, evaluatedWith: tables.staticTexts["CustomOptionCell"], handler: nil) 140 | waitForExpectations(timeout: 5, handler: nil) 141 | } 142 | 143 | } 144 | --------------------------------------------------------------------------------