├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── LICENSE.md ├── README.md ├── ReSwiftRecorder.podspec ├── ReSwiftRecorder.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── ReSwiftRecorder.xcscheme └── ReSwiftRecorder ├── Info.plist ├── ReSwiftRecorder.h ├── RecordingStore.swift ├── StandardAction.swift └── UI ├── StateHistoryCollectionViewCell.swift ├── StateHistorySliderView.swift └── StateHistoryView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Mac OS X 6 | .DS_Store 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | 23 | ## Other 24 | *.xccheckout 25 | *.moved-aside 26 | *.xcuserstate 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 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 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReSwift/ReSwift" "3.0.0" 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "ReSwift/ReSwift" "3.0.0" 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Benjamin Encz 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 2 | [![](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Swift-Flow/Swift-Flow/blob/master/LICENSE.md) 3 | 4 | A recording store for [ReSwift][]. Enables hot-reloading and time travel for ReSwift apps. 5 | 6 | # ⚠️ ReSwift-Recorder is Deprecated 7 | 8 | ⚠️ Proof of concept. Needs a lot of love! ⚠️ 9 | 10 | The recorder was written to work with [ReSwift][] v3. It uses an internal state setter which is not supported by more recent releases of ReSwift; so the recorder would need to be rewritten. 11 | 12 | [reswift]: https://github.com/ReSwift/ReSwift 13 | 14 | # About ReSwiftRecorder 15 | 16 | ReSwiftRecorder is an extension for ReSwift that allows developers to record and replay actions. ReSwiftRecorder supports serializing these actions to disk, which allows to replay recorded sessions and to restart apps at the point you left them off. 17 | 18 | This is especially useful during development. If you run into a crash, while recording, you now have a recorded JSON file with all the actions needed to reproduce crash. If you restart your app with this recorded session, it will crash in exactly the same way every single time - this allows you to fix the underlying issue without manually navigating through the app over and over again. 19 | 20 | The long term goal of this extension is to implement some of the most important features from [Redux Devtools](https://github.com/gaearon/redux-devtools). 21 | 22 | **This extension is working - you can record and replay actions, but it still in a proof-of-concept state**. 23 | 24 | ## Next Steps 25 | 26 | - Make it easier for developers to make actions serializable, ideally cutting down on some of the boilerplate code that is currently necessary. 27 | - Improve the implementation of this extension, the current implementation is a hack. 28 | 29 | ## CocoaPods 30 | 31 | You can install ReSwiftRecorder via CocoaPods by adding it to your `Podfile`: 32 | 33 | use_frameworks! 34 | 35 | source 'https://github.com/CocoaPods/Specs.git' 36 | platform :ios, '8.0' 37 | 38 | pod 'ReSwift' 39 | pod 'ReSwiftRecorder' 40 | 41 | And run `pod install`. 42 | 43 | ## Carthage 44 | 45 | You can install ReSwiftRecorder via [Carthage]() by adding the following line to your Cartfile: 46 | 47 | github "ReSwift/ReSwift-Recorder" 48 | 49 | # Configuration 50 | 51 | When creating your app's store you need to create an instance of `RecordingStore` instead of an instance of `MainStore`. You also need to provide a `typeMaps` argument that is used to deserialize actions: 52 | 53 | ```swift 54 | RecordingMainStore(reducer: CombinedReducer([CounterReducer(), NavigationReducer()]), 55 | appState: AppState(), typeMaps:[counterActionTypeMap, ReSwiftRouter.typeMap], recording: "recording.json") 56 | ``` 57 | The `typeMaps` array is an array that maps type names (Strings) to action types. 58 | The last argument `recording`, can either be `nil` or the path to a recording stored in the documents directory of the app. If you set the path to a specific recording, the store will load all actions upon launch and replay them, thereby restoring the state of the application. 59 | 60 | For a practical example of how to use ReSwiftRecorder, check out the [Counter App Example](https://github.com/ReSwift/CounterExample-Navigation-TimeTravel). 61 | -------------------------------------------------------------------------------- /ReSwiftRecorder.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReSwiftRecorder" 3 | s.version = "0.4.1" 4 | s.summary = "Time Travel and Hot Reloading for ReSwift" 5 | s.description = <<-DESC 6 | A recording store for ReSwift. Enables hot-reloading and time travel for ReSwift apps. 7 | Still in experimental stage! 8 | DESC 9 | s.homepage = "https://github.com/ReSwift/ReSwift-Recorder" 10 | s.license = { :type => "MIT", :file => "LICENSE.md" } 11 | s.author = { "Benjamin Encz" => "me@benjamin-encz.de" } 12 | s.social_media_url = "http://twitter.com/benjaminencz" 13 | s.source = { :git => "https://github.com/ReSwift/ReSwift-Recorder.git", :tag => s.version.to_s } 14 | s.ios.deployment_target = '8.0' 15 | s.requires_arc = true 16 | s.source_files = 'ReSwiftRecorder/**/*.swift' 17 | s.dependency 'ReSwift', '~> 3.0.0' 18 | end 19 | -------------------------------------------------------------------------------- /ReSwiftRecorder.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 254133671C50B37C003C8E93 /* RecordingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 254133641C50B37C003C8E93 /* RecordingStore.swift */; }; 11 | 254133681C50B37C003C8E93 /* ReSwiftRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 254133651C50B37C003C8E93 /* ReSwiftRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 254133841C50B47D003C8E93 /* StateHistoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 254133811C50B47D003C8E93 /* StateHistoryCollectionViewCell.swift */; }; 13 | 254133851C50B47D003C8E93 /* StateHistorySliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 254133821C50B47D003C8E93 /* StateHistorySliderView.swift */; }; 14 | 254133861C50B47D003C8E93 /* StateHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 254133831C50B47D003C8E93 /* StateHistoryView.swift */; }; 15 | 254133871C50B4D5003C8E93 /* ReSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 254133741C50B429003C8E93 /* ReSwift.framework */; }; 16 | D43E1C411F04201D00184A11 /* StandardAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43E1C401F04201D00184A11 /* StandardAction.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXContainerItemProxy section */ 20 | 254133731C50B429003C8E93 /* PBXContainerItemProxy */ = { 21 | isa = PBXContainerItemProxy; 22 | containerPortal = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 23 | proxyType = 2; 24 | remoteGlobalIDString = 625E66831C1FF97E0027C288; 25 | remoteInfo = "ReSwift-iOS"; 26 | }; 27 | 254133751C50B429003C8E93 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 30 | proxyType = 2; 31 | remoteGlobalIDString = 625E669A1C1FFA3C0027C288; 32 | remoteInfo = "ReSwift-iOSTests"; 33 | }; 34 | 254133771C50B429003C8E93 /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 37 | proxyType = 2; 38 | remoteGlobalIDString = 25DBCF7B1C30C4AA00D63A58; 39 | remoteInfo = "ReSwift-OSX"; 40 | }; 41 | 254133791C50B429003C8E93 /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 44 | proxyType = 2; 45 | remoteGlobalIDString = 25DBCF871C30C4DB00D63A58; 46 | remoteInfo = "ReSwift-OSXTests"; 47 | }; 48 | 2541337B1C50B429003C8E93 /* PBXContainerItemProxy */ = { 49 | isa = PBXContainerItemProxy; 50 | containerPortal = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 51 | proxyType = 2; 52 | remoteGlobalIDString = 25DBCF4E1C30C18D00D63A58; 53 | remoteInfo = "ReSwift-tvOS"; 54 | }; 55 | 2541337D1C50B429003C8E93 /* PBXContainerItemProxy */ = { 56 | isa = PBXContainerItemProxy; 57 | containerPortal = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 58 | proxyType = 2; 59 | remoteGlobalIDString = 25DBCF641C30C1AC00D63A58; 60 | remoteInfo = "ReSwift-tvOSTests"; 61 | }; 62 | 2541337F1C50B429003C8E93 /* PBXContainerItemProxy */ = { 63 | isa = PBXContainerItemProxy; 64 | containerPortal = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 65 | proxyType = 2; 66 | remoteGlobalIDString = 25DBCF371C30BF2B00D63A58; 67 | remoteInfo = "ReSwift-watchOS"; 68 | }; 69 | /* End PBXContainerItemProxy section */ 70 | 71 | /* Begin PBXFileReference section */ 72 | 254133631C50B37C003C8E93 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReSwiftRecorder/Info.plist; sourceTree = SOURCE_ROOT; }; 73 | 254133641C50B37C003C8E93 /* RecordingStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RecordingStore.swift; path = ReSwiftRecorder/RecordingStore.swift; sourceTree = SOURCE_ROOT; }; 74 | 254133651C50B37C003C8E93 /* ReSwiftRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ReSwiftRecorder.h; path = ReSwiftRecorder/ReSwiftRecorder.h; sourceTree = SOURCE_ROOT; }; 75 | 254133691C50B429003C8E93 /* ReSwift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReSwift.xcodeproj; path = Carthage/Checkouts/ReSwift/ReSwift.xcodeproj; sourceTree = SOURCE_ROOT; }; 76 | 254133811C50B47D003C8E93 /* StateHistoryCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StateHistoryCollectionViewCell.swift; path = ReSwiftRecorder/UI/StateHistoryCollectionViewCell.swift; sourceTree = SOURCE_ROOT; }; 77 | 254133821C50B47D003C8E93 /* StateHistorySliderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StateHistorySliderView.swift; path = ReSwiftRecorder/UI/StateHistorySliderView.swift; sourceTree = SOURCE_ROOT; }; 78 | 254133831C50B47D003C8E93 /* StateHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StateHistoryView.swift; path = ReSwiftRecorder/UI/StateHistoryView.swift; sourceTree = SOURCE_ROOT; }; 79 | 625E67151C2007B10027C288 /* ReSwiftRecorder.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReSwiftRecorder.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | D43E1C401F04201D00184A11 /* StandardAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StandardAction.swift; path = ReSwiftRecorder/StandardAction.swift; sourceTree = SOURCE_ROOT; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | 625E67111C2007B10027C288 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | 254133871C50B4D5003C8E93 /* ReSwift.framework in Frameworks */, 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | /* End PBXFrameworksBuildPhase section */ 93 | 94 | /* Begin PBXGroup section */ 95 | 2541336A1C50B429003C8E93 /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 254133741C50B429003C8E93 /* ReSwift.framework */, 99 | 254133761C50B429003C8E93 /* ReSwift-iOSTests.xctest */, 100 | 254133781C50B429003C8E93 /* ReSwift.framework */, 101 | 2541337A1C50B429003C8E93 /* ReSwift-macOSTests.xctest */, 102 | 2541337C1C50B429003C8E93 /* ReSwift.framework */, 103 | 2541337E1C50B429003C8E93 /* ReSwift-tvOSTests.xctest */, 104 | 254133801C50B429003C8E93 /* ReSwift.framework */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | 625E670B1C2007B10027C288 = { 110 | isa = PBXGroup; 111 | children = ( 112 | 625E67171C2007B10027C288 /* SwiftFlowRecorder */, 113 | 625E67161C2007B10027C288 /* Products */, 114 | ); 115 | sourceTree = ""; 116 | }; 117 | 625E67161C2007B10027C288 /* Products */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 625E67151C2007B10027C288 /* ReSwiftRecorder.framework */, 121 | ); 122 | name = Products; 123 | sourceTree = ""; 124 | }; 125 | 625E67171C2007B10027C288 /* SwiftFlowRecorder */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 254133631C50B37C003C8E93 /* Info.plist */, 129 | 254133641C50B37C003C8E93 /* RecordingStore.swift */, 130 | D43E1C401F04201D00184A11 /* StandardAction.swift */, 131 | 254133651C50B37C003C8E93 /* ReSwiftRecorder.h */, 132 | 625E67291C20096E0027C288 /* Frameworks */, 133 | 625E67211C2008EE0027C288 /* UI */, 134 | ); 135 | path = SwiftFlowRecorder; 136 | sourceTree = ""; 137 | }; 138 | 625E67211C2008EE0027C288 /* UI */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 254133811C50B47D003C8E93 /* StateHistoryCollectionViewCell.swift */, 142 | 254133821C50B47D003C8E93 /* StateHistorySliderView.swift */, 143 | 254133831C50B47D003C8E93 /* StateHistoryView.swift */, 144 | ); 145 | path = UI; 146 | sourceTree = ""; 147 | }; 148 | 625E67291C20096E0027C288 /* Frameworks */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 254133691C50B429003C8E93 /* ReSwift.xcodeproj */, 152 | ); 153 | name = Frameworks; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXHeadersBuildPhase section */ 159 | 625E67121C2007B10027C288 /* Headers */ = { 160 | isa = PBXHeadersBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 254133681C50B37C003C8E93 /* ReSwiftRecorder.h in Headers */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXHeadersBuildPhase section */ 168 | 169 | /* Begin PBXNativeTarget section */ 170 | 625E67141C2007B10027C288 /* ReSwiftRecorder */ = { 171 | isa = PBXNativeTarget; 172 | buildConfigurationList = 625E671D1C2007B10027C288 /* Build configuration list for PBXNativeTarget "ReSwiftRecorder" */; 173 | buildPhases = ( 174 | 625E67101C2007B10027C288 /* Sources */, 175 | 625E67111C2007B10027C288 /* Frameworks */, 176 | 625E67121C2007B10027C288 /* Headers */, 177 | 625E67131C2007B10027C288 /* Resources */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | ); 183 | name = ReSwiftRecorder; 184 | productName = SwiftFlowRecorder; 185 | productReference = 625E67151C2007B10027C288 /* ReSwiftRecorder.framework */; 186 | productType = "com.apple.product-type.framework"; 187 | }; 188 | /* End PBXNativeTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 625E670C1C2007B10027C288 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastUpgradeCheck = 0710; 195 | ORGANIZATIONNAME = "Benjamin Encz"; 196 | TargetAttributes = { 197 | 625E67141C2007B10027C288 = { 198 | CreatedOnToolsVersion = 7.1.1; 199 | LastSwiftMigration = 0800; 200 | }; 201 | }; 202 | }; 203 | buildConfigurationList = 625E670F1C2007B10027C288 /* Build configuration list for PBXProject "ReSwiftRecorder" */; 204 | compatibilityVersion = "Xcode 3.2"; 205 | developmentRegion = English; 206 | hasScannedForEncodings = 0; 207 | knownRegions = ( 208 | en, 209 | ); 210 | mainGroup = 625E670B1C2007B10027C288; 211 | productRefGroup = 625E67161C2007B10027C288 /* Products */; 212 | projectDirPath = ""; 213 | projectReferences = ( 214 | { 215 | ProductGroup = 2541336A1C50B429003C8E93 /* Products */; 216 | ProjectRef = 254133691C50B429003C8E93 /* ReSwift.xcodeproj */; 217 | }, 218 | ); 219 | projectRoot = ""; 220 | targets = ( 221 | 625E67141C2007B10027C288 /* ReSwiftRecorder */, 222 | ); 223 | }; 224 | /* End PBXProject section */ 225 | 226 | /* Begin PBXReferenceProxy section */ 227 | 254133741C50B429003C8E93 /* ReSwift.framework */ = { 228 | isa = PBXReferenceProxy; 229 | fileType = wrapper.framework; 230 | path = ReSwift.framework; 231 | remoteRef = 254133731C50B429003C8E93 /* PBXContainerItemProxy */; 232 | sourceTree = BUILT_PRODUCTS_DIR; 233 | }; 234 | 254133761C50B429003C8E93 /* ReSwift-iOSTests.xctest */ = { 235 | isa = PBXReferenceProxy; 236 | fileType = wrapper.cfbundle; 237 | path = "ReSwift-iOSTests.xctest"; 238 | remoteRef = 254133751C50B429003C8E93 /* PBXContainerItemProxy */; 239 | sourceTree = BUILT_PRODUCTS_DIR; 240 | }; 241 | 254133781C50B429003C8E93 /* ReSwift.framework */ = { 242 | isa = PBXReferenceProxy; 243 | fileType = wrapper.framework; 244 | path = ReSwift.framework; 245 | remoteRef = 254133771C50B429003C8E93 /* PBXContainerItemProxy */; 246 | sourceTree = BUILT_PRODUCTS_DIR; 247 | }; 248 | 2541337A1C50B429003C8E93 /* ReSwift-macOSTests.xctest */ = { 249 | isa = PBXReferenceProxy; 250 | fileType = wrapper.cfbundle; 251 | path = "ReSwift-macOSTests.xctest"; 252 | remoteRef = 254133791C50B429003C8E93 /* PBXContainerItemProxy */; 253 | sourceTree = BUILT_PRODUCTS_DIR; 254 | }; 255 | 2541337C1C50B429003C8E93 /* ReSwift.framework */ = { 256 | isa = PBXReferenceProxy; 257 | fileType = wrapper.framework; 258 | path = ReSwift.framework; 259 | remoteRef = 2541337B1C50B429003C8E93 /* PBXContainerItemProxy */; 260 | sourceTree = BUILT_PRODUCTS_DIR; 261 | }; 262 | 2541337E1C50B429003C8E93 /* ReSwift-tvOSTests.xctest */ = { 263 | isa = PBXReferenceProxy; 264 | fileType = wrapper.cfbundle; 265 | path = "ReSwift-tvOSTests.xctest"; 266 | remoteRef = 2541337D1C50B429003C8E93 /* PBXContainerItemProxy */; 267 | sourceTree = BUILT_PRODUCTS_DIR; 268 | }; 269 | 254133801C50B429003C8E93 /* ReSwift.framework */ = { 270 | isa = PBXReferenceProxy; 271 | fileType = wrapper.framework; 272 | path = ReSwift.framework; 273 | remoteRef = 2541337F1C50B429003C8E93 /* PBXContainerItemProxy */; 274 | sourceTree = BUILT_PRODUCTS_DIR; 275 | }; 276 | /* End PBXReferenceProxy section */ 277 | 278 | /* Begin PBXResourcesBuildPhase section */ 279 | 625E67131C2007B10027C288 /* Resources */ = { 280 | isa = PBXResourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | /* End PBXResourcesBuildPhase section */ 287 | 288 | /* Begin PBXSourcesBuildPhase section */ 289 | 625E67101C2007B10027C288 /* Sources */ = { 290 | isa = PBXSourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | 254133671C50B37C003C8E93 /* RecordingStore.swift in Sources */, 294 | 254133851C50B47D003C8E93 /* StateHistorySliderView.swift in Sources */, 295 | 254133841C50B47D003C8E93 /* StateHistoryCollectionViewCell.swift in Sources */, 296 | 254133861C50B47D003C8E93 /* StateHistoryView.swift in Sources */, 297 | D43E1C411F04201D00184A11 /* StandardAction.swift in Sources */, 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | /* End PBXSourcesBuildPhase section */ 302 | 303 | /* Begin XCBuildConfiguration section */ 304 | 625E671B1C2007B10027C288 /* Debug */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 309 | CLANG_CXX_LIBRARY = "libc++"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_CONSTANT_CONVERSION = YES; 314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INT_CONVERSION = YES; 318 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 319 | CLANG_WARN_UNREACHABLE_CODE = YES; 320 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 321 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 322 | COPY_PHASE_STRIP = NO; 323 | CURRENT_PROJECT_VERSION = 1; 324 | DEBUG_INFORMATION_FORMAT = dwarf; 325 | ENABLE_STRICT_OBJC_MSGSEND = YES; 326 | ENABLE_TESTABILITY = YES; 327 | GCC_C_LANGUAGE_STANDARD = gnu99; 328 | GCC_DYNAMIC_NO_PIC = NO; 329 | GCC_NO_COMMON_BLOCKS = YES; 330 | GCC_OPTIMIZATION_LEVEL = 0; 331 | GCC_PREPROCESSOR_DEFINITIONS = ( 332 | "DEBUG=1", 333 | "$(inherited)", 334 | ); 335 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 336 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 337 | GCC_WARN_UNDECLARED_SELECTOR = YES; 338 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 339 | GCC_WARN_UNUSED_FUNCTION = YES; 340 | GCC_WARN_UNUSED_VARIABLE = YES; 341 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 342 | MTL_ENABLE_DEBUG_INFO = YES; 343 | ONLY_ACTIVE_ARCH = YES; 344 | SDKROOT = iphoneos; 345 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 346 | TARGETED_DEVICE_FAMILY = "1,2"; 347 | VERSIONING_SYSTEM = "apple-generic"; 348 | VERSION_INFO_PREFIX = ""; 349 | }; 350 | name = Debug; 351 | }; 352 | 625E671C1C2007B10027C288 /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ALWAYS_SEARCH_USER_PATHS = NO; 356 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 357 | CLANG_CXX_LIBRARY = "libc++"; 358 | CLANG_ENABLE_MODULES = YES; 359 | CLANG_ENABLE_OBJC_ARC = YES; 360 | CLANG_WARN_BOOL_CONVERSION = YES; 361 | CLANG_WARN_CONSTANT_CONVERSION = YES; 362 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 363 | CLANG_WARN_EMPTY_BODY = YES; 364 | CLANG_WARN_ENUM_CONVERSION = YES; 365 | CLANG_WARN_INT_CONVERSION = YES; 366 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 367 | CLANG_WARN_UNREACHABLE_CODE = YES; 368 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 369 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 370 | COPY_PHASE_STRIP = NO; 371 | CURRENT_PROJECT_VERSION = 1; 372 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 373 | ENABLE_NS_ASSERTIONS = NO; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | GCC_C_LANGUAGE_STANDARD = gnu99; 376 | GCC_NO_COMMON_BLOCKS = YES; 377 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 378 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 379 | GCC_WARN_UNDECLARED_SELECTOR = YES; 380 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 381 | GCC_WARN_UNUSED_FUNCTION = YES; 382 | GCC_WARN_UNUSED_VARIABLE = YES; 383 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 384 | MTL_ENABLE_DEBUG_INFO = NO; 385 | SDKROOT = iphoneos; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | VALIDATE_PRODUCT = YES; 388 | VERSIONING_SYSTEM = "apple-generic"; 389 | VERSION_INFO_PREFIX = ""; 390 | }; 391 | name = Release; 392 | }; 393 | 625E671E1C2007B10027C288 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | CLANG_ENABLE_MODULES = YES; 397 | DEFINES_MODULE = YES; 398 | DYLIB_COMPATIBILITY_VERSION = 1; 399 | DYLIB_CURRENT_VERSION = 1; 400 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 401 | INFOPLIST_FILE = ReSwiftRecorder/Info.plist; 402 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 403 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 404 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 405 | PRODUCT_BUNDLE_IDENTIFIER = "de.benjamin-encz.ReSwiftRecorder"; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SKIP_INSTALL = YES; 408 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 409 | SWIFT_VERSION = 3.0; 410 | }; 411 | name = Debug; 412 | }; 413 | 625E671F1C2007B10027C288 /* Release */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | CLANG_ENABLE_MODULES = YES; 417 | DEFINES_MODULE = YES; 418 | DYLIB_COMPATIBILITY_VERSION = 1; 419 | DYLIB_CURRENT_VERSION = 1; 420 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 421 | INFOPLIST_FILE = ReSwiftRecorder/Info.plist; 422 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 423 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 424 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 425 | PRODUCT_BUNDLE_IDENTIFIER = "de.benjamin-encz.ReSwiftRecorder"; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | SKIP_INSTALL = YES; 428 | SWIFT_VERSION = 3.0; 429 | }; 430 | name = Release; 431 | }; 432 | /* End XCBuildConfiguration section */ 433 | 434 | /* Begin XCConfigurationList section */ 435 | 625E670F1C2007B10027C288 /* Build configuration list for PBXProject "ReSwiftRecorder" */ = { 436 | isa = XCConfigurationList; 437 | buildConfigurations = ( 438 | 625E671B1C2007B10027C288 /* Debug */, 439 | 625E671C1C2007B10027C288 /* Release */, 440 | ); 441 | defaultConfigurationIsVisible = 0; 442 | defaultConfigurationName = Release; 443 | }; 444 | 625E671D1C2007B10027C288 /* Build configuration list for PBXNativeTarget "ReSwiftRecorder" */ = { 445 | isa = XCConfigurationList; 446 | buildConfigurations = ( 447 | 625E671E1C2007B10027C288 /* Debug */, 448 | 625E671F1C2007B10027C288 /* Release */, 449 | ); 450 | defaultConfigurationIsVisible = 0; 451 | defaultConfigurationName = Release; 452 | }; 453 | /* End XCConfigurationList section */ 454 | }; 455 | rootObject = 625E670C1C2007B10027C288 /* Project object */; 456 | } 457 | -------------------------------------------------------------------------------- /ReSwiftRecorder.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ReSwiftRecorder.xcodeproj/xcshareddata/xcschemes/ReSwiftRecorder.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ReSwiftRecorder/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 | 0.4.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ReSwiftRecorder/ReSwiftRecorder.h: -------------------------------------------------------------------------------- 1 | // 2 | // ReSwiftRecorder.h 3 | // ReSwiftRecorder 4 | // 5 | // Created by Benjamin Encz on 12/15/15. 6 | // Copyright © 2015 Benjamin Encz. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ReSwiftRecorder. 12 | FOUNDATION_EXPORT double ReSwiftRecorderVersionNumber; 13 | 14 | //! Project version string for ReSwiftRecorder. 15 | FOUNDATION_EXPORT const unsigned char ReSwiftRecorderVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /ReSwiftRecorder/RecordingStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordingStore.swift 3 | // Meet 4 | // 5 | // Created by Benjamin Encz on 12/1/15. 6 | // Copyright © 2015 DigiTales. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReSwift 11 | 12 | public typealias TypeMap = [String: StandardActionConvertible.Type] 13 | 14 | open class RecordingMainStore: Store { 15 | 16 | typealias RecordedActions = [[String : AnyObject]] 17 | 18 | var recordedActions: RecordedActions = [] 19 | var initialState: State! 20 | var computedStates: [State] = [] 21 | var actionsToReplay: Int? 22 | let recordingPath: String? 23 | fileprivate var typeMap: TypeMap = [:] 24 | 25 | /// Position of the rewind/replay control from the bottom of the screen 26 | /// defaults to 100 27 | open var rewindControlYOffset: CGFloat = 100 28 | 29 | var loadedActions: [Action] = [] { 30 | didSet { 31 | stateHistoryView?.statesCount = loadedActions.count 32 | } 33 | } 34 | 35 | var stateHistoryView: StateHistorySliderView? 36 | 37 | open var window: UIWindow? { 38 | didSet { 39 | if let window = window { 40 | let windowSize = window.bounds.size 41 | stateHistoryView = StateHistorySliderView(frame: CGRect(x: 0, 42 | y: windowSize.height - rewindControlYOffset, 43 | width: windowSize.width, height: 100)) 44 | 45 | window.addSubview(stateHistoryView!) 46 | window.bringSubview(toFront: stateHistoryView!) 47 | 48 | stateHistoryView?.stateSelectionCallback = { [unowned self] selection in 49 | self.replayToState(self.loadedActions, state: selection) 50 | } 51 | 52 | stateHistoryView?.statesCount = loadedActions.count 53 | } 54 | } 55 | } 56 | 57 | public init( 58 | reducer: @escaping Reducer, 59 | state: State?, 60 | typeMaps: [TypeMap], 61 | recording: String? = nil, 62 | middleware: [Middleware] = [] 63 | ) { 64 | 65 | self.recordingPath = recording 66 | 67 | super.init(reducer: reducer, state: state, middleware: middleware) 68 | 69 | self.initialState = self.state 70 | self.computedStates.append(initialState) 71 | 72 | // merge all typemaps into one 73 | typeMaps.forEach { typeMap in 74 | for (key, value) in typeMap { 75 | self.typeMap[key] = value 76 | } 77 | } 78 | 79 | if let recording = recording { 80 | loadedActions = loadActions(recording) 81 | self.replayToState(loadedActions, state: loadedActions.count) 82 | } 83 | } 84 | 85 | public required init(reducer: Reducer, appState: StateType, middleware: [Middleware]) { 86 | fatalError("The current barebones implementation of ReSwiftRecorder does not support this initializer!") 87 | } 88 | 89 | public required convenience init(reducer: Reducer, appState: StateType) { 90 | fatalError("The current barebones implementation of ReSwiftRecorder does not support this initializer!") 91 | } 92 | 93 | required convenience public init(reducer: Reducer, state: State?) { 94 | fatalError("init(reducer:state:) has not been implemented") 95 | } 96 | 97 | required public init(reducer: @escaping Reducer, state: State?, middleware: [Middleware]) { 98 | fatalError("init(reducer:state:middleware:) has not been implemented") 99 | } 100 | 101 | func dispatchRecorded(_ action: Action) { 102 | super.dispatch(action) 103 | 104 | recordAction(action) 105 | } 106 | 107 | open override func dispatch(_ action: Action) { 108 | if let actionsToReplay = actionsToReplay , actionsToReplay > 0 { 109 | // ignore actions that are dispatched during replay 110 | return 111 | } 112 | 113 | super.dispatch(action) 114 | 115 | self.computedStates.append(self.state) 116 | 117 | if let standardAction = convertActionToStandardAction(action) { 118 | recordAction(standardAction) 119 | loadedActions.append(standardAction) 120 | } 121 | } 122 | 123 | func recordAction(_ action: Action) { 124 | let standardAction = convertActionToStandardAction(action) 125 | 126 | if let standardAction = standardAction { 127 | let recordedAction: [String : AnyObject] = [ 128 | "timestamp": Date.timeIntervalSinceReferenceDate as AnyObject, 129 | "action": standardAction.dictionaryRepresentation as AnyObject 130 | ] 131 | 132 | recordedActions.append(recordedAction) 133 | storeActions(recordedActions) 134 | } else { 135 | print("ReSwiftRecorder Warning: Could not log following action because it does not " + 136 | "conform to StandardActionConvertible: \(action)") 137 | } 138 | } 139 | 140 | fileprivate func convertActionToStandardAction(_ action: Action) -> StandardAction? { 141 | 142 | if let standardAction = action as? StandardAction { 143 | return standardAction 144 | } else if let standardActionConvertible = action as? StandardActionConvertible { 145 | return standardActionConvertible.toStandardAction() 146 | } 147 | 148 | return nil 149 | } 150 | 151 | fileprivate func decodeAction(_ jsonDictionary: [String : AnyObject]) -> Action { 152 | let standardAction = StandardAction(dictionary: jsonDictionary) 153 | 154 | if !standardAction!.isTypedAction { 155 | return standardAction! 156 | } else { 157 | let typedActionType = self.typeMap[standardAction!.type]! 158 | return typedActionType.init(standardAction!) 159 | } 160 | } 161 | 162 | lazy var recordingDirectory: URL? = { 163 | let timestamp = Int(Date.timeIntervalSinceReferenceDate) 164 | 165 | let documentDirectoryURL = try? FileManager.default 166 | .url(for: .documentDirectory, in: 167 | .userDomainMask, appropriateFor: nil, create: true) 168 | 169 | // let path = documentDirectoryURL? 170 | // .URLByAppendingPathComponent("recording_\(timestamp).json") 171 | let path = documentDirectoryURL? 172 | .appendingPathComponent(self.recordingPath ?? "recording.json") 173 | 174 | print("Recording to path: \(String(describing: path))") 175 | return path 176 | }() 177 | 178 | lazy var documentsDirectory: URL? = { 179 | let documentDirectoryURL = try? FileManager.default 180 | .url(for: .documentDirectory, in: 181 | .userDomainMask, appropriateFor: nil, create: true) 182 | 183 | return documentDirectoryURL 184 | }() 185 | 186 | fileprivate func storeActions(_ actions: RecordedActions) { 187 | let data = try! JSONSerialization.data(withJSONObject: actions, options: .prettyPrinted) 188 | 189 | if let path = recordingDirectory { 190 | try? data.write(to: path, options: [.atomic]) 191 | } 192 | } 193 | 194 | fileprivate func loadActions(_ recording: String) -> [Action] { 195 | guard let recordingPath = documentsDirectory?.appendingPathComponent(recording) else { 196 | return [] 197 | } 198 | guard let data = try? Data(contentsOf: recordingPath) else { return [] } 199 | 200 | let jsonArray = try! JSONSerialization.jsonObject(with: data, 201 | options: JSONSerialization.ReadingOptions(rawValue: 0)) as! Array 202 | 203 | let actionsArray: [Action] = jsonArray.map { 204 | return decodeAction($0["action"] as! [String : AnyObject]) 205 | } 206 | 207 | return actionsArray 208 | } 209 | 210 | fileprivate func replayToState(_ actions: [Action], state: Int) { 211 | if (state > computedStates.count - 1) { 212 | print("Rewind to \(state)...") 213 | self.state = initialState 214 | recordedActions = [] 215 | actionsToReplay = state 216 | 217 | for i in 0.. StandardAction { 98 | let payload = ["twitterUser": encode(self.twitterUser)] 99 | 100 | return StandardAction(type: SearchTwitterScene.SetSelectedTwitterUser.type, 101 | payload: payload, isTypedAction: true) 102 | } 103 | ``` 104 | 105 | */ 106 | func toStandardAction() -> StandardAction 107 | } 108 | -------------------------------------------------------------------------------- /ReSwiftRecorder/UI/StateHistoryCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateHistoryCollectionViewCell.swift 3 | // Meet 4 | // 5 | // Created by Benjamin Encz on 12/1/15. 6 | // Copyright © 2015 DigiTales. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StateHistoryCollectionViewCell: UICollectionViewCell { 12 | 13 | var label: UILabel! 14 | var text: String = "" { 15 | didSet { 16 | label.text = text 17 | label.sizeToFit() 18 | label.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) 19 | } 20 | } 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | 25 | label = UILabel() 26 | label.font = label.font.withSize(18) 27 | addSubview(label) 28 | 29 | backgroundColor = UIColor.red 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ReSwiftRecorder/UI/StateHistorySliderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateHistorySliderView.swift 3 | // Meet 4 | // 5 | // Created by Benjamin Encz on 12/3/15. 6 | // Copyright © 2015 DigiTales. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StateHistorySliderView: UIView { 12 | 13 | var slider: UISlider! 14 | 15 | var statesCount: Int = 0 { 16 | didSet { 17 | slider.maximumValue = Float(statesCount) 18 | slider.value = Float(statesCount) 19 | } 20 | } 21 | 22 | var stateSelectionCallback: ((Int) -> Void)? 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | 27 | slider = UISlider(frame: bounds) 28 | slider.minimumValue = 0 29 | slider.addTarget(self, action: #selector(StateHistorySliderView.sliderValueChanged), for: .valueChanged) 30 | 31 | addSubview(slider) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | @objc 39 | dynamic func sliderValueChanged() { 40 | stateSelectionCallback?(Int(slider.value)) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /ReSwiftRecorder/UI/StateHistoryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateHistoryView.swift 3 | // Meet 4 | // 5 | // Created by Benjamin Encz on 12/1/15. 6 | // Copyright © 2015 DigiTales. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StateHistoryView: UIView { 12 | 13 | var statesCount: Int = 0 { 14 | didSet { 15 | collectionView.reloadData() 16 | } 17 | } 18 | 19 | var cellSelectionCallback: ((Int) -> Void)? 20 | 21 | fileprivate let collectionView: UICollectionView 22 | fileprivate let collectionViewCellReuseIdentifier = "StateCell" 23 | 24 | override init(frame: CGRect) { 25 | let layout = UICollectionViewFlowLayout() 26 | layout.scrollDirection = .horizontal 27 | collectionView = UICollectionView(frame: frame, collectionViewLayout: layout) 28 | collectionView.backgroundColor = UIColor.green 29 | collectionView.register(StateHistoryCollectionViewCell.self, 30 | forCellWithReuseIdentifier: collectionViewCellReuseIdentifier) 31 | 32 | super.init(frame: frame) 33 | 34 | collectionView.dataSource = self 35 | collectionView.delegate = self 36 | 37 | addSubview(collectionView) 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | } 45 | 46 | extension StateHistoryView: UICollectionViewDataSource, UICollectionViewDelegate { 47 | 48 | func collectionView(_ collectionView: UICollectionView, 49 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 50 | 51 | let cell = collectionView.dequeueReusableCell( 52 | withReuseIdentifier: collectionViewCellReuseIdentifier, for: indexPath) as! StateHistoryCollectionViewCell 53 | 54 | cell.text = "\((indexPath as NSIndexPath).row + 1)" 55 | 56 | return cell 57 | } 58 | 59 | func collectionView(_ collectionView: UICollectionView, 60 | layout collectionViewLayout: UICollectionViewLayout, 61 | sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize { 62 | 63 | return CGSize(width: frame.size.height, height: frame.size.height) 64 | } 65 | 66 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 67 | return statesCount 68 | } 69 | 70 | func collectionView(_ collectionView: UICollectionView, 71 | didSelectItemAt indexPath: IndexPath) { 72 | 73 | cellSelectionCallback?((indexPath as NSIndexPath).row + 1) 74 | } 75 | 76 | } 77 | --------------------------------------------------------------------------------