├── .gitignore ├── .gitmodules ├── .hound.yml ├── CONTRIBUTING.md ├── Cartfile.private ├── Cartfile.resolved ├── Delta.podspec ├── Delta.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Delta.xcscheme ├── Delta.xcworkspace └── contents.xcworkspacedata ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── RELEASING.md ├── Scanfile ├── Sources ├── Action.swift ├── Delta.h ├── DynamicAction.swift ├── Info.plist ├── ObservableProperty.swift └── Store.swift ├── Tests ├── Helpers │ └── delay.swift ├── Resources │ └── Info.plist ├── Setup │ ├── Actions.swift │ ├── Models.swift │ └── Store.swift └── Tests │ ├── ObservablePropertySpec.swift │ └── StoreSpec.swift ├── bin ├── archive ├── setup ├── test ├── update └── update-docs ├── circle.yml └── documentation ├── getting-started.md └── reactive-extensions.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## OS X Finder 2 | .DS_Store 3 | 4 | ## Build generated 5 | .build/ 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | Carthage/Build 31 | docs 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/Nimble"] 2 | path = Carthage/Checkouts/Nimble 3 | url = https://github.com/Quick/Nimble.git 4 | [submodule "Carthage/Checkouts/Quick"] 5 | path = Carthage/Checkouts/Quick 6 | url = https://github.com/Quick/Quick.git 7 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | swift: 2 | enabled: true 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone. 4 | By participating in this project, 5 | you agree to abide by the thoughtbot [code of conduct]. 6 | 7 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 8 | 9 | We expect everyone to follow the code of conduct 10 | anywhere in thoughtbot's project codebases, 11 | issue trackers, chatrooms, and mailing lists. 12 | 13 | Fork the repo. 14 | 15 | Run the setup: 16 | 17 | ```sh 18 | bin/setup 19 | ``` 20 | 21 | Make sure the tests pass: 22 | 23 | ```sh 24 | bin/test 25 | ``` 26 | 27 | Make your change, with new passing tests. Follow the [style guide][style]. 28 | 29 | [style]: https://github.com/thoughtbot/guides/tree/master/style 30 | 31 | Push to your fork. Write a [good commit message][commit]. Submit a pull request. 32 | 33 | [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 34 | 35 | Others will give constructive feedback. 36 | This is a time for discussion and improvements, 37 | and making the necessary changes will be required before we can 38 | merge the contribution. 39 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" 2 | github "Quick/Nimble" 3 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v3.0.0" 2 | github "Quick/Quick" "v0.8.0" 3 | -------------------------------------------------------------------------------- /Delta.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "Delta" 3 | spec.version = "1.0.0" 4 | spec.summary = "Managing state is hard. Delta aims to make it simple." 5 | spec.homepage = "https://github.com/thoughtbot/Delta" 6 | spec.license = { :type => 'MIT', :file => 'LICENSE' } 7 | spec.authors = { 8 | "Jake Craige" => "james.craige@gmail.com", 9 | "Giles Van Gruisen" => "giles@thoughtbot.com", 10 | "thoughtbot" => nil, 11 | } 12 | spec.social_media_url = "http://twitter.com/thoughtbot" 13 | 14 | spec.source = { :git => "https://github.com/thoughtbot/Delta.git", :tag => "v#{spec.version}", :submodules => true } 15 | spec.source_files = "Sources/**/*.{h,swift}" 16 | spec.requires_arc = true 17 | spec.platform = :ios 18 | spec.ios.deployment_target = "8.0" 19 | end 20 | -------------------------------------------------------------------------------- /Delta.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 089B593A1C163FA600840D9F /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B59371C163FA600840D9F /* Models.swift */; }; 11 | 089B593B1C163FA600840D9F /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B59381C163FA600840D9F /* Actions.swift */; }; 12 | 089B593C1C163FA600840D9F /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B59391C163FA600840D9F /* Store.swift */; }; 13 | 089B593F1C163FC000840D9F /* ObservablePropertySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B593D1C163FC000840D9F /* ObservablePropertySpec.swift */; }; 14 | 089B59401C163FC000840D9F /* StoreSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B593E1C163FC000840D9F /* StoreSpec.swift */; }; 15 | 089B59451C1642C900840D9F /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 089B59441C1642C900840D9F /* Info.plist */; }; 16 | E799CF091BFE218600E751E5 /* ObservableProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = E799CF081BFE218600E751E5 /* ObservableProperty.swift */; }; 17 | E799CF1A1C05FD6100E751E5 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E799CF191C05FD6100E751E5 /* Nimble.framework */; }; 18 | E799CF1C1C05FD6E00E751E5 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E799CF1B1C05FD6E00E751E5 /* Quick.framework */; }; 19 | E7ACEA641BF62BD40045CA6A /* Delta.h in Headers */ = {isa = PBXBuildFile; fileRef = E7ACEA631BF62BD40045CA6A /* Delta.h */; settings = {ATTRIBUTES = (Public, ); }; }; 20 | E7ACEA6B1BF62BD50045CA6A /* Delta.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7ACEA601BF62BD40045CA6A /* Delta.framework */; }; 21 | E7ACEA7B1BF62E9A0045CA6A /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ACEA7A1BF62E9A0045CA6A /* Store.swift */; }; 22 | E7ACEA881BF6302D0045CA6A /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ACEA871BF6302D0045CA6A /* Action.swift */; }; 23 | E7ACEA8A1BF630350045CA6A /* DynamicAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ACEA891BF630350045CA6A /* DynamicAction.swift */; }; 24 | E7ACEA971BF63CC90045CA6A /* delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ACEA961BF63CC90045CA6A /* delay.swift */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXContainerItemProxy section */ 28 | E7ACEA6C1BF62BD50045CA6A /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = E7ACEA571BF62BD40045CA6A /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = E7ACEA5F1BF62BD40045CA6A; 33 | remoteInfo = Delta; 34 | }; 35 | /* End PBXContainerItemProxy section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 089B59371C163FA600840D9F /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; 39 | 089B59381C163FA600840D9F /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; 40 | 089B59391C163FA600840D9F /* Store.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 41 | 089B593D1C163FC000840D9F /* ObservablePropertySpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservablePropertySpec.swift; sourceTree = ""; }; 42 | 089B593E1C163FC000840D9F /* StoreSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreSpec.swift; sourceTree = ""; }; 43 | 089B59441C1642C900840D9F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | E799CF081BFE218600E751E5 /* ObservableProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableProperty.swift; sourceTree = ""; }; 45 | E799CF191C05FD6100E751E5 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = "Carthage/Checkouts/Nimble/build/Debug-iphoneos/Nimble.framework"; sourceTree = ""; }; 46 | E799CF1B1C05FD6E00E751E5 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Checkouts/Quick/build/Debug/Quick.framework; sourceTree = ""; }; 47 | E7ACEA601BF62BD40045CA6A /* Delta.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Delta.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | E7ACEA631BF62BD40045CA6A /* Delta.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Delta.h; sourceTree = ""; }; 49 | E7ACEA651BF62BD40045CA6A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | E7ACEA6A1BF62BD50045CA6A /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | E7ACEA7A1BF62E9A0045CA6A /* Store.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 52 | E7ACEA871BF6302D0045CA6A /* Action.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; }; 53 | E7ACEA891BF630350045CA6A /* DynamicAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicAction.swift; sourceTree = ""; }; 54 | E7ACEA961BF63CC90045CA6A /* delay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = delay.swift; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | E7ACEA5C1BF62BD40045CA6A /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | E7ACEA671BF62BD50045CA6A /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | E799CF1C1C05FD6E00E751E5 /* Quick.framework in Frameworks */, 70 | E799CF1A1C05FD6100E751E5 /* Nimble.framework in Frameworks */, 71 | E7ACEA6B1BF62BD50045CA6A /* Delta.framework in Frameworks */, 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 089B59341C163CFB00840D9F /* Helpers */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | E7ACEA961BF63CC90045CA6A /* delay.swift */, 82 | ); 83 | path = Helpers; 84 | sourceTree = ""; 85 | }; 86 | 089B59351C163CFF00840D9F /* Tests */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 089B593E1C163FC000840D9F /* StoreSpec.swift */, 90 | 089B593D1C163FC000840D9F /* ObservablePropertySpec.swift */, 91 | ); 92 | path = Tests; 93 | sourceTree = ""; 94 | }; 95 | 089B59361C163D0600840D9F /* Setup */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 089B59391C163FA600840D9F /* Store.swift */, 99 | 089B59371C163FA600840D9F /* Models.swift */, 100 | 089B59381C163FA600840D9F /* Actions.swift */, 101 | ); 102 | path = Setup; 103 | sourceTree = ""; 104 | }; 105 | 089B59431C16429200840D9F /* Resources */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 089B59441C1642C900840D9F /* Info.plist */, 109 | ); 110 | path = Resources; 111 | sourceTree = ""; 112 | }; 113 | E797CB961BF6741D00A653EB /* Frameworks */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | E799CF1B1C05FD6E00E751E5 /* Quick.framework */, 117 | E799CF191C05FD6100E751E5 /* Nimble.framework */, 118 | ); 119 | name = Frameworks; 120 | sourceTree = ""; 121 | }; 122 | E7ACEA561BF62BD40045CA6A = { 123 | isa = PBXGroup; 124 | children = ( 125 | E797CB961BF6741D00A653EB /* Frameworks */, 126 | E7ACEA621BF62BD40045CA6A /* Sources */, 127 | E7ACEA6E1BF62BD50045CA6A /* Tests */, 128 | E7ACEA611BF62BD40045CA6A /* Products */, 129 | ); 130 | indentWidth = 4; 131 | sourceTree = ""; 132 | tabWidth = 4; 133 | }; 134 | E7ACEA611BF62BD40045CA6A /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | E7ACEA601BF62BD40045CA6A /* Delta.framework */, 138 | E7ACEA6A1BF62BD50045CA6A /* UnitTests.xctest */, 139 | ); 140 | name = Products; 141 | sourceTree = ""; 142 | }; 143 | E7ACEA621BF62BD40045CA6A /* Sources */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | E7ACEA631BF62BD40045CA6A /* Delta.h */, 147 | E7ACEA651BF62BD40045CA6A /* Info.plist */, 148 | E7ACEA7A1BF62E9A0045CA6A /* Store.swift */, 149 | E7ACEA871BF6302D0045CA6A /* Action.swift */, 150 | E7ACEA891BF630350045CA6A /* DynamicAction.swift */, 151 | E799CF081BFE218600E751E5 /* ObservableProperty.swift */, 152 | ); 153 | path = Sources; 154 | sourceTree = ""; 155 | }; 156 | E7ACEA6E1BF62BD50045CA6A /* Tests */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 089B59341C163CFB00840D9F /* Helpers */, 160 | 089B59361C163D0600840D9F /* Setup */, 161 | 089B59351C163CFF00840D9F /* Tests */, 162 | 089B59431C16429200840D9F /* Resources */, 163 | ); 164 | path = Tests; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXGroup section */ 168 | 169 | /* Begin PBXHeadersBuildPhase section */ 170 | E7ACEA5D1BF62BD40045CA6A /* Headers */ = { 171 | isa = PBXHeadersBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | E7ACEA641BF62BD40045CA6A /* Delta.h in Headers */, 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | }; 178 | /* End PBXHeadersBuildPhase section */ 179 | 180 | /* Begin PBXNativeTarget section */ 181 | E7ACEA5F1BF62BD40045CA6A /* Delta */ = { 182 | isa = PBXNativeTarget; 183 | buildConfigurationList = E7ACEA741BF62BD50045CA6A /* Build configuration list for PBXNativeTarget "Delta" */; 184 | buildPhases = ( 185 | E7ACEA5B1BF62BD40045CA6A /* Sources */, 186 | E7ACEA5C1BF62BD40045CA6A /* Frameworks */, 187 | E7ACEA5D1BF62BD40045CA6A /* Headers */, 188 | E7ACEA5E1BF62BD40045CA6A /* Resources */, 189 | ); 190 | buildRules = ( 191 | ); 192 | dependencies = ( 193 | ); 194 | name = Delta; 195 | productName = Delta; 196 | productReference = E7ACEA601BF62BD40045CA6A /* Delta.framework */; 197 | productType = "com.apple.product-type.framework"; 198 | }; 199 | E7ACEA691BF62BD50045CA6A /* UnitTests */ = { 200 | isa = PBXNativeTarget; 201 | buildConfigurationList = E7ACEA771BF62BD50045CA6A /* Build configuration list for PBXNativeTarget "UnitTests" */; 202 | buildPhases = ( 203 | E7ACEA661BF62BD50045CA6A /* Sources */, 204 | E7ACEA671BF62BD50045CA6A /* Frameworks */, 205 | E7ACEA681BF62BD50045CA6A /* Resources */, 206 | ); 207 | buildRules = ( 208 | ); 209 | dependencies = ( 210 | E7ACEA6D1BF62BD50045CA6A /* PBXTargetDependency */, 211 | ); 212 | name = UnitTests; 213 | productName = DeltaTests; 214 | productReference = E7ACEA6A1BF62BD50045CA6A /* UnitTests.xctest */; 215 | productType = "com.apple.product-type.bundle.unit-test"; 216 | }; 217 | /* End PBXNativeTarget section */ 218 | 219 | /* Begin PBXProject section */ 220 | E7ACEA571BF62BD40045CA6A /* Project object */ = { 221 | isa = PBXProject; 222 | attributes = { 223 | LastSwiftUpdateCheck = 0710; 224 | LastUpgradeCheck = 0710; 225 | ORGANIZATIONNAME = thoughtbot; 226 | TargetAttributes = { 227 | E7ACEA5F1BF62BD40045CA6A = { 228 | CreatedOnToolsVersion = 7.1; 229 | }; 230 | E7ACEA691BF62BD50045CA6A = { 231 | CreatedOnToolsVersion = 7.1; 232 | }; 233 | }; 234 | }; 235 | buildConfigurationList = E7ACEA5A1BF62BD40045CA6A /* Build configuration list for PBXProject "Delta" */; 236 | compatibilityVersion = "Xcode 3.2"; 237 | developmentRegion = English; 238 | hasScannedForEncodings = 0; 239 | knownRegions = ( 240 | en, 241 | ); 242 | mainGroup = E7ACEA561BF62BD40045CA6A; 243 | productRefGroup = E7ACEA611BF62BD40045CA6A /* Products */; 244 | projectDirPath = ""; 245 | projectRoot = ""; 246 | targets = ( 247 | E7ACEA5F1BF62BD40045CA6A /* Delta */, 248 | E7ACEA691BF62BD50045CA6A /* UnitTests */, 249 | ); 250 | }; 251 | /* End PBXProject section */ 252 | 253 | /* Begin PBXResourcesBuildPhase section */ 254 | E7ACEA5E1BF62BD40045CA6A /* Resources */ = { 255 | isa = PBXResourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | E7ACEA681BF62BD50045CA6A /* Resources */ = { 262 | isa = PBXResourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | 089B59451C1642C900840D9F /* Info.plist in Resources */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXResourcesBuildPhase section */ 270 | 271 | /* Begin PBXSourcesBuildPhase section */ 272 | E7ACEA5B1BF62BD40045CA6A /* Sources */ = { 273 | isa = PBXSourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | E7ACEA8A1BF630350045CA6A /* DynamicAction.swift in Sources */, 277 | E7ACEA7B1BF62E9A0045CA6A /* Store.swift in Sources */, 278 | E7ACEA881BF6302D0045CA6A /* Action.swift in Sources */, 279 | E799CF091BFE218600E751E5 /* ObservableProperty.swift in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | E7ACEA661BF62BD50045CA6A /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | 089B593F1C163FC000840D9F /* ObservablePropertySpec.swift in Sources */, 288 | 089B593B1C163FA600840D9F /* Actions.swift in Sources */, 289 | E7ACEA971BF63CC90045CA6A /* delay.swift in Sources */, 290 | 089B593C1C163FA600840D9F /* Store.swift in Sources */, 291 | 089B59401C163FC000840D9F /* StoreSpec.swift in Sources */, 292 | 089B593A1C163FA600840D9F /* Models.swift in Sources */, 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | /* End PBXSourcesBuildPhase section */ 297 | 298 | /* Begin PBXTargetDependency section */ 299 | E7ACEA6D1BF62BD50045CA6A /* PBXTargetDependency */ = { 300 | isa = PBXTargetDependency; 301 | target = E7ACEA5F1BF62BD40045CA6A /* Delta */; 302 | targetProxy = E7ACEA6C1BF62BD50045CA6A /* PBXContainerItemProxy */; 303 | }; 304 | /* End PBXTargetDependency section */ 305 | 306 | /* Begin XCBuildConfiguration section */ 307 | E7ACEA721BF62BD50045CA6A /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 312 | CLANG_CXX_LIBRARY = "libc++"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 318 | CLANG_WARN_EMPTY_BODY = YES; 319 | CLANG_WARN_ENUM_CONVERSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 322 | CLANG_WARN_UNREACHABLE_CODE = YES; 323 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 324 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 325 | COPY_PHASE_STRIP = NO; 326 | CURRENT_PROJECT_VERSION = 1; 327 | DEBUG_INFORMATION_FORMAT = dwarf; 328 | ENABLE_STRICT_OBJC_MSGSEND = YES; 329 | ENABLE_TESTABILITY = YES; 330 | GCC_C_LANGUAGE_STANDARD = gnu99; 331 | GCC_DYNAMIC_NO_PIC = NO; 332 | GCC_NO_COMMON_BLOCKS = YES; 333 | GCC_OPTIMIZATION_LEVEL = 0; 334 | GCC_PREPROCESSOR_DEFINITIONS = ( 335 | "DEBUG=1", 336 | "$(inherited)", 337 | ); 338 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 339 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 340 | GCC_WARN_UNDECLARED_SELECTOR = YES; 341 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 342 | GCC_WARN_UNUSED_FUNCTION = YES; 343 | GCC_WARN_UNUSED_VARIABLE = YES; 344 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 345 | MTL_ENABLE_DEBUG_INFO = YES; 346 | ONLY_ACTIVE_ARCH = YES; 347 | SDKROOT = iphoneos; 348 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 349 | TARGETED_DEVICE_FAMILY = "1,2"; 350 | VERSIONING_SYSTEM = "apple-generic"; 351 | VERSION_INFO_PREFIX = ""; 352 | }; 353 | name = Debug; 354 | }; 355 | E7ACEA731BF62BD50045CA6A /* Release */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 360 | CLANG_CXX_LIBRARY = "libc++"; 361 | CLANG_ENABLE_MODULES = YES; 362 | CLANG_ENABLE_OBJC_ARC = YES; 363 | CLANG_WARN_BOOL_CONVERSION = YES; 364 | CLANG_WARN_CONSTANT_CONVERSION = YES; 365 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 366 | CLANG_WARN_EMPTY_BODY = YES; 367 | CLANG_WARN_ENUM_CONVERSION = YES; 368 | CLANG_WARN_INT_CONVERSION = YES; 369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 373 | COPY_PHASE_STRIP = NO; 374 | CURRENT_PROJECT_VERSION = 1; 375 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 376 | ENABLE_NS_ASSERTIONS = NO; 377 | ENABLE_STRICT_OBJC_MSGSEND = YES; 378 | GCC_C_LANGUAGE_STANDARD = gnu99; 379 | GCC_NO_COMMON_BLOCKS = YES; 380 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 381 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 382 | GCC_WARN_UNDECLARED_SELECTOR = YES; 383 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 384 | GCC_WARN_UNUSED_FUNCTION = YES; 385 | GCC_WARN_UNUSED_VARIABLE = YES; 386 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 387 | MTL_ENABLE_DEBUG_INFO = NO; 388 | SDKROOT = iphoneos; 389 | TARGETED_DEVICE_FAMILY = "1,2"; 390 | VALIDATE_PRODUCT = YES; 391 | VERSIONING_SYSTEM = "apple-generic"; 392 | VERSION_INFO_PREFIX = ""; 393 | }; 394 | name = Release; 395 | }; 396 | E7ACEA751BF62BD50045CA6A /* Debug */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | CLANG_ENABLE_MODULES = YES; 400 | CODE_SIGN_IDENTITY = ""; 401 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 402 | DEFINES_MODULE = YES; 403 | DYLIB_COMPATIBILITY_VERSION = 1; 404 | DYLIB_CURRENT_VERSION = 1; 405 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 406 | INFOPLIST_FILE = Sources/Info.plist; 407 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 408 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 409 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 410 | PRODUCT_BUNDLE_IDENTIFIER = com.thoughtbot.Delta; 411 | PRODUCT_NAME = "$(TARGET_NAME)"; 412 | SKIP_INSTALL = YES; 413 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 414 | }; 415 | name = Debug; 416 | }; 417 | E7ACEA761BF62BD50045CA6A /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | CLANG_ENABLE_MODULES = YES; 421 | CODE_SIGN_IDENTITY = ""; 422 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 423 | DEFINES_MODULE = YES; 424 | DYLIB_COMPATIBILITY_VERSION = 1; 425 | DYLIB_CURRENT_VERSION = 1; 426 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 427 | INFOPLIST_FILE = Sources/Info.plist; 428 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 429 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 430 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 431 | PRODUCT_BUNDLE_IDENTIFIER = com.thoughtbot.Delta; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | SKIP_INSTALL = YES; 434 | }; 435 | name = Release; 436 | }; 437 | E7ACEA781BF62BD50045CA6A /* Debug */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | INFOPLIST_FILE = Tests/Resources/Info.plist; 441 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 442 | PRODUCT_BUNDLE_IDENTIFIER = com.thoughtbot.DeltaTests; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | }; 445 | name = Debug; 446 | }; 447 | E7ACEA791BF62BD50045CA6A /* Release */ = { 448 | isa = XCBuildConfiguration; 449 | buildSettings = { 450 | INFOPLIST_FILE = Tests/Resources/Info.plist; 451 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 452 | PRODUCT_BUNDLE_IDENTIFIER = com.thoughtbot.DeltaTests; 453 | PRODUCT_NAME = "$(TARGET_NAME)"; 454 | }; 455 | name = Release; 456 | }; 457 | /* End XCBuildConfiguration section */ 458 | 459 | /* Begin XCConfigurationList section */ 460 | E7ACEA5A1BF62BD40045CA6A /* Build configuration list for PBXProject "Delta" */ = { 461 | isa = XCConfigurationList; 462 | buildConfigurations = ( 463 | E7ACEA721BF62BD50045CA6A /* Debug */, 464 | E7ACEA731BF62BD50045CA6A /* Release */, 465 | ); 466 | defaultConfigurationIsVisible = 0; 467 | defaultConfigurationName = Release; 468 | }; 469 | E7ACEA741BF62BD50045CA6A /* Build configuration list for PBXNativeTarget "Delta" */ = { 470 | isa = XCConfigurationList; 471 | buildConfigurations = ( 472 | E7ACEA751BF62BD50045CA6A /* Debug */, 473 | E7ACEA761BF62BD50045CA6A /* Release */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | E7ACEA771BF62BD50045CA6A /* Build configuration list for PBXNativeTarget "UnitTests" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | E7ACEA781BF62BD50045CA6A /* Debug */, 482 | E7ACEA791BF62BD50045CA6A /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | /* End XCConfigurationList section */ 488 | }; 489 | rootObject = E7ACEA571BF62BD40045CA6A /* Project object */; 490 | } 491 | -------------------------------------------------------------------------------- /Delta.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Delta.xcodeproj/xcshareddata/xcschemes/Delta.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Delta.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "scan" 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | babosa (1.0.2) 5 | colored (1.2) 6 | commander (4.3.5) 7 | highline (~> 1.7.2) 8 | credentials_manager (0.11.0) 9 | colored 10 | highline (>= 1.7.1) 11 | security 12 | excon (0.45.4) 13 | faraday (0.9.1) 14 | multipart-post (>= 1.2, < 3) 15 | fastlane_core (0.26.6) 16 | babosa 17 | colored 18 | commander (>= 4.3.5) 19 | credentials_manager (>= 0.11.0, < 1.0.0) 20 | excon (~> 0.45.0) 21 | highline (>= 1.7.2) 22 | json 23 | multi_json 24 | plist (~> 3.1) 25 | rubyzip (~> 1.1.6) 26 | sentry-raven (~> 0.15) 27 | terminal-table (~> 1.4.5) 28 | highline (1.7.3) 29 | json (1.8.3) 30 | multi_json (1.11.2) 31 | multipart-post (2.0.0) 32 | plist (3.1.0) 33 | rouge (1.10.1) 34 | rubyzip (1.1.7) 35 | scan (0.3.2) 36 | fastlane_core (>= 0.26.6, < 1.0.0) 37 | slack-notifier (~> 1.3) 38 | terminal-table 39 | xcpretty (>= 0.2.1) 40 | xcpretty-travis-formatter (>= 0.0.3) 41 | security (0.1.3) 42 | sentry-raven (0.15.2) 43 | faraday (>= 0.7.6) 44 | slack-notifier (1.4.0) 45 | terminal-table (1.4.5) 46 | xcpretty (0.2.1) 47 | rouge (~> 1.8) 48 | xcpretty-travis-formatter (0.0.4) 49 | xcpretty (~> 0.2, >= 0.0.7) 50 | 51 | PLATFORMS 52 | ruby 53 | 54 | DEPENDENCIES 55 | scan 56 | 57 | BUNDLED WITH 58 | 1.10.6 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jake Craige and thoughtbot, inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "Delta" 5 | ) 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | 5 | Managing state is hard. Delta aims to make it simple. 6 | 7 | Delta takes an app that has custom state management spread throughout all the VCs 8 | and simplifies it by providing a simple interface to change state and subscribe 9 | to its changes. 10 | 11 | It can be used standalone or with your choice of reactive framework 12 | plugged in. We recommend using a reactive framework to get the most value. 13 | 14 | ## Source Compatibility ## 15 | 16 | The source on `master` assumes Swift 2.1 17 | 18 | ## Framework Installation ## 19 | 20 | ### [Carthage] ### 21 | 22 | [Carthage]: https://github.com/Carthage/Carthage 23 | 24 | ``` 25 | github "thoughtbot/Delta" 26 | ``` 27 | 28 | Then run `carthage update`. 29 | 30 | Follow the current instructions in [Carthage's README][carthage-installation] 31 | for up to date installation instructions. 32 | 33 | [carthage-installation]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application 34 | 35 | ### [CocoaPods] 36 | 37 | [CocoaPods]: http://cocoapods.org 38 | 39 | Add the following to your [Podfile](http://guides.cocoapods.org/using/the-podfile.html): 40 | 41 | ```ruby 42 | pod 'Delta', :git => "https://github.com/thoughtbot/Delta.git" 43 | ``` 44 | 45 | You also need to make sure you're opting into using frameworks: 46 | 47 | ```ruby 48 | use_frameworks! 49 | ``` 50 | 51 | Then run `pod install` with CocoaPods 0.36 or newer. 52 | 53 | ### Git Submodules 54 | 55 | Add this repo as a submodule, and add the project file to your workspace. You 56 | can then link against `Delta.framework` in your application target. 57 | 58 | ## Usage 59 | 60 | - [Getting Started] 61 | - [Using Reactive Extensions][Using RX] 62 | - [Example Application using Delta and ReactiveCocoa][Example Application] 63 | - [API Documentation] 64 | 65 | [Getting Started]: ./documentation/getting-started.md 66 | [Using RX]: ./documentation/reactive-extensions.md 67 | [API Documentation]: https://thoughtbot.github.io/Delta 68 | [Example Application]: https://github.com/thoughtbot/DeltaTodoExample 69 | 70 | ## Contributing 71 | 72 | See the [CONTRIBUTING] document. 73 | Thank you, [contributors]! 74 | 75 | [CONTRIBUTING]: CONTRIBUTING.md 76 | [contributors]: https://github.com/thoughtbot/Delta/graphs/contributors 77 | 78 | ## License 79 | 80 | Delta is Copyright (c) 2015 thoughtbot, inc. 81 | It is free software, and may be redistributed 82 | under the terms specified in the [LICENSE] file. 83 | 84 | [LICENSE]: /LICENSE 85 | 86 | ## About 87 | 88 | Delta is maintained by Jake Craige. 89 | 90 | ![thoughtbot](https://thoughtbot.com/logo.png) 91 | 92 | Delta is maintained and funded by thoughtbot, inc. 93 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 94 | 95 | We love open source software! 96 | See [our other projects][community] 97 | or [hire us][hire] to help build your product. 98 | 99 | [community]: https://thoughtbot.com/community?utm_source=github 100 | [hire]: https://thoughtbot.com/hire-us?utm_source=github 101 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Update version file accordingly. 4 | 1. Commit changes. 5 | 1. Tag the release: `git tag vVERSION` 6 | 1. Push changes: `git push --tags` 7 | 1. Build the binary to attach to the release: 8 | ```bash 9 | bin/archive 10 | ``` 11 | 12 | 1. Add a new GitHub release and populate the content. Sample 13 | URL: https://github.com/thoughtbot/Delta/releases/new?tag=vVERSION 14 | 1. Announce the new release, 15 | making sure to say "thank you" to the contributors 16 | who helped shape this version! 17 | -------------------------------------------------------------------------------- /Scanfile: -------------------------------------------------------------------------------- 1 | scheme "Delta" 2 | 3 | clean true 4 | 5 | output_types "" 6 | -------------------------------------------------------------------------------- /Sources/Action.swift: -------------------------------------------------------------------------------- 1 | /** 2 | This protocol is used when you want to make modifications to the store's state. 3 | All changes to the store go through this type. 4 | 5 | Sample Action: 6 | 7 | ```swift 8 | struct UpdateIdAction: ActionType { 9 | let id: Int 10 | 11 | func reduce(state: AppState) -> AppState { 12 | state.id.value = id 13 | return state 14 | } 15 | } 16 | 17 | store.dispatch(UpdateIdAction(id: 1)) 18 | ``` 19 | */ 20 | public protocol ActionType { 21 | /** 22 | The type of the app's state. 23 | 24 | - note: This is inferred from the `reduce` method implementation. 25 | */ 26 | typealias StateValueType 27 | 28 | /** 29 | This method is called when this action is dispatched. Its purpose is to 30 | make modifications to the state and return a new version of it. 31 | 32 | - note: This is the only place that changes to the state are permitted. 33 | - parameter state: The current state of the store. 34 | - returns: The new state. 35 | */ 36 | func reduce(state: StateValueType) -> StateValueType 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Delta.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for Delta. 4 | FOUNDATION_EXPORT double DeltaVersionNumber; 5 | 6 | //! Project version string for Delta. 7 | FOUNDATION_EXPORT const unsigned char DeltaVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | 12 | -------------------------------------------------------------------------------- /Sources/DynamicAction.swift: -------------------------------------------------------------------------------- 1 | /** 2 | This protocol is used when you want to do some async behavior that updates the 3 | store. It is very minimal in that it's not allowed to modify the store 4 | directly. The async behavior is done within the `call` method and to make 5 | changes it should dispatch a synchronous action. 6 | 7 | Sample Action: 8 | 9 | ```swift 10 | struct FetchUsers: DynamicActionType { 11 | func call() { 12 | someApi.fetchUsers { users in 13 | store.dispatch(SetUsersAction(users: users)) 14 | } 15 | } 16 | } 17 | 18 | store.dispatch(UpdateIdAction(id: 1)) 19 | ``` 20 | */ 21 | public protocol DynamicActionType { 22 | /** 23 | The return type from the `call` method. 24 | 25 | - note: This is inferred from the `call` method implementation. 26 | */ 27 | typealias ResponseType 28 | 29 | /** 30 | This method is where you perform some async behavior that when completed, 31 | should dispatch a synchronous action on the store. 32 | 33 | You can optionally return an object that wraps async behavior. This might 34 | be a `Promise` from PromiseKit or `SignalProducer` from ReactiveCocoa. 35 | */ 36 | func call() -> ResponseType 37 | } 38 | -------------------------------------------------------------------------------- /Sources/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.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/ObservableProperty.swift: -------------------------------------------------------------------------------- 1 | /** 2 | This is the protocol that the state the store holds must implement. 3 | 4 | To use a custom state type, this protocol must be implemented on that object. 5 | 6 | This is useful if you want to plug in a reactive programming library and use 7 | that for state instead of the built-in ObservableProperty type. 8 | */ 9 | public protocol ObservablePropertyType { 10 | /** 11 | The type of the value that `Self` will hold. 12 | 13 | - note: This is inferred from the `value` property implementation. 14 | */ 15 | typealias ValueType 16 | 17 | /** 18 | The value to be observed and mutated. 19 | */ 20 | var value: ValueType { get set } 21 | } 22 | 23 | /** 24 | A basic implementation of a property whose `value` can be observed 25 | using callbacks. 26 | 27 | 28 | ```swift 29 | Example: 30 | 31 | var property = ObservableProperty(1) 32 | 33 | property.subscribe { newValue in 34 | print("newValue: \(newValue)") 35 | } 36 | 37 | property.value = 2 38 | 39 | // Executing the above code prints: 40 | // "newValue: 2" 41 | ``` 42 | */ 43 | public class ObservableProperty { 44 | /** 45 | The type of the callback to be called when the `value` changes. 46 | */ 47 | public typealias CallbackType = (ValueType -> ()) 48 | 49 | private var subscriptions = [CallbackType]() 50 | 51 | /** 52 | The `value` stored in this instance. Setting it to a new value will notify 53 | any subscriptions registered through the `subscribe` method. 54 | */ 55 | public var value: ValueType { 56 | didSet { 57 | notifySubscriptions() 58 | } 59 | } 60 | 61 | /** 62 | - parameter value: The initial value to store. 63 | */ 64 | public init(_ value: ValueType) { 65 | self.value = value 66 | } 67 | 68 | /** 69 | Register a subscriber that will be called with the new value when the `value` changes. 70 | 71 | - parameter callback: The function to call when the value changes. 72 | */ 73 | public func subscribe(callback: CallbackType) { 74 | subscriptions.append(callback) 75 | } 76 | 77 | private func notifySubscriptions() { 78 | for subscription in subscriptions { 79 | subscription(value) 80 | } 81 | } 82 | } 83 | 84 | extension ObservableProperty: ObservablePropertyType { } 85 | -------------------------------------------------------------------------------- /Sources/Store.swift: -------------------------------------------------------------------------------- 1 | /** 2 | A protocol that defines storage of an observable state and dispatch methods to 3 | modify it. Typically you will implement this on a struct and create a shared 4 | instance that you reference throughout your application to get the state or 5 | dispatch actions to change it. 6 | 7 | Sample store: 8 | 9 | ```swift 10 | struct AppState { 11 | var id = ObservableProperty(0) 12 | } 13 | 14 | struct Store: StoreType { 15 | var state: ObservableProperty 16 | } 17 | 18 | let initialState = AppState() 19 | var store = Store(state: ObservableProperty(initialState)) 20 | ``` 21 | */ 22 | public protocol StoreType { 23 | /** 24 | An observable state of the store. This is accessed directly to subscribe to 25 | changes. 26 | */ 27 | typealias ObservableState: ObservablePropertyType 28 | 29 | /** 30 | The type of the root state of the application. 31 | 32 | - note: This is inferred from the `reduce` method implementation. 33 | */ 34 | var state: ObservableState { get set } 35 | 36 | /** 37 | Dispatch an action that will mutate the state of the store. 38 | */ 39 | mutating func dispatch(action: Action) 40 | 41 | /** 42 | Dispatch an async action that when called should trigger another dispatch 43 | with a synchronous action. 44 | */ 45 | func dispatch(action: DynamicAction) -> DynamicAction.ResponseType 46 | } 47 | 48 | public extension StoreType { 49 | /** 50 | Dispatches an action by settings the state's value to the result of 51 | calling it's `reduce` method. 52 | */ 53 | public mutating func dispatch(action: Action) { 54 | state.value = action.reduce(state.value) 55 | } 56 | 57 | /** 58 | Dispatches an async action by calling it's `call` method. 59 | */ 60 | public func dispatch(action: DynamicAction) -> DynamicAction.ResponseType { 61 | return action.call() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Helpers/delay.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // http://stackoverflow.com/a/24318861/1720355 4 | func delay(seconds: Double, closure: () -> ()) { 5 | dispatch_after( 6 | dispatch_time( 7 | DISPATCH_TIME_NOW, 8 | Int64(seconds * Double(NSEC_PER_SEC)) 9 | ), 10 | dispatch_get_main_queue(), closure) 11 | } 12 | -------------------------------------------------------------------------------- /Tests/Resources/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/Setup/Actions.swift: -------------------------------------------------------------------------------- 1 | import Delta 2 | 3 | struct SetCurrentUserAction: ActionType { 4 | let user: User 5 | 6 | func reduce(state: AppState) -> AppState { 7 | state.currentUser.value = user 8 | return state 9 | } 10 | } 11 | 12 | struct SetUsersAction: ActionType { 13 | let users: [User] 14 | 15 | func reduce(state: AppState) -> AppState { 16 | state.users.value = users 17 | return state 18 | } 19 | } 20 | 21 | struct FetchUsersAction: DynamicActionType { 22 | typealias ResponseType = Void 23 | 24 | let usersToReturn: [User] 25 | 26 | func call() { 27 | delay(0.1) { 28 | store.dispatch(SetUsersAction(users: self.usersToReturn)) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/Setup/Models.swift: -------------------------------------------------------------------------------- 1 | struct User { 2 | let name: String 3 | } 4 | 5 | extension User: Equatable { } 6 | 7 | func == (lhs: User, rhs: User) -> Bool { 8 | return lhs.name == rhs.name 9 | } 10 | -------------------------------------------------------------------------------- /Tests/Setup/Store.swift: -------------------------------------------------------------------------------- 1 | import Delta 2 | 3 | struct AppState { 4 | let currentUser = ObservableProperty(.None) 5 | let users = ObservableProperty<[User]>([]) 6 | } 7 | 8 | struct Store: StoreType { 9 | var state: ObservableProperty 10 | } 11 | 12 | // MARK: Getters 13 | extension Store { 14 | var currentUser: User? { 15 | return state.value.currentUser.value 16 | } 17 | 18 | var users: [User] { 19 | return state.value.users.value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Tests/ObservablePropertySpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import Delta 4 | 5 | class ObservablePropertySpec: QuickSpec { 6 | override func spec() { 7 | describe("ObservableProperty") { 8 | it("is initialized with a value") { 9 | let property = ObservableProperty(1) 10 | 11 | expect(property.value).to(equal(1)) 12 | } 13 | 14 | describe("subscriptions") { 15 | it("calls subscriptions when value changes") { 16 | let property = ObservableProperty(1) 17 | 18 | var called = 0 19 | property.subscribe { _ in called++ } 20 | property.subscribe { _ in called++ } 21 | property.value = 5 22 | 23 | expect(called).to(equal(2)) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Tests/StoreSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import Delta 4 | 5 | var store: Store! 6 | 7 | class StoreSpec: QuickSpec { 8 | override func spec() { 9 | describe("Store") { 10 | describe(".dispatch") { 11 | beforeEach() { 12 | let initialState = ObservableProperty(AppState()) 13 | store = Store(state: initialState) 14 | } 15 | 16 | it("triggers action") { 17 | let user = User(name: "Jane Doe") 18 | 19 | store.dispatch(SetCurrentUserAction(user: user)) 20 | 21 | expect(store.currentUser).to(equal(user)) 22 | } 23 | 24 | it("triggers async action") { 25 | let usersToReturn = [User(name: "Jane Doe"), User(name: "John Doe")] 26 | 27 | let action = FetchUsersAction(usersToReturn: usersToReturn) 28 | store.dispatch(action) 29 | 30 | expect(store.users).toEventually(equal(usersToReturn)) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bin/archive: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | carthage build --no-skip-current --platform iOS 6 | carthage archive Delta 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | bundle install 4 | 5 | git submodule update --init --recursive 6 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | scan 4 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | carthage update --use-submodules --no-use-binaries --platform iOS 4 | -------------------------------------------------------------------------------- /bin/update-docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | echo "Generating docs with jazzy..." 6 | jazzy \ 7 | --swift-version=2.1.1 \ 8 | --module Delta \ 9 | --github_url https://github.com/thoughtbot/Delta \ 10 | --author thoughtbot \ 11 | --author_url https://thoughtbot.com 12 | 13 | current_branch_name=$(git symbolic-ref --short HEAD) 14 | current_sha=$(git rev-parse HEAD) 15 | commit_message="Generated documentation for ${current_sha}" 16 | 17 | echo "Checking out gh-pages..." 18 | git checkout gh-pages 19 | echo "Copying docs from 'docs/' directory..." 20 | cp -Rf docs/* . 21 | echo "Commit..." 22 | git add . 23 | git commit -m "${commit_message}" 24 | echo "Pushing to GitHub..." 25 | git push 26 | echo "Done. Checking out ${current_branch_name}..." 27 | git checkout "${current_branch_name}" 28 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | xcode: 3 | version: "7.1" 4 | 5 | dependencies: 6 | override: 7 | - bin/setup 8 | 9 | test: 10 | override: 11 | - bin/test 12 | -------------------------------------------------------------------------------- /documentation/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | The main idea is that we have a singular place that holds the "app state", "the 4 | store", and we can dispatch "actions" that update that state. Using the 5 | [observer pattern], we can easily subscribe to changes in the state to update 6 | the UI, run background tasks, etc. 7 | 8 | Let's begin learning about the concepts by walking through how we'll set up an 9 | app to use it. 10 | 11 | [observer pattern]: https://en.wikipedia.org/wiki/Observer_pattern 12 | 13 | ### The State 14 | 15 | First we need to define what your app state is. The properties will need to 16 | mutable so that we can mutate this when we dispatch actions. We do this by 17 | defining a `struct` that stores a user id. 18 | 19 | ```swift 20 | struct AppState { 21 | var userId: Int? = .None 22 | } 23 | ``` 24 | 25 | Earlier I mentioned "subscribing" to changes. With this implementation we'll 26 | only be able to subscribe to when _anything_ in the store changes. While in this 27 | example that's fine, in the real world we want to be able to subscribe to 28 | changes in specific values. We can add that functionality by using the 29 | provided `ObservableProperty` type. 30 | 31 | ```swift 32 | import Delta 33 | 34 | struct AppState { 35 | let userId = ObservableProperty(.None) 36 | } 37 | ``` 38 | 39 | One small change is that we're now able to mark the property as `let` instead of 40 | `var`. This is because `ObservableProperty` stores it's value internally which 41 | allows us to mutate it while having the same instance stored in the state. 42 | 43 | We'll soon see how to use this new power. 44 | 45 | ### The Store 46 | 47 | Next up we need to define our "store". This is the core of this library 48 | and it's what you'll be using to access the state and dispatch actions to mutate 49 | it. 50 | 51 | We'll also define it as a `struct` that implements the `StoreType` protocol. 52 | This protocol provides all the functionality of the store you need. 53 | 54 | ```swift 55 | import Delta 56 | 57 | struct Store: StoreType { 58 | var state: ObservableProperty 59 | } 60 | ``` 61 | 62 | Notice how we also wrap `state` in the `ObservableProperty` class. As mentioned 63 | earlier, this class is what provides the ability to subscribe to changes. 64 | 65 | Note: The state can be any object that conforms to the `ObservablePropertyType` 66 | protocol. Implementing that protocol on a different object is how you can plug 67 | in a custom reactive framework implementation. For more information on this, see [the docs on 68 | reactive extensions](./reactive-extensions.md) 69 | 70 | Now we need to create an instance of our store and give it it's initial 71 | state. 72 | 73 | ```swift 74 | let initialState = AppState() 75 | var store = Store(state: ObservableProperty(initialState)) 76 | ``` 77 | 78 | We _must_ define the store as a `var` because once we start dispatching actions, 79 | they will be mutating it and the compiler will throw a cryptic error if it's not 80 | a `var`. At this point you'll probably see a warning that it's not mutated but 81 | we can ignore that for now. Once we start dispatching actions, that will go 82 | away. 83 | 84 | While we still don't have a way to modify it, let's take a second to see how 85 | subscriptions work: 86 | 87 | ```swift 88 | // Subscribe to any change in the app's state 89 | store.state.subscribe { (newState: AppState) in 90 | print("new state: \(newState)") 91 | } 92 | 93 | // Subscribe to a change in the userId 94 | store.state.value.userId.subscribe { (newId: Int?) in 95 | print("new id: \(newId)") 96 | } 97 | 98 | // Update the state 99 | store.dispatch(SetUserIdAction(id: 5)) 100 | ``` 101 | 102 | Assuming `SetUserIdAction` is defined and does what it says it does, running 103 | this code we should see an output of: 104 | 105 | ``` 106 | new state: AppState(userId: 5) 107 | new id: 5 108 | ``` 109 | 110 | Let's learn how to implement that action. 111 | 112 | ### Actions 113 | 114 | Actions are how we change the application state. Because all changes go through 115 | an action, it's very easy to see where state is changing within your 116 | app by searching for where all places that actions are dispatched. 117 | 118 | There are two types of actions `ActionType` and `DynamicActionType`. 119 | 120 | An action's job is to make synchronous modifications to the store. Its 121 | implementation and `reduce` method should be [pure][pure function], meaning that 122 | given the same inputs, it should produce the same output. 123 | 124 | [pure function]: https://en.wikipedia.org/wiki/Pure_function 125 | 126 | A dynamic action's job is to do some work and then dispatch regular actions to 127 | make the store modifications. A dynamic action is typically used to do 128 | asynchronous work or group multiple actions under one name. 129 | 130 | Let's define one of each. 131 | 132 | First, the action: 133 | 134 | ```swift 135 | struct SetUserIdAction: ActionType { 136 | let id: Int 137 | 138 | func reduce(state: AppState) -> AppState { 139 | state.userId.value = id 140 | 141 | return state 142 | } 143 | } 144 | ``` 145 | 146 | The protocol requires us to implement the `reduce` method we see above. Its job 147 | is to directly modify the state and return a new one. The `.value` is there 148 | because we have to reach into the `ObservableProperty` to get the value. 149 | 150 | If we wanted to see this in action, we just have to dispatch it through the 151 | store. 152 | 153 | ```swift 154 | store.dispatch(SetUserIdAction(id: 5)) 155 | ``` 156 | 157 | Now let's pretend that we needed to get the user id from the server and set it. 158 | 159 | To do this we implement an async behavior and then dispatch the synchronous one we 160 | wrote when it's complete. 161 | 162 | ```swift 163 | struct GetUserIdAction: DynamicActionType { 164 | func call() { 165 | getUserIdFromSomeApi() { id in 166 | store.dispatch(SetUserIdAction(id: id)) 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | It is dispatched the same way as before: 173 | 174 | ```swift 175 | store.dispatch(GetUserIdAction()) 176 | ``` 177 | 178 | The `DynamicActionType` protocol requires you to define the `call` method. It can 179 | optionally return a value of your choosing. This allows you to return the status 180 | of some async action via a `Promise`, `SignalProducer`, `Observer`, etc and 181 | chain off it whatever you need. 182 | 183 | ### Summary 184 | 185 | By combining the state, store and actions we get a powerful system for managing 186 | state and subscribing to changes across our app as needed. 187 | -------------------------------------------------------------------------------- /documentation/reactive-extensions.md: -------------------------------------------------------------------------------- 1 | # Using Reactive Extensions Frameworks 2 | 3 | Delta is built to be pluggable. The driving force for this was so it could be 4 | used with reactive frameworks. This allows you to get the benefits of Delta and the reactive 5 | framework of your choice. 6 | 7 | To plug in a framework is as simple as replacing the state of the store with 8 | a custom observable type. The type must implement the `ObservablePropertyType` 9 | protocol. 10 | 11 | Here's how that looks with 2 popular reactive implementations. First implementing 12 | `ObservablePropertyType` then initializing a state and store using it. 13 | 14 | ## ReactiveCocoa 15 | 16 | ```swift 17 | import Delta 18 | import ReactiveCocoa 19 | 20 | extension MutableProperty: ObservablePropertyType { 21 | typealias ValueType = Value 22 | } 23 | 24 | struct AppState { 25 | let userId: MutableProperty(.None) 26 | } 27 | 28 | struct Store: StoreType { 29 | var state: MutableProperty 30 | } 31 | 32 | let initialState = AppState() 33 | var store = Store(state: MutableProperty(initialState)) 34 | 35 | // Subscribe to any change in the app's state 36 | store.state.producer.startWithNext { (newState: AppState) in 37 | print("new state: \(newState)") 38 | } 39 | 40 | // Subscribe to a change in the userId 41 | store.state.value.userId.producer.startWithNext { (newId: Int) in 42 | print("new id: \(newId)") 43 | } 44 | ``` 45 | 46 | ## RxSwift 47 | 48 | ```swift 49 | import Delta 50 | import RxSwift 51 | 52 | extension Variable: ObservablePropertyType { 53 | typealias ValueType = Element 54 | } 55 | 56 | struct AppState { 57 | let userId: Variable(.None) 58 | } 59 | 60 | struct Store: StoreType { 61 | var state: Variable 62 | } 63 | 64 | let initialState = AppState() 65 | var store = Store(state: Variable(initialState)) 66 | 67 | // Subscribe to any change in the app's state 68 | store.state.subscribeNext { (newState: AppState) in 69 | print("new state: \(newState)") 70 | } 71 | 72 | // Subscribe to a change in the userId 73 | store.state.value.userId.subscribeNext { (newId: Int) in 74 | print("new id: \(newId)") 75 | } 76 | ``` 77 | --------------------------------------------------------------------------------