├── .ci ├── buildkite │ ├── pipeline.template.yml │ └── upload └── scripts │ ├── lint │ ├── send-coverage │ └── test ├── .fastlane ├── Appfile └── Fastfile ├── .gitignore ├── .slather.yml ├── .swiftlint.yml ├── CHANGELOG.md ├── Cartfile ├── Cartfile.resolved ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Sources ├── Base │ ├── Basic.swift │ ├── Datasource.swift │ ├── Entity.swift │ ├── Factory.swift │ ├── Sectioned.swift │ ├── Segmented.swift │ ├── Selection.swift │ ├── UICollectionView+TaylorSource.h │ ├── UICollectionView+TaylorSource.m │ ├── UITableView+TaylorSource.h │ ├── UITableView+TaylorSource.m │ ├── Utilities.swift │ └── Views.swift ├── CoreData │ ├── NSFRCDatasource.swift │ ├── NSFRCFactory.swift │ ├── NSFRCIndexedUpdates.swift │ └── NSFRCUpdateHandler.swift └── YapDatabase │ ├── YapDB.swift │ ├── YapDBDatasource.swift │ ├── YapDBEntity.swift │ └── YapDBFactory.swift ├── Supporting Files ├── Info.plist ├── TaylorSource.h └── TaylorSource.xcconfig ├── TaylorSource.podspec ├── TaylorSource.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── TaylorSource.xcscheme ├── Tests ├── DatasourceProviderTests.swift ├── EntityDatasourceTests.swift ├── FactoryTests.swift ├── Helpers.swift ├── Info.plist ├── Models.swift ├── StaticDatasourceTests.swift ├── StaticSectionDatasourceTests.swift ├── TaylorSourceTests.swift ├── YapDBDatasourceTests.swift ├── YapDBMapperTests.swift └── YapDBObserverTests.swift └── header.png /.ci/buildkite/pipeline.template.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | # - 3 | # name: ":muscle: Lint" 4 | # command: .ci/scripts/lint 5 | - 6 | name: ":fastlane: Test iOS" 7 | command: .ci/scripts/test 8 | agents: 9 | swift: "$BUILDKITE_AGENT_META_DATA_SWIFT" 10 | name: "$BUILDKITE_AGENT_META_DATA_NAME" 11 | # - 12 | # type: "waiter" 13 | # - 14 | # name: ":muscle: Send Coverage" 15 | # command: .ci/scripts/send-coverage 16 | # agents: 17 | # name: "$BUILDKITE_AGENT_META_DATA_NAME" 18 | 19 | -------------------------------------------------------------------------------- /.ci/buildkite/upload: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Makes sure all the steps run on this same agent 6 | sed "s/\$BUILDKITE_AGENT_META_DATA_NAME/$BUILDKITE_AGENT_META_DATA_NAME/" .ci/buildkite/pipeline.template.yml | sed "s/\$BUILDKITE_AGENT_META_DATA_SWIFT/$BUILDKITE_AGENT_META_DATA_SWIFT/" 7 | 8 | -------------------------------------------------------------------------------- /.ci/scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source /usr/local/opt/chruby/share/chruby/chruby.sh 3 | chruby ruby 4 | bundle exec fastlane lint 5 | -------------------------------------------------------------------------------- /.ci/scripts/send-coverage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source /usr/local/opt/chruby/share/chruby/chruby.sh 3 | chruby ruby 4 | bundle exec slather coverage --scheme "TaylorSource" --buildkite --coveralls --build-directory .ci/xcodebuild-data -------------------------------------------------------------------------------- /.ci/scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source /usr/local/opt/chruby/share/chruby/chruby.sh 3 | chruby ruby 4 | bundle exec fastlane test 5 | 6 | -------------------------------------------------------------------------------- /.fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier "me.danthorpe.TaylorSource" 2 | apple_id "someone@somewhere.com" 3 | -------------------------------------------------------------------------------- /.fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | before_all { 2 | carthage(platform: "iOS") 3 | } 4 | 5 | lane :lint do 6 | 7 | swiftLint( 8 | mode: :lint, 9 | config_file: '.swiftlint.yml' 10 | ) 11 | end 12 | 13 | desc "Runs all the tests" 14 | lane :test do 15 | 16 | scan( 17 | scheme: "TaylorSource", 18 | output_directory: ".ci/xcodebuild-data", 19 | xcargs: "-derivedDataPath .ci/xcodebuild-data" 20 | ) 21 | 22 | end 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.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 | *.moved-aside 22 | *.xcuserstate 23 | *.xccheckout 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | # Packages/ 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | # Pods/ 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | Carthage/Checkouts 51 | Carthage/Build 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 56 | # screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 59 | 60 | .fastlane/report.xml 61 | .fastlane/screenshots 62 | .fastlane/xcodebuild-data 63 | 64 | *.coverage.txt 65 | test_output 66 | -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | coverage_service: coveralls 2 | xcodeproj: TaylorSource.xcodeproj 3 | build_directory: .ci/xcodebuild-data 4 | ignore: 5 | - Tests/* 6 | - Supporting Files/* -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Tests 3 | use_nested_configs: true 4 | disabled_rules: 5 | - valid_docs 6 | - statement_position 7 | - line_length 8 | - type_name 9 | file_length: 10 | warning: 500 11 | error: 1200 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.15.0 2 | 1. [[TAY-21](https://github.com/danthorpe/TaylorSource/pull/21)]: Refactors the editor functionality requirements of `DatasourceProviderType` into a single property. For ease of use, read-only providers can use: 3 | 4 | let editor = NoEditor() 5 | 6 | 2. [[TAY-24](https://github.com/danthorpe/TaylorSource/pull/24)]: Adds initial work on an “Entity Datasource”. This is the start of what I hope will become quite a significant TaylorSource feature - essentially a way to drive “detail” controllers for a single instance of a model type. 7 | 3. [[TAY-25](https://github.com/danthorpe/TaylorSource/pull/25)]: Fixes the restrictive dependencies, this was a mistake. At the moment, YapDatabaseExtensions is pinned to version 1.4. 8 | 9 | 10 | # 0.14.0 11 | 1. [[TAY-18](https://github.com/danthorpe/TaylorSource/pull/18)]: Supports the optional editing methods in `UITableViewDataSource` via optional closures in `DatasourceProviderType`. 12 | 2. [[TAY-20](https://github.com/danthorpe/TaylorSource/pull/20)]: Supports Sliceable on Observer, Mapper and YapDatabaseViewMappings. 13 | 14 | 15 | # 0.13.0 16 | 1. [[TAY-17](https://github.com/danthorpe/TaylorSource/pull/17)]: Added a bitter badge to the README. 17 | 1. [[TAY-2](https://github.com/danthorpe/TaylorSource/pull/2)]: Improvements to the README for multiple cells and models. 18 | 1. [[TAY-16](https://github.com/danthorpe/TaylorSource/pull/16)]: Adds another example project which demonstrates usage of different cell classes in the same datasource. 19 | 2. 20 | # 0.12.0 21 | 1. [[TAY-13](https://github.com/danthorpe/TaylorSource/pull/13)]: Make a few subtle changes to increase the ease of implementing DatasourceType from scratch outside of TaylorSource. No longer is a SequenceType and CollectionType implementation required. And `YapDBCellIndex` and `YapDBSupplementaryViewIndex` have public constructors. 22 | 1. [[TAY-15](https://github.com/danthorpe/TaylorSource/pull/15)]: Preparing for Xcode 7 and Swift 2.0, this PR refactors the project structure, so that there is a framework project with tests, and example projects which build the framework using Pods. The Datasources and US Cities have been moved into the examples project. 23 | 24 | # 0.11.0 25 | 1. [[TAY-12](https://github.com/danthorpe/TaylorSource/pull/12)]: Modifies [TableView|CollectionView]DataSourceProviders to access arguments of DatasourceProviderType instead of DatasourceType. This allows for improved composition and code re-use. 26 | 27 | 28 | # 0.10.0 29 | 1. [[TAY-5](https://github.com/danthorpe/TaylorSource/pull/5)]: Adopts Quick BDD testing framework, adds coverage to YapDB Observer & Datasource. 30 | 1. [[TAY-7](https://github.com/danthorpe/TaylorSource/pull/7)]: Simplifies the common case of having a factory with a single cell type. In such a scenario, initialization of the Factory class requires no key closures, and cell/view registration takes no key parameter. 31 | 1. [[TAY-10](https://github.com/danthorpe/TaylorSource/pull/10)]: StaticDatasource and YapDBDatasource now expose an API for registering supplementary text values. This is used automatically for header and footer titles by the UITableViewDatasourceProvider. 32 | 1. [[TAY-11](https://github.com/danthorpe/TaylorSource/pull/11)]: Adds another example project (US Cities) which uses TaylorSource & YapDatabase. -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "danthorpe/YapDatabaseExtensions" "development" -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "CocoaLumberjack/CocoaLumberjack" "2.2.0" 2 | github "danthorpe/ValueCoding" "1.2.0" 3 | github "yapstudios/YapDatabase" "2.8.3" 4 | github "danthorpe/YapDatabaseExtensions" "19b39f5df2012c538697cb520253d39db0cc042a" 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'slather' 4 | gem 'fastlane' 5 | gem 'scan' 6 | gem 'xcpretty' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.5.2) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.3.8) 11 | babosa (1.0.2) 12 | cert (1.4.0) 13 | fastlane_core (>= 0.29.1, < 1.0.0) 14 | spaceship (>= 0.22.0, < 1.0.0) 15 | claide (0.9.1) 16 | clamp (0.6.5) 17 | coderay (1.1.1) 18 | colored (1.2) 19 | commander (4.3.5) 20 | highline (~> 1.7.2) 21 | credentials_manager (0.15.0) 22 | colored 23 | commander (>= 4.3.5) 24 | highline (>= 1.7.1) 25 | security 26 | deliver (1.10.3) 27 | credentials_manager (>= 0.12.0, < 1.0.0) 28 | fastimage (~> 1.6) 29 | fastlane_core (>= 0.36.4, < 1.0.0) 30 | plist (~> 3.1.0) 31 | spaceship (>= 0.19.0, <= 1.0.0) 32 | domain_name (0.5.20160216) 33 | unf (>= 0.0.5, < 1.0.0) 34 | dotenv (2.1.0) 35 | excon (0.45.4) 36 | faraday (0.9.2) 37 | multipart-post (>= 1.2, < 3) 38 | faraday-cookie_jar (0.0.6) 39 | faraday (>= 0.7.4) 40 | http-cookie (~> 1.0.0) 41 | faraday_middleware (0.10.0) 42 | faraday (>= 0.7.4, < 0.10) 43 | fastimage (1.6.8) 44 | addressable (~> 2.3, >= 2.3.5) 45 | fastlane (1.66.0) 46 | addressable (~> 2.3.8) 47 | cert (>= 1.3.0, < 2.0.0) 48 | credentials_manager (>= 0.15.0, < 1.0.0) 49 | deliver (>= 1.10.1, < 2.0.0) 50 | fastlane_core (>= 0.36.8, < 1.0.0) 51 | frameit (>= 2.4.1, < 3.0.0) 52 | gym (>= 1.6.1, < 2.0.0) 53 | krausefx-shenzhen (>= 0.14.7) 54 | match (>= 0.3.0, < 1.0.0) 55 | pem (>= 1.2.0, < 2.0.0) 56 | pilot (>= 1.3.0, < 2.0.0) 57 | plist (~> 3.1.0) 58 | produce (>= 1.1.0, < 2.0.0) 59 | scan (>= 0.5.0, < 1.0.0) 60 | screengrab (>= 0.2.1, < 1.0.0) 61 | sigh (>= 1.3.1, < 2.0.0) 62 | slack-notifier (~> 1.3) 63 | snapshot (>= 1.7.0, < 2.0.0) 64 | spaceship (>= 0.22.0, < 1.0.0) 65 | supply (>= 0.4.0, < 1.0.0) 66 | terminal-notifier (~> 1.6.2) 67 | terminal-table (~> 1.4.5) 68 | xcodeproj (>= 0.20, < 2.0.0) 69 | xcpretty (>= 0.2.1) 70 | fastlane_core (0.37.0) 71 | babosa 72 | colored 73 | commander (= 4.3.5) 74 | credentials_manager (>= 0.11.0, < 1.0.0) 75 | excon (~> 0.45.0) 76 | highline (>= 1.7.2) 77 | json 78 | multi_json 79 | plist (~> 3.1) 80 | rubyzip (~> 1.1.6) 81 | sentry-raven (~> 0.15) 82 | terminal-table (~> 1.4.5) 83 | frameit (2.5.1) 84 | deliver (> 0.3) 85 | fastimage (~> 1.6.3) 86 | fastlane_core (>= 0.36.1, < 1.0.0) 87 | mini_magick (~> 4.0.2) 88 | google-api-client (0.9.3) 89 | addressable (~> 2.3) 90 | googleauth (~> 0.5) 91 | httpclient (~> 2.7) 92 | hurley (~> 0.1) 93 | memoist (~> 0.11) 94 | mime-types (>= 1.6) 95 | representable (~> 2.3.0) 96 | retriable (~> 2.0) 97 | thor (~> 0.19) 98 | googleauth (0.5.1) 99 | faraday (~> 0.9) 100 | jwt (~> 1.4) 101 | logging (~> 2.0) 102 | memoist (~> 0.12) 103 | multi_json (~> 1.11) 104 | os (~> 0.9) 105 | signet (~> 0.7) 106 | gym (1.6.1) 107 | fastlane_core (>= 0.36.1, < 1.0.0) 108 | plist 109 | rubyzip (>= 1.1.7) 110 | terminal-table 111 | xcpretty (>= 0.2.1) 112 | highline (1.7.8) 113 | http-cookie (1.0.2) 114 | domain_name (~> 0.5) 115 | httpclient (2.7.1) 116 | hurley (0.2) 117 | i18n (0.7.0) 118 | json (1.8.3) 119 | jwt (1.5.3) 120 | krausefx-shenzhen (0.14.7) 121 | commander (~> 4.3) 122 | dotenv (>= 0.7) 123 | faraday (~> 0.9) 124 | faraday_middleware (~> 0.9) 125 | highline (>= 1.7.2) 126 | json (~> 1.8) 127 | net-sftp (~> 2.1.2) 128 | plist (~> 3.1.0) 129 | rubyzip (~> 1.1) 130 | security (~> 0.1.3) 131 | terminal-table (~> 1.4.5) 132 | little-plugger (1.1.4) 133 | logging (2.0.0) 134 | little-plugger (~> 1.1) 135 | multi_json (~> 1.10) 136 | match (0.3.0) 137 | cert (>= 1.2.8, < 2.0.0) 138 | credentials_manager (>= 0.13.0, < 1.0.0) 139 | fastlane_core (>= 0.36.1, < 1.0.0) 140 | security 141 | sigh (>= 1.2.2, < 2.0.0) 142 | spaceship (>= 0.18.1, < 1.0.0) 143 | memoist (0.14.0) 144 | method_source (0.8.2) 145 | mime-types (3.0) 146 | mime-types-data (~> 3.2015) 147 | mime-types-data (3.2016.0221) 148 | mini_magick (4.0.4) 149 | mini_portile2 (2.0.0) 150 | minitest (5.8.4) 151 | multi_json (1.11.2) 152 | multi_xml (0.5.5) 153 | multipart-post (2.0.0) 154 | net-sftp (2.1.2) 155 | net-ssh (>= 2.6.5) 156 | net-ssh (3.0.2) 157 | nokogiri (1.6.7.2) 158 | mini_portile2 (~> 2.0.0.rc2) 159 | os (0.9.6) 160 | pem (1.3.0) 161 | fastlane_core (>= 0.36.1, < 1.0.0) 162 | spaceship (>= 0.22.0, < 1.0.0) 163 | pilot (1.4.1) 164 | credentials_manager (>= 0.3.0) 165 | fastlane_core (>= 0.36.5, < 1.0.0) 166 | spaceship (>= 0.20.0, < 1.0.0) 167 | terminal-table (~> 1.4.5) 168 | plist (3.1.0) 169 | produce (1.1.1) 170 | fastlane_core (>= 0.30.0, < 1.0.0) 171 | spaceship (>= 0.16.0) 172 | pry (0.10.3) 173 | coderay (~> 1.1.0) 174 | method_source (~> 0.8.1) 175 | slop (~> 3.4) 176 | representable (2.3.0) 177 | uber (~> 0.0.7) 178 | retriable (2.1.0) 179 | rouge (1.10.1) 180 | rubyzip (1.1.7) 181 | scan (0.5.2) 182 | fastlane_core (>= 0.36.1, < 1.0.0) 183 | slack-notifier (~> 1.3) 184 | terminal-table 185 | xcpretty (>= 0.2.1) 186 | xcpretty-travis-formatter (>= 0.0.3) 187 | screengrab (0.3.0) 188 | fastlane_core (>= 0.36.8, < 1.0.0) 189 | security (0.1.3) 190 | sentry-raven (0.15.6) 191 | faraday (>= 0.7.6) 192 | sigh (1.4.0) 193 | fastlane_core (>= 0.36.1, < 1.0.0) 194 | plist (~> 3.1) 195 | spaceship (>= 0.22.0, < 1.0.0) 196 | signet (0.7.2) 197 | addressable (~> 2.3) 198 | faraday (~> 0.9) 199 | jwt (~> 1.5) 200 | multi_json (~> 1.10) 201 | slack-notifier (1.5.1) 202 | slather (2.0.1) 203 | clamp (~> 0.6) 204 | nokogiri (~> 1.6.3) 205 | xcodeproj (>= 0.28.2, < 1.1.0) 206 | slop (3.6.0) 207 | snapshot (1.11.0) 208 | fastimage (~> 1.6.3) 209 | fastlane_core (>= 0.36.1, < 1.0.0) 210 | plist (~> 3.1.0) 211 | xcpretty (>= 0.2.1) 212 | spaceship (0.22.0) 213 | colored 214 | credentials_manager (>= 0.9.0) 215 | faraday (~> 0.9) 216 | faraday-cookie_jar (~> 0.0.6) 217 | faraday_middleware (~> 0.9) 218 | fastimage (~> 1.6) 219 | multi_xml (~> 0.5) 220 | plist (~> 3.1) 221 | pry 222 | supply (0.5.2) 223 | credentials_manager (>= 0.15.0) 224 | fastlane_core (>= 0.35.0) 225 | google-api-client (~> 0.9.1) 226 | terminal-notifier (1.6.3) 227 | terminal-table (1.4.5) 228 | thor (0.19.1) 229 | thread_safe (0.3.5) 230 | tzinfo (1.2.2) 231 | thread_safe (~> 0.1) 232 | uber (0.0.15) 233 | unf (0.1.4) 234 | unf_ext 235 | unf_ext (0.0.7.2) 236 | xcodeproj (0.28.2) 237 | activesupport (>= 3) 238 | claide (~> 0.9.1) 239 | colored (~> 1.2) 240 | xcpretty (0.2.2) 241 | rouge (~> 1.8) 242 | xcpretty-travis-formatter (0.0.4) 243 | xcpretty (~> 0.2, >= 0.0.7) 244 | 245 | PLATFORMS 246 | ruby 247 | 248 | DEPENDENCIES 249 | fastlane 250 | scan 251 | slather 252 | xcpretty 253 | 254 | BUNDLED WITH 255 | 1.11.2 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Thorpe 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](header.png) 2 | 3 | [![Build status](https://badge.buildkite.com/2110c7f551cf50f252b3c1e998a163d21008d0deb1011ccd53.svg)](https://buildkite.com/blindingskies/taylorsource?branch=development) 4 | [![codecov.io](https://codecov.io/github/danthorpe/TaylorSource/coverage.svg?branch=development)](https://codecov.io/github/danthorpe/TaylorSource?branch=development) 5 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/TaylorSource.svg)](https://img.shields.io/cocoapods/v/TaylorSource.svg) 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![Platform](https://img.shields.io/cocoapods/p/TaylorSource.svg?style=flat)](http://cocoadocs.org/docsets/TaylorSource) 8 | 9 | # Taylor Source 10 | 11 | Taylor Source is a Swift framework for creating highly configurable and reusable data sources. 12 | 13 | ## Installation 14 | Taylor Source is available through [CocoaPods](http://cocoapods.org). To install 15 | it, simply add the following line to your Podfile: 16 | 17 | ```ruby 18 | pod ’TaylorSource’ 19 | ``` 20 | 21 | ## Usage 22 | Taylor Source defines a `DatasourceType` protocol, which has an associated `FactoryType` protocol. These two protocols are the building blocks of the framework. Each has basic concrete implementation classes: `StaticDatasource` and `StaticSectionDatasource` for `DatasourceType`, and `BasicFactory` for `FactoryType`. Both protocols have been designed to be easily implemented. 23 | 24 | ### The Basics 25 | The factory is responsible for registering and vending views. It is a generic protocol, which allows you to configure the base type for cell & view types etc. 26 | 27 | The datasource in initialised with a factory and data, and it can be used to generate `UITableViewDataSource` for example. 28 | 29 | We recommend that a datasource be composed inside a bespoke class. For example, assuming a model type `Event`, a bespoke datasource would be typed: 30 | 31 | ```swift 32 | class EventDatasource: DatasourceProviderType { 33 | typealias Factory = BasicFactory 34 | typealias Datasource = StaticDatasource 35 | 36 | let datasource: Datasource 37 | } 38 | ``` 39 | ### Configuring Cells 40 | Configuring cells is done by providing a closure along with a key and description of the cell type. Huh? Lets break this down. 41 | 42 | The closure type receives three arguments, the cell, an instance of the model, and the index to the item. In the basic factory, this is an `NSIndexPath`. But it can be customised. 43 | 44 | The cell is described with either, a reuse identifier and class: 45 | 46 | ```swift 47 | .ClassWithIdentifier(EventCell.self, EventCell.reuseIdentifier) 48 | ``` 49 | 50 | with a reuse identifier and nib: 51 | 52 | ```swift 53 | .NibWithIdentifier(EventCell.nib, EventCell.reuseIdentifier) 54 | ``` 55 | 56 | or with just a reuse identifier, when the cell is already accessible to the view (such as storyboard prototype cells): 57 | 58 | ```swift 59 | .DynamicWithIdentifier(EventCell.reuseIdentifier) 60 | ``` 61 | 62 | (see `ReusableView` and `ResuableViewDescriptor`) 63 | 64 | The key is just a string used to associate the configuration closure with the cell. Putting this all together means registering and configuring cells is as easy as: 65 | 66 | ```swift 67 | datasource.factory.registerCell(.ClassWithIdentifier(EventCell.self, EventCell.reuseIdentifier), inView: tableView, withKey: “Events”) { (cell, event, indexPath) in 68 | cell.textLabel!.text = “\(event.date.timeAgoSinceNow())” 69 | } 70 | ``` 71 | 72 | although it’s recommended to provide the configuration block as a class function on your custom cell type. See the example project for this. 73 | 74 | ### Supplementary Views 75 | Supplementary views are headers and footers, although `UICollectionView` allows you to define your own. This is all supported, but headers and footers have convenience registration methods. 76 | 77 | Apart from the `kind` of the supplementary view, they function just like cells, except only one closure can be registered per `kind`. In other words, the same closure will configure all your table view section headers. 78 | 79 | ## Advanced 80 | The basics only support static immutable data. Because if you want more than that, it’s best to use a database. The example project in the repo, is inspired by the default Core Data templates from Apple, but are for use with [YapDatabase](http://github.com/yapstudios/yapdatabase). To get started: 81 | 82 | ```ruby 83 | pod ’TaylorSource/YapDatabase’ 84 | ``` 85 | 86 | which make new Factory and Datasource types available. The bespoke datasource from earlier would become: 87 | 88 | ```swift 89 | class EventDatasource: DatasourceProviderType { 90 | typealias Factory = YapDBFactory 91 | typealias Datasource = YapDBDatasource 92 | 93 | let datasource: Datasource 94 | } 95 | ``` 96 | 97 | `YapDBDatasource` implements `DatasourceType` except it fetchs its data from a provided `YapDatabase` instance. Internally, it uses YapDatabase’s [view mappings](https://github.com/yapstudios/YapDatabase/wiki/Views#mappings), which are configured and composed by a database `Observer`. It can be used configured with YapDatabase [views](https://github.com/yapstudios/YapDatabase/wiki/Views), [filtered views](https://github.com/yapstudios/YapDatabase/wiki/FilteredViews) and [searches](https://github.com/yapstudios/YapDatabase/wiki/Full-Text-Search). I strongly recommend you read the YapDatabase wiki pages. But a simple way to think of views, is that they are like saved database queries, and perform filtering, sorting and mapping of your model items. `YapDBDatabase` then provides a common interface for UI components - for all of your queries. 98 | 99 | ### YapDBFactory Cell & View Configuration 100 | The index type provided to the cell (and supplementary view) configuration closure is customisable. For `BasicFactory` these are both `NSIndexPath`. However for `YapDBFactory` the cell index type is a structure which provides not only an `NSIndexPath` but also a `YapDatabaseReadTransaction`. This means that any additional objects required to configure the cell can be read from the database in the closure. 101 | 102 | For supplementary view configure blocks, the index type further has the group which the `YapDatabaseView` defined for the current section. Often, this is the best way to define a “model” for a supplementary view - by making the group an identifier of another type which can be read from the database using the read transaction. 103 | 104 | 105 | ## Multiple Cell & View Types 106 | Often it might be necessary to have different cell designs in the same screen. Or, perhaps different supplementary views. This can be achieved by registering each cell and view with the datasource’s factory. There are some caveats however. 107 | 108 | The Datasource’s Factory implements `FactoryType` which defines the generic type `CellType` and `SupplementaryViewType`. When the design encompasses more than one cell class (or supplementary view class), this generic type becomes the common parent class. 109 | 110 | For a table view design, all cells inherit from `UITableViewCell`, so given two cell subclasses, `EventCell` and `ReminderCell` the definition from the example above would be: 111 | 112 | ```swift 113 | class EventCell: UITableViewCell { } 114 | 115 | class ReminderCell: UITableViewCell { } 116 | 117 | class EventDatasource: DatasourceProviderType { 118 | // Note that cell is common parent class. 119 | typealias Factory = YapDBFactory 120 | typealias Datasource = YapDBDatasource 121 | 122 | let datasource: Datasource 123 | } 124 | ``` 125 | 126 | In some situations it is often efficient to use a base cell class, for example, `ReminderCell` could in fact be a specialized `EventCell`. 127 | 128 | ```swift 129 | class EventCell: UITableViewCell { } 130 | 131 | class ReminderCell: EventCell { } 132 | 133 | class EventDatasource: DatasourceProviderType { 134 | // Note that here EventCell is our common parent cell. 135 | typealias Factory = YapDBFactory 136 | typealias Datasource = YapDBDatasource 137 | 138 | let datasource: Datasource 139 | } 140 | ``` 141 | 142 | The same rules apply for supplementary views e.g. section headers and footers, which inherit from `UITableHeaderFooterView` for table views, and `UICollectionReusableView` for collection views. 143 | 144 | ### Registering multiple cells 145 | 146 | Registering a cell requires the containing view, e.g. `UITableView` instance. So, recommended best practice is to initialise a custom datasource provider with the table view. Following on with the example of two cell subclasses: 147 | 148 | ```swift 149 | class EventDatasource: DatasourceProviderType { 150 | typealias Factory = YapDBFactory 151 | typealias Datasource = YapDBDatasource 152 | 153 | let datasource: Datasource 154 | 155 | init(db: YapDatabase, view: Factory.ViewType) { 156 | // Create a factory. This closure is discussed below. 157 | let factory = Factory(cell: { (event, index) in return event.isReminder ? "reminder" : "event" } ) 158 | 159 | // Create the datasource 160 | datasource = Datasource( 161 | // this can be whatever you want, to help debugging. 162 | id: "Events datasource", 163 | // the YapDatabase instance - see YapDB from YapDatabaseExtensions. 164 | database: db, 165 | // the factory we defined above. 166 | factory: factory, 167 | // provided by TaylorSource to auto-update from YapDatabase changes. 168 | processChanges: view.processChanges, 169 | // See example code for info on creating YapDB Configurations. Essentially this is a database fetch request for Event objects. 170 | configuration: eventsConfiguration() 171 | ) 172 | 173 | // Register the cells 174 | datasource.factory.registerCell( 175 | // See ReusableView in TaylorSource. 176 | .ClassWithIdentifier(EventCell.self, EventCell.reuseIdentifier), 177 | inView: view, 178 | // The key used to look up the correct cell. See discussion below. 179 | withKey: "event", 180 | // This is a static fnction which returns a closure. See below. 181 | configuration: EventCell.configuration() 182 | ) 183 | 184 | datasource.factory.registerCell( 185 | .NibWithIdentifier(ReminderCell.nib, ReminderCell.reuseIdentifier), 186 | inView: view, 187 | withKey: "reminder", 188 | configuration: ReminderCell.configuration() 189 | ) 190 | } 191 | } 192 | ``` 193 | 194 | This requires both cell classes to implement `ReusableView` and return a configuration block. This is a feature of using TaylorSource, the configuration of cells is defined by a static closure on the cell itself, which decouples them from view controllers - increasing usability. 195 | 196 | The factory will vend an instance of the cell Therefore inside the configuration closure, it must be cast. For example: 197 | 198 | ```swift 199 | 200 | class EventCell: UITableViewCell { 201 | 202 | @IBOutlet var iconView: UIImageView! 203 | 204 | class func configuration() -> EventsDatasource.Datasource.FactoryType.CellConfiguration { 205 | /* The `cell` constant here is typed as EventsDatasource.Datasource.FactoryType.CellType, 206 | which in this example is UITableViewCell because we also have ReminderCell registered. */ 207 | return { (cell, event, index) in 208 | cell.textLabel!.text = “\(event.date.timeAgoSinceNow())” 209 | if let eventCell = cell as! EventCell { 210 | eventCell.iconView.image = event.icon.image 211 | } 212 | // Note that we are only concerned with configuring the EventCell here. 213 | } 214 | } 215 | } 216 | ``` 217 | 218 | ### How does the factory dequeue and configure the correct cell class? 219 | 220 | The example above glossed over a closure which the factory was initialized with. This is a critical detail when using multiple cell classes in the same container. In such a scenario, some cells are one design, other cells are another. The logic of this switch is provided to TaylorSource's Factory class (which is the base implementation of `FactoryType`) via closures at its initialization. These closures are typed as follows: 221 | 222 | ```swift 223 | Factory.GetCellKey = (Item, CellIndexType) -> String 224 | Factory.GetSupplementaryKey = (SupplementaryIndexType) -> String 225 | ``` 226 | 227 | For cells, this means that the closure will receive the model item, and it's index inside the datasource. For YapDBDatasources, this means that the index will be a struct providing both the `NSIndexPath` but also a `YapDatabaseReadTransaction`. The closure should use this information and return a `String`, which is used as a look up key for the cell. In our example above, the `Event` model has an `isReminder` boolean property. 228 | 229 | The keys returned by the closure are used as the `withKey` argument when registering the corresponding cell. 230 | 231 | See the Gallery example project for an demonstration of using multiple cell styles in the same datasource. 232 | 233 | ## Using enums for multiple models 234 | 235 | If the design calls for a table view (or collection view) with multiple different cells, each with their own model, use an enum to wrap the corresponding models. For the example above, consider that `EventCell` requires an `Event` model, and `ReminderCell` requires a `Reminder` model. How would we put this into a datasource? 236 | 237 | ```swift 238 | struct Event {} 239 | struct Reminder {} 240 | 241 | enum CellModel { // but come up with a better name! 242 | case Event(ModuleName.Event) // Use full name definition to avoid clashes. 243 | case Reminder(ModuleName.Reminder) 244 | } 245 | 246 | extension CellModel { 247 | static var getCellKey: EventDatasource.Datasource.FactoryType.GetCellKey { 248 | return { (item, _) in 249 | switch item { 250 | case .Event(_): return "event" 251 | case .Reminder(_): return "reminder" 252 | } 253 | } 254 | } 255 | } 256 | 257 | class EventDatasource: DatasourceProviderType { 258 | typealias Factory = YapDBFactory 259 | typealias Datasource = YapDBDatasource 260 | 261 | let datasource: Datasource 262 | 263 | init(db: YapDatabase, view: Factory.ViewType) { 264 | // Create a factory. Use the closure as defined on the cell model. 265 | let factory = Factory(cell: CellModel.getCellKey) 266 | 267 | 268 | // etc 269 | } 270 | } 271 | ``` 272 | 273 | ## Editable Table View Data Sources 274 | 275 | Apple’s `UITableViewDataSource` has some optional methods which support the table view when it is in editing mode. Editing a table view in general means inserting, deleting or moving rows. 276 | 277 | TaylorSource support this functionality via optional closures defined on `DatasourceProviderType`. To enable the optional methods on the generated `UITableViewDataSource` from `TableViewDatasourceProvider` all four closures must be provided, using an empty implementation if necessary. 278 | 279 | See the Events example project for how deleting rows from a `YapDatabase` backed table view works. 280 | 281 | 282 | 283 | ## Design Goals 284 | 1. Be D.R.Y. - I never want to have to implement `func tableView(_: UITableView, numberOfRowsInSection: Int) -> Int` ever again. 285 | 2. Be super easy to decouple data source types from view controllers. 286 | 3. Support custom cell and supplementary view classes. 287 | 4. Customise cells and views with type safe closures. 288 | 5. Be super easy to compose and extend data source types. 289 | 290 | ## Contributing 291 | 292 | Until TaylorSource's APIs have stabilized (post Swift 2.0) I am not looking for contribution for code. Contributions in the form of pull requests for improvements to documentation, spelling mistakes, unit tests, integration aids are always welcome. 293 | 294 | ## Author 295 | 296 | Daniel Thorpe - @danthorpe 297 | 298 | ## Licence 299 | 300 | Taylor Source is available under the MIT license. See the LICENSE file for more info. 301 | 302 | -------------------------------------------------------------------------------- /Sources/Base/Basic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Basic.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 29/07/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Simple wrapper for a Datasource. TaylorSource is designed for composing Datasources 13 | inside custom classes, referred to as *datasource providers*. There are Table View 14 | and Collection View data source generators which accept datasource providers. 15 | 16 | Therefore, if you absolutely don't want your own custom class to act as the datasource 17 | provider, this structure is available to easily wrap any DatasourceType. e.g. 18 | 19 | let datasource: UITableViewDataSourceProvider> 20 | tableView.dataSource = datasource.tableViewDataSource 21 | */ 22 | public struct BasicDatasourceProvider: DatasourceProviderType { 23 | 24 | /// The wrapped Datasource 25 | public let datasource: Datasource 26 | 27 | public let editor = NoEditor() 28 | 29 | public init(_ d: Datasource) { 30 | datasource = d 31 | } 32 | } 33 | 34 | /** 35 | A concrete implementation of DatasourceType for simple immutable arrays of objects. 36 | The static datasource is initalized with the model items to display. They all 37 | are in the same section. 38 | 39 | The cell and supplementary index types are both NSIndexPath, which means using a 40 | BasicFactory. This means that the configure block for cells and supplementary views 41 | will receive an NSIndexPath as their index argument. 42 | */ 43 | public final class StaticDatasource< 44 | Factory 45 | where 46 | Factory: _FactoryType, 47 | Factory.CellIndexType == NSIndexPath, 48 | Factory.SupplementaryIndexType == NSIndexPath>: DatasourceType, SequenceType, CollectionType { 49 | 50 | public typealias FactoryType = Factory 51 | 52 | public let factory: Factory 53 | public let identifier: String 54 | public var title: String? = .None 55 | private var items: [Factory.ItemType] 56 | 57 | /** 58 | The initializer. 59 | 60 | - parameter id: a String identifier 61 | - parameter factory: a Factory whose CellIndexType and SupplementaryIndexType must be NSIndexPath, such as BasicFactory. 62 | - parameter items: an array of Factory.ItemType instances. 63 | */ 64 | public init(id: String, factory f: Factory, items i: [Factory.ItemType]) { 65 | identifier = id 66 | factory = f 67 | items = i 68 | } 69 | 70 | /// The number of section, always 1 for a static datasource 71 | public var numberOfSections: Int { 72 | return 1 73 | } 74 | 75 | /// The number of items in a section, always the item count for a static datasource 76 | public func numberOfItemsInSection(sectionIndex: Int) -> Int { 77 | return items.count 78 | } 79 | 80 | /** 81 | The item at an indexPath. Will ignore the section property of the NSIndexPath. 82 | Will also return .None if the indexPath item index is out of bounds of the 83 | array of items. 84 | 85 | - parameter indexPath: an NSIndexPath 86 | - returns: an optional Factory.ItemType 87 | */ 88 | public func itemAtIndexPath(indexPath: NSIndexPath) -> Factory.ItemType? { 89 | if items.startIndex <= indexPath.item && indexPath.item < items.endIndex { 90 | return items[indexPath.item] 91 | } 92 | return .None 93 | } 94 | 95 | /** 96 | Will return a cell. 97 | 98 | The cell is configured with the item at the index path first. 99 | Note, that the itemAtIndexPath method will gracefully return a .None if the 100 | indexPath is out of range. Here, we fatalError which will deliberately crash the 101 | app. 102 | 103 | - parameter view: the view instance. 104 | - parameter indexPath: an NSIndexPath 105 | - returns: a dequeued and configured Factory.CellType 106 | */ 107 | public func cellForItemInView(view: Factory.ViewType, atIndexPath indexPath: NSIndexPath) -> Factory.CellType { 108 | if let item = itemAtIndexPath(indexPath) { 109 | return factory.cellForItem(item, inView: view, atIndex: indexPath) 110 | } 111 | fatalError("No item available at index path: \(indexPath)") 112 | } 113 | 114 | /** 115 | Will return a supplementary view. 116 | This is the result of running any registered closure from the factory 117 | for this supplementary element kind. 118 | 119 | - parameter view: the view instance. 120 | - parameter kind" the SupplementaryElementKind of the supplementary view. 121 | - returns: a dequeued and configured Factory.SupplementaryViewType 122 | */ 123 | public func viewForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.SupplementaryViewType? { 124 | return factory.supplementaryViewForKind(kind, inView: view, atIndex: indexPath) 125 | } 126 | 127 | /** 128 | Will return an optional text for the supplementary kind 129 | 130 | - parameter view: the View which should dequeue the cell. 131 | - parameter kind: the kind of the supplementary element. See SupplementaryElementKind 132 | - parameter indexPath: the NSIndexPath of the item. 133 | - returns: a TextType? 134 | */ 135 | public func textForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.TextType? { 136 | return factory.supplementaryTextForKind(kind, atIndex: indexPath) 137 | } 138 | 139 | // SequenceType 140 | 141 | public func generate() -> Array.Generator { 142 | return items.generate() 143 | } 144 | 145 | // CollectionType 146 | 147 | public var startIndex: Int { 148 | return items.startIndex 149 | } 150 | 151 | public var endIndex: Int { 152 | return items.endIndex 153 | } 154 | 155 | public subscript(i: Int) -> Factory.ItemType { 156 | return items[i] 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /Sources/Base/Datasource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 16/04/2015. 3 | // 4 | 5 | import UIKit 6 | 7 | /** 8 | The core protocol of Datasource functionality. 9 | 10 | It has an associated type, _FactoryType which in turn is responsible for 11 | associates types for item model, cell class, supplementary view classes, 12 | parent view class and index types. 13 | 14 | The factory provides an API to register cells and views, and in 15 | turn it can be used to vend cells and view. 16 | 17 | Types which implement DatasourceType (this protocol) should use the 18 | factory in conjuction with a storage medium for data items. 19 | 20 | This protocol exists to allow for the definition of different kinds of 21 | datasources. Coupled with DatasourceProviderType, datasources can be 22 | composed and extended with ease. See SegmentedDatasource for example. 23 | */ 24 | public protocol DatasourceType { 25 | associatedtype FactoryType: _FactoryType 26 | 27 | /// Access the factory from the datasource, likely should be a stored property. 28 | var factory: FactoryType { get } 29 | 30 | /// An identifier which is primarily to ease debugging and logging. 31 | var identifier: String { get } 32 | 33 | /// Optional human readable title 34 | var title: String? { get } 35 | 36 | /// The number of sections in the data source 37 | var numberOfSections: Int { get } 38 | 39 | /** 40 | The number of items in the section. 41 | 42 | - parameter section: The section index 43 | - returns: An Int, the number of items. 44 | */ 45 | func numberOfItemsInSection(sectionIndex: Int) -> Int 46 | 47 | /** 48 | Access the underlying data item at the indexPath. 49 | 50 | - parameter indexPath: A NSIndexPath instance. 51 | - returns: An optional Item 52 | */ 53 | func itemAtIndexPath(indexPath: NSIndexPath) -> FactoryType.ItemType? 54 | 55 | /** 56 | Vends a configured cell for the item. 57 | 58 | - parameter view: the View which should dequeue the cell. 59 | - parameter indexPath: the NSIndexPath of the item. 60 | - returns: a FactoryType.CellType instance, this should be dequeued and configured. 61 | */ 62 | func cellForItemInView(view: FactoryType.ViewType, atIndexPath indexPath: NSIndexPath) -> FactoryType.CellType 63 | 64 | /** 65 | Vends a configured supplementary view of kind. 66 | 67 | - parameter view: the View which should dequeue the cell. 68 | - parameter kind: the kind of the supplementary element. See SupplementaryElementKind 69 | - parameter indexPath: the NSIndexPath of the item. 70 | - returns: a Factory.Type.SupplementaryViewType instance, this should be dequeued and configured. 71 | */ 72 | func viewForSupplementaryElementInView(view: FactoryType.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> FactoryType.SupplementaryViewType? 73 | 74 | /** 75 | Vends a optional text for the supplementary kind 76 | 77 | - parameter view: the View which should dequeue the cell. 78 | - parameter kind: the kind of the supplementary element. See SupplementaryElementKind 79 | - parameter indexPath: the NSIndexPath of the item. 80 | - returns: a TextType? 81 | */ 82 | func textForSupplementaryElementInView(view: FactoryType.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> FactoryType.TextType? 83 | } 84 | 85 | public enum EditableDatasourceAction: Int { 86 | case None = 1, Insert, Delete 87 | 88 | public var editingStyle: UITableViewCellEditingStyle { 89 | switch self { 90 | case .None: return .None 91 | case .Insert: return .Insert 92 | case .Delete: return .Delete 93 | } 94 | } 95 | 96 | public init?(editingStyle: UITableViewCellEditingStyle) { 97 | switch editingStyle { 98 | case .None: 99 | self = .None 100 | case .Insert: 101 | self = .Insert 102 | case .Delete: 103 | self = .Delete 104 | } 105 | } 106 | } 107 | 108 | public typealias CanEditItemAtIndexPath = (indexPath: NSIndexPath) -> Bool 109 | public typealias CommitEditActionForItemAtIndexPath = (action: EditableDatasourceAction, indexPath: NSIndexPath) -> Void 110 | public typealias EditActionForItemAtIndexPath = (indexPath: NSIndexPath) -> EditableDatasourceAction 111 | public typealias CanMoveItemAtIndexPath = (indexPath: NSIndexPath) -> Bool 112 | public typealias CommitMoveItemAtIndexPathToIndexPath = (from: NSIndexPath, to: NSIndexPath) -> Void 113 | 114 | public protocol DatasourceEditorType { 115 | 116 | var canEditItemAtIndexPath: CanEditItemAtIndexPath? { get } 117 | var commitEditActionForItemAtIndexPath: CommitEditActionForItemAtIndexPath? { get } 118 | var editActionForItemAtIndexPath: EditActionForItemAtIndexPath? { get } 119 | var canMoveItemAtIndexPath: CanMoveItemAtIndexPath? { get } 120 | var commitMoveItemAtIndexPathToIndexPath: CommitMoveItemAtIndexPathToIndexPath? { get } 121 | } 122 | 123 | /** 124 | Suggested usage is not to use a DatasourceType directly, but instead to create 125 | a bespoke type which implements this protocol, DatasourceProviderType, and vend 126 | a configured datasource. This type could be considered to be like a view model 127 | in MVVM paradigms. But in traditional MVC, such a type is just a model, which 128 | the view controller initalizes and owns. 129 | */ 130 | public protocol DatasourceProviderType { 131 | 132 | associatedtype Datasource: DatasourceType 133 | associatedtype Editor: DatasourceEditorType 134 | 135 | /// The underlying Datasource. 136 | var datasource: Datasource { get } 137 | 138 | /// An datasource editor 139 | var editor: Editor { get } 140 | } 141 | 142 | public struct NoEditor: DatasourceEditorType { 143 | public let canEditItemAtIndexPath: CanEditItemAtIndexPath? = .None 144 | public let commitEditActionForItemAtIndexPath: CommitEditActionForItemAtIndexPath? = .None 145 | public let editActionForItemAtIndexPath: EditActionForItemAtIndexPath? = .None 146 | public let canMoveItemAtIndexPath: CanMoveItemAtIndexPath? = .None 147 | public let commitMoveItemAtIndexPathToIndexPath: CommitMoveItemAtIndexPathToIndexPath? = .None 148 | public init() {} 149 | } 150 | 151 | public struct Editor: DatasourceEditorType { 152 | 153 | public let canEditItemAtIndexPath: CanEditItemAtIndexPath? 154 | public let commitEditActionForItemAtIndexPath: CommitEditActionForItemAtIndexPath? 155 | public let editActionForItemAtIndexPath: EditActionForItemAtIndexPath? 156 | public let canMoveItemAtIndexPath: CanMoveItemAtIndexPath? 157 | public let commitMoveItemAtIndexPathToIndexPath: CommitMoveItemAtIndexPathToIndexPath? 158 | 159 | public init( 160 | canEdit: CanEditItemAtIndexPath? = .None, 161 | commitEdit: CommitEditActionForItemAtIndexPath? = .None, 162 | editAction: EditActionForItemAtIndexPath? = .None, 163 | canMove: CanMoveItemAtIndexPath? = .None, 164 | commitMove: CommitMoveItemAtIndexPathToIndexPath? = .None) { 165 | canEditItemAtIndexPath = canEdit 166 | commitEditActionForItemAtIndexPath = commitEdit 167 | editActionForItemAtIndexPath = editAction 168 | canMoveItemAtIndexPath = canMove 169 | commitMoveItemAtIndexPathToIndexPath = commitMove 170 | } 171 | } 172 | 173 | public struct ComposedEditor: DatasourceEditorType { 174 | 175 | public let canEditItemAtIndexPath: CanEditItemAtIndexPath? 176 | public let commitEditActionForItemAtIndexPath: CommitEditActionForItemAtIndexPath? 177 | public let editActionForItemAtIndexPath: EditActionForItemAtIndexPath? 178 | public let canMoveItemAtIndexPath: CanMoveItemAtIndexPath? 179 | public let commitMoveItemAtIndexPathToIndexPath: CommitMoveItemAtIndexPathToIndexPath? 180 | 181 | public init(editor: DatasourceEditorType) { 182 | canEditItemAtIndexPath = editor.canEditItemAtIndexPath 183 | commitEditActionForItemAtIndexPath = editor.commitEditActionForItemAtIndexPath 184 | editActionForItemAtIndexPath = editor.editActionForItemAtIndexPath 185 | canMoveItemAtIndexPath = editor.canMoveItemAtIndexPath 186 | commitMoveItemAtIndexPathToIndexPath = editor.commitMoveItemAtIndexPathToIndexPath 187 | } 188 | } 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /Sources/Base/Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entity.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 29/07/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol EntityType { 12 | associatedtype ItemType 13 | 14 | var numberOfSections: Int { get } 15 | 16 | func numberOfItemsInSection(sectionIndex: Int) -> Int 17 | 18 | func itemAtIndexPath(indexPath: NSIndexPath) -> ItemType? 19 | } 20 | 21 | public struct EntityDatasource< 22 | Factory, Entity 23 | where 24 | Entity: EntityType, 25 | Factory: _FactoryType, 26 | Entity.ItemType == Factory.ItemType, 27 | Factory.CellIndexType == NSIndexPath, 28 | Factory.SupplementaryIndexType == NSIndexPath>: DatasourceType { 29 | 30 | public typealias FactoryType = Factory 31 | 32 | public let factory: Factory 33 | public let identifier: String 34 | public var title: String? = .None 35 | public private(set) var entity: Entity 36 | 37 | public init(id: String, factory f: Factory, entity e: Entity) { 38 | identifier = id 39 | factory = f 40 | entity = e 41 | } 42 | 43 | // Datasource 44 | 45 | public var numberOfSections: Int { 46 | return entity.numberOfSections 47 | } 48 | 49 | public func numberOfItemsInSection(sectionIndex: Int) -> Int { 50 | return entity.numberOfItemsInSection(sectionIndex) 51 | } 52 | 53 | public func itemAtIndexPath(indexPath: NSIndexPath) -> Factory.ItemType? { 54 | return entity.itemAtIndexPath(indexPath) 55 | } 56 | 57 | public func cellForItemInView(view: Factory.ViewType, atIndexPath indexPath: NSIndexPath) -> Factory.CellType { 58 | if let item = itemAtIndexPath(indexPath) { 59 | return factory.cellForItem(item, inView: view, atIndex: indexPath) 60 | } 61 | fatalError("No item available at index path: \(indexPath)") 62 | } 63 | 64 | public func viewForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.SupplementaryViewType? { 65 | return factory.supplementaryViewForKind(kind, inView: view, atIndex: indexPath) 66 | } 67 | 68 | public func textForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.TextType? { 69 | return factory.supplementaryTextForKind(kind, atIndex: indexPath) 70 | } 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /Sources/Base/Sectioned.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Objects adopting this protocol can be used as sections in a sectioned data source. 5 | 6 | A section can be anything, as long as it supplies an array of items in that section. 7 | 8 | Adding additional properties, such as the section title or an icon, will allow 9 | you to properly customize supplementary views, such as section headers or footers. 10 | */ 11 | public protocol SectionType: SequenceType, CollectionType { 12 | associatedtype ItemType 13 | var items: [ItemType] { get } 14 | } 15 | 16 | // Let SectionType behave as a sequence of ItemType elements 17 | extension SectionType where Self: SequenceType { 18 | public func generate() -> Array.Generator { 19 | return items.generate() 20 | } 21 | } 22 | 23 | // Let SectionType behave as a collection of ItemType elements 24 | extension SectionType where Self: CollectionType { 25 | public var startIndex: Int { 26 | return items.startIndex 27 | } 28 | 29 | public var endIndex: Int { 30 | return items.endIndex 31 | } 32 | 33 | public subscript(i: Int) -> ItemType { 34 | return items[i] 35 | } 36 | } 37 | 38 | /** 39 | A concrete implementation of `DatasourceType` for a fixed set of sectioned data. 40 | 41 | The data source is initalized with the section models it contains. Each section 42 | contains an immutable array of the items in that section. 43 | 44 | The cell and supplementary index types are both `NSIndexPath`, making this class 45 | compatible with a `BasicFactory`. This also means that the configuration block for 46 | cells and supplementary views will receive an NSIndexPath as their index argument. 47 | */ 48 | public class StaticSectionDatasource: DatasourceType { 54 | 55 | public typealias FactoryType = Factory 56 | 57 | public var title: String? 58 | public let factory: Factory 59 | public let identifier: String 60 | private var sections: [StaticSectionType] 61 | 62 | /** 63 | The designated initializer. 64 | 65 | - parameter id: A `String` identifier. 66 | - parameter factory: A `Factory` whose `CellIndexType` and `SupplementaryIndexType` must be `NSIndexPath`, such as `BasicFactory`. 67 | - parameter sections: An array of `SectionType` instances where `SectionType.ItemType` matches `Factory.ItemType`. 68 | */ 69 | public init(id: String, factory f: Factory, sections s: [StaticSectionType]) { 70 | identifier = id 71 | factory = f 72 | sections = s 73 | } 74 | 75 | /// The number of sections. 76 | public var numberOfSections: Int { 77 | return sections.count 78 | } 79 | 80 | /// The number of items in the section with the given index. 81 | public func numberOfItemsInSection(sectionIndex: Int) -> Int { 82 | return sections[sectionIndex].items.count 83 | } 84 | 85 | /** 86 | Access the section model object for a given index. 87 | 88 | Use this to configure any supplementary views, headers and footers. 89 | 90 | - parameter index: The index of the section. 91 | - returns: The section object at `index` or `.None` if `index` is out of bounds. 92 | */ 93 | public func sectionAtIndex(index: Int) -> StaticSectionType? { 94 | if sections.startIndex <= index && index < sections.endIndex { 95 | return sections[index] 96 | } 97 | return nil 98 | } 99 | 100 | /** 101 | The item for a given index path. 102 | 103 | - parameter indexPath: The index path of the item. 104 | - returns: The item at `indexPath` or `.None` if `indexPath` is out of bounds. 105 | */ 106 | public func itemAtIndexPath(indexPath: NSIndexPath) -> Factory.ItemType? { 107 | guard let section = sectionAtIndex(indexPath.section) else { 108 | return .None 109 | } 110 | if sections.startIndex <= indexPath.item && indexPath.item < sections.endIndex { 111 | return section[indexPath.item] 112 | } 113 | return nil 114 | } 115 | 116 | /** 117 | Returns a configured cell. 118 | 119 | The cell is dequeued from the supplied view and configured with the item at the 120 | supplied index path. 121 | 122 | Note, that while `itemAtIndexPath` will gracefully return `.None` if the 123 | index path is out of range, this method will trigger a fatal error if 124 | `indexPath` does not reference a valid entry in the dataset. 125 | 126 | - parameter view: The containing view instance responsible for dequeueing. 127 | - parameter indexPath: The `NSIndexPath` for the item. 128 | - returns: A dequeued and configured instance of `Factory.CellType`. 129 | */ 130 | public func cellForItemInView(view: Factory.ViewType, atIndexPath indexPath: NSIndexPath) -> Factory.CellType { 131 | if let item = itemAtIndexPath(indexPath) { 132 | return factory.cellForItem(item, inView: view, atIndex: indexPath) 133 | } 134 | fatalError("No item available at index path: \(indexPath)") 135 | } 136 | 137 | /** 138 | Returns a configured supplementary view. 139 | 140 | This is the result of running any registered closure from the factory 141 | for this supplementary element kind. 142 | 143 | - parameter view: The containing view instance responsible for dequeueing. 144 | - parameter kind: The `SupplementaryElementKind` of the supplementary view. 145 | - parameter indexPath: The `NSIndexPath` for the item. 146 | - returns: A dequeued and configured instance of `Factory.SupplementaryViewType`. 147 | */ 148 | public func viewForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.SupplementaryViewType? { 149 | return factory.supplementaryViewForKind(kind, inView: view, atIndex: indexPath) 150 | } 151 | 152 | /** 153 | Returns an optional text for the supplementary element kind 154 | 155 | - parameter view: The containing view instance responsible for dequeueing. 156 | - parameter kind: The `SupplementaryElementKind` of the supplementary view. 157 | - parameter indexPath: The `NSIndexPath` for the item. 158 | - returns: A `TextType?` for the supplementary element. 159 | */ 160 | public func textForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.TextType? { 161 | return factory.supplementaryTextForKind(kind, atIndex: indexPath) 162 | } 163 | } 164 | 165 | // Let StaticSectionDatasource behave as a sequence of StaticSectionType elements 166 | extension StaticSectionDatasource: SequenceType { 167 | public func generate() -> Array.Generator { 168 | return sections.generate() 169 | } 170 | } 171 | 172 | // Let StaticSectionDatasource behave as a collection of StaticSectionType elements 173 | extension StaticSectionDatasource: CollectionType { 174 | public var startIndex: Int { 175 | return sections.startIndex 176 | } 177 | 178 | public var endIndex: Int { 179 | return sections.endIndex 180 | } 181 | 182 | public subscript(i: Int) -> StaticSectionType { 183 | return sections[i] 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Sources/Base/Segmented.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Segmented.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 29/07/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct SegmentedState { 12 | var selectedIndex: Int = 0 13 | } 14 | 15 | /** 16 | A Segmented Datasource. 17 | 18 | Usage scenario is where a view (e.g. UITableView) displays the content relevant to 19 | a selected UISegmentedControl tab, typically displayed above or below the table. 20 | 21 | The SegmentedDatasource receives an array of DatasourceProviderType instances. This 22 | means they must all be the same type, and therefore have the same underlying associated 23 | types, e.g. model type. However, typical usage would involve using the tabs to 24 | filter the same type of objects with some other metric. See the Example project. 25 | 26 | */ 27 | public final class SegmentedDatasource: DatasourceType, SequenceType, CollectionType { 28 | 29 | public typealias UpdateBlock = () -> Void 30 | 31 | private let state: Protector 32 | private var valueChangedHandler: TargetActionHandler? = .None 33 | internal let update: UpdateBlock? 34 | 35 | public let datasources: [DatasourceProvider] 36 | 37 | public let identifier: String 38 | 39 | /// The index of the currently selected datasource provider 40 | public var indexOfSelectedDatasource: Int { 41 | return state.read { state in 42 | state.selectedIndex 43 | } 44 | } 45 | 46 | /// The currently selected datasource provider 47 | public var selectedDatasourceProvider: DatasourceProvider { 48 | return datasources[indexOfSelectedDatasource] 49 | } 50 | 51 | /// The currently selected datasource 52 | public var selectedDatasource: DatasourceProvider.Datasource { 53 | return selectedDatasourceProvider.datasource 54 | } 55 | 56 | /// The currently selected datasource's factory 57 | public var factory: DatasourceProvider.Datasource.FactoryType { 58 | return selectedDatasource.factory 59 | } 60 | 61 | /// The currently selected datasource's title 62 | public var title: String? { 63 | return selectedDatasource.title 64 | } 65 | 66 | init(id: String, datasources d: [DatasourceProvider], selectedIndex: Int = 0, didSelectDatasourceCompletion: UpdateBlock) { 67 | identifier = id 68 | datasources = d 69 | state = Protector(SegmentedState(selectedIndex: selectedIndex)) 70 | update = didSelectDatasourceCompletion 71 | } 72 | 73 | /** 74 | Configures a segmented control with the datasource. 75 | 76 | This will iterate through the datasources and insert a segment using the 77 | datasource title for each one. 78 | 79 | Additionally, it will add a handler for the .ValueChanged control event. The 80 | action will select the appropriate datasource and call the 81 | didSelectDatasourceCompletion completion block. 82 | 83 | - parameter segmentedControl: the UISegmentedControl to configure. 84 | */ 85 | public func configureSegmentedControl(segmentedControl: UISegmentedControl) { 86 | segmentedControl.removeAllSegments() 87 | 88 | for (index, provider) in datasources.enumerate() { 89 | let title = provider.datasource.title ?? "No title" 90 | segmentedControl.insertSegmentWithTitle(title, atIndex: index, animated: false) 91 | } 92 | 93 | valueChangedHandler = TargetActionHandler { self.selectedSegmentedIndexDidChange($0) } 94 | 95 | segmentedControl.addTarget(valueChangedHandler!, action: valueChangedHandler!.dynamicType.selector, forControlEvents: .ValueChanged) 96 | segmentedControl.selectedSegmentIndex = indexOfSelectedDatasource 97 | } 98 | 99 | 100 | /** 101 | Programatic interface to select a datasource at a given index. 102 | 103 | - parameter index: an Int index. 104 | */ 105 | public func selectDatasourceAtIndex(index: Int) { 106 | precondition(0 <= index, "Index must be greater than zero.") 107 | precondition(index < datasources.count, "Index must be less than maximum number of datasources.") 108 | 109 | state.write({ (inout state: SegmentedState) in 110 | state.selectedIndex = index 111 | }, completion: update) 112 | } 113 | 114 | func selectedSegmentedIndexDidChange(sender: AnyObject?) { 115 | if let segmentedControl = sender as? UISegmentedControl { 116 | segmentedControl.userInteractionEnabled = false 117 | selectDatasourceAtIndex(segmentedControl.selectedSegmentIndex) 118 | segmentedControl.userInteractionEnabled = true 119 | } 120 | } 121 | 122 | // DatasourceType 123 | 124 | public var numberOfSections: Int { 125 | return selectedDatasource.numberOfSections 126 | } 127 | 128 | public func numberOfItemsInSection(sectionIndex: Int) -> Int { 129 | return selectedDatasource.numberOfItemsInSection(sectionIndex) 130 | } 131 | 132 | public func itemAtIndexPath(indexPath: NSIndexPath) -> DatasourceProvider.Datasource.FactoryType.ItemType? { 133 | return selectedDatasource.itemAtIndexPath(indexPath) 134 | } 135 | 136 | public func cellForItemInView(view: DatasourceProvider.Datasource.FactoryType.ViewType, atIndexPath indexPath: NSIndexPath) -> DatasourceProvider.Datasource.FactoryType.CellType { 137 | return selectedDatasource.cellForItemInView(view, atIndexPath: indexPath) 138 | } 139 | 140 | public func viewForSupplementaryElementInView(view: DatasourceProvider.Datasource.FactoryType.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> DatasourceProvider.Datasource.FactoryType.SupplementaryViewType? { 141 | return selectedDatasource.viewForSupplementaryElementInView(view, kind: kind, atIndexPath: indexPath) 142 | } 143 | 144 | public func textForSupplementaryElementInView(view: DatasourceProvider.Datasource.FactoryType.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> DatasourceProvider.Datasource.FactoryType.TextType? { 145 | return selectedDatasource.textForSupplementaryElementInView(view, kind: kind, atIndexPath: indexPath) 146 | } 147 | 148 | // SequenceType 149 | 150 | public func generate() -> Array.Generator { 151 | return datasources.generate() 152 | } 153 | 154 | // CollectionType 155 | 156 | public var startIndex: Int { 157 | return datasources.startIndex 158 | } 159 | 160 | public var endIndex: Int { 161 | return datasources.endIndex 162 | } 163 | 164 | public subscript(i: Int) -> DatasourceProvider { 165 | return datasources[i] 166 | } 167 | } 168 | 169 | public struct SegmentedDatasourceProvider: DatasourceProviderType { 170 | 171 | public typealias UpdateBlock = () -> Void 172 | 173 | public let datasource: SegmentedDatasource 174 | 175 | public var editor: ComposedEditor { 176 | return ComposedEditor(editor: selectedDatasourceProvider.editor) 177 | } 178 | 179 | /// The index of the currently selected datasource provider 180 | public var indexOfSelectedDatasource: Int { 181 | return datasource.indexOfSelectedDatasource 182 | } 183 | 184 | /// The currently selected datasource provider 185 | public var selectedDatasourceProvider: DatasourceProvider { 186 | return datasource.selectedDatasourceProvider 187 | } 188 | 189 | /// The currently selected datasource 190 | public var selectedDatasource: DatasourceProvider.Datasource { 191 | return datasource.selectedDatasource 192 | } 193 | 194 | /** 195 | The initializer. 196 | 197 | - parameter id: a String identifier for the datasource. 198 | - parameter datasources: an array of DatasourceProvider instances. 199 | - parameter selectedIndex: the index of the initial selection. 200 | - parameter didSelectDatasourceCompletion: a completion block which executes when selecting the datasource has completed. This block should reload the view. 201 | */ 202 | public init(id: String, datasources: [DatasourceProvider], selectedIndex: Int = 0, didSelectDatasourceCompletion: () -> Void) { 203 | datasource = SegmentedDatasource(id: id, datasources: datasources, selectedIndex: selectedIndex, didSelectDatasourceCompletion: didSelectDatasourceCompletion) 204 | } 205 | 206 | /** 207 | Call the equivalent function on SegmentedDatasource. 208 | 209 | - parameter segmentedControl: the UISegmentedControl to configure. 210 | */ 211 | public func configureSegmentedControl(segmentedControl: UISegmentedControl) { 212 | datasource.configureSegmentedControl(segmentedControl) 213 | } 214 | 215 | } 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /Sources/Base/Selection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Selection.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 17/08/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct SelectionState { 12 | var selectedItems = Set() 13 | init(initialSelection: Set = Set()) { 14 | selectedItems.unionInPlace(initialSelection) 15 | } 16 | } 17 | 18 | public class SelectionManager { 19 | 20 | let state: Protector> 21 | 22 | public var allowsMultipleSelection = false 23 | public var enabled = false 24 | 25 | public var selectedItems: Set { 26 | return state.read { $0.selectedItems } 27 | } 28 | 29 | public init(initialSelection: Set = Set()) { 30 | state = Protector(SelectionState(initialSelection: initialSelection)) 31 | } 32 | 33 | public func contains(item: Item) -> Bool { 34 | return state.read { $0.selectedItems.contains(item) } 35 | } 36 | 37 | public func selectItem(item: Item, shouldRefreshItems: ((itemsToRefresh: [Item]) -> Void)? = .None) { 38 | if enabled { 39 | var itemsToUpdate = Set(arrayLiteral: item) 40 | state.write({ (inout state: SelectionState) in 41 | if state.selectedItems.contains(item) { 42 | state.selectedItems.remove(item) 43 | } 44 | else { 45 | if !self.allowsMultipleSelection { 46 | itemsToUpdate.unionInPlace(state.selectedItems) 47 | state.selectedItems.removeAll(keepCapacity: true) 48 | } 49 | state.selectedItems.insert(item) 50 | } 51 | }, completion: { 52 | shouldRefreshItems?(itemsToRefresh: Array(itemsToUpdate)) 53 | }) 54 | } 55 | } 56 | } 57 | 58 | public typealias IndexPathSelectionManager = SelectionManager 59 | 60 | -------------------------------------------------------------------------------- /Sources/Base/UICollectionView+TaylorSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 20/04/2015. 3 | // 4 | 5 | #import 6 | 7 | @interface UICollectionView (TaylorSource) 8 | 9 | - (void)tay_performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL))completion; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Sources/Base/UICollectionView+TaylorSource.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 20/04/2015. 3 | // 4 | 5 | #import "UICollectionView+TaylorSource.h" 6 | 7 | @implementation UICollectionView (TaylorSource) 8 | 9 | - (void)tay_performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL))completion { 10 | @try { 11 | [self performBatchUpdates:updates completion:completion]; 12 | } 13 | @catch (NSException *exception) { 14 | [self reloadData]; 15 | } 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Sources/Base/UITableView+TaylorSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 20/04/2015. 3 | // 4 | 5 | #import 6 | 7 | @interface UITableView (TaylorSource) 8 | 9 | - (void)tay_performBatchUpdates:(void (^)(void))updates; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Sources/Base/UITableView+TaylorSource.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 20/04/2015. 3 | // 4 | 5 | #import "UITableView+TaylorSource.h" 6 | 7 | @implementation UITableView (TaylorSource) 8 | 9 | - (void)tay_performBatchUpdates:(void (^)(void))updates { 10 | @try { 11 | [self beginUpdates]; 12 | updates(); 13 | [self endUpdates]; 14 | } 15 | @catch (NSException *exception) { 16 | [self reloadData]; 17 | } 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Sources/Base/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 21/04/2015. 3 | // 4 | 5 | import Foundation 6 | 7 | // MARK: - Thread Safety & Locks 8 | 9 | enum Queue { 10 | 11 | case Main, UserInteractive, UserInitiated, Default, Utility, Background 12 | 13 | private var id: Int { 14 | switch self { 15 | case .Main: return Int(qos_class_main().rawValue) 16 | case .UserInteractive: return Int(QOS_CLASS_USER_INTERACTIVE.rawValue) 17 | case .UserInitiated: return Int(QOS_CLASS_USER_INITIATED.rawValue) 18 | case .Default: return Int(QOS_CLASS_DEFAULT.rawValue) 19 | case .Utility: return Int(QOS_CLASS_UTILITY.rawValue) 20 | case .Background: return Int(QOS_CLASS_BACKGROUND.rawValue) 21 | } 22 | } 23 | 24 | internal var queue: dispatch_queue_t { 25 | switch self { 26 | case .Main: return dispatch_get_main_queue() 27 | default: return dispatch_get_global_queue(id, 0) 28 | } 29 | } 30 | } 31 | 32 | class Protector { 33 | private var lock: ReadWriteLock = Lock() 34 | private var ward: T 35 | 36 | init(_ ward: T) { 37 | self.ward = ward 38 | } 39 | 40 | func read(block: (T) -> U) -> U { 41 | return lock.read { [unowned self] in block(self.ward) } 42 | } 43 | 44 | func write(block: (inout T) -> Void, completion: (() -> Void)? = .None) { 45 | lock.write({ 46 | block(&self.ward) 47 | }, completion: completion) 48 | } 49 | } 50 | 51 | protocol ReadWriteLock { 52 | mutating func read(block: () -> T) -> T 53 | mutating func write(block: () -> ()) 54 | // Execute a completion block asynchronously on a global queue. 55 | mutating func write(block: () -> (), completion: (() -> Void)?) 56 | // Note: synchronous write is deliberatly ommited as it blocks the queue 57 | } 58 | 59 | struct Lock: ReadWriteLock { 60 | 61 | let queue = dispatch_queue_create("me.danthorpe.lock", DISPATCH_QUEUE_CONCURRENT) 62 | 63 | mutating func read(block: () -> T) -> T { 64 | var object: T? 65 | dispatch_sync(queue) { 66 | object = block() 67 | } 68 | return object! 69 | } 70 | 71 | mutating func write(block: () -> Void) { 72 | write(block, completion: nil) 73 | } 74 | 75 | mutating func write(block: () -> Void, completion: (() -> Void)? = .None) { 76 | dispatch_barrier_async(queue) { 77 | block() 78 | if let completion = completion { 79 | dispatch_async(Queue.Main.queue, completion) 80 | } 81 | } 82 | } 83 | } 84 | 85 | // MARK: - Notifications 86 | 87 | class NotificationCenterHandler: NSObject { 88 | typealias Callback = (NSNotification) -> Void 89 | let name: String 90 | let callback: Callback 91 | 92 | class var selector: Selector { 93 | return #selector(NotificationCenterHandler.handleNotification(_:)) 94 | } 95 | 96 | init(name n: String, callback c: Callback) { 97 | name = n 98 | callback = c 99 | } 100 | 101 | deinit { 102 | NSNotificationCenter.defaultCenter().removeObserver(self, name: name, object: .None) 103 | } 104 | 105 | func handleNotification(notification: NSNotification) { 106 | callback(notification) 107 | } 108 | } 109 | 110 | extension NSNotificationCenter { 111 | 112 | class func addObserverForName(name: String, object: AnyObject? = .None, withCallback callback: NotificationCenterHandler.Callback) -> NotificationCenterHandler { 113 | let handler = NotificationCenterHandler(name: name, callback: callback) 114 | let center = NSNotificationCenter.defaultCenter() 115 | center.removeObserver(handler, name: name, object: object) 116 | center.addObserver(handler, selector: NotificationCenterHandler.selector, name: name, object: object) 117 | return handler 118 | } 119 | } 120 | 121 | // MARK: - Target/Action 122 | 123 | public class TargetActionHandler: NSObject { 124 | public typealias Callback = (sender: AnyObject?) -> Void 125 | 126 | public class var selector: Selector { 127 | return #selector(TargetActionHandler.handleAction(_:)) 128 | } 129 | 130 | private let callback: Callback 131 | 132 | public init(callback c: Callback) { 133 | callback = c 134 | } 135 | 136 | public func handleAction(sender: AnyObject?) { 137 | callback(sender: sender) 138 | } 139 | } 140 | 141 | 142 | -------------------------------------------------------------------------------- /Sources/Base/Views.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 21/04/2015. 3 | // 4 | 5 | import UIKit 6 | 7 | // MARK: UITableView Support 8 | 9 | /// A provider of a UITableViewDataSource. 10 | public protocol UITableViewDataSourceProvider { 11 | var tableViewDataSource: UITableViewDataSource { get } 12 | } 13 | 14 | /// An empty protocol to allow constraining a view type to UITableView. 15 | public protocol UITableViewType { } 16 | extension UITableView: UITableViewType { } 17 | 18 | /** 19 | A provider of table view datasource. The provider owns the Datasource (an 20 | instance of something which implements DatasourceType), which it uses 21 | to construct and a bridging class which implements UITableViewDataSource. 22 | 23 | The ViewType of the Datasource's Factory is constrained to be a UITableViewType 24 | 25 | This architecture allows for different kinds of DatasourceType(s) to 26 | be used as the basic for a UITableViewDataSource, without the need 27 | to implement UITableViewDataSource on any of them. 28 | */ 29 | public struct TableViewDataSourceProvider< 30 | DatasourceProvider 31 | where 32 | DatasourceProvider: DatasourceProviderType, 33 | DatasourceProvider.Datasource.FactoryType.ViewType: UITableViewType, 34 | DatasourceProvider.Datasource.FactoryType.TextType == String>: UITableViewDataSourceProvider { 35 | 36 | typealias TableView = DatasourceProvider.Datasource.FactoryType.ViewType 37 | 38 | public let provider: DatasourceProvider 39 | 40 | public var datasource: DatasourceProvider.Datasource { 41 | return provider.datasource 42 | } 43 | 44 | public var factory: DatasourceProvider.Datasource.FactoryType { 45 | return datasource.factory 46 | } 47 | 48 | private let bridgedTableViewDataSource: TableViewDataSource 49 | 50 | /// Initalizes with a Datasource instance. 51 | public init(_ p: DatasourceProvider) { 52 | provider = p 53 | 54 | if p.editor.canEditItemAtIndexPath != nil && 55 | p.editor.commitEditActionForItemAtIndexPath != nil && 56 | p.editor.canMoveItemAtIndexPath != nil && 57 | p.editor.commitMoveItemAtIndexPathToIndexPath != nil { 58 | 59 | bridgedTableViewDataSource = TableViewDataSource.Editable( 60 | EditableTableViewDataSource( 61 | canEditRowAtIndexPath: { (_, indexPath) -> Bool in 62 | p.editor.canEditItemAtIndexPath!(indexPath: indexPath) 63 | }, 64 | commitEditingStyleForRowAtIndexPath: { (_, editingStyle, indexPath) -> Void in 65 | if let action = EditableDatasourceAction(editingStyle: editingStyle) { 66 | p.editor.commitEditActionForItemAtIndexPath!(action: action, indexPath: indexPath) 67 | } 68 | }, 69 | canMoveRowAtIndexPath: { (_, indexPath) -> Bool in 70 | p.editor.canMoveItemAtIndexPath!(indexPath: indexPath) 71 | }, 72 | moveRowAtIndexPathToIndexPath: { (_, from, to) -> Void in 73 | p.editor.commitMoveItemAtIndexPathToIndexPath!(from: from, to: to) 74 | }, 75 | numberOfSections: { _ -> Int in 76 | p.datasource.numberOfSections }, 77 | numberOfRowsInSection: { (_, section) -> Int in 78 | p.datasource.numberOfItemsInSection(section) }, 79 | cellForRowAtIndexPath: { (view, indexPath) -> UITableViewCell in 80 | p.datasource.cellForItemInView(view as! TableView, atIndexPath: indexPath) as! UITableViewCell }, 81 | titleForHeaderInSection: { (view, section) -> String? in 82 | p.datasource.textForSupplementaryElementInView(view as! TableView, kind: .Header, atIndexPath: NSIndexPath(forRow: 0, inSection: section)) }, 83 | titleForFooterInSection: { (view, section) -> String? in 84 | p.datasource.textForSupplementaryElementInView(view as! TableView, kind: .Footer, atIndexPath: NSIndexPath(forRow: 0, inSection: section)) } 85 | ) 86 | ) 87 | } 88 | else { 89 | bridgedTableViewDataSource = TableViewDataSource.Readonly( 90 | BaseTableViewDataSource( 91 | numberOfSections: { (view) -> Int in 92 | p.datasource.numberOfSections }, 93 | numberOfRowsInSection: { (view, section) -> Int in 94 | p.datasource.numberOfItemsInSection(section) }, 95 | cellForRowAtIndexPath: { (view, indexPath) -> UITableViewCell in 96 | p.datasource.cellForItemInView(view as! TableView, atIndexPath: indexPath) as! UITableViewCell }, 97 | titleForHeaderInSection: { (view, section) -> String? in 98 | p.datasource.textForSupplementaryElementInView(view as! TableView, kind: .Header, atIndexPath: NSIndexPath(forRow: 0, inSection: section)) }, 99 | titleForFooterInSection: { (view, section) -> String? in 100 | p.datasource.textForSupplementaryElementInView(view as! TableView, kind: .Footer, atIndexPath: NSIndexPath(forRow: 0, inSection: section)) } 101 | ) 102 | ) 103 | } 104 | } 105 | 106 | public var tableViewDataSource: UITableViewDataSource { 107 | return bridgedTableViewDataSource.tableViewDataSource 108 | } 109 | } 110 | 111 | class BaseTableViewDataSource: NSObject, UITableViewDataSource { 112 | 113 | private let numberOfSections: (UITableView) -> Int 114 | private let numberOfRowsInSection: (UITableView, Int) -> Int 115 | private let cellForRowAtIndexPath: (UITableView, NSIndexPath) -> UITableViewCell 116 | private let titleForHeaderInSection: (UITableView, Int) -> String? 117 | private let titleForFooterInSection: (UITableView, Int) -> String? 118 | 119 | init( 120 | numberOfSections: (UITableView) -> Int, 121 | numberOfRowsInSection: (UITableView, Int) -> Int, 122 | cellForRowAtIndexPath: (UITableView, NSIndexPath) -> UITableViewCell, 123 | titleForHeaderInSection: (UITableView, Int) -> String?, 124 | titleForFooterInSection: (UITableView, Int) -> String?) { 125 | 126 | self.numberOfSections = numberOfSections 127 | self.numberOfRowsInSection = numberOfRowsInSection 128 | self.cellForRowAtIndexPath = cellForRowAtIndexPath 129 | self.titleForHeaderInSection = titleForHeaderInSection 130 | self.titleForFooterInSection = titleForFooterInSection 131 | } 132 | 133 | func numberOfSectionsInTableView(tableView: UITableView) -> Int { 134 | return numberOfSections(tableView) 135 | } 136 | 137 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 138 | return numberOfRowsInSection(tableView, section) 139 | } 140 | 141 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 142 | return cellForRowAtIndexPath(tableView, indexPath) 143 | } 144 | 145 | func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 146 | return titleForHeaderInSection(tableView, section) 147 | } 148 | 149 | func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { 150 | return titleForFooterInSection(tableView, section) 151 | } 152 | } 153 | 154 | class EditableTableViewDataSource: BaseTableViewDataSource { 155 | 156 | private let canEditRowAtIndexPath: (UITableView, NSIndexPath) -> Bool 157 | private let commitEditingStyleForRowAtIndexPath: (UITableView, UITableViewCellEditingStyle, NSIndexPath) -> Void 158 | private let canMoveRowAtIndexPath: (UITableView, NSIndexPath) -> Bool 159 | private let moveRowAtIndexPathToIndexPath: (UITableView, NSIndexPath, NSIndexPath) -> Void 160 | 161 | init( 162 | canEditRowAtIndexPath: (UITableView, NSIndexPath) -> Bool, 163 | commitEditingStyleForRowAtIndexPath: (UITableView, UITableViewCellEditingStyle, NSIndexPath) -> Void, 164 | canMoveRowAtIndexPath: (UITableView, NSIndexPath) -> Bool, 165 | moveRowAtIndexPathToIndexPath: (UITableView, NSIndexPath, NSIndexPath) -> Void, 166 | // For Base 167 | numberOfSections: (UITableView) -> Int, 168 | numberOfRowsInSection: (UITableView, Int) -> Int, 169 | cellForRowAtIndexPath: (UITableView, NSIndexPath) -> UITableViewCell, 170 | titleForHeaderInSection: (UITableView, Int) -> String?, 171 | titleForFooterInSection: (UITableView, Int) -> String?) { 172 | 173 | self.canEditRowAtIndexPath = canEditRowAtIndexPath 174 | self.commitEditingStyleForRowAtIndexPath = commitEditingStyleForRowAtIndexPath 175 | self.canMoveRowAtIndexPath = canMoveRowAtIndexPath 176 | self.moveRowAtIndexPathToIndexPath = moveRowAtIndexPathToIndexPath 177 | 178 | super.init( 179 | numberOfSections: numberOfSections, 180 | numberOfRowsInSection: numberOfRowsInSection, 181 | cellForRowAtIndexPath: cellForRowAtIndexPath, 182 | titleForHeaderInSection: titleForHeaderInSection, 183 | titleForFooterInSection: titleForFooterInSection) 184 | } 185 | 186 | 187 | func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { 188 | return canEditRowAtIndexPath(tableView, indexPath) 189 | } 190 | 191 | func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { 192 | commitEditingStyleForRowAtIndexPath(tableView, editingStyle, indexPath) 193 | } 194 | 195 | func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool { 196 | return canMoveRowAtIndexPath(tableView, indexPath) 197 | } 198 | 199 | func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) { 200 | moveRowAtIndexPathToIndexPath(tableView, sourceIndexPath, destinationIndexPath) 201 | } 202 | } 203 | 204 | enum TableViewDataSource { 205 | 206 | case Readonly(BaseTableViewDataSource) 207 | case Editable(EditableTableViewDataSource) 208 | 209 | var tableViewDataSource: UITableViewDataSource { 210 | switch self { 211 | case .Readonly(let tableViewDataSource): return tableViewDataSource 212 | case .Editable(let tableViewDataSource): return tableViewDataSource 213 | } 214 | } 215 | } 216 | 217 | // MARK: - UICollectionView Support 218 | 219 | /// A provider of a UICollectionViewDataSource 220 | public protocol UICollectionViewDataSourceProvider { 221 | var collectionViewDataSource: UICollectionViewDataSource { get } 222 | } 223 | 224 | /// An empty protocol to allow constraining a view type to UICollectionView. 225 | public protocol UICollectionViewType { } 226 | extension UICollectionView: UICollectionViewType { } 227 | 228 | /** 229 | A provider of a UICollectionViewDataSource. The provider owns a Datasource (an 230 | instance of something which implements DatasourceType), which it uses 231 | to construct and a bridging class which implements UICollectionViewDataSource. 232 | 233 | The ViewType of the Datasource's Factory is constrained to be a UICollectionViewType 234 | */ 235 | public struct CollectionViewDataSourceProvider: UICollectionViewDataSourceProvider { 236 | 237 | typealias CollectionView = DatasourceProvider.Datasource.FactoryType.ViewType 238 | 239 | let provider: DatasourceProvider 240 | 241 | let bridgedDataSource: CollectionViewDataSource 242 | 243 | /// Initializes with a Datasource instance 244 | public init(_ p: DatasourceProvider) { 245 | provider = p 246 | bridgedDataSource = CollectionViewDataSource( 247 | numberOfSections: { (view) -> Int in 248 | p.datasource.numberOfSections }, 249 | numberOfItemsInSection: { (view, section) -> Int in 250 | p.datasource.numberOfItemsInSection(section) }, 251 | cellForItemAtIndexPath: { (view, indexPath) -> UICollectionViewCell in 252 | p.datasource.cellForItemInView(view as! CollectionView, atIndexPath: indexPath) as! UICollectionViewCell }, 253 | viewForElementKindAtIndexPath: { (view, indexPath, element) -> UICollectionReusableView in 254 | p.datasource.viewForSupplementaryElementInView(view as! CollectionView, kind: SupplementaryElementKind(element), atIndexPath: indexPath) as! UICollectionReusableView } 255 | ) 256 | } 257 | 258 | public var collectionViewDataSource: UICollectionViewDataSource { 259 | return bridgedDataSource 260 | } 261 | } 262 | 263 | class CollectionViewDataSource: NSObject, UICollectionViewDataSource { 264 | 265 | private let numberOfSections: (UICollectionView) -> Int 266 | private let numberOfItemsInSection: (UICollectionView, Int) -> Int 267 | private let cellForItemAtIndexPath: (UICollectionView, NSIndexPath) -> UICollectionViewCell 268 | private let viewForElementKindAtIndexPath: (UICollectionView, NSIndexPath, String) -> UICollectionReusableView 269 | 270 | init(numberOfSections: (UICollectionView) -> Int, numberOfItemsInSection: (UICollectionView, Int) -> Int, cellForItemAtIndexPath: (UICollectionView, NSIndexPath) -> UICollectionViewCell, viewForElementKindAtIndexPath: (UICollectionView, NSIndexPath, String) -> UICollectionReusableView) { 271 | self.numberOfSections = numberOfSections 272 | self.numberOfItemsInSection = numberOfItemsInSection 273 | self.cellForItemAtIndexPath = cellForItemAtIndexPath 274 | self.viewForElementKindAtIndexPath = viewForElementKindAtIndexPath 275 | } 276 | 277 | func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { 278 | return numberOfSections(collectionView) 279 | } 280 | 281 | func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 282 | return numberOfItemsInSection(collectionView, section) 283 | } 284 | 285 | func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 286 | return cellForItemAtIndexPath(collectionView, indexPath) 287 | } 288 | 289 | func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { 290 | return viewForElementKindAtIndexPath(collectionView, indexPath, kind) 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /Sources/CoreData/NSFRCDatasource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import CoreData 3 | 4 | public protocol FetchedResultsControllerType { 5 | var delegate: NSFetchedResultsControllerDelegate? { get set } 6 | var sections: [NSFetchedResultsSectionInfo]? { get } 7 | func objectAtIndexPath(_ indexPath: NSIndexPath) -> AnyObject 8 | } 9 | 10 | extension NSFetchedResultsController: FetchedResultsControllerType {} 11 | 12 | public struct NSFRCDatasource< 13 | Factory 14 | where 15 | Factory: _FactoryType, 16 | Factory.ViewType: IndexedUpdateProcessing, 17 | Factory.CellIndexType == NSFRCCellIndex, 18 | Factory.SupplementaryIndexType == NSFRCSupplementaryIndex>: DatasourceType { 19 | 20 | public typealias FactoryType = Factory 21 | 22 | public let identifier: String 23 | public let factory: Factory 24 | public var title: String? = .None 25 | public let updateHandler = NSFRCUpdateHandler() 26 | public var selectionManager = IndexPathSelectionManager() 27 | 28 | private let fetchedResultsController: FetchedResultsControllerType 29 | 30 | public init(id: String, fetchedResultsController: FetchedResultsControllerType, factory f: Factory, processChanges changes: IndexedUpdateProcessor) { 31 | self.fetchedResultsController = fetchedResultsController 32 | self.fetchedResultsController.delegate = updateHandler 33 | updateHandler.addUpdateObserver(changes) 34 | identifier = id 35 | factory = f 36 | } 37 | 38 | public var numberOfSections: Int { 39 | return fetchedResultsController.sections?.count ?? 0 40 | } 41 | 42 | public func numberOfItemsInSection(sectionIndex: Int) -> Int { 43 | if let section = fetchedResultsController.sections?[sectionIndex] { 44 | return section.numberOfObjects 45 | } 46 | return 0 47 | } 48 | 49 | public func itemAtIndexPath(indexPath: NSIndexPath) -> Factory.ItemType? { 50 | if let obj = fetchedResultsController.objectAtIndexPath(indexPath) as? Factory.ItemType { 51 | return obj 52 | } 53 | return nil 54 | } 55 | 56 | public func cellForItemInView(view: Factory.ViewType, atIndexPath indexPath: NSIndexPath) -> Factory.CellType { 57 | let selected = selectionManager.enabled && selectionManager.contains(indexPath) 58 | if let item = itemAtIndexPath(indexPath) { 59 | let index = NSFRCCellIndex(indexPath: indexPath, selected: selected) 60 | return factory.cellForItem(item, inView: view, atIndex: index) 61 | } 62 | fatalError("No item available at index path: \(indexPath)") 63 | } 64 | 65 | public func viewForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.SupplementaryViewType? { 66 | if 67 | let section = fetchedResultsController.sections?[indexPath.section], 68 | let title = section.indexTitle 69 | { 70 | let index = NSFRCSupplementaryIndex(group: title, indexPath: indexPath) 71 | return factory.supplementaryViewForKind(kind, inView: view, atIndex: index) 72 | } 73 | return nil 74 | } 75 | 76 | public func textForSupplementaryElementInView(view: FactoryType.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> FactoryType.TextType? { 77 | if 78 | let section = fetchedResultsController.sections?[indexPath.section], 79 | let title = section.indexTitle 80 | { 81 | let index = NSFRCSupplementaryIndex(group: title, indexPath: indexPath) 82 | return factory.supplementaryTextForKind(kind, atIndex: index) 83 | } 84 | return nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/CoreData/NSFRCFactory.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct NSFRCCellIndex: IndexPathIndexType { 3 | public let indexPath: NSIndexPath 4 | public let selected: Bool? 5 | 6 | public init(indexPath: NSIndexPath, selected: Bool? = .None) { 7 | self.indexPath = indexPath 8 | self.selected = selected 9 | } 10 | } 11 | 12 | public struct NSFRCSupplementaryIndex: IndexPathIndexType { 13 | public let group: String 14 | public let indexPath: NSIndexPath 15 | 16 | public init(group: String, indexPath: NSIndexPath) { 17 | self.group = group 18 | self.indexPath = indexPath 19 | } 20 | } 21 | 22 | public class NSFRCFactory< 23 | Item, Cell, SupplementaryView, View 24 | where 25 | View: CellBasedView>: Factory { 26 | 27 | public override init(cell: GetCellKey? = .None, supplementary: GetSupplementaryKey? = .None) { 28 | super.init(cell: cell, supplementary: supplementary) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Sources/CoreData/NSFRCIndexedUpdates.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum NSFRCIndexedUpdate { 4 | case DeltaUpdate( 5 | insertedSections: NSIndexSet, 6 | deletedSections: NSIndexSet, 7 | insertedRows: [NSIndexPath], 8 | updatedRows: [NSIndexPath], 9 | deletedRows: [NSIndexPath] 10 | ) 11 | case FullUpdate 12 | } 13 | 14 | public typealias IndexedUpdateProcessor = (NSFRCIndexedUpdate -> ()) 15 | 16 | public protocol IndexedUpdateProcessing { 17 | var updateProcessor: IndexedUpdateProcessor { get } 18 | } 19 | 20 | extension UITableView: IndexedUpdateProcessing { 21 | 22 | public var updateProcessor: IndexedUpdateProcessor { 23 | return { [weak self] update in 24 | switch update { 25 | case .DeltaUpdate(let insertedSections, let deletedSections, let insertedRows, let updatedRows, let deletedRows): 26 | self?.beginUpdates() 27 | self?.insertSections(insertedSections, withRowAnimation: .Automatic) 28 | self?.deleteSections(deletedSections, withRowAnimation: .Automatic) 29 | self?.insertRowsAtIndexPaths(insertedRows, withRowAnimation: .Automatic) 30 | self?.deleteRowsAtIndexPaths(deletedRows, withRowAnimation: .Automatic) 31 | self?.reloadRowsAtIndexPaths(updatedRows, withRowAnimation: .Automatic) 32 | self?.endUpdates() 33 | case .FullUpdate: 34 | self?.reloadData() 35 | } 36 | } 37 | } 38 | } 39 | 40 | extension UICollectionView: IndexedUpdateProcessing { 41 | 42 | public var updateProcessor: IndexedUpdateProcessor { 43 | return { [weak self] update in 44 | switch update { 45 | case .DeltaUpdate(let insertedSections, let deletedSections, let insertedRows, let updatedRows, let deletedRows): 46 | guard let strongSelf = self else { return } 47 | strongSelf.performBatchUpdates({ 48 | strongSelf.insertSections(insertedSections) 49 | strongSelf.deleteSections(deletedSections) 50 | strongSelf.insertItemsAtIndexPaths(insertedRows) 51 | strongSelf.deleteItemsAtIndexPaths(deletedRows) 52 | strongSelf.reloadItemsAtIndexPaths(updatedRows) 53 | }, completion: { _ in }) 54 | case .FullUpdate: 55 | self?.reloadData() 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Sources/CoreData/NSFRCUpdateHandler.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import CoreData 3 | 4 | extension NSIndexSet { 5 | class func indexSetFromSet(set: Set) -> NSIndexSet { 6 | var indexSet = NSMutableIndexSet() 7 | set.forEach { indexSet.addIndex($0) } 8 | return indexSet.copy() as! NSIndexSet 9 | } 10 | } 11 | 12 | public class NSFRCUpdateHandler: NSObject, NSFetchedResultsControllerDelegate { 13 | 14 | private struct PendingUpdates { 15 | var insertedSections = Set() 16 | var deletedSections = Set() 17 | var insertedRows = Set() 18 | var updatedRows = Set() 19 | var deletedRows = Set() 20 | 21 | func createUpdate() -> NSFRCIndexedUpdate { 22 | let update: NSFRCIndexedUpdate = .DeltaUpdate( 23 | insertedSections: NSIndexSet.indexSetFromSet(insertedSections), 24 | deletedSections: NSIndexSet.indexSetFromSet(deletedSections), 25 | insertedRows: Array(insertedRows), 26 | updatedRows: Array(updatedRows), 27 | deletedRows: Array(deletedRows) 28 | ) 29 | return update 30 | } 31 | } 32 | 33 | private var observers = [IndexedUpdateProcessor]() 34 | private var pendingUpdates = PendingUpdates() 35 | 36 | deinit { 37 | observers.removeAll() 38 | } 39 | 40 | public func addUpdateObserver(observer: IndexedUpdateProcessor) { 41 | observers.append(observer) 42 | } 43 | 44 | public func sendFullUpdate() { 45 | sendUpdate(.FullUpdate) 46 | } 47 | 48 | private func sendUpdate(update: NSFRCIndexedUpdate) { 49 | observers.forEach { $0(update) } 50 | } 51 | 52 | // MARK: NSFetchedResultsControllerDelegate 53 | 54 | public func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { 55 | switch (type) { 56 | case NSFetchedResultsChangeType.Delete: 57 | pendingUpdates.deletedSections.insert(sectionIndex) 58 | case NSFetchedResultsChangeType.Insert: 59 | pendingUpdates.insertedSections.insert(sectionIndex) 60 | default: 61 | break 62 | } 63 | } 64 | 65 | public func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { 66 | switch (type) { 67 | case NSFetchedResultsChangeType.Insert: 68 | if indexPath == nil { // iOS 9 / Swift 2.0 BUG with running 8.4 (https://forums.developer.apple.com/thread/12184) 69 | if let newIndexPath = newIndexPath { 70 | pendingUpdates.insertedRows.insert(newIndexPath) 71 | } 72 | } 73 | case NSFetchedResultsChangeType.Delete: 74 | if let indexPath = indexPath { 75 | pendingUpdates.deletedRows.insert(indexPath) 76 | } 77 | case NSFetchedResultsChangeType.Update: 78 | if let indexPath = indexPath { 79 | pendingUpdates.updatedRows.insert(indexPath) 80 | } 81 | case NSFetchedResultsChangeType.Move: 82 | if 83 | let newIndexPath = newIndexPath, 84 | let indexPath = indexPath 85 | { 86 | pendingUpdates.insertedRows.insert(newIndexPath) 87 | pendingUpdates.deletedRows.insert(indexPath) 88 | } 89 | } 90 | } 91 | 92 | public func controllerDidChangeContent(controller: NSFetchedResultsController) { 93 | let update = pendingUpdates.createUpdate() 94 | sendUpdate(update) 95 | pendingUpdates = PendingUpdates() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/YapDatabase/YapDB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 20/04/2015. 3 | // 4 | 5 | import UIKit 6 | import YapDatabase 7 | import YapDatabaseExtensions 8 | 9 | /** 10 | A struct which owns a data mapper closure, used to map AnyObject? which is 11 | stored in YapDatabase into strongly typed T? instances. 12 | */ 13 | public struct Configuration { 14 | public typealias DataItemMapper = (AnyObject?) -> T? 15 | 16 | let fetchConfiguration: YapDB.FetchConfiguration 17 | let itemMapper: DataItemMapper 18 | 19 | /** 20 | Initializer for the configuration. 21 | 22 | The DataItemMapper is designed to be used to un-archive value type. For example 23 | 24 | let config: Configuration = Configuration(fetch: events()) { valueFromArchive($0) } 25 | 26 | - parameter fetch: A YapDB.FetchConfiguration value. This is essentially the View extension with mappings configure block. 27 | - parameter itemMapper: A closure which is used to strongly type data coming out of YapDatabase. 28 | */ 29 | public init(fetch: YapDB.FetchConfiguration, itemMapper i: DataItemMapper) { 30 | fetchConfiguration = fetch 31 | itemMapper = i 32 | } 33 | 34 | func createMappingsRegisteredInDatabase(database: YapDatabase, withConnection connection: YapDatabaseConnection? = .None) -> YapDatabaseViewMappings { 35 | return fetchConfiguration.createMappingsRegisteredInDatabase(database, withConnection: connection) 36 | } 37 | } 38 | 39 | /** 40 | A struct which receives a database instance and Configuration instance. It is 41 | responsible for owning the YapDatabaseViewMappings object, and its 42 | readOnlyConnection. 43 | 44 | It implements SequenceType, and Int based CollectionType. 45 | 46 | It has an NSIndexPath based API for accessing items. 47 | */ 48 | public struct Mapper: SequenceType, CollectionType { 49 | 50 | let readOnlyConnection: YapDatabaseConnection 51 | var configuration: Configuration 52 | var mappings: YapDatabaseViewMappings 53 | 54 | var name: String { 55 | return configuration.fetchConfiguration.name 56 | } 57 | 58 | var get: (inTransaction: YapDatabaseReadTransaction, atIndexPaths: [NSIndexPath]) -> [T] { 59 | return { (transaction, indexPaths) in 60 | if let viewTransaction = transaction.ext(self.name) as? YapDatabaseViewTransaction { 61 | return indexPaths.flatMap { 62 | self.configuration.itemMapper(viewTransaction.objectAtIndexPath($0, withMappings: self.mappings)) 63 | } 64 | } 65 | return [] 66 | } 67 | } 68 | 69 | var fetch: (inTransaction: YapDatabaseReadTransaction, atIndexPath: NSIndexPath) -> T? { 70 | let get = self.get 71 | return { transaction, indexPath in get(inTransaction: transaction, atIndexPaths: [indexPath]).first } 72 | } 73 | 74 | public let startIndex: Int = 0 75 | public var endIndex: Int 76 | 77 | public subscript(i: Int) -> T { 78 | return itemAtIndexPath(mappings[i])! 79 | } 80 | 81 | public subscript(bounds: Range) -> [T] { 82 | return mappings[bounds].map { self.itemAtIndexPath($0)! } 83 | } 84 | 85 | /** 86 | Initialiser for Mapper. During initialization, the 87 | YapDatabaseViewMappings is created (and registered in the database). 88 | 89 | A new connection is created, and long lived read transaction started. 90 | 91 | On said transaction, the mappings object is updated. 92 | 93 | - parameter database: The YapDatabase instance. 94 | - parameter configuration: A Configuration value. 95 | */ 96 | public init(database: YapDatabase, configuration c: Configuration) { 97 | configuration = c 98 | let _mappings = configuration.createMappingsRegisteredInDatabase(database, withConnection: .None) 99 | 100 | readOnlyConnection = database.newConnection() 101 | readOnlyConnection.beginLongLivedReadTransaction() 102 | readOnlyConnection.readWithBlock { transaction in 103 | _mappings.updateWithTransaction(transaction) 104 | } 105 | 106 | mappings = _mappings 107 | endIndex = Int(mappings.numberOfItemsInAllGroups()) 108 | } 109 | 110 | mutating func replaceConfiguration(configuration c: Configuration) { 111 | configuration = c 112 | let _mappings = configuration.createMappingsRegisteredInDatabase(readOnlyConnection.database, withConnection: .None) 113 | readOnlyConnection.readWithBlock { transaction in 114 | _mappings.updateWithTransaction(transaction) 115 | } 116 | mappings = _mappings 117 | endIndex = Int(mappings.numberOfItemsInAllGroups()) 118 | } 119 | 120 | /** 121 | Returns a closure which will access the item at the index path in a provided read transaction. 122 | 123 | - parameter indexPath: The NSIndexPath to look up the item. 124 | - returns: (YapDatabaseReadTransaction) -> T? closure. 125 | */ 126 | public func itemInTransactionAtIndexPath(indexPath: NSIndexPath) -> (YapDatabaseReadTransaction) -> T? { 127 | return { transaction in self.fetch(inTransaction: transaction, atIndexPath: indexPath) } 128 | } 129 | 130 | /** 131 | Returns a closure which will access the item in a read transaction at the provided index path. 132 | 133 | - parameter transaction: A YapDatabaseReadTransaction 134 | - returns: (NSIndexPath) -> T? closure. 135 | */ 136 | public func itemAtIndexPathInTransaction(transaction: YapDatabaseReadTransaction) -> (NSIndexPath) -> T? { 137 | return { indexPath in self.fetch(inTransaction: transaction, atIndexPath: indexPath) } 138 | } 139 | 140 | /** 141 | Gets the item at the index path, using the internal readOnlyTransaction. 142 | 143 | - parameter indexPath: A NSIndexPath 144 | - returns: An optional T 145 | */ 146 | public func itemAtIndexPath(indexPath: NSIndexPath) -> T? { 147 | return readOnlyConnection.read(itemInTransactionAtIndexPath(indexPath)) 148 | } 149 | 150 | /** 151 | Gets the item at the index path, using a provided read transaction. 152 | 153 | - parameter indexPath: A NSIndexPath 154 | - parameter transaction: A YapDatabaseReadTransaction 155 | - returns: An optional T 156 | */ 157 | public func itemAtIndexPath(indexPath: NSIndexPath, inTransaction transaction: YapDatabaseReadTransaction) -> T? { 158 | return fetch(inTransaction: transaction, atIndexPath: indexPath) 159 | } 160 | 161 | /** 162 | Reverse looks up the NSIndexPath for a key in a collection. 163 | 164 | - parameter key: A String 165 | - parameter collection: A String 166 | - returns: An optional NSIndexPath 167 | */ 168 | public func indexPathForKey(key: String, inCollection collection: String) -> NSIndexPath? { 169 | return readOnlyConnection.read { transaction in 170 | if let viewTransaction = transaction.ext(self.name) as? YapDatabaseViewTransaction { 171 | return viewTransaction.indexPathForKey(key, inCollection: collection, withMappings: self.mappings) 172 | } 173 | return .None 174 | } 175 | } 176 | 177 | /** 178 | Reverse looks up the [NSIndexPath] for an array of keys in a collection. Only items 179 | which are in this mappings are returned. No optional NSIndexPaths are returned, 180 | therefore it is not guaranteed that the item index corresponds to the equivalent 181 | index path. Only if the length of the resultant array is equal to the length of 182 | items is this true. 183 | 184 | :param: items An array, [T] where T: Persistable 185 | :returns: An array, [NSIndexPath] 186 | */ 187 | public func indexPathsForKeys(keys: [String], inCollection collection: String) -> [NSIndexPath] { 188 | return readOnlyConnection.read { transaction in 189 | if let viewTransaction = transaction.ext(self.name) as? YapDatabaseViewTransaction { 190 | return keys.flatMap { 191 | viewTransaction.indexPathForKey($0, inCollection: collection, withMappings: self.mappings) 192 | } 193 | } 194 | return [] 195 | } 196 | } 197 | 198 | /** 199 | Returns a closure which will access the items at the index paths in a provided read transaction. 200 | 201 | :param: indexPath The NSIndexPath to look up the item. 202 | :returns: (YapDatabaseReadTransaction) -> [T] closure. 203 | */ 204 | public func itemsInTransactionAtIndexPaths(indexPaths: [NSIndexPath]) -> (YapDatabaseReadTransaction) -> [T] { 205 | return { self.get(inTransaction: $0, atIndexPaths: indexPaths) } 206 | } 207 | 208 | /** 209 | Returns a closure which will access the items in a read transaction at the provided index paths. 210 | 211 | :param: transaction A YapDatabaseReadTransaction 212 | :returns: ([NSIndexPath]) -> [T] closure. 213 | */ 214 | public func itemsAtIndexPathsInTransaction(transaction: YapDatabaseReadTransaction) -> ([NSIndexPath]) -> [T] { 215 | return { self.get(inTransaction: transaction, atIndexPaths: $0) } 216 | } 217 | 218 | /** 219 | Gets the items at the index paths, using the internal readOnlyTransaction. 220 | 221 | :param: indexPaths An [NSIndexPath] 222 | :returns: An [T] 223 | */ 224 | public func itemsAtIndexPaths(indexPaths: [NSIndexPath]) -> [T] { 225 | return readOnlyConnection.read(itemsInTransactionAtIndexPaths(indexPaths)) 226 | } 227 | 228 | /** 229 | Gets the items at the index paths, using a provided read transaction. 230 | 231 | :param: indexPaths A [NSIndexPath] 232 | :param: transaction A YapDatabaseReadTransaction 233 | :returns: An [T] 234 | */ 235 | public func itemsAtIndexPaths(indexPaths: [NSIndexPath], inTransaction transaction: YapDatabaseReadTransaction) -> [T] { 236 | return get(inTransaction: transaction, atIndexPaths: indexPaths) 237 | } 238 | 239 | 240 | public func generate() -> AnyGenerator { 241 | let mappingsGenerator = mappings.generate() 242 | return AnyGenerator { 243 | if let indexPath = mappingsGenerator.next() { 244 | return self.itemAtIndexPath(indexPath) 245 | } 246 | return .None 247 | } 248 | } 249 | } 250 | 251 | /** 252 | A database observer. This struct is used to respond to database changes and 253 | execute changes sets on the provided update block. 254 | 255 | Observer implements SequenceType, and Int based CollectionType. 256 | */ 257 | public struct Observer { 258 | 259 | private let queue: dispatch_queue_t 260 | private var notificationHandler: NotificationCenterHandler! 261 | 262 | let database: YapDatabase 263 | let mapper: Mapper 264 | let changes: YapDatabaseViewMappings.Changes 265 | 266 | public var shouldProcessChanges = true 267 | 268 | public var configuration: Configuration { 269 | return mapper.configuration 270 | } 271 | 272 | public var name: String { 273 | return mapper.name 274 | } 275 | 276 | public var mappings: YapDatabaseViewMappings { 277 | return mapper.mappings 278 | } 279 | 280 | public var readOnlyConnection: YapDatabaseConnection { 281 | return mapper.readOnlyConnection 282 | } 283 | 284 | /** 285 | The initaliser. The observer owns the database instances, and then 286 | creates and owns a Mapper using the configuration. 287 | 288 | Lastly, it registers for database changes. 289 | 290 | When YapDatabase posts a notification, the Observer posts it's update 291 | block through a concurrent queue using a dispatch_barrier_async. 292 | 293 | - parameter database: The YapDatabase instance. 294 | - parameter update: An update block, see extension on YapDatabaseViewMappings. 295 | - parameter configuration: A Configuration instance. 296 | */ 297 | public init(database db: YapDatabase, changes c: YapDatabaseViewMappings.Changes, configuration: Configuration) { 298 | database = db 299 | queue = dispatch_queue_create("TaylorSource.Database.Observer", DISPATCH_QUEUE_CONCURRENT) 300 | dispatch_set_target_queue(queue, Queue.UserInitiated.queue) 301 | mapper = Mapper(database: db, configuration: configuration) 302 | changes = c 303 | registerForDatabaseChanges() 304 | } 305 | 306 | mutating func unregisterForDatabaseChanges() { 307 | notificationHandler = .None 308 | } 309 | 310 | mutating func registerForDatabaseChanges() { 311 | notificationHandler = NSNotificationCenter.addObserverForName(YapDatabaseModifiedNotification, object: database, withCallback: processChangesWithBlock(changes)) 312 | } 313 | 314 | func processChangesWithBlock(changes: YapDatabaseViewMappings.Changes) -> (NSNotification) -> Void { 315 | return { _ in 316 | if self.shouldProcessChanges, let changeset = self.createChangeset() { 317 | self.processChanges { 318 | changes(changeset) 319 | } 320 | } 321 | } 322 | } 323 | 324 | func createChangeset() -> YapDatabaseViewMappings.Changeset? { 325 | let notifications = readOnlyConnection.beginLongLivedReadTransaction() 326 | 327 | var sectionChanges: NSArray? = nil 328 | var rowChanges: NSArray? = nil 329 | 330 | if let viewConnection = readOnlyConnection.ext(name) as? YapDatabaseViewConnection { 331 | viewConnection.getSectionChanges(§ionChanges, rowChanges: &rowChanges, forNotifications: notifications, withMappings: mappings) 332 | } 333 | 334 | if (sectionChanges?.count ?? 0) == 0 && (rowChanges?.count ?? 0) == 0 { 335 | return .None 336 | } 337 | 338 | let changes: YapDatabaseViewMappings.Changeset = (sectionChanges as? [YapDatabaseViewSectionChange] ?? [], rowChanges as? [YapDatabaseViewRowChange] ?? []) 339 | return changes 340 | } 341 | 342 | // Thread Safety 343 | 344 | private func processChanges(block: dispatch_block_t) { 345 | dispatch_barrier_async(queue) { 346 | dispatch_async(Queue.Main.queue, block) 347 | } 348 | } 349 | 350 | // Public API 351 | 352 | /** 353 | Returns a closure which will access the item at the index path in a provided read transaction. 354 | 355 | - parameter indexPath: The NSIndexPath to look up the item. 356 | - returns: (YapDatabaseReadTransaction) -> T? closure. 357 | */ 358 | public func itemInTransactionAtIndexPath(indexPath: NSIndexPath) -> (YapDatabaseReadTransaction) -> T? { 359 | return mapper.itemInTransactionAtIndexPath(indexPath) 360 | } 361 | 362 | /** 363 | Returns a closure which will access the item in a read transaction at the provided index path. 364 | 365 | - parameter transaction: A YapDatabaseReadTransaction 366 | - returns: (NSIndexPath) -> T? closure. 367 | */ 368 | public func itemAtIndexPathInTransaction(transaction: YapDatabaseReadTransaction) -> (NSIndexPath) -> T? { 369 | return mapper.itemAtIndexPathInTransaction(transaction) 370 | } 371 | 372 | /** 373 | Gets the item at the index path, using the internal readOnlyTransaction. 374 | 375 | - parameter indexPath: A NSIndexPath 376 | - returns: An optional T 377 | */ 378 | public func itemAtIndexPath(indexPath: NSIndexPath) -> T? { 379 | return mapper.itemAtIndexPath(indexPath) 380 | } 381 | 382 | /** 383 | Gets the item at the index path, using a provided read transaction. 384 | 385 | - parameter indexPath: A NSIndexPath 386 | - parameter transaction: A YapDatabaseReadTransaction 387 | - returns: An optional T 388 | */ 389 | public func itemAtIndexPath(indexPath: NSIndexPath, inTransaction transaction: YapDatabaseReadTransaction) -> T? { 390 | return mapper.itemAtIndexPath(indexPath, inTransaction: transaction) 391 | } 392 | 393 | /** 394 | Reverse looks up the NSIndexPath for a key in a collection. 395 | 396 | - parameter key: A String 397 | - parameter collection: A String 398 | - returns: An optional NSIndexPath 399 | */ 400 | public func indexPathForKey(key: String, inCollection collection: String) -> NSIndexPath? { 401 | return mapper.indexPathForKey(key, inCollection: collection) 402 | } 403 | 404 | /** 405 | Reverse looks up the [NSIndexPath] for an array of keys in a collection. Only items 406 | which are in this mappings are returned. No optional NSIndexPaths are returned, 407 | therefore it is not guaranteed that the item index corresponds to the equivalent 408 | index path. Only if the length of the resultant array is equal to the length of 409 | items is this true. 410 | 411 | :param: items An array, [T] where T: Persistable 412 | :returns: An array, [NSIndexPath] 413 | */ 414 | public func indexPathsForKeys(keys: [String], inCollection collection: String) -> [NSIndexPath] { 415 | return mapper.indexPathsForKeys(keys, inCollection: collection) 416 | } 417 | 418 | /** 419 | Returns a closure which will access the items at the index paths in a provided read transaction. 420 | 421 | :param: indexPath The NSIndexPath to look up the item. 422 | :returns: (YapDatabaseReadTransaction) -> [T] closure. 423 | */ 424 | public func itemsInTransactionAtIndexPaths(indexPaths: [NSIndexPath]) -> (YapDatabaseReadTransaction) -> [T] { 425 | return mapper.itemsInTransactionAtIndexPaths(indexPaths) 426 | } 427 | 428 | /** 429 | Returns a closure which will access the items in a read transaction at the provided index paths. 430 | 431 | :param: transaction A YapDatabaseReadTransaction 432 | :returns: ([NSIndexPath]) -> [T] closure. 433 | */ 434 | public func itemsAtIndexPathsInTransaction(transaction: YapDatabaseReadTransaction) -> ([NSIndexPath]) -> [T] { 435 | return mapper.itemsAtIndexPathsInTransaction(transaction) 436 | } 437 | 438 | /** 439 | Gets the items at the index paths, using the internal readOnlyTransaction. 440 | 441 | :param: indexPaths An [NSIndexPath] 442 | :returns: An [T] 443 | */ 444 | public func itemsAtIndexPaths(indexPaths: [NSIndexPath]) -> [T] { 445 | return mapper.itemsAtIndexPaths(indexPaths) 446 | } 447 | 448 | /** 449 | Gets the items at the index paths, using a provided read transaction. 450 | 451 | :param: indexPaths A [NSIndexPath] 452 | :param: transaction A YapDatabaseReadTransaction 453 | :returns: An [T] 454 | */ 455 | public func itemsAtIndexPaths(indexPaths: [NSIndexPath], inTransaction transaction: YapDatabaseReadTransaction) -> [T] { 456 | return mapper.itemsAtIndexPaths(indexPaths, inTransaction: transaction) 457 | } 458 | } 459 | 460 | extension Observer: SequenceType { 461 | 462 | public func generate() -> AnyGenerator { 463 | let mappingsGenerator = mappings.generate() 464 | return AnyGenerator { () -> T? in 465 | if let indexPath = mappingsGenerator.next() { 466 | return self.itemAtIndexPath(indexPath) 467 | } 468 | return nil 469 | } 470 | } 471 | } 472 | 473 | extension Observer: CollectionType { 474 | 475 | public var startIndex: Int { 476 | return mapper.startIndex 477 | } 478 | 479 | public var endIndex: Int { 480 | return mapper.endIndex 481 | } 482 | 483 | public subscript(i: Int) -> T { 484 | return mapper[i] 485 | } 486 | 487 | public subscript(bounds: Range) -> [T] { 488 | return mapper[bounds] 489 | } 490 | } 491 | 492 | extension YapDatabaseViewMappings { 493 | 494 | /** 495 | Tuple type used to collect changes from YapDatabase. 496 | */ 497 | public typealias Changeset = (sections: [YapDatabaseViewSectionChange], items: [YapDatabaseViewRowChange]) 498 | 499 | /** 500 | Definition of a closure type which receives the changes from YapDatabase. 501 | */ 502 | public typealias Changes = (Changeset) -> Void 503 | } 504 | 505 | 506 | // MARK: - SequenceType 507 | 508 | /** 509 | Implements SequenceType with NSIndexPath elements. 510 | */ 511 | extension YapDatabaseViewMappings: SequenceType { 512 | 513 | public func generate() -> AnyGenerator { 514 | let countSections = Int(numberOfSections()) 515 | var next = (section: 0, item: 0) 516 | return AnyGenerator { 517 | let countItemsInSection = Int(self.numberOfItemsInSection(UInt(next.section))) 518 | if next.item < countItemsInSection { 519 | let result = NSIndexPath(forItem: next.item, inSection: next.section) 520 | next.item += 1 521 | return result 522 | } 523 | else if next.section < countSections - 1 { 524 | next.item = 0 525 | let result = NSIndexPath(forItem: next.item, inSection: next.section) 526 | next.section += 1 527 | next.item += 1 528 | return result 529 | } 530 | return .None 531 | } 532 | } 533 | } 534 | 535 | // MARK: - CollectionType 536 | 537 | /** 538 | Implements CollectionType with an Int index type returning NSIndexPath. 539 | */ 540 | extension YapDatabaseViewMappings: CollectionType { 541 | 542 | public var startIndex: Int { 543 | return 0 544 | } 545 | 546 | public var endIndex: Int { 547 | return Int(numberOfItemsInAllGroups()) 548 | } 549 | 550 | public subscript(i: Int) -> NSIndexPath { 551 | get { 552 | var (section, item, accumulator, remainder, target) = (0, 0, 0, i, i) 553 | 554 | while accumulator < target { 555 | let count = Int(numberOfItemsInSection(UInt(section))) 556 | if (accumulator + count - 1) < target { 557 | accumulator += count 558 | remainder -= count 559 | section += 1 560 | } 561 | else { 562 | break 563 | } 564 | } 565 | 566 | item = remainder 567 | 568 | return NSIndexPath(forItem: item, inSection: section) 569 | } 570 | } 571 | 572 | public subscript(bounds: Range) -> [NSIndexPath] { 573 | return bounds.reduce(Array()) { (acc, index) in 574 | var acc = acc 575 | acc.append(self[index]) 576 | return acc 577 | } 578 | } 579 | } 580 | 581 | -------------------------------------------------------------------------------- /Sources/YapDatabase/YapDBDatasource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 20/04/2015. 3 | // 4 | 5 | import UIKit 6 | import YapDatabase 7 | import YapDatabaseExtensions 8 | 9 | public struct YapDBDatasource< 10 | Factory 11 | where 12 | Factory: _FactoryType, 13 | Factory.ViewType: UpdatableView, 14 | Factory.ViewType.ProcessChangesType == YapDatabaseViewMappings.Changes, 15 | Factory.CellIndexType == YapDBCellIndex, 16 | Factory.SupplementaryIndexType == YapDBSupplementaryIndex>: DatasourceType, SequenceType, CollectionType { 17 | 18 | public typealias FactoryType = Factory 19 | 20 | public let identifier: String 21 | public let factory: Factory 22 | public var title: String? = .None 23 | 24 | public let observer: Observer 25 | 26 | public var selectionManager = IndexPathSelectionManager() 27 | 28 | var mappings: YapDatabaseViewMappings { 29 | return observer.mappings 30 | } 31 | 32 | public var readOnlyConnection: YapDatabaseConnection { 33 | return observer.readOnlyConnection 34 | } 35 | 36 | var configuration: Configuration { 37 | return observer.configuration 38 | } 39 | 40 | public init(id: String, database: YapDatabase, factory f: Factory, processChanges changes: YapDatabaseViewMappings.Changes, configuration: Configuration) { 41 | identifier = id 42 | factory = f 43 | observer = Observer(database: database, changes: changes, configuration: configuration) 44 | } 45 | 46 | public var numberOfSections: Int { 47 | return Int(mappings.numberOfSections()) 48 | } 49 | 50 | public func numberOfItemsInSection(sectionIndex: Int) -> Int { 51 | return Int(mappings.numberOfItemsInSection(UInt(sectionIndex))) 52 | } 53 | 54 | public func itemAtIndexPath(indexPath: NSIndexPath) -> Factory.ItemType? { 55 | return observer.itemAtIndexPath(indexPath) 56 | } 57 | 58 | public func cellForItemInView(view: Factory.ViewType, atIndexPath indexPath: NSIndexPath) -> Factory.CellType { 59 | let selected = selectionManager.enabled && selectionManager.contains(indexPath) 60 | return readOnlyConnection.read { transaction in 61 | if let item = self.observer.itemAtIndexPath(indexPath, inTransaction: transaction) { 62 | let index = YapDBCellIndex(indexPath: indexPath, transaction: transaction, selected: selected) 63 | return self.factory.cellForItem(item, inView: view, atIndex: index) 64 | } 65 | fatalError("No item available at index path: \(indexPath)") 66 | } 67 | } 68 | 69 | public func viewForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.SupplementaryViewType? { 70 | 71 | let group = mappings.groupForSection(UInt(indexPath.section)) 72 | return readOnlyConnection.read { transaction in 73 | let index = YapDBSupplementaryIndex(group: group, indexPath: indexPath, transaction: transaction) 74 | return self.factory.supplementaryViewForKind(kind, inView: view, atIndex: index) 75 | } 76 | } 77 | 78 | /** 79 | Will return an optional text for the supplementary kind 80 | 81 | - parameter view: the View which should dequeue the cell. 82 | - parameter kind: the kind of the supplementary element. See SupplementaryElementKind 83 | - parameter indexPath: the NSIndexPath of the item. 84 | - returns: a TextType? 85 | */ 86 | public func textForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.TextType? { 87 | let group = mappings.groupForSection(UInt(indexPath.section)) 88 | return readOnlyConnection.read { transaction in 89 | let index = YapDBSupplementaryIndex(group: group, indexPath: indexPath, transaction: transaction) 90 | return self.factory.supplementaryTextForKind(kind, atIndex: index) 91 | } 92 | } 93 | 94 | // SequenceType 95 | 96 | public func generate() -> AnyGenerator { 97 | return observer.generate() 98 | } 99 | 100 | // CollectionType 101 | 102 | public var startIndex: Int { 103 | return observer.startIndex 104 | } 105 | 106 | public var endIndex: Int { 107 | return observer.endIndex 108 | } 109 | 110 | public subscript(i: Int) -> Factory.ItemType { 111 | return observer[i] 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /Sources/YapDatabase/YapDBEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YapDBEntity.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 30/07/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import YapDatabase 11 | import YapDatabaseExtensions 12 | 13 | public struct YapDBEntityDatasource< 14 | Factory, Entity 15 | where 16 | Entity: EntityType, 17 | Entity: Persistable, 18 | Factory: _FactoryType, 19 | Entity.ItemType == Factory.ItemType, 20 | Factory.CellIndexType == YapDBCellIndex, 21 | Factory.SupplementaryIndexType == YapDBSupplementaryIndex>: DatasourceType { 22 | 23 | public typealias FactoryType = Factory 24 | 25 | public let factory: Factory 26 | public let identifier: String 27 | public var title: String? = .None 28 | public private(set) var entity: Entity 29 | 30 | public var readOnlyConnection: YapDatabaseConnection { 31 | return observer.readOnlyConnection 32 | } 33 | 34 | private let observer: Observer 35 | 36 | var configuration: Configuration { 37 | return observer.configuration 38 | } 39 | 40 | var mappings: YapDatabaseViewMappings { 41 | return observer.mappings 42 | } 43 | 44 | public init(id: String, database: YapDatabase, factory f: Factory, entity e: Entity, entityDidChange didChange: dispatch_block_t, itemMapper: Configuration.DataItemMapper) { 45 | 46 | identifier = id 47 | factory = f 48 | entity = e 49 | 50 | let index = indexForPersistable(e) 51 | let view: YapDB.View = { 52 | 53 | let grouping = YapDB.View.Grouping.ByKey({ (_, collection, key) -> String! in 54 | if collection == index.collection && key == index.key { 55 | return collection 56 | } 57 | return nil 58 | }) 59 | 60 | let sorting = YapDB.View.Sorting.ByKey({ (_, _, _, _, _, _) -> NSComparisonResult in 61 | return .OrderedSame 62 | }) 63 | 64 | let view = YapDB.View( 65 | name: "Fetch Entity for \(id)", 66 | grouping: grouping, 67 | sorting: sorting, 68 | collections: [index.collection]) 69 | 70 | return view 71 | }() 72 | 73 | let configuration: Configuration = { 74 | let fetchConfig = YapDB.FetchConfiguration(view: view) 75 | return Configuration(fetch: fetchConfig, itemMapper: itemMapper) 76 | }() 77 | 78 | observer = Observer(database: database, changes: { _ in didChange() }, configuration: configuration) 79 | } 80 | 81 | // Datasource 82 | 83 | public var numberOfSections: Int { 84 | return entity.numberOfSections 85 | } 86 | 87 | public func numberOfItemsInSection(sectionIndex: Int) -> Int { 88 | return entity.numberOfItemsInSection(sectionIndex) 89 | } 90 | 91 | public func itemAtIndexPath(indexPath: NSIndexPath) -> Factory.ItemType? { 92 | return entity.itemAtIndexPath(indexPath) 93 | } 94 | 95 | 96 | public func cellForItemInView(view: Factory.ViewType, atIndexPath indexPath: NSIndexPath) -> Factory.CellType { 97 | return readOnlyConnection.read { transaction in 98 | if let item = self.itemAtIndexPath(indexPath) { 99 | let index = YapDBCellIndex(indexPath: indexPath, transaction: transaction) 100 | return self.factory.cellForItem(item, inView: view, atIndex: index) 101 | } 102 | fatalError("No item available at index path: \(indexPath)") 103 | } 104 | } 105 | 106 | public func viewForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.SupplementaryViewType? { 107 | return readOnlyConnection.read { transaction in 108 | let index = YapDBSupplementaryIndex(group: "", indexPath: indexPath, transaction: transaction) 109 | return self.factory.supplementaryViewForKind(kind, inView: view, atIndex: index) 110 | } 111 | } 112 | 113 | public func textForSupplementaryElementInView(view: Factory.ViewType, kind: SupplementaryElementKind, atIndexPath indexPath: NSIndexPath) -> Factory.TextType? { 114 | return readOnlyConnection.read { transaction in 115 | let index = YapDBSupplementaryIndex(group: "", indexPath: indexPath, transaction: transaction) 116 | return self.factory.supplementaryTextForKind(kind, atIndex: index) 117 | } 118 | } 119 | } 120 | 121 | 122 | -------------------------------------------------------------------------------- /Sources/YapDatabase/YapDBFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 16/04/2015. 3 | // 4 | 5 | import YapDatabase 6 | 7 | public protocol UpdatableView { 8 | associatedtype ProcessChangesType 9 | var processChanges: ProcessChangesType { get } 10 | } 11 | 12 | public struct YapDBCellIndex: IndexPathIndexType { 13 | public let indexPath: NSIndexPath 14 | public let transaction: YapDatabaseReadTransaction 15 | public let selected: Bool? 16 | 17 | public init(indexPath: NSIndexPath, transaction: YapDatabaseReadTransaction, selected: Bool? = .None) { 18 | self.indexPath = indexPath 19 | self.transaction = transaction 20 | self.selected = selected 21 | } 22 | } 23 | 24 | public struct YapDBSupplementaryIndex: IndexPathIndexType { 25 | public let group: String 26 | public let indexPath: NSIndexPath 27 | public let transaction: YapDatabaseReadTransaction 28 | 29 | public init(group: String, indexPath: NSIndexPath, transaction: YapDatabaseReadTransaction) { 30 | self.group = group 31 | self.indexPath = indexPath 32 | self.transaction = transaction 33 | } 34 | } 35 | 36 | public class YapDBFactory< 37 | Item, Cell, SupplementaryView, View 38 | where 39 | View: CellBasedView>: Factory { 40 | 41 | public override init(cell: GetCellKey? = .None, supplementary: GetSupplementaryKey? = .None) { 42 | super.init(cell: cell, supplementary: supplementary) 43 | } 44 | } 45 | 46 | // MARK: - UpdatableView 47 | 48 | extension UITableView: UpdatableView { 49 | 50 | public var processChanges: YapDatabaseViewMappings.Changes { 51 | return { [weak self] changeset in 52 | if let weakSelf = self { 53 | if weakSelf.tay_shouldProcessChangeset(changeset) { 54 | weakSelf.tay_performBatchUpdates { 55 | weakSelf.tay_processSectionChanges(changeset.sections) 56 | weakSelf.tay_processRowChanges(changeset.items) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | 64 | /** 65 | Consumers can override this to intercept whether or not the default change set processing 66 | should kick in. This is because in some scenarios it makes sense to prevent it. For example 67 | a common scenario is that a table view controller presents a "create new item" modal. When 68 | the save action occurs, the modal is dismissed, and the table view controller is reloaded, 69 | and the changeset can subsequently occur. Running the changeset in this situation will 70 | result in an exception. TaylorSource will suppress this exception, regardless, but still.. 71 | */ 72 | public func tay_shouldProcessChangeset(changeset: YapDatabaseViewMappings.Changeset) -> Bool { 73 | return true 74 | } 75 | 76 | public func tay_processSectionChanges(sectionChanges: [YapDatabaseViewSectionChange]) { 77 | for change in sectionChanges { 78 | let indexes = NSIndexSet(index: Int(change.index)) 79 | switch change.type { 80 | case .Delete: 81 | deleteSections(indexes, withRowAnimation: .Automatic) 82 | case .Insert: 83 | insertSections(indexes, withRowAnimation: .Automatic) 84 | default: 85 | break 86 | } 87 | } 88 | } 89 | 90 | public func tay_processRowChanges(rowChanges: [YapDatabaseViewRowChange]) { 91 | for change in rowChanges { 92 | switch change.type { 93 | case .Delete: 94 | deleteRowsAtIndexPaths([change.indexPath], withRowAnimation: .Automatic) 95 | case .Insert: 96 | insertRowsAtIndexPaths([change.newIndexPath], withRowAnimation: .Automatic) 97 | case .Move: 98 | deleteRowsAtIndexPaths([change.indexPath], withRowAnimation: .Automatic) 99 | insertRowsAtIndexPaths([change.newIndexPath], withRowAnimation: .Automatic) 100 | case .Update: 101 | reloadRowsAtIndexPaths([change.indexPath], withRowAnimation: .Automatic) 102 | } 103 | } 104 | } 105 | } 106 | 107 | extension UICollectionView: UpdatableView { 108 | 109 | public var processChanges: YapDatabaseViewMappings.Changes { 110 | return { [weak self] changeset in 111 | if let weakSelf = self { 112 | weakSelf.performBatchUpdates({ 113 | weakSelf.processSectionChanges(changeset.sections) 114 | weakSelf.processItemChanges(changeset.items) 115 | }, completion: nil) 116 | } 117 | } 118 | } 119 | 120 | func processSectionChanges(sectionChanges: [YapDatabaseViewSectionChange]) { 121 | for change in sectionChanges { 122 | let indexes = NSIndexSet(index: Int(change.index)) 123 | switch change.type { 124 | case .Delete: 125 | deleteSections(indexes) 126 | case .Insert: 127 | insertSections(indexes) 128 | default: 129 | break 130 | } 131 | } 132 | } 133 | 134 | func processItemChanges(itemChanges: [YapDatabaseViewRowChange]) { 135 | for change in itemChanges { 136 | switch change.type { 137 | case .Delete: 138 | deleteItemsAtIndexPaths([change.indexPath]) 139 | case .Insert: 140 | insertItemsAtIndexPaths([change.newIndexPath]) 141 | case .Move: 142 | deleteItemsAtIndexPaths([change.indexPath]) 143 | insertItemsAtIndexPaths([change.newIndexPath]) 144 | case .Update: 145 | reloadItemsAtIndexPaths([change.indexPath]) 146 | } 147 | } 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /Supporting Files/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 | FMWK 17 | CFBundleShortVersionString 18 | $(TAYLOR_SOURCE_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Supporting Files/TaylorSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // TaylorSource.h 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 10/06/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TaylorSource. 12 | FOUNDATION_EXPORT double TaylorSourceVersionNumber; 13 | 14 | //! Project version string for TaylorSource. 15 | FOUNDATION_EXPORT const unsigned char TaylorSourceVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | #import 19 | #import 20 | 21 | 22 | -------------------------------------------------------------------------------- /Supporting Files/TaylorSource.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // TaylorSource.xcconfig 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 07/03/2016. 6 | // 7 | // 8 | 9 | 10 | TAYLOR_SOURCE_VERSION = 2.1.0 11 | 12 | DEFINES_MODULE = YES 13 | INFOPLIST_FILE = $(SRCROOT)/Supporting Files/Info.plist 14 | PRODUCT_BUNDLE_IDENTIFIER = me.danthorpe.TaylorSource 15 | PRODUCT_NAME = TaylorSource 16 | -------------------------------------------------------------------------------- /TaylorSource.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "TaylorSource" 3 | s.version = "2.0.0" 4 | s.summary = "Generic table view & collection view datasources in Swift, for use with YapDatabase." 5 | s.description = <<-DESC 6 | 7 | Provides static datasource and view factory for simple 8 | table views and collection views. However, real 9 | power comes from using YapDatabase & YapDatabaseExtensions, 10 | to get database driven, auto-updating table 11 | and collection view data sources. 12 | 13 | DESC 14 | s.homepage = "https://github.com/danthorpe/TaylorSource" 15 | s.license = 'MIT' 16 | s.author = { "Daniel Thorpe" => "@danthorpe" } 17 | s.source = { :git => "https://github.com/danthorpe/TaylorSource.git", :tag => s.version.to_s } 18 | s.module_name = 'TaylorSource' 19 | s.social_media_url = 'https://twitter.com/danthorpe' 20 | s.requires_arc = true 21 | s.platform = :ios, '8.0' 22 | s.default_subspec = 'Base' 23 | 24 | s.subspec 'Base' do |ss| 25 | ss.source_files = 'Sources/Base/*.{m,h,swift}' 26 | end 27 | 28 | s.subspec 'YapDatabase' do |ss| 29 | ss.dependency 'TaylorSource/Base' 30 | ss.dependency 'YapDatabase', '~> 2.7' 31 | ss.dependency 'YapDatabaseExtensions', '~> 2' 32 | ss.source_files = 'Sources/YapDatabase/*.{m,h,swift}' 33 | end 34 | 35 | s.subspec 'CoreData' do |ss| 36 | ss.dependency 'TaylorSource/Base' 37 | ss.source_files = 'Sources/CoreData/*.{m,h,swift}' 38 | end 39 | end 40 | 41 | -------------------------------------------------------------------------------- /TaylorSource.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TaylorSource.xcodeproj/xcshareddata/xcschemes/TaylorSource.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Tests/DatasourceProviderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatasourceProviderTests.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 13/07/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | import TaylorSource 12 | 13 | // Convenience shorthands for commonly used selectors. 14 | private extension Selector { 15 | 16 | // UITableViewDataSource methods 17 | static let canEditRow = #selector(UITableViewDataSource.tableView(_:canEditRowAtIndexPath:)) 18 | static let commitEditingStyle = #selector(UITableViewDataSource.tableView(_:commitEditingStyle:forRowAtIndexPath:)) 19 | static let canMoveRow = #selector(UITableViewDataSource.tableView(_:canMoveRowAtIndexPath:)) 20 | static let moveRow = #selector(UITableViewDataSource.tableView(_:moveRowAtIndexPath:toIndexPath:)) 21 | static let numberOfSectionsInTableView = #selector(UITableViewDataSource.numberOfSectionsInTableView(_:)) 22 | static let numberOfRowsInSection = #selector(UITableViewDataSource.tableView(_:numberOfRowsInSection:)) 23 | static let cellForRowAtIndexPath = #selector(UITableViewDataSource.tableView(_:cellForRowAtIndexPath:)) 24 | } 25 | 26 | class DatasourceProviderTests: XCTestCase { 27 | 28 | struct EditableEventDatasourceProvider: DatasourceProviderType { 29 | typealias Factory = BasicFactory 30 | typealias Datasource = StaticDatasource 31 | 32 | let datasource: Datasource 33 | let editor: Editor 34 | 35 | init( 36 | data: [Event], 37 | canEdit: CanEditItemAtIndexPath? = .None, 38 | commitEdit: CommitEditActionForItemAtIndexPath? = .None, 39 | canMove: CanMoveItemAtIndexPath? = .None, 40 | commitMove: CommitMoveItemAtIndexPathToIndexPath? = .None, 41 | editAction: EditActionForItemAtIndexPath? = .None) { 42 | 43 | datasource = Datasource(id: "test", factory: Factory(), items: data) 44 | editor = Editor(canEdit: canEdit, commitEdit: commitEdit, editAction: editAction, canMove: canMove, commitMove: commitMove) 45 | } 46 | } 47 | 48 | let data: [Event] = (0..<5).map { (index) -> Event in Event.create() } 49 | var wrapper: TableViewDataSourceProvider! 50 | 51 | 52 | // MARK: - Editing 53 | 54 | func test__EditableDatasourceAction__vs__UITableViewCellEditingStyle() { 55 | XCTAssertEqual(EditableDatasourceAction(editingStyle: .None)!.editingStyle, UITableViewCellEditingStyle.None) 56 | XCTAssertEqual(EditableDatasourceAction(editingStyle: .Insert)!.editingStyle, UITableViewCellEditingStyle.Insert) 57 | XCTAssertEqual(EditableDatasourceAction(editingStyle: .Delete)!.editingStyle, UITableViewCellEditingStyle.Delete) 58 | } 59 | 60 | 61 | func test__provider_with_no_edit_closures__table_view_datasource_is_readonly() { 62 | wrapper = TableViewDataSourceProvider(EditableEventDatasourceProvider(data: data)) 63 | let tableViewDataSource = wrapper.tableViewDataSource 64 | assertTableViewDataSourceImplementsBaseMethods(tableViewDataSource) 65 | XCTAssertFalse(tableViewDataSource.respondsToSelector(.canEditRow)) 66 | XCTAssertFalse(tableViewDataSource.respondsToSelector(.commitEditingStyle)) 67 | XCTAssertFalse(tableViewDataSource.respondsToSelector(.canMoveRow)) 68 | XCTAssertFalse(tableViewDataSource.respondsToSelector(.moveRow)) 69 | } 70 | 71 | func test__provider_with_edit_closures__table_view_datasource_is_editable() { 72 | wrapper = TableViewDataSourceProvider(EditableEventDatasourceProvider( 73 | data: data, 74 | canEdit: { _ in return true }, 75 | commitEdit: { (_, _) in }, 76 | canMove: { _ in return true }, 77 | commitMove: { (_, _) in }, 78 | editAction: { _ in return .Delete })) 79 | 80 | let tableViewDataSource = wrapper.tableViewDataSource 81 | assertTableViewDataSourceImplementsBaseMethods(tableViewDataSource) 82 | XCTAssertTrue(tableViewDataSource.respondsToSelector(.canEditRow)) 83 | XCTAssertTrue(tableViewDataSource.respondsToSelector(.commitEditingStyle)) 84 | XCTAssertTrue(tableViewDataSource.respondsToSelector(.canMoveRow)) 85 | XCTAssertTrue(tableViewDataSource.respondsToSelector(.moveRow)) 86 | } 87 | 88 | func test__editable_provider__receives_calls_for__can_edit() { 89 | 90 | var canEditIndexPath: NSIndexPath? = nil 91 | 92 | wrapper = TableViewDataSourceProvider(EditableEventDatasourceProvider( 93 | data: data, 94 | canEdit: { indexPath in 95 | canEditIndexPath = indexPath 96 | return true 97 | }, 98 | commitEdit: { (_, _) in }, 99 | canMove: { _ in return true }, 100 | commitMove: { (_, _) in }, 101 | editAction: { _ in return .Delete })) 102 | 103 | let tableViewDataSource = wrapper.tableViewDataSource 104 | let view = StubbedTableView() 105 | 106 | tableViewDataSource.tableView?(view, canEditRowAtIndexPath: NSIndexPath.first) 107 | 108 | XCTAssertNotNil(canEditIndexPath) 109 | XCTAssertEqual(canEditIndexPath!, NSIndexPath.first) 110 | } 111 | 112 | func test__editable_provider__receives_calls_for__commit_edit() { 113 | 114 | var commitEditAction: EditableDatasourceAction? = nil 115 | var commitEditIndexPath: NSIndexPath? = nil 116 | 117 | wrapper = TableViewDataSourceProvider(EditableEventDatasourceProvider( 118 | data: data, 119 | canEdit: { _ in return true }, 120 | commitEdit: { (action, indexPath) in 121 | commitEditAction = action 122 | commitEditIndexPath = indexPath 123 | }, 124 | canMove: { _ in return true }, 125 | commitMove: { (_, _) in }, 126 | editAction: { _ in return .Delete })) 127 | 128 | let tableViewDataSource = wrapper.tableViewDataSource 129 | let view = StubbedTableView() 130 | 131 | tableViewDataSource.tableView?(view, commitEditingStyle: .Delete, forRowAtIndexPath: NSIndexPath.first) 132 | 133 | XCTAssertNotNil(commitEditIndexPath) 134 | XCTAssertEqual(commitEditIndexPath!, NSIndexPath.first) 135 | 136 | XCTAssertTrue(commitEditAction != nil) 137 | XCTAssertEqual(commitEditAction!, EditableDatasourceAction.Delete) 138 | } 139 | 140 | func test__editable_provider__receives_calls_for__can_move() { 141 | 142 | var canMoveIndexPath: NSIndexPath? = nil 143 | 144 | wrapper = TableViewDataSourceProvider(EditableEventDatasourceProvider( 145 | data: data, 146 | canEdit: { _ in return true }, 147 | commitEdit: { (_, _) in }, 148 | canMove: { indexPath in 149 | canMoveIndexPath = indexPath 150 | return true }, 151 | commitMove: { (_, _) in }, 152 | editAction: { _ in return .Delete })) 153 | 154 | let tableViewDataSource = wrapper.tableViewDataSource 155 | let view = StubbedTableView() 156 | 157 | tableViewDataSource.tableView?(view, canMoveRowAtIndexPath: NSIndexPath.first) 158 | 159 | XCTAssertNotNil(canMoveIndexPath) 160 | XCTAssertEqual(canMoveIndexPath!, NSIndexPath.first) 161 | } 162 | 163 | func test__editable_provider__receives_calls_for__commit_move() { 164 | 165 | var commitMoveFromIndexPath: NSIndexPath? = nil 166 | var commitMoveToIndexPath: NSIndexPath? = nil 167 | 168 | wrapper = TableViewDataSourceProvider(EditableEventDatasourceProvider( 169 | data: data, 170 | canEdit: { _ in return true }, 171 | commitEdit: { (_, _) in }, 172 | canMove: { _ in return true }, 173 | commitMove: { (from, to) in 174 | commitMoveFromIndexPath = from 175 | commitMoveToIndexPath = to 176 | }, 177 | editAction: { _ in return .Delete })) 178 | 179 | let tableViewDataSource = wrapper.tableViewDataSource 180 | let view = StubbedTableView() 181 | 182 | let to = NSIndexPath(forRow: 1, inSection: 0) 183 | tableViewDataSource.tableView?(view, moveRowAtIndexPath: NSIndexPath.first, toIndexPath: to) 184 | 185 | XCTAssertNotNil(commitMoveFromIndexPath) 186 | XCTAssertEqual(commitMoveFromIndexPath!, NSIndexPath.first) 187 | 188 | XCTAssertNotNil(commitMoveToIndexPath) 189 | XCTAssertEqual(commitMoveToIndexPath!, to) 190 | } 191 | 192 | // MARK: - Helpers 193 | 194 | func assertTableViewDataSourceImplementsBaseMethods(tableViewDataSource: UITableViewDataSource) { 195 | XCTAssertTrue(tableViewDataSource.respondsToSelector(.numberOfRowsInSection)) 196 | XCTAssertTrue(tableViewDataSource.respondsToSelector(.cellForRowAtIndexPath)) 197 | XCTAssertTrue(tableViewDataSource.respondsToSelector(.numberOfSectionsInTableView)) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Tests/EntityDatasourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityDatasourceTests.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 29/07/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import XCTest 12 | import TaylorSource 13 | 14 | enum TestEntity: String, EntityType { 15 | 16 | case Foo = "Foo" 17 | case Bar = "Bar" 18 | case Baz = "Baz" 19 | case Bat = "Bat" 20 | 21 | var numberOfSections: Int { 22 | switch self { 23 | case .Foo: return 2 24 | case .Bar: return 2 25 | case .Baz: return 3 26 | case .Bat: return 4 27 | } 28 | } 29 | 30 | func numberOfItemsInSection(sectionIndex: Int) -> Int { 31 | switch (self, sectionIndex) { 32 | case (.Foo, _): return 2 33 | case (.Bar, 0): return 2 34 | case (.Bar, _): return 3 35 | case (.Baz, 0): return 1 36 | case (.Baz, 1): return 3 37 | case (.Baz, _): return 2 38 | case (.Bat, 0): return 3 39 | case (.Bat, 1): return 2 40 | case (.Bat, _): return 1 41 | } 42 | } 43 | 44 | func itemAtIndexPath(indexPath: NSIndexPath) -> String? { 45 | return "\(rawValue): \(indexPath)" 46 | } 47 | } 48 | 49 | class EntityDatasourceTests: XCTestCase { 50 | 51 | typealias Factory = BasicFactory 52 | typealias Datasource = EntityDatasource 53 | 54 | let view = StubbedTableView() 55 | let factory = Factory() 56 | let entity = TestEntity.Bat 57 | var datasource: Datasource! 58 | 59 | override func setUp() { 60 | factory.registerCell(.ClassWithIdentifier(UITableViewCell.self, "cell"), inView: view) { (_, _, _) in } 61 | datasource = Datasource(id: "test entity datasource", factory: factory, entity: entity) 62 | } 63 | 64 | func test__number_of_sections_in_datasource() { 65 | // Note - properties does not include .Baz 66 | XCTAssertEqual(datasource.numberOfSections, 4) 67 | } 68 | 69 | func test__number_of_items_in_sections() { 70 | 71 | // Bat 72 | XCTAssertEqual(datasource.numberOfItemsInSection(0), 3) 73 | XCTAssertEqual(datasource.numberOfItemsInSection(1), 2) 74 | XCTAssertEqual(datasource.numberOfItemsInSection(2), 1) 75 | XCTAssertEqual(datasource.numberOfItemsInSection(3), 1) 76 | } 77 | 78 | func test__item_at_index_path() { 79 | for section in 0.. 12 | 13 | let view = StubbedTableView() 14 | let factory = Factory() 15 | 16 | var validIndexPath: NSIndexPath { 17 | return NSIndexPath(forRow: 0, inSection: 0) 18 | } 19 | 20 | override func setUp() { 21 | view.registerClass(UITableViewCell.self, withIdentifier: "cell") 22 | view.registerClass(UITableViewHeaderFooterView.self, forSupplementaryViewKind: .Header, withIdentifier: "header") 23 | } 24 | 25 | // Tests 26 | 27 | func test_GivenRegisteredCell_WhenAccessingCellForItem_ThatCellIsReturned() { 28 | registerCell { (_, _, _) in } 29 | let cell = factory.cellForItem(Event.create(), inView: view, atIndex: validIndexPath) 30 | XCTAssertTrue(cell.isKindOfClass(UITableViewCell.self)) 31 | } 32 | 33 | func test_GivenRegisteredCell_WhenAccessingCellForItem_ThatConfigurationBlockIsRun() { 34 | var blockDidRun = false 35 | registerCell { (_, _, _) in blockDidRun = true } 36 | let _ = factory.cellForItem(Event.create(), inView: view, atIndex: validIndexPath) 37 | XCTAssertTrue(blockDidRun) 38 | } 39 | 40 | func test_GivenRegisteredHeaderView_WhenAccessingHeader_ThatViewIsReturned() { 41 | registerHeader { (_, _) in } 42 | let supplementary = factory.supplementaryViewForKind(.Header, inView: view, atIndex: validIndexPath) 43 | XCTAssertTrue(supplementary!.isKindOfClass(UITableViewHeaderFooterView.self)) 44 | } 45 | 46 | func test_GivenRegisteredHeaderView_WhenAccessingHeader_ThatConfigurationBlockIsRun() { 47 | var blockDidRun = false 48 | registerHeader { (_, _) in blockDidRun = true } 49 | let _ = factory.supplementaryViewForKind(.Header, inView: view, atIndex: validIndexPath) 50 | XCTAssertTrue(blockDidRun) 51 | } 52 | 53 | func test_GivenRegisteredHeaderView_WhenAccessingFooter_ThatViewIsNotReturned() { 54 | var blockDidRun = false 55 | registerHeader { (_, _) in blockDidRun = true } 56 | let supplementary = factory.supplementaryViewForKind(.Footer, inView: view, atIndex: validIndexPath) 57 | XCTAssertNil(supplementary) 58 | XCTAssertFalse(blockDidRun) 59 | } 60 | 61 | func test_GivenRegisteredFooterView_WhenAccessingFooter_ThatViewIsReturned() { 62 | registerFooter { (_, _) in } 63 | let supplementary = factory.supplementaryViewForKind(.Footer, inView: view, atIndex: validIndexPath) 64 | XCTAssertTrue(supplementary!.isKindOfClass(UITableViewHeaderFooterView.self)) 65 | } 66 | 67 | func test_GivenRegisteredFooterView_WhenAccessingHeader_ThatViewIsNotReturned() { 68 | var blockDidRun = false 69 | registerFooter { (_, _) in blockDidRun = true } 70 | let supplementary = factory.supplementaryViewForKind(.Header, inView: view, atIndex: validIndexPath) 71 | XCTAssertNil(supplementary) 72 | XCTAssertFalse(blockDidRun) 73 | } 74 | 75 | func test_GivenRegisteredCustomView_WhenAccessingCustomView_ThatViewIsReturned() { 76 | factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "sidebar"), kind: .Custom("Sidebar"), inView: view) { (_, _) in } 77 | let supplementary = factory.supplementaryViewForKind(.Custom("Sidebar"), inView: view, atIndex: validIndexPath) 78 | XCTAssertTrue(supplementary!.isKindOfClass(UITableViewHeaderFooterView.self)) 79 | } 80 | 81 | func test_GivenRegisteredCustomView_WhenAccessingDifferentCustomView_ThatViewIsNotReturned() { 82 | var blockDidRun = false 83 | factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "sidebar"), kind: .Custom("Left Sidebar"), inView: view) { (_, _) in blockDidRun = true } 84 | let supplementary = factory.supplementaryViewForKind(.Custom("Right Sidebar"), inView: view, atIndex: validIndexPath) 85 | XCTAssertNil(supplementary) 86 | XCTAssertFalse(blockDidRun) 87 | } 88 | 89 | // Helpers 90 | 91 | func registerCell(key: String? = .None, config: Factory.CellConfiguration) { 92 | if let key = key { 93 | factory.registerCell(.ClassWithIdentifier(UITableViewCell.self, "cell"), inView: view, withKey: key, configuration: config) 94 | } 95 | else { 96 | factory.registerCell(.ClassWithIdentifier(UITableViewCell.self, "cell"), inView: view, configuration: config) 97 | } 98 | } 99 | 100 | func registerHeader(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 101 | if let key = key { 102 | factory.registerHeaderView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "header"), inView: view, withKey: key, configuration: config) 103 | } 104 | else { 105 | factory.registerHeaderView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "header"), inView: view, configuration: config) 106 | } 107 | } 108 | 109 | func registerFooter(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 110 | if let key = key { 111 | factory.registerFooterView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "footer"), inView: view, withKey: key, configuration: config) 112 | } 113 | else { 114 | factory.registerFooterView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "footer"), inView: view, configuration: config) 115 | } 116 | } 117 | } 118 | 119 | class UICollectionViewTests: XCTestCase { 120 | 121 | typealias Factory = BasicFactory 122 | 123 | let view = StubbedCollectionView(frame: CGRectZero, collectionViewLayout: UICollectionViewFlowLayout()) 124 | let factory = Factory() 125 | 126 | var validIndexPath: NSIndexPath { 127 | return NSIndexPath(forItem: 0, inSection: 0) 128 | } 129 | 130 | override func setUp() { 131 | view.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell") 132 | view.registerClass(UICollectionReusableView.self, forSupplementaryViewKind: .Header, withIdentifier: "whatever") 133 | } 134 | 135 | // Tests 136 | 137 | func test_GivenRegisteredCell_WhenAccessingCellForItem_ThatCellIsReturned() { 138 | registerCell { (_, _, _) in } 139 | let cell = factory.cellForItem(Event.create(), inView: view, atIndex: validIndexPath) 140 | XCTAssertNotNil(cell, "Cell should be returned") 141 | } 142 | 143 | func test_GivenRegisteredCell_WhenAccessingCellForItem_ThatConfigurationBlockIsRun() { 144 | var blockDidRun = false 145 | registerCell { (_, _, _) in blockDidRun = true } 146 | _ = factory.cellForItem(Event.create(), inView: view, atIndex: validIndexPath) 147 | XCTAssertTrue(blockDidRun, "Configuration block was not run.") 148 | } 149 | 150 | func test_GivenRegisteredHeaderView_WhenAccessingHeader_ThatViewIsReturned() { 151 | registerHeader { (_, _) in } 152 | let supplementary = factory.supplementaryViewForKind(.Header, inView: view, atIndex: validIndexPath) 153 | XCTAssertNotNil(supplementary, "View should be returned.") 154 | } 155 | 156 | func test_GivenRegisteredHeaderView_WhenAccessingHeader_ThatConfigurationBlockIsRun() { 157 | var blockDidRun = false 158 | registerHeader { (_, _) in blockDidRun = true } 159 | _ = factory.supplementaryViewForKind(.Header, inView: view, atIndex: validIndexPath) 160 | XCTAssertTrue(blockDidRun, "Configuration block was not run.") 161 | } 162 | 163 | func test_GivenRegisteredHeaderView_WhenAccessingFooter_ThatViewIsNotReturned() { 164 | var blockDidRun = false 165 | registerHeader { (_, _) in blockDidRun = true } 166 | let supplementary = factory.supplementaryViewForKind(.Footer, inView: view, atIndex: validIndexPath) 167 | XCTAssertNil(supplementary, "No view should be returned.") 168 | XCTAssertFalse(blockDidRun, "Configuration block should not have been run.") 169 | } 170 | 171 | func test_GivenRegisteredFooterView_WhenAccessingFooter_ThatViewIsReturned() { 172 | registerFooter { (_, _) in } 173 | let supplementary = factory.supplementaryViewForKind(.Footer, inView: view, atIndex: validIndexPath) 174 | XCTAssertNotNil(supplementary, "View should be returned.") 175 | } 176 | 177 | func test_GivenRegisteredFooterView_WhenAccessingHeader_ThatViewIsNotReturned() { 178 | var blockDidRun = false 179 | registerFooter { (_, _) in blockDidRun = true } 180 | let supplementary = factory.supplementaryViewForKind(.Header, inView: view, atIndex: validIndexPath) 181 | XCTAssertNil(supplementary, "No view should be returned.") 182 | XCTAssertFalse(blockDidRun, "Configuration block should not have been run.") 183 | } 184 | 185 | func test_GivenRegisteredCustomView_WhenAccessingCustomView_ThatViewIsReturned() { 186 | factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "sidebar"), kind: .Custom("Sidebar"), inView: view) { (_, _) in } 187 | let supplementary = factory.supplementaryViewForKind(.Custom("Sidebar"), inView: view, atIndex: validIndexPath) 188 | XCTAssertNotNil(supplementary, "View should be returned.") 189 | } 190 | 191 | func test_GivenRegisteredCustomView_WhenAccessingDifferentCustomView_ThatViewIsNotReturned() { 192 | var blockDidRun = false 193 | factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "sidebar"), kind: .Custom("Left Sidebar"), inView: view) { (_, _) in blockDidRun = true } 194 | let supplementary = factory.supplementaryViewForKind(.Custom("Right Sidebar"), inView: view, atIndex: validIndexPath) 195 | XCTAssertNil(supplementary, "No view should be returned.") 196 | XCTAssertFalse(blockDidRun, "Configuration block should not have been run.") 197 | } 198 | 199 | // MARK: Helpers 200 | 201 | func registerCell(key: String? = .None, config: Factory.CellConfiguration) { 202 | if let key = key { 203 | factory.registerCell(.ClassWithIdentifier(UICollectionViewCell.self, "cell"), inView: view, withKey: key, configuration: config) 204 | } 205 | else { 206 | factory.registerCell(.ClassWithIdentifier(UICollectionViewCell.self, "cell"), inView: view, configuration: config) 207 | } 208 | } 209 | 210 | func registerHeader(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 211 | if let key = key { 212 | factory.registerHeaderView(.ClassWithIdentifier(UICollectionReusableView.self, "header"), inView: view, withKey: key, configuration: config) 213 | } 214 | else { 215 | factory.registerHeaderView(.ClassWithIdentifier(UICollectionReusableView.self, "header"), inView: view, configuration: config) 216 | } 217 | } 218 | 219 | func registerFooter(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 220 | if let key = key { 221 | factory.registerFooterView(.ClassWithIdentifier(UICollectionReusableView.self, "footer"), inView: view, withKey: key, configuration: config) 222 | } 223 | else { 224 | factory.registerFooterView(.ClassWithIdentifier(UICollectionReusableView.self, "footer"), inView: view, configuration: config) 225 | } 226 | } 227 | } 228 | 229 | -------------------------------------------------------------------------------- /Tests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 19/04/2015. 3 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | 8 | import YapDatabase 9 | import ValueCoding 10 | import YapDatabaseExtensions 11 | import TaylorSource 12 | 13 | class StubbedTableView: UITableView { 14 | override func dequeueCellWithIdentifier(id: String, atIndexPath indexPath: NSIndexPath) -> UITableViewCell { 15 | return UITableViewCell(style: .Default, reuseIdentifier: id) 16 | } 17 | 18 | override func dequeueReusableHeaderFooterViewWithIdentifier(identifier: String) -> UITableViewHeaderFooterView? { 19 | return UITableViewHeaderFooterView(reuseIdentifier: identifier) 20 | } 21 | } 22 | 23 | class StubbedCollectionView: UICollectionView { 24 | override func dequeueCellWithIdentifier(id: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { 25 | return UICollectionViewCell() 26 | } 27 | 28 | override func dequeueReusableSupplementaryViewOfKind(elementKind: String, withReuseIdentifier identifier: String, forIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { 29 | return UICollectionReusableView() 30 | } 31 | } 32 | 33 | extension NSIndexPath { 34 | static var first: NSIndexPath { 35 | return NSIndexPath(forItem: 0, inSection: 0) 36 | } 37 | } 38 | 39 | // MARK: - Fake Models 40 | 41 | struct Person { 42 | enum Gender: Int { 43 | case Unknown = 1, Female, Male 44 | } 45 | 46 | let age: Int 47 | let gender: Gender 48 | let name: String 49 | } 50 | 51 | class PersonCoder: NSObject, NSCoding, CodingType { 52 | let value: Person 53 | 54 | required init(_ v: Person) { 55 | value = v 56 | } 57 | 58 | required init?(coder aDecoder: NSCoder) { 59 | if let gender = Person.Gender(rawValue: aDecoder.decodeIntegerForKey("gender")) { 60 | let age = aDecoder.decodeIntegerForKey("age") 61 | let name = aDecoder.decodeObjectForKey("name") as! String 62 | value = Person(age: age, gender: gender, name: name) 63 | } 64 | else { fatalError("Person.Gender not encoded correctly") } 65 | } 66 | 67 | func encodeWithCoder(aCoder: NSCoder) { 68 | aCoder.encodeInteger(value.age, forKey: "age") 69 | aCoder.encodeInteger(value.gender.rawValue, forKey: "gender") 70 | aCoder.encodeObject(value.name, forKey: "name") 71 | } 72 | } 73 | 74 | extension Person: ValueCoding { 75 | typealias Coder = PersonCoder 76 | } 77 | 78 | extension Person.Gender: CustomStringConvertible { 79 | var description: String { 80 | switch self { 81 | case .Unknown: return "Unknown" 82 | case .Female: return "Female" 83 | case .Male: return "Male" 84 | } 85 | } 86 | } 87 | 88 | extension Person: Identifiable { 89 | 90 | var identifier: Identifier { 91 | return "\(name) - \(gender) - \(age)" 92 | } 93 | } 94 | 95 | extension Person: Persistable { 96 | 97 | static var collection: String { 98 | return "People" 99 | } 100 | } 101 | 102 | extension Person: CustomStringConvertible { 103 | var description: String { 104 | return "\(name), \(gender) \(age)" 105 | } 106 | } 107 | 108 | func generateRandomPeople(count: Int) -> [Person] { 109 | 110 | let possibleNames: [(Person.Gender, String)] = [ 111 | (.Male, "Tony"), 112 | (.Male, "Thor"), 113 | (.Male, "Bruce"), 114 | (.Male, "Steve"), 115 | (.Female, "Natasha"), 116 | (.Male, "Clint"), 117 | (.Unknown, "Ultron"), 118 | (.Male, "Nick"), 119 | (.Male, "James"), 120 | (.Male, "Pietro"), 121 | (.Female, "Wanda"), 122 | (.Unknown, "Jarvis"), 123 | (.Female, "Maria"), 124 | (.Male, "Sam"), 125 | (.Female, "Peggy") 126 | ] 127 | 128 | func createRandomPerson(_: Int) -> Person { 129 | let index = Int(arc4random_uniform(UInt32(possibleNames.endIndex))) 130 | let rando = possibleNames[index] 131 | let age = 25 + Int(arc4random_uniform(20)) 132 | return Person(age: age, gender: rando.0, name: rando.1) 133 | } 134 | 135 | return Array(0.. String) -> YapDB.Fetch { 139 | 140 | let grouping: YapDB.View.Grouping = .ByObject({ (_, collection, key, object) -> String! in 141 | if collection == Person.collection { 142 | if let person = Person.decode(object) { 143 | return createGroup(person) 144 | } 145 | } 146 | return .None 147 | }) 148 | 149 | let sorting: YapDB.View.Sorting = .ByObject({ (_, group, collection1, key1, object1, collection2, key2, object2) -> NSComparisonResult in 150 | if let person1 = Person.decode(object1), 151 | let person2 = Person.decode(object2) { 152 | let comparison = person1.name.caseInsensitiveCompare(person2.name) 153 | switch comparison { 154 | case .OrderedSame: 155 | return person1.age < person2.age ? .OrderedAscending : .OrderedDescending 156 | default: 157 | return comparison 158 | } 159 | } 160 | return .OrderedSame 161 | }) 162 | 163 | let view = YapDB.View(name: name, grouping: grouping, sorting: sorting, collections: [Person.collection]) 164 | return .View(view) 165 | } 166 | 167 | func people(name: String, byGroup createGroup: (Person) -> String) -> YapDB.FetchConfiguration { 168 | return YapDB.FetchConfiguration(fetch: people(name, byGroup: createGroup)) 169 | } 170 | 171 | func people(name: String, byGroup createGroup: (Person) -> String) -> Configuration { 172 | return Configuration(fetch: people(name, byGroup: createGroup), itemMapper: Person.decode) 173 | } 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /Tests/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.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // TaylorSource 4 | // 5 | // Created by Daniel Thorpe on 08/03/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | import ValueCoding 11 | import YapDatabaseExtensions 12 | import YapDatabase 13 | @testable import TaylorSource 14 | 15 | struct EventSection: SectionType { 16 | typealias ItemType = Event 17 | let title: String 18 | let items: [Event] 19 | } 20 | 21 | extension EventSection: CustomStringConvertible { 22 | var description: String { 23 | return "\(title) (\(items.count) items)" 24 | } 25 | } 26 | 27 | extension EventSection: Equatable { } 28 | 29 | func == (lhs: EventSection, rhs: EventSection) -> Bool { 30 | return lhs.title == rhs.title && lhs.items == rhs.items 31 | } 32 | 33 | struct Event { 34 | 35 | enum Color { 36 | case Red, Blue, Green 37 | } 38 | 39 | let uuid: String 40 | let color: Color 41 | let date: NSDate 42 | 43 | init(uuid: String = NSUUID().UUIDString, color: Color, date: NSDate = NSDate()) { 44 | self.uuid = uuid 45 | self.color = color 46 | self.date = date 47 | } 48 | 49 | static func create(color color: Color = .Red) -> Event { 50 | return Event(color: color) 51 | } 52 | } 53 | 54 | extension Event.Color: CustomStringConvertible { 55 | 56 | var description: String { 57 | switch self { 58 | case .Red: return "Red" 59 | case .Blue: return "Blue" 60 | case .Green: return "Green" 61 | } 62 | } 63 | } 64 | 65 | extension Event.Color: Equatable { } 66 | 67 | func == (lhs: Event.Color, rhs: Event.Color) -> Bool { 68 | switch (lhs,rhs) { 69 | case (.Red, .Red), (.Blue, .Blue), (.Green, .Green): 70 | return true 71 | default: 72 | return false 73 | } 74 | } 75 | 76 | extension Event: Equatable { } 77 | 78 | func == (lhs: Event, rhs: Event) -> Bool { 79 | return (lhs.color == rhs.color) && (lhs.uuid == rhs.uuid) && (lhs.date == rhs.date) 80 | } 81 | 82 | extension Event.Color: ValueCoding { 83 | 84 | typealias Coder = EventColorCoder 85 | 86 | enum Kind: Int { 87 | case Red = 1, Blue, Green 88 | } 89 | 90 | var kind: Kind { 91 | switch self { 92 | case .Red: return Kind.Red 93 | case .Blue: return Kind.Blue 94 | case .Green: return Kind.Green 95 | } 96 | } 97 | } 98 | 99 | class EventColorCoder: NSObject, NSCoding, CodingType { 100 | 101 | let value: Event.Color 102 | 103 | required init(_ v: Event.Color) { 104 | value = v 105 | } 106 | 107 | required init?(coder aDecoder: NSCoder) { 108 | if let kind = Event.Color.Kind(rawValue: aDecoder.decodeIntegerForKey("kind")) { 109 | switch kind { 110 | case .Red: 111 | value = Event.Color.Red 112 | case .Blue: 113 | value = Event.Color.Blue 114 | case .Green: 115 | value = Event.Color.Green 116 | } 117 | } 118 | else { fatalError("Event.Color.Kind not correctly encoded.") } 119 | } 120 | 121 | func encodeWithCoder(aCoder: NSCoder) { 122 | aCoder.encodeInteger(value.kind.rawValue, forKey: "kind") 123 | } 124 | } 125 | 126 | extension Event: Identifiable { 127 | var identifier: String { 128 | return uuid 129 | } 130 | } 131 | 132 | extension Event: Persistable { 133 | static var collection: String { 134 | return "Events" 135 | } 136 | } 137 | 138 | extension Event: ValueCoding { 139 | typealias Coder = EventCoder 140 | } 141 | 142 | class EventCoder: NSObject, NSCoding, CodingType { 143 | 144 | let value: Event 145 | 146 | required init(_ v: Event) { 147 | value = v 148 | } 149 | 150 | required init?(coder aDecoder: NSCoder) { 151 | let color = Event.Color.decode(aDecoder.decodeObjectForKey("color")) 152 | let uuid = aDecoder.decodeObjectForKey("uuid") as? String 153 | let date = aDecoder.decodeObjectForKey("date") as? NSDate 154 | value = Event(uuid: uuid!, color: color!, date: date!) 155 | } 156 | 157 | func encodeWithCoder(aCoder: NSCoder) { 158 | aCoder.encodeObject(value.color.encoded, forKey: "color") 159 | aCoder.encodeObject(value.uuid, forKey: "uuid") 160 | aCoder.encodeObject(value.date, forKey: "date") 161 | } 162 | } 163 | 164 | func createSomeEvents(numberOfDays: Int = 10) -> [Event] { 165 | let today = NSDate() 166 | let interval: NSTimeInterval = 86_400 167 | return (0.. YapDB.Fetch { 174 | 175 | let grouping: YapDB.View.Grouping = .ByObject({ (_, collection, key, object) -> String! in 176 | if collection == Event.collection { 177 | if !byColor { 178 | return collection 179 | } 180 | 181 | if let event = Event.decode(object) { 182 | return event.color.description 183 | } 184 | } 185 | return .None 186 | }) 187 | 188 | let sorting: YapDB.View.Sorting = .ByObject({ (_, group, collection1, key1, object1, collection2, key2, object2) -> NSComparisonResult in 189 | if let event1 = Event.decode(object1), 190 | let event2 = Event.decode(object2) { 191 | return event1.date.compare(event2.date) 192 | } 193 | return .OrderedSame 194 | }) 195 | 196 | let view = YapDB.View(name: Event.collection, grouping: grouping, sorting: sorting, collections: [Event.collection]) 197 | 198 | return .View(view) 199 | } 200 | 201 | func events(byColor: Bool = false, mappingBlock: YapDB.FetchConfiguration.MappingsConfigurationBlock? = .None) -> YapDB.FetchConfiguration { 202 | return YapDB.FetchConfiguration(fetch: events(byColor), block: mappingBlock) 203 | } 204 | 205 | func events(byColor: Bool = false, mappingBlock: YapDB.FetchConfiguration.MappingsConfigurationBlock? = .None) -> Configuration { 206 | return Configuration(fetch: events(byColor), itemMapper: Event.decode) 207 | } 208 | 209 | func eventsWithColor(color: Event.Color, byColor: Bool = false) -> YapDB.Fetch { 210 | 211 | let filtering: YapDB.Filter.Filtering = .ByObject({ (_, group, collection, key, object) -> Bool in 212 | if let event = Event.decode(object) { 213 | return event.color == color 214 | } 215 | return false 216 | }) 217 | 218 | let filter = YapDB.Filter(name: "\(color) Events", parent: events(byColor), filtering: filtering, collections: [Event.collection]) 219 | 220 | return .Filter(filter) 221 | } 222 | 223 | func eventsWithColor(color: Event.Color, byColor: Bool = false, mappingBlock: YapDB.FetchConfiguration.MappingsConfigurationBlock? = .None) -> YapDB.FetchConfiguration { 224 | return YapDB.FetchConfiguration(fetch: eventsWithColor(color, byColor: byColor), block: mappingBlock) 225 | } 226 | 227 | func eventsWithColor(color: Event.Color, byColor: Bool = false, mappingBlock: YapDB.FetchConfiguration.MappingsConfigurationBlock? = .None) -> Configuration { 228 | return Configuration(fetch: eventsWithColor(color, byColor: byColor), itemMapper: Event.decode) 229 | } 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /Tests/StaticDatasourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 16/04/2015. 3 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | import XCTest 8 | import TaylorSource 9 | 10 | class StaticDatasourceTests: XCTestCase { 11 | 12 | typealias Factory = BasicFactory 13 | typealias Datasource = StaticDatasource 14 | 15 | let view = StubbedTableView() 16 | let factory = Factory() 17 | let data: [Event] = (0..<5).map { (index) -> Event in Event.create() } 18 | var datasource: Datasource! 19 | var provider: BasicDatasourceProvider! 20 | 21 | var lessThanStartIndexPath: NSIndexPath { 22 | return NSIndexPath(forRow: data.count * -1, inSection: 0) 23 | } 24 | 25 | var validIndexPath: NSIndexPath { 26 | return NSIndexPath(forRow: 0, inSection: 0) 27 | } 28 | 29 | var greaterThanEndIndexPath: NSIndexPath { 30 | return NSIndexPath(forRow: data.count * 2, inSection: 0) 31 | } 32 | 33 | override func setUp() { 34 | factory.registerCell(.ClassWithIdentifier(UITableViewCell.self, "cell"), inView: view) { (_, _, _) in } 35 | datasource = Datasource(id: "test datasource", factory: factory, items: data) 36 | provider = BasicDatasourceProvider(datasource) 37 | } 38 | 39 | func registerHeader(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 40 | registerSupplementaryView(.Header, key: key, config: config) 41 | } 42 | 43 | func registerFooter(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 44 | registerSupplementaryView(.Footer, key: key, config: config) 45 | } 46 | 47 | func registerSupplementaryView(kind: SupplementaryElementKind, key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 48 | if let key = key { 49 | datasource.factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "\(kind)"), kind: kind, inView: view, withKey: key, configuration: config) 50 | } 51 | else { 52 | datasource.factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "\(kind)"), kind: kind, inView: view, configuration: config) 53 | } 54 | } 55 | 56 | func validateSupplementaryView(kind: SupplementaryElementKind, exists: Bool, atIndexPath indexPath: NSIndexPath) { 57 | let supplementary = datasource.viewForSupplementaryElementInView(view, kind: kind, atIndexPath: indexPath) 58 | if exists { 59 | XCTAssertNotNil(supplementary, "Supplementary view should be returned.") 60 | } 61 | else { 62 | XCTAssertNil(supplementary, "No supplementary view should be returned.") 63 | } 64 | } 65 | 66 | func registerHeaderText(config: Factory.SupplementaryTextConfiguration) { 67 | registerSupplementaryText(.Header, config: config) 68 | } 69 | 70 | func registerFooterText(config: Factory.SupplementaryTextConfiguration) { 71 | registerSupplementaryText(.Footer, config: config) 72 | } 73 | 74 | func registerSupplementaryText(kind: SupplementaryElementKind, config: Factory.SupplementaryTextConfiguration) { 75 | datasource.factory.registerTextWithKind(kind, configuration: config) 76 | } 77 | 78 | func validateSupplementaryText(kind: SupplementaryElementKind, equals test: String?, atIndexPath indexPath: NSIndexPath) { 79 | let text: String? = datasource.textForSupplementaryElementInView(view, kind: kind, atIndexPath: indexPath) 80 | if let test = test { 81 | XCTAssertEqual(test, text!) 82 | } 83 | else { 84 | XCTAssertNil(text) 85 | } 86 | } 87 | } 88 | 89 | extension StaticDatasourceTests { 90 | 91 | func test_BasicDatasourceProvider_VendsDatasource() { 92 | XCTAssertNotNil(provider.datasource) 93 | } 94 | 95 | func test_StaticDatasource_is_a_sequence() { 96 | XCTAssertEqual(datasource.generate().map { $0.color }, data.map { $0.color }) 97 | } 98 | 99 | func test_StaticDatasource_startIndex_is_zero() { 100 | XCTAssertEqual(datasource.startIndex, 0) 101 | } 102 | 103 | func test_StaticDatasource_endIndex_is_5() { 104 | XCTAssertEqual(datasource.endIndex, 5) 105 | } 106 | 107 | func test_StaticDatasource_allows_random_access() { 108 | XCTAssertEqual(datasource[0], data[0]) 109 | } 110 | } 111 | 112 | extension StaticDatasourceTests { 113 | 114 | func test_GivenStaticDatasource_ThatNumberOfSectionsIsOne() { 115 | XCTAssertEqual(datasource.numberOfSections, 1, "Number of sections should be 1 for a static data source") 116 | } 117 | 118 | func test_GivenStaticDatasource_ThatNumberOfItemIsCorrect() { 119 | XCTAssertEqual(datasource.numberOfItemsInSection(0), data.count, "The number of items should be equal in length to the items argument.") 120 | } 121 | 122 | func test_GivenStaticDatasource_WhenAccessingItemsAtANegativeIndex_ThatResultIsNone() { 123 | XCTAssertTrue(datasource.itemAtIndexPath(lessThanStartIndexPath) == nil, "Result should be none for negative indexes.") 124 | } 125 | 126 | func test_GivenStaticDatsource_WhenAccessingItemsGreaterThanMaxIndex_ThatResultIsNone() { 127 | XCTAssertTrue(datasource.itemAtIndexPath(greaterThanEndIndexPath) == nil, "Result should be none for indexes > max index.") 128 | } 129 | 130 | func test_GivenStaticDatasource_WhenAccessingItems_ThatCorrectItemIsReturned() { 131 | let item = datasource.itemAtIndexPath(validIndexPath) 132 | XCTAssertTrue(item != nil, "Item should not be nil.") 133 | XCTAssertEqual(item!, data[0], "Items at valid indexes should be correct.") 134 | } 135 | } 136 | 137 | extension StaticDatasourceTests { 138 | 139 | func test_GivenStaticDatasource_WhenAccessingCellAtValidIndex_ThatCellIsReturned() { 140 | let cell = datasource.cellForItemInView(view, atIndexPath: validIndexPath) 141 | XCTAssertNotNil(cell, "Cell should be returned") 142 | } 143 | } 144 | 145 | extension StaticDatasourceTests { // Cases where supplementary view should not be returned 146 | 147 | func test_GivenNoHeadersRegistered_WhenAccessingHeaderAtValidIndex_ThatResponseIsNone() { 148 | validateSupplementaryView(.Header, exists: false, atIndexPath: validIndexPath) 149 | } 150 | 151 | func test_GivenNoHeadersRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 152 | validateSupplementaryView(.Header, exists: false, atIndexPath: greaterThanEndIndexPath) 153 | } 154 | 155 | func test_GivenNoFootersRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 156 | validateSupplementaryView(.Footer, exists: false, atIndexPath: validIndexPath) 157 | } 158 | 159 | func test_GivenNoFootersRegistered_WhenAccessingFooterAtInvalidIndex_ThatResponseIsNone() { 160 | validateSupplementaryView(.Footer, exists: false, atIndexPath: lessThanStartIndexPath) 161 | } 162 | 163 | func test_GivenNoCustomViewsRegistered_WhenAccessingCustomViewAtValidIndex_ThatResponseIsNone() { 164 | validateSupplementaryView(.Custom("Sidebar"), exists: false, atIndexPath: validIndexPath) 165 | } 166 | 167 | func test_GivenHeaderRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 168 | registerHeader { (_, _) -> Void in } 169 | validateSupplementaryView(.Footer, exists: false, atIndexPath: validIndexPath) 170 | } 171 | 172 | func test_GivenHeaderRegistered_WhenAccessingFooterAtInvalidIndex_ThatResponseIsNone() { 173 | registerHeader { (_, _) -> Void in } 174 | validateSupplementaryView(.Footer, exists: false, atIndexPath: greaterThanEndIndexPath) 175 | } 176 | 177 | func test_GivenFooterRegistered_WhenAccessingHeaderAtValidIndex_ThatResponseIsNone() { 178 | registerFooter { (_, _) -> Void in } 179 | validateSupplementaryView(.Header, exists: false, atIndexPath: validIndexPath) 180 | } 181 | 182 | func test_GivenFooterRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 183 | registerFooter { (_, _) -> Void in } 184 | validateSupplementaryView(.Header, exists: false, atIndexPath: lessThanStartIndexPath) 185 | } 186 | } 187 | 188 | extension StaticDatasourceTests { // Cases where supplementary view should be returned 189 | 190 | func test_GivenHeaderRegistered_WhenAccessingHeaderAtValidIndex_ThatHeaderIsReturned() { 191 | registerHeader { (_, _) -> Void in } 192 | validateSupplementaryView(.Header, exists: true, atIndexPath: validIndexPath) 193 | } 194 | 195 | func test_GivenFooterRegistered_WhenAccessingFooterAtValidIndex_ThatFooterIsReturned() { 196 | registerFooter { (_, _) -> Void in } 197 | validateSupplementaryView(.Footer, exists: true, atIndexPath: validIndexPath) 198 | } 199 | 200 | func test_GivenCustomViewRegistered_WhenAccessingFooterAtValidIndex_ThatFooterIsReturned() { 201 | registerSupplementaryView(.Custom("Sidebar")) { (_, _) -> Void in } 202 | validateSupplementaryView(.Custom("Sidebar"), exists: true, atIndexPath: validIndexPath) 203 | } 204 | } 205 | 206 | extension StaticDatasourceTests { 207 | 208 | func test_GivenNoHeaderTextRegistered_WhenAccessingHeaderAtValidIndex_ThatResponseIsNone() { 209 | validateSupplementaryText(.Header, equals: .None, atIndexPath: validIndexPath) 210 | } 211 | 212 | func test_GivenNoHeaderTextRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 213 | validateSupplementaryText(.Header, equals: .None, atIndexPath: greaterThanEndIndexPath) 214 | } 215 | 216 | func test_GivenNoFooterTextRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 217 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: validIndexPath) 218 | } 219 | 220 | func test_GivenNoFooterTextRegistered_WhenAccessingFooterAtInvalidIndex_ThatResponseIsNone() { 221 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: lessThanStartIndexPath) 222 | } 223 | 224 | func test_GivenHeaderTextRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 225 | registerHeaderText { index in "Hello" } 226 | validateSupplementaryText(.Header, equals: "Hello", atIndexPath: validIndexPath) 227 | } 228 | 229 | func test_GivenFooterTextRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 230 | registerFooterText { index in "World" } 231 | validateSupplementaryText(.Footer, equals: "World", atIndexPath: validIndexPath) 232 | } 233 | } 234 | 235 | 236 | -------------------------------------------------------------------------------- /Tests/StaticSectionDatasourceTests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import TaylorSource 4 | 5 | class StaticSectionDatasourceTests: XCTestCase { 6 | 7 | typealias Factory = BasicFactory 8 | typealias Datasource = StaticSectionDatasource 9 | 10 | let view = StubbedTableView() 11 | let factory = Factory() 12 | 13 | let sectionCount = 4 14 | let itemCount = 5 15 | 16 | lazy var sections: [EventSection] = (0 ..< self.sectionCount).map { section in 17 | let items = (0 ..< self.itemCount).map { (index) -> Event in Event.create() } 18 | return EventSection(title: "Section \(section)", items: items) 19 | } 20 | 21 | var datasource: Datasource! 22 | var provider: BasicDatasourceProvider! 23 | 24 | var validIndexPath: NSIndexPath { 25 | return NSIndexPath(forRow: 0, inSection: 0) 26 | } 27 | 28 | var lessThanStartItemIndexPath: NSIndexPath { 29 | return NSIndexPath(forRow: -1, inSection: 0) 30 | } 31 | 32 | var greaterThanEndItemIndexPath: NSIndexPath { 33 | return NSIndexPath(forRow: sections[0].items.count * 2, inSection: 0) 34 | } 35 | 36 | var lessThanStartSectionIndexPath: NSIndexPath { 37 | return NSIndexPath(forRow: 0, inSection: -1) 38 | } 39 | 40 | var greaterThanEndSectionIndexPath: NSIndexPath { 41 | return NSIndexPath(forRow: 0, inSection: sections.count * 2) 42 | } 43 | 44 | 45 | override func setUp() { 46 | factory.registerCell(.ClassWithIdentifier(UITableViewCell.self, "cell"), inView: view) { (_, _, _) in } 47 | datasource = Datasource(id: "test datasource", factory: factory, sections: sections) 48 | provider = BasicDatasourceProvider(datasource) 49 | } 50 | 51 | func registerHeader(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 52 | registerSupplementaryView(.Header, key: key, config: config) 53 | } 54 | 55 | func registerFooter(key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 56 | registerSupplementaryView(.Footer, key: key, config: config) 57 | } 58 | 59 | func registerSupplementaryView(kind: SupplementaryElementKind, key: String? = .None, config: Factory.SupplementaryViewConfiguration) { 60 | if let key = key { 61 | datasource.factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "\(kind)"), kind: kind, inView: view, withKey: key, configuration: config) 62 | } 63 | else { 64 | datasource.factory.registerSupplementaryView(.ClassWithIdentifier(UITableViewHeaderFooterView.self, "\(kind)"), kind: kind, inView: view, configuration: config) 65 | } 66 | } 67 | 68 | func validateSupplementaryView(kind: SupplementaryElementKind, exists: Bool, atIndexPath indexPath: NSIndexPath) { 69 | let supplementary = datasource.viewForSupplementaryElementInView(view, kind: kind, atIndexPath: indexPath) 70 | if exists { 71 | XCTAssertNotNil(supplementary, "Supplementary view should be returned.") 72 | } 73 | else { 74 | XCTAssertNil(supplementary, "No supplementary view should be returned.") 75 | } 76 | } 77 | 78 | func registerHeaderText(config: Factory.SupplementaryTextConfiguration) { 79 | registerSupplementaryText(.Header, config: config) 80 | } 81 | 82 | func registerFooterText(config: Factory.SupplementaryTextConfiguration) { 83 | registerSupplementaryText(.Footer, config: config) 84 | } 85 | 86 | func registerSupplementaryText(kind: SupplementaryElementKind, config: Factory.SupplementaryTextConfiguration) { 87 | datasource.factory.registerTextWithKind(kind, configuration: config) 88 | } 89 | 90 | func validateSupplementaryText(kind: SupplementaryElementKind, equals test: String?, atIndexPath indexPath: NSIndexPath) { 91 | let text: String? = datasource.textForSupplementaryElementInView(view, kind: kind, atIndexPath: indexPath) 92 | if let test = test { 93 | XCTAssertEqual(test, text!) 94 | } 95 | else { 96 | XCTAssertNil(text) 97 | } 98 | } 99 | } 100 | 101 | extension StaticSectionDatasourceTests { 102 | 103 | func test_BasicDatasourceProvider_VendsDatasource() { 104 | XCTAssertNotNil(provider.datasource) 105 | } 106 | 107 | func test_StaticDatasource_DatasourceIsASequence() { 108 | XCTAssertEqual(datasource.generate().map { $0.title }, sections.map { $0.title }) 109 | } 110 | 111 | func test_StaticDatasource_DatasourceStartIndexIsZero() { 112 | XCTAssertEqual(datasource.startIndex, 0) 113 | } 114 | 115 | func test_StaticDatasource_DatasourceEndIndexIsCorrect() { 116 | XCTAssertEqual(datasource.endIndex, sectionCount) 117 | } 118 | 119 | func test_StaticDatasource_DataSourceAllowsRandomAccess() { 120 | for sectionIndex in 0 ..< sectionCount { 121 | XCTAssertEqual(datasource[sectionIndex], sections[sectionIndex]) 122 | } 123 | } 124 | 125 | func test_StaticDatasource_SectionsArePresent() { 126 | XCTAssertEqual(datasource.numberOfSections, sections.count) 127 | for sectionIndex in 0 ..< datasource.numberOfSections { 128 | XCTAssertNotNil(provider.datasource.sectionAtIndex(sectionIndex)) 129 | } 130 | } 131 | 132 | func test_StaticDatasource_SectionsAreSequences() { 133 | for (index, section) in datasource.enumerate() { 134 | XCTAssertEqual(section.generate().map { $0.color }, sections[index].map { $0.color }) 135 | } 136 | } 137 | 138 | func test_StaticDatasource_SectionsStartAtIndex0() { 139 | for section in datasource { 140 | XCTAssertEqual(section.startIndex, 0) 141 | } 142 | } 143 | 144 | func test_StaticDatasource_SectionsEndAtCorrectIndex() { 145 | for section in datasource { 146 | XCTAssertEqual(section.endIndex, itemCount) 147 | } 148 | } 149 | 150 | func test_StaticDatasource_SectionsAllowsRandomAccess() { 151 | for sectionIndex in 0 ..< sectionCount { 152 | for itemIndex in 0 ..< itemCount { 153 | XCTAssertEqual(datasource[sectionIndex][itemIndex], sections[sectionIndex][itemIndex]) 154 | } 155 | } 156 | } 157 | } 158 | 159 | extension StaticSectionDatasourceTests { 160 | 161 | func test_GivenStaticDatasource_ThatNumberOfSectionsIsCorrect() { 162 | XCTAssertEqual(datasource.numberOfSections, sections.count, "The number of sections should be equal to the sections argument.") 163 | } 164 | 165 | func test_GivenStaticDatasource_WhenAccessingItems_AtANegativeIndex_ThatResultIsNone() { 166 | XCTAssertTrue(datasource.itemAtIndexPath(lessThanStartSectionIndexPath) == nil, "Result should be none for negative indexes.") 167 | XCTAssertTrue(datasource.itemAtIndexPath(lessThanStartItemIndexPath) == nil, "Result should be none for negative indexes.") 168 | } 169 | 170 | func test_GivenStaticDatsource_WhenAccessingItems_GreaterThanMaxIndex_ThatResultIsNone() { 171 | XCTAssertTrue(datasource.itemAtIndexPath(greaterThanEndSectionIndexPath) == nil, "Result should be none for indexes > max index.") 172 | XCTAssertTrue(datasource.itemAtIndexPath(greaterThanEndItemIndexPath) == nil, "Result should be none for indexes > max index.") 173 | } 174 | 175 | func test_GivenStaticDatasource_WhenAccessingItems_AtValidIndexPath_ThatCorrectItemIsReturned() { 176 | let item = datasource.itemAtIndexPath(validIndexPath) 177 | XCTAssertTrue(item != nil, "Item should not be nil.") 178 | XCTAssertEqual(item!, sections[validIndexPath.section][validIndexPath.item], "Items at valid indexes should be correct.") 179 | } 180 | } 181 | 182 | extension StaticSectionDatasourceTests { 183 | 184 | func test_GivenStaticDatasource_WhenAccessingCellAtValidIndex_ThatCellIsReturned() { 185 | let cell = datasource.cellForItemInView(view, atIndexPath: validIndexPath) 186 | XCTAssertNotNil(cell, "Cell should be returned") 187 | } 188 | } 189 | 190 | extension StaticSectionDatasourceTests { // Cases where supplementary view should not be returned 191 | 192 | func test_GivenNoHeadersRegistered_WhenAccessingHeaderAtValidIndex_ThatResponseIsNone() { 193 | validateSupplementaryView(.Header, exists: false, atIndexPath: validIndexPath) 194 | } 195 | 196 | func test_GivenNoHeadersRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 197 | validateSupplementaryView(.Header, exists: false, atIndexPath: greaterThanEndSectionIndexPath) 198 | validateSupplementaryView(.Header, exists: false, atIndexPath: greaterThanEndItemIndexPath) 199 | validateSupplementaryView(.Header, exists: false, atIndexPath: lessThanStartSectionIndexPath) 200 | validateSupplementaryView(.Header, exists: false, atIndexPath: lessThanStartItemIndexPath) 201 | } 202 | 203 | func test_GivenNoFootersRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 204 | validateSupplementaryView(.Footer, exists: false, atIndexPath: validIndexPath) 205 | } 206 | 207 | func test_GivenNoFootersRegistered_WhenAccessingFooterAtInvalidIndex_ThatResponseIsNone() { 208 | validateSupplementaryView(.Footer, exists: false, atIndexPath: greaterThanEndSectionIndexPath) 209 | validateSupplementaryView(.Footer, exists: false, atIndexPath: greaterThanEndItemIndexPath) 210 | validateSupplementaryView(.Footer, exists: false, atIndexPath: lessThanStartSectionIndexPath) 211 | validateSupplementaryView(.Footer, exists: false, atIndexPath: lessThanStartItemIndexPath) 212 | } 213 | 214 | func test_GivenNoCustomViewsRegistered_WhenAccessingCustomViewAtValidIndex_ThatResponseIsNone() { 215 | validateSupplementaryView(.Custom("Sidebar"), exists: false, atIndexPath: validIndexPath) 216 | } 217 | 218 | func test_GivenHeaderRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 219 | registerHeader { (_, _) -> Void in } 220 | validateSupplementaryView(.Footer, exists: false, atIndexPath: validIndexPath) 221 | } 222 | 223 | func test_GivenHeaderRegistered_WhenAccessingFooterAtInvalidIndex_ThatResponseIsNone() { 224 | registerHeader { (_, _) -> Void in } 225 | validateSupplementaryView(.Footer, exists: false, atIndexPath: greaterThanEndSectionIndexPath) 226 | validateSupplementaryView(.Footer, exists: false, atIndexPath: greaterThanEndItemIndexPath) 227 | validateSupplementaryView(.Footer, exists: false, atIndexPath: lessThanStartSectionIndexPath) 228 | validateSupplementaryView(.Footer, exists: false, atIndexPath: lessThanStartItemIndexPath) 229 | } 230 | 231 | func test_GivenFooterRegistered_WhenAccessingHeaderAtValidIndex_ThatResponseIsNone() { 232 | registerFooter { (_, _) -> Void in } 233 | validateSupplementaryView(.Header, exists: false, atIndexPath: validIndexPath) 234 | } 235 | 236 | func test_GivenFooterRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 237 | registerFooter { (_, _) -> Void in } 238 | validateSupplementaryView(.Header, exists: false, atIndexPath: greaterThanEndSectionIndexPath) 239 | validateSupplementaryView(.Header, exists: false, atIndexPath: greaterThanEndItemIndexPath) 240 | validateSupplementaryView(.Header, exists: false, atIndexPath: lessThanStartSectionIndexPath) 241 | validateSupplementaryView(.Header, exists: false, atIndexPath: lessThanStartItemIndexPath) 242 | } 243 | } 244 | 245 | extension StaticSectionDatasourceTests { // Cases where supplementary view should be returned 246 | 247 | func test_GivenHeaderRegistered_WhenAccessingHeaderAtValidIndex_ThatHeaderIsReturned() { 248 | registerHeader { (_, _) -> Void in } 249 | validateSupplementaryView(.Header, exists: true, atIndexPath: validIndexPath) 250 | } 251 | 252 | func test_GivenFooterRegistered_WhenAccessingFooterAtValidIndex_ThatFooterIsReturned() { 253 | registerFooter { (_, _) -> Void in } 254 | validateSupplementaryView(.Footer, exists: true, atIndexPath: validIndexPath) 255 | } 256 | 257 | func test_GivenCustomViewRegistered_WhenAccessingFooterAtValidIndex_ThatFooterIsReturned() { 258 | registerSupplementaryView(.Custom("Sidebar")) { (_, _) -> Void in } 259 | validateSupplementaryView(.Custom("Sidebar"), exists: true, atIndexPath: validIndexPath) 260 | } 261 | } 262 | 263 | extension StaticSectionDatasourceTests { 264 | 265 | func test_GivenNoHeaderTextRegistered_WhenAccessingHeaderAtValidIndex_ThatResponseIsNone() { 266 | validateSupplementaryText(.Header, equals: .None, atIndexPath: validIndexPath) 267 | } 268 | 269 | func test_GivenNoHeaderTextRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 270 | validateSupplementaryText(.Header, equals: .None, atIndexPath: greaterThanEndSectionIndexPath) 271 | validateSupplementaryText(.Header, equals: .None, atIndexPath: greaterThanEndItemIndexPath) 272 | validateSupplementaryText(.Header, equals: .None, atIndexPath: lessThanStartSectionIndexPath) 273 | validateSupplementaryText(.Header, equals: .None, atIndexPath: lessThanStartItemIndexPath) 274 | } 275 | 276 | func test_GivenNoFooterTextRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 277 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: validIndexPath) 278 | } 279 | 280 | func test_GivenNoFooterTextRegistered_WhenAccessingFooterAtInvalidIndex_ThatResponseIsNone() { 281 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: greaterThanEndSectionIndexPath) 282 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: greaterThanEndItemIndexPath) 283 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: lessThanStartSectionIndexPath) 284 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: lessThanStartItemIndexPath) 285 | } 286 | 287 | func test_GivenHeaderTextRegistered_WhenAccessingFooterAtValidIndex_ThatResponseIsNone() { 288 | registerHeaderText { index in "Hello" } 289 | validateSupplementaryText(.Footer, equals: .None, atIndexPath: validIndexPath) 290 | } 291 | 292 | func test_GivenFooterTextRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsNone() { 293 | registerFooterText { index in "World" } 294 | validateSupplementaryText(.Header, equals: .None, atIndexPath: validIndexPath) 295 | } 296 | 297 | func test_GivenHeaderTextRegistered_WhenAccessingHeaderAtValidIndex_ThatResponseIsReturned() { 298 | registerHeaderText { index in "Hello" } 299 | validateSupplementaryText(.Header, equals: "Hello", atIndexPath: validIndexPath) 300 | } 301 | 302 | func test_GivenFooterTextRegistered_WhenAccessingHeaderAtInvalidIndex_ThatResponseIsReturned() { 303 | registerFooterText { index in "World" } 304 | validateSupplementaryText(.Footer, equals: "World", atIndexPath: validIndexPath) 305 | } 306 | } 307 | 308 | 309 | -------------------------------------------------------------------------------- /Tests/TaylorSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaylorSourceTests.swift 3 | // TaylorSourceTests 4 | // 5 | // Created by Daniel Thorpe on 07/03/2016. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TaylorSource 11 | 12 | class TaylorSourceTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/YapDBDatasourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YapDBDatasourceTests.swift 3 | // Datasources 4 | // 5 | // Created by Daniel Thorpe on 08/05/2015. 6 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | import YapDatabase 13 | import YapDatabaseExtensions 14 | import TaylorSource 15 | 16 | class YapDBDatasourceTests: XCTestCase { 17 | 18 | typealias Factory = YapDBFactory 19 | typealias Datasource = YapDBDatasource 20 | 21 | let configuration: TaylorSource.Configuration = events(true) 22 | let view = StubbedTableView() 23 | let factory = Factory() 24 | 25 | var someEvents: [Event]! 26 | var numberOfEvents: Int! 27 | 28 | override func setUp() { 29 | super.setUp() 30 | someEvents = createManyEvents() 31 | numberOfEvents = someEvents.count 32 | } 33 | 34 | func datasourceWithDatabase(db: YapDatabase, changesValidator: YapDatabaseViewMappings.Changes? = .None) -> Datasource { 35 | if let changes = changesValidator { 36 | return Datasource(id: "test datasource", database: db, factory: factory, processChanges: changes, configuration: configuration) 37 | } 38 | return Datasource(id: "test datasource", database: db, factory: factory, processChanges: { changeset in }, configuration: configuration) 39 | } 40 | } 41 | 42 | extension YapDBDatasourceTests { 43 | 44 | func test_GivenEmptyDatabase_ThatHasCorrectSections() { 45 | let db = YapDB.testDatabase() 46 | let datasource = datasourceWithDatabase(db) 47 | XCTAssertEqual(datasource.numberOfSections, 0) 48 | } 49 | 50 | func test_GivenDatabaseWithOneRedEvent_ThatHasCorrectSections() { 51 | let db = YapDB.testDatabase() { database in 52 | database.newConnection().write(createOneEvent()) 53 | } 54 | let datasource = datasourceWithDatabase(db) 55 | XCTAssertEqual(datasource.numberOfSections, 1) 56 | XCTAssertEqual(datasource.numberOfItemsInSection(0), 1) 57 | } 58 | 59 | func test_GivenDatabaseWithManyRedEvents_ThatHasCorrectSections() { 60 | let db = YapDB.testDatabase() { database in 61 | database.newConnection().write(self.someEvents) 62 | } 63 | let datasource = datasourceWithDatabase(db) 64 | XCTAssertEqual(datasource.numberOfSections, 1) 65 | XCTAssertEqual(datasource.numberOfItemsInSection(0), numberOfEvents) 66 | } 67 | 68 | func test_GivenDatabaseWithManyRedAndManyBlueEvents_ThatHasCorrectSections() { 69 | let redEvents = createManyEvents() 70 | let numberOfRedEvents = redEvents.count 71 | let blueEvents = createManyEvents(.Blue) 72 | let numberOfBlueEvents = blueEvents.count 73 | let db = YapDB.testDatabase() { database in 74 | database.newConnection().write { transaction in 75 | transaction.write(redEvents) 76 | transaction.write(blueEvents) 77 | } 78 | } 79 | let datasource = datasourceWithDatabase(db) 80 | XCTAssertEqual(datasource.numberOfSections, 2) 81 | XCTAssertEqual(datasource.numberOfItemsInSection(0), numberOfRedEvents) 82 | XCTAssertEqual(datasource.numberOfItemsInSection(1), numberOfBlueEvents) 83 | } 84 | 85 | func test_GivenStaticDatasource_WhenAccessingItemsAtANegativeIndex_ThatResultIsNone() { 86 | let db = YapDB.testDatabase() { database in 87 | database.newConnection().write(self.someEvents) 88 | } 89 | let datasource = datasourceWithDatabase(db) 90 | XCTAssertTrue(datasource.itemAtIndexPath(NSIndexPath(forRow: numberOfEvents * -1, inSection: 0)) == nil) 91 | } 92 | 93 | func test_GivenStaticDatasource_WhenAccessingItemsGreaterThanMaxIndex_ThatResultIsNone() { 94 | let db = YapDB.testDatabase() { database in 95 | database.newConnection().write(self.someEvents) 96 | } 97 | let datasource = datasourceWithDatabase(db) 98 | XCTAssertTrue(datasource.itemAtIndexPath(NSIndexPath(forRow: numberOfEvents * -1, inSection: 0)) == nil) 99 | } 100 | 101 | func test_GivenStaticDatasource_WhenAccessingItems_ThatCorrectItemIsReturned() { 102 | let db = YapDB.testDatabase() { database in 103 | database.newConnection().write(self.someEvents) 104 | } 105 | let datasource = datasourceWithDatabase(db) 106 | XCTAssertEqual(datasource.itemAtIndexPath(NSIndexPath.first)!, someEvents[0]) 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /Tests/YapDBMapperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 16/04/2015. 3 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | import XCTest 8 | 9 | import YapDatabase 10 | import YapDatabaseExtensions 11 | import TaylorSource 12 | 13 | class MapperTests: XCTestCase { 14 | 15 | var db: YapDatabase! 16 | var mapper: Mapper! 17 | 18 | func test__initially__the_endIndex__is__zero() { 19 | db = YapDB.testDatabase() 20 | mapper = Mapper(database: db, configuration: events()) 21 | XCTAssertEqual(mapper.startIndex, 0) 22 | XCTAssertEqual(mapper.endIndex, 0) 23 | } 24 | 25 | func test__when_database_has_one_item__initially__the_endIndex_is_1() { 26 | db = YapDB.testDatabase() 27 | db.newConnection().write(Event.create(color: .Red)) 28 | mapper = Mapper(database: db, configuration: events()) 29 | XCTAssertEqual(mapper.startIndex, 0) 30 | XCTAssertEqual(mapper.endIndex, 1) 31 | } 32 | 33 | func test__when_database_has_one_item__lookup_items_by_indexPath__the_first_indexPath__is_the_item() { 34 | db = YapDB.testDatabase() 35 | let event = Event.create(color: .Red) 36 | db.newConnection().write(event) 37 | mapper = Mapper(database: db, configuration: events()) 38 | XCTAssertEqual(mapper.itemAtIndexPath(NSIndexPath.first)!, event) 39 | } 40 | 41 | func test__when_database_has_one_item__reverse_loop_items__the_first_item__is_the_first_indexPath() { 42 | db = YapDB.testDatabase() 43 | let event = Event.create(color: .Red) 44 | db.newConnection().write(event) 45 | mapper = Mapper(database: db, configuration: events()) 46 | XCTAssertEqual(mapper.indexPathForKey(keyForPersistable(event), inCollection: Event.collection)!, NSIndexPath.first) 47 | } 48 | 49 | func test__when_database_has_three_events__can_slice_last_two() { 50 | let someEvents = createSomeEvents() 51 | db = YapDB.testDatabase() { db in 52 | db.newConnection().write(someEvents) 53 | } 54 | mapper = Mapper(database: db, configuration: events()) 55 | XCTAssertEqual(Array(someEvents.reverse()[2..<5]), mapper[2..<5]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/YapDBObserverTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Daniel Thorpe on 16/04/2015. 3 | // Copyright (c) 2015 Daniel Thorpe. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | import XCTest 8 | 9 | import YapDatabase 10 | import YapDatabaseExtensions 11 | import TaylorSource 12 | 13 | func numberOfChangesInChangeset(changeset: YapDatabaseViewMappings.Changeset) -> Int { 14 | return changeset.sections.count + changeset.items.count 15 | } 16 | 17 | func numberOfSectionChangesOfType(type: YapDatabaseViewChangeType, inChangeset changeset: YapDatabaseViewMappings.Changeset) -> Int { 18 | return changeset.sections.filter { $0.type == type }.count 19 | } 20 | 21 | func numberOfItemChangesOfType(type: YapDatabaseViewChangeType, inChangeset changeset: YapDatabaseViewMappings.Changeset) -> Int { 22 | return changeset.items.filter { $0.type == type }.count 23 | } 24 | 25 | func validateChangeset(expectation: XCTestExpectation, validations: [YapDatabaseViewMappings.Changes]) -> YapDatabaseViewMappings.Changes { 26 | return { changeset in 27 | for validation in validations { 28 | validation((sections: changeset.sections, items: changeset.items)) 29 | } 30 | expectation.fulfill() 31 | } 32 | } 33 | 34 | func validateChangesetHasSectionInsert(count: Int = 1) -> YapDatabaseViewMappings.Changes { 35 | return { changeset in 36 | XCTAssertEqual(numberOfSectionChangesOfType(.Insert, inChangeset: changeset), count) 37 | } 38 | } 39 | 40 | func validateChangesetHasRowInsert(count: Int = 1) -> YapDatabaseViewMappings.Changes { 41 | return { changeset in 42 | XCTAssertEqual(numberOfItemChangesOfType(.Insert, inChangeset: changeset), count) 43 | } 44 | } 45 | 46 | func createOneEvent(color: Event.Color = .Red) -> Event { 47 | return Event.create(color: color) 48 | } 49 | 50 | func createManyEvents(color: Event.Color = .Red) -> [Event] { 51 | return (0..<5).map { _ in createOneEvent(color) } 52 | } 53 | 54 | class ObserverTests: XCTestCase { 55 | 56 | var observer: Observer! 57 | let configuration: TaylorSource.Configuration = events(true) 58 | } 59 | 60 | extension ObserverTests { 61 | 62 | func testObserver_EmptyDatabase_EndIndexIsZero() { 63 | let db = YapDB.testDatabase() 64 | let observer = Observer(database: db, changes: { changeset in }, configuration: configuration) 65 | XCTAssertEqual(observer.startIndex, 0) 66 | XCTAssertEqual(observer.endIndex, 0) 67 | } 68 | 69 | func testObserver_WriteOneObject_ChangesetHasOneSectionInsert() { 70 | let db = YapDB.testDatabase() 71 | let connection = db.newConnection() 72 | let expectation = expectationWithDescription("Writing one object") 73 | 74 | observer = Observer( 75 | database: db, 76 | changes: validateChangeset(expectation, validations: [validateChangesetHasSectionInsert()]), 77 | configuration: configuration) 78 | 79 | connection.write(createOneEvent()) 80 | waitForExpectationsWithTimeout(5.0, handler: nil) 81 | } 82 | 83 | func testObserver_DatabaseWithOneRow_WriteOneObject_ChangesetHasOneRowInsert() { 84 | let db = YapDB.testDatabase() 85 | let connection = db.newConnection() 86 | let expectation = expectationWithDescription("Writing one object") 87 | 88 | connection.write(createOneEvent()) 89 | 90 | observer = Observer( 91 | database: db, 92 | changes: validateChangeset(expectation, validations: [validateChangesetHasRowInsert()]), 93 | configuration: configuration) 94 | 95 | connection.write(createOneEvent()) 96 | waitForExpectationsWithTimeout(5.0, handler: nil) 97 | } 98 | 99 | func testObserver_WriteManyObjectToOneGroup_ChangesetHasOneSectionInsert() { 100 | let db = YapDB.testDatabase() 101 | let connection = db.newConnection() 102 | let expectation = expectationWithDescription("Writing many object") 103 | 104 | observer = Observer( 105 | database: db, 106 | changes: validateChangeset(expectation, validations: [validateChangesetHasSectionInsert()]), 107 | configuration: configuration) 108 | 109 | connection.write(createManyEvents()) 110 | waitForExpectationsWithTimeout(5.0, handler: nil) 111 | } 112 | 113 | func testObserver_WriteManyObjectToTwoGroups_ChangesetHasTwoSectionInsert() { 114 | let db = YapDB.testDatabase() 115 | let connection = db.newConnection() 116 | let expectation = expectationWithDescription("Writing many object to groups") 117 | 118 | observer = Observer( 119 | database: db, 120 | changes: validateChangeset(expectation, validations: [validateChangesetHasSectionInsert(2)]), 121 | configuration: configuration) 122 | 123 | var events = createManyEvents(.Red) 124 | events += createManyEvents(.Blue) 125 | 126 | connection.write(events) 127 | waitForExpectationsWithTimeout(5.0, handler: nil) 128 | } 129 | } 130 | 131 | 132 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danthorpe/TaylorSource/05d3141e43c35a0f6d7bc222e59e5249263be5a5/header.png --------------------------------------------------------------------------------