├── .gitignore ├── .travis.yml ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── Example │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── Example.entitlements │ ├── ExampleApp.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ExampleTests │ └── ExampleTests.swift └── ExampleUITests │ ├── ExampleUITests.swift │ └── ExampleUITestsLaunchTests.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── SwiftyRedux.podspec ├── SwiftyRedux ├── Sources │ ├── BatchedActions │ │ └── BatchedActions.swift │ ├── Command │ │ ├── Command.swift │ │ └── Redux+Command.swift │ ├── Core │ │ ├── Action.swift │ │ ├── Concurrency │ │ │ ├── Atomic.swift │ │ │ └── ReadWriteQueue.swift │ │ ├── Disposing │ │ │ ├── CompositeDisposable.swift │ │ │ └── Disposable.swift │ │ ├── Middleware.swift │ │ ├── Observing │ │ │ ├── Observable.swift │ │ │ └── Observer.swift │ │ ├── Reducer.swift │ │ └── Store.swift │ ├── Epics │ │ └── Epics.swift │ ├── ReactiveExtensions │ │ └── ReactiveExtensions.swift │ ├── SideEffects │ │ └── SideEffects.swift │ └── Steroids │ │ ├── Observable+Steroids.swift │ │ ├── ObservableProducer.swift │ │ └── Store+Steroids.swift └── Tests │ ├── BatchedActions │ ├── BatchDispatchMiddlewareTests.swift │ └── EnableBatchingTests.swift │ ├── Command │ ├── CommandTests.swift │ └── Redux+CommandTests.swift │ ├── Core │ ├── DisposableTests.swift │ ├── MiddlewareTests.swift │ ├── ObservableTests.swift │ ├── PerformanceTests.swift │ └── StoreTests.swift │ ├── Epics │ └── EpicsTests.swift │ ├── ReactiveExtensions │ └── ReactiveExtensionsTests.swift │ ├── SideEffects │ └── SideEffectsTests.swift │ └── Steroids │ ├── Observable+SteroidsTests.swift │ ├── ObservableProducerTests.swift │ └── Store+SteroidsTests.swift ├── _Pods.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcbaselines │ └── 76C5A9AE486FC6D6EABE3DC5363BA01C.xcbaseline │ ├── 5646B978-E510-4B79-A411-46F1E1E64A24.plist │ └── Info.plist └── redux.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Vim/Sublime 23 | vim-markdown-preview.html 24 | README.html 25 | 26 | # Bundler 27 | .bundle 28 | 29 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 30 | # Carthage/Checkouts 31 | 32 | Carthage/Build 33 | 34 | # We recommend against adding the Pods directory to your .gitignore. However 35 | # you should judge for yourself, the pros and cons are mentioned at: 36 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 37 | # 38 | # Note: if you ignore the Pods directory, make sure to uncomment 39 | # `pod install` in .travis.yml 40 | # 41 | # Pods/ 42 | 43 | # SPM 44 | .swiftpm 45 | 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode10.2 6 | 7 | language: objective-c 8 | podfile: Example/Podfile 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | # travis ill create as many jobs for a single build as there combintations in matrix 15 | # would be cool to randomly choose one of those to trigger only one job 🤔 16 | matrix: 17 | include: 18 | - xcode_sdk: iphonesimulator12.2 19 | 20 | script: 21 | - pod lib lint --allow-warnings 22 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FAE83D21294A091C00CD4060 /* SwiftyRedux in Frameworks */ = {isa = PBXBuildFile; productRef = FAE83D20294A091C00CD4060 /* SwiftyRedux */; }; 11 | FAE83D23294A091C00CD4060 /* SwiftyReduxEpics in Frameworks */ = {isa = PBXBuildFile; productRef = FAE83D22294A091C00CD4060 /* SwiftyReduxEpics */; }; 12 | FAE83D25294A091C00CD4060 /* SwiftyReduxReactiveExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = FAE83D24294A091C00CD4060 /* SwiftyReduxReactiveExtensions */; }; 13 | FAF25B9F294A025A005F9866 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF25B9E294A025A005F9866 /* ExampleApp.swift */; }; 14 | FAF25BA1294A025A005F9866 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF25BA0294A025A005F9866 /* ContentView.swift */; }; 15 | FAF25BA3294A025B005F9866 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAF25BA2294A025B005F9866 /* Assets.xcassets */; }; 16 | FAF25BA7294A025B005F9866 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAF25BA6294A025B005F9866 /* Preview Assets.xcassets */; }; 17 | FAF25BB1294A025B005F9866 /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF25BB0294A025B005F9866 /* ExampleTests.swift */; }; 18 | FAF25BBB294A025B005F9866 /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF25BBA294A025B005F9866 /* ExampleUITests.swift */; }; 19 | FAF25BBD294A025B005F9866 /* ExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF25BBC294A025B005F9866 /* ExampleUITestsLaunchTests.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | FAF25BAD294A025B005F9866 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = FAF25B93294A025A005F9866 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = FAF25B9A294A025A005F9866; 28 | remoteInfo = Example; 29 | }; 30 | FAF25BB7294A025B005F9866 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = FAF25B93294A025A005F9866 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = FAF25B9A294A025A005F9866; 35 | remoteInfo = Example; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | FAE83D1F294A090C00CD4060 /* swifty-redux */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swifty-redux"; path = ..; sourceTree = ""; }; 41 | FAF25B9B294A025A005F9866 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | FAF25B9E294A025A005F9866 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 43 | FAF25BA0294A025A005F9866 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 44 | FAF25BA2294A025B005F9866 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 45 | FAF25BA4294A025B005F9866 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; 46 | FAF25BA6294A025B005F9866 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 47 | FAF25BAC294A025B005F9866 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | FAF25BB0294A025B005F9866 /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; 49 | FAF25BB6294A025B005F9866 /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | FAF25BBA294A025B005F9866 /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; 51 | FAF25BBC294A025B005F9866 /* ExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITestsLaunchTests.swift; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | FAF25B98294A025A005F9866 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | FAE83D25294A091C00CD4060 /* SwiftyReduxReactiveExtensions in Frameworks */, 60 | FAE83D23294A091C00CD4060 /* SwiftyReduxEpics in Frameworks */, 61 | FAE83D21294A091C00CD4060 /* SwiftyRedux in Frameworks */, 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | FAF25BA9294A025B005F9866 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | FAF25BB3294A025B005F9866 /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | FAF25B92294A025A005F9866 = { 83 | isa = PBXGroup; 84 | children = ( 85 | FAF25BC9294A0277005F9866 /* Packages */, 86 | FAF25B9D294A025A005F9866 /* Example */, 87 | FAF25BAF294A025B005F9866 /* ExampleTests */, 88 | FAF25BB9294A025B005F9866 /* ExampleUITests */, 89 | FAF25B9C294A025A005F9866 /* Products */, 90 | FAF25BCC294A02E4005F9866 /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | FAF25B9C294A025A005F9866 /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | FAF25B9B294A025A005F9866 /* Example.app */, 98 | FAF25BAC294A025B005F9866 /* ExampleTests.xctest */, 99 | FAF25BB6294A025B005F9866 /* ExampleUITests.xctest */, 100 | ); 101 | name = Products; 102 | sourceTree = ""; 103 | }; 104 | FAF25B9D294A025A005F9866 /* Example */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | FAF25B9E294A025A005F9866 /* ExampleApp.swift */, 108 | FAF25BA0294A025A005F9866 /* ContentView.swift */, 109 | FAF25BA2294A025B005F9866 /* Assets.xcassets */, 110 | FAF25BA4294A025B005F9866 /* Example.entitlements */, 111 | FAF25BA5294A025B005F9866 /* Preview Content */, 112 | ); 113 | path = Example; 114 | sourceTree = ""; 115 | }; 116 | FAF25BA5294A025B005F9866 /* Preview Content */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | FAF25BA6294A025B005F9866 /* Preview Assets.xcassets */, 120 | ); 121 | path = "Preview Content"; 122 | sourceTree = ""; 123 | }; 124 | FAF25BAF294A025B005F9866 /* ExampleTests */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | FAF25BB0294A025B005F9866 /* ExampleTests.swift */, 128 | ); 129 | path = ExampleTests; 130 | sourceTree = ""; 131 | }; 132 | FAF25BB9294A025B005F9866 /* ExampleUITests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | FAF25BBA294A025B005F9866 /* ExampleUITests.swift */, 136 | FAF25BBC294A025B005F9866 /* ExampleUITestsLaunchTests.swift */, 137 | ); 138 | path = ExampleUITests; 139 | sourceTree = ""; 140 | }; 141 | FAF25BC9294A0277005F9866 /* Packages */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | FAE83D1F294A090C00CD4060 /* swifty-redux */, 145 | ); 146 | name = Packages; 147 | sourceTree = ""; 148 | }; 149 | FAF25BCC294A02E4005F9866 /* Frameworks */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | ); 153 | name = Frameworks; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXNativeTarget section */ 159 | FAF25B9A294A025A005F9866 /* Example */ = { 160 | isa = PBXNativeTarget; 161 | buildConfigurationList = FAF25BC0294A025B005F9866 /* Build configuration list for PBXNativeTarget "Example" */; 162 | buildPhases = ( 163 | FAF25B97294A025A005F9866 /* Sources */, 164 | FAF25B98294A025A005F9866 /* Frameworks */, 165 | FAF25B99294A025A005F9866 /* Resources */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | ); 171 | name = Example; 172 | packageProductDependencies = ( 173 | FAE83D20294A091C00CD4060 /* SwiftyRedux */, 174 | FAE83D22294A091C00CD4060 /* SwiftyReduxEpics */, 175 | FAE83D24294A091C00CD4060 /* SwiftyReduxReactiveExtensions */, 176 | ); 177 | productName = Example; 178 | productReference = FAF25B9B294A025A005F9866 /* Example.app */; 179 | productType = "com.apple.product-type.application"; 180 | }; 181 | FAF25BAB294A025B005F9866 /* ExampleTests */ = { 182 | isa = PBXNativeTarget; 183 | buildConfigurationList = FAF25BC3294A025B005F9866 /* Build configuration list for PBXNativeTarget "ExampleTests" */; 184 | buildPhases = ( 185 | FAF25BA8294A025B005F9866 /* Sources */, 186 | FAF25BA9294A025B005F9866 /* Frameworks */, 187 | FAF25BAA294A025B005F9866 /* Resources */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | FAF25BAE294A025B005F9866 /* PBXTargetDependency */, 193 | ); 194 | name = ExampleTests; 195 | productName = ExampleTests; 196 | productReference = FAF25BAC294A025B005F9866 /* ExampleTests.xctest */; 197 | productType = "com.apple.product-type.bundle.unit-test"; 198 | }; 199 | FAF25BB5294A025B005F9866 /* ExampleUITests */ = { 200 | isa = PBXNativeTarget; 201 | buildConfigurationList = FAF25BC6294A025B005F9866 /* Build configuration list for PBXNativeTarget "ExampleUITests" */; 202 | buildPhases = ( 203 | FAF25BB2294A025B005F9866 /* Sources */, 204 | FAF25BB3294A025B005F9866 /* Frameworks */, 205 | FAF25BB4294A025B005F9866 /* Resources */, 206 | ); 207 | buildRules = ( 208 | ); 209 | dependencies = ( 210 | FAF25BB8294A025B005F9866 /* PBXTargetDependency */, 211 | ); 212 | name = ExampleUITests; 213 | productName = ExampleUITests; 214 | productReference = FAF25BB6294A025B005F9866 /* ExampleUITests.xctest */; 215 | productType = "com.apple.product-type.bundle.ui-testing"; 216 | }; 217 | /* End PBXNativeTarget section */ 218 | 219 | /* Begin PBXProject section */ 220 | FAF25B93294A025A005F9866 /* Project object */ = { 221 | isa = PBXProject; 222 | attributes = { 223 | BuildIndependentTargetsInParallel = 1; 224 | LastSwiftUpdateCheck = 1410; 225 | LastUpgradeCheck = 1410; 226 | TargetAttributes = { 227 | FAF25B9A294A025A005F9866 = { 228 | CreatedOnToolsVersion = 14.1; 229 | }; 230 | FAF25BAB294A025B005F9866 = { 231 | CreatedOnToolsVersion = 14.1; 232 | TestTargetID = FAF25B9A294A025A005F9866; 233 | }; 234 | FAF25BB5294A025B005F9866 = { 235 | CreatedOnToolsVersion = 14.1; 236 | TestTargetID = FAF25B9A294A025A005F9866; 237 | }; 238 | }; 239 | }; 240 | buildConfigurationList = FAF25B96294A025A005F9866 /* Build configuration list for PBXProject "Example" */; 241 | compatibilityVersion = "Xcode 14.0"; 242 | developmentRegion = en; 243 | hasScannedForEncodings = 0; 244 | knownRegions = ( 245 | en, 246 | Base, 247 | ); 248 | mainGroup = FAF25B92294A025A005F9866; 249 | productRefGroup = FAF25B9C294A025A005F9866 /* Products */; 250 | projectDirPath = ""; 251 | projectRoot = ""; 252 | targets = ( 253 | FAF25B9A294A025A005F9866 /* Example */, 254 | FAF25BAB294A025B005F9866 /* ExampleTests */, 255 | FAF25BB5294A025B005F9866 /* ExampleUITests */, 256 | ); 257 | }; 258 | /* End PBXProject section */ 259 | 260 | /* Begin PBXResourcesBuildPhase section */ 261 | FAF25B99294A025A005F9866 /* Resources */ = { 262 | isa = PBXResourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | FAF25BA7294A025B005F9866 /* Preview Assets.xcassets in Resources */, 266 | FAF25BA3294A025B005F9866 /* Assets.xcassets in Resources */, 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | FAF25BAA294A025B005F9866 /* Resources */ = { 271 | isa = PBXResourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | FAF25BB4294A025B005F9866 /* Resources */ = { 278 | isa = PBXResourcesBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | ); 282 | runOnlyForDeploymentPostprocessing = 0; 283 | }; 284 | /* End PBXResourcesBuildPhase section */ 285 | 286 | /* Begin PBXSourcesBuildPhase section */ 287 | FAF25B97294A025A005F9866 /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | FAF25BA1294A025A005F9866 /* ContentView.swift in Sources */, 292 | FAF25B9F294A025A005F9866 /* ExampleApp.swift in Sources */, 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | FAF25BA8294A025B005F9866 /* Sources */ = { 297 | isa = PBXSourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | FAF25BB1294A025B005F9866 /* ExampleTests.swift in Sources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | FAF25BB2294A025B005F9866 /* Sources */ = { 305 | isa = PBXSourcesBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | FAF25BBB294A025B005F9866 /* ExampleUITests.swift in Sources */, 309 | FAF25BBD294A025B005F9866 /* ExampleUITestsLaunchTests.swift in Sources */, 310 | ); 311 | runOnlyForDeploymentPostprocessing = 0; 312 | }; 313 | /* End PBXSourcesBuildPhase section */ 314 | 315 | /* Begin PBXTargetDependency section */ 316 | FAF25BAE294A025B005F9866 /* PBXTargetDependency */ = { 317 | isa = PBXTargetDependency; 318 | target = FAF25B9A294A025A005F9866 /* Example */; 319 | targetProxy = FAF25BAD294A025B005F9866 /* PBXContainerItemProxy */; 320 | }; 321 | FAF25BB8294A025B005F9866 /* PBXTargetDependency */ = { 322 | isa = PBXTargetDependency; 323 | target = FAF25B9A294A025A005F9866 /* Example */; 324 | targetProxy = FAF25BB7294A025B005F9866 /* PBXContainerItemProxy */; 325 | }; 326 | /* End PBXTargetDependency section */ 327 | 328 | /* Begin XCBuildConfiguration section */ 329 | FAF25BBE294A025B005F9866 /* Debug */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 336 | CLANG_ENABLE_MODULES = YES; 337 | CLANG_ENABLE_OBJC_ARC = YES; 338 | CLANG_ENABLE_OBJC_WEAK = YES; 339 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 340 | CLANG_WARN_BOOL_CONVERSION = YES; 341 | CLANG_WARN_COMMA = YES; 342 | CLANG_WARN_CONSTANT_CONVERSION = YES; 343 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 344 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 345 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 346 | CLANG_WARN_EMPTY_BODY = YES; 347 | CLANG_WARN_ENUM_CONVERSION = YES; 348 | CLANG_WARN_INFINITE_RECURSION = YES; 349 | CLANG_WARN_INT_CONVERSION = YES; 350 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 352 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 354 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = dwarf; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | ENABLE_TESTABILITY = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu11; 366 | GCC_DYNAMIC_NO_PIC = NO; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_OPTIMIZATION_LEVEL = 0; 369 | GCC_PREPROCESSOR_DEFINITIONS = ( 370 | "DEBUG=1", 371 | "$(inherited)", 372 | ); 373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 375 | GCC_WARN_UNDECLARED_SELECTOR = YES; 376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 377 | GCC_WARN_UNUSED_FUNCTION = YES; 378 | GCC_WARN_UNUSED_VARIABLE = YES; 379 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 380 | MTL_FAST_MATH = YES; 381 | ONLY_ACTIVE_ARCH = YES; 382 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 383 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 384 | }; 385 | name = Debug; 386 | }; 387 | FAF25BBF294A025B005F9866 /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ALWAYS_SEARCH_USER_PATHS = NO; 391 | CLANG_ANALYZER_NONNULL = YES; 392 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 393 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 394 | CLANG_ENABLE_MODULES = YES; 395 | CLANG_ENABLE_OBJC_ARC = YES; 396 | CLANG_ENABLE_OBJC_WEAK = YES; 397 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 398 | CLANG_WARN_BOOL_CONVERSION = YES; 399 | CLANG_WARN_COMMA = YES; 400 | CLANG_WARN_CONSTANT_CONVERSION = YES; 401 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 402 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 403 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 404 | CLANG_WARN_EMPTY_BODY = YES; 405 | CLANG_WARN_ENUM_CONVERSION = YES; 406 | CLANG_WARN_INFINITE_RECURSION = YES; 407 | CLANG_WARN_INT_CONVERSION = YES; 408 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 409 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 410 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 412 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 413 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 414 | CLANG_WARN_STRICT_PROTOTYPES = YES; 415 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 416 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 417 | CLANG_WARN_UNREACHABLE_CODE = YES; 418 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 419 | COPY_PHASE_STRIP = NO; 420 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 421 | ENABLE_NS_ASSERTIONS = NO; 422 | ENABLE_STRICT_OBJC_MSGSEND = YES; 423 | GCC_C_LANGUAGE_STANDARD = gnu11; 424 | GCC_NO_COMMON_BLOCKS = YES; 425 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 426 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 427 | GCC_WARN_UNDECLARED_SELECTOR = YES; 428 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 429 | GCC_WARN_UNUSED_FUNCTION = YES; 430 | GCC_WARN_UNUSED_VARIABLE = YES; 431 | MTL_ENABLE_DEBUG_INFO = NO; 432 | MTL_FAST_MATH = YES; 433 | SWIFT_COMPILATION_MODE = wholemodule; 434 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 435 | }; 436 | name = Release; 437 | }; 438 | FAF25BC1294A025B005F9866 /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 442 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 443 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 444 | CODE_SIGN_STYLE = Automatic; 445 | CURRENT_PROJECT_VERSION = 1; 446 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 447 | ENABLE_PREVIEWS = YES; 448 | GENERATE_INFOPLIST_FILE = YES; 449 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 450 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 451 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 452 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 453 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 454 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 455 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 456 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 457 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 458 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 459 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 460 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 461 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 462 | MACOSX_DEPLOYMENT_TARGET = 13.0; 463 | MARKETING_VERSION = 1.0; 464 | PRODUCT_BUNDLE_IDENTIFIER = com.spm.Example; 465 | PRODUCT_NAME = "$(TARGET_NAME)"; 466 | SDKROOT = auto; 467 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 468 | SWIFT_EMIT_LOC_STRINGS = YES; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Debug; 473 | }; 474 | FAF25BC2294A025B005F9866 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 479 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 480 | CODE_SIGN_STYLE = Automatic; 481 | CURRENT_PROJECT_VERSION = 1; 482 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 483 | ENABLE_PREVIEWS = YES; 484 | GENERATE_INFOPLIST_FILE = YES; 485 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 486 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 487 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 488 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 489 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 490 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 491 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 492 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 493 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 494 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 495 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 496 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 497 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 498 | MACOSX_DEPLOYMENT_TARGET = 13.0; 499 | MARKETING_VERSION = 1.0; 500 | PRODUCT_BUNDLE_IDENTIFIER = com.spm.Example; 501 | PRODUCT_NAME = "$(TARGET_NAME)"; 502 | SDKROOT = auto; 503 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 504 | SWIFT_EMIT_LOC_STRINGS = YES; 505 | SWIFT_VERSION = 5.0; 506 | TARGETED_DEVICE_FAMILY = "1,2"; 507 | }; 508 | name = Release; 509 | }; 510 | FAF25BC4294A025B005F9866 /* Debug */ = { 511 | isa = XCBuildConfiguration; 512 | buildSettings = { 513 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 514 | BUNDLE_LOADER = "$(TEST_HOST)"; 515 | CODE_SIGN_STYLE = Automatic; 516 | CURRENT_PROJECT_VERSION = 1; 517 | GENERATE_INFOPLIST_FILE = YES; 518 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 519 | MACOSX_DEPLOYMENT_TARGET = 13.0; 520 | MARKETING_VERSION = 1.0; 521 | PRODUCT_BUNDLE_IDENTIFIER = com.spm.ExampleTests; 522 | PRODUCT_NAME = "$(TARGET_NAME)"; 523 | SDKROOT = auto; 524 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 525 | SWIFT_EMIT_LOC_STRINGS = NO; 526 | SWIFT_VERSION = 5.0; 527 | TARGETED_DEVICE_FAMILY = "1,2"; 528 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; 529 | }; 530 | name = Debug; 531 | }; 532 | FAF25BC5294A025B005F9866 /* Release */ = { 533 | isa = XCBuildConfiguration; 534 | buildSettings = { 535 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 536 | BUNDLE_LOADER = "$(TEST_HOST)"; 537 | CODE_SIGN_STYLE = Automatic; 538 | CURRENT_PROJECT_VERSION = 1; 539 | GENERATE_INFOPLIST_FILE = YES; 540 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 541 | MACOSX_DEPLOYMENT_TARGET = 13.0; 542 | MARKETING_VERSION = 1.0; 543 | PRODUCT_BUNDLE_IDENTIFIER = com.spm.ExampleTests; 544 | PRODUCT_NAME = "$(TARGET_NAME)"; 545 | SDKROOT = auto; 546 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 547 | SWIFT_EMIT_LOC_STRINGS = NO; 548 | SWIFT_VERSION = 5.0; 549 | TARGETED_DEVICE_FAMILY = "1,2"; 550 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; 551 | }; 552 | name = Release; 553 | }; 554 | FAF25BC7294A025B005F9866 /* Debug */ = { 555 | isa = XCBuildConfiguration; 556 | buildSettings = { 557 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 558 | CODE_SIGN_STYLE = Automatic; 559 | CURRENT_PROJECT_VERSION = 1; 560 | GENERATE_INFOPLIST_FILE = YES; 561 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 562 | MACOSX_DEPLOYMENT_TARGET = 13.0; 563 | MARKETING_VERSION = 1.0; 564 | PRODUCT_BUNDLE_IDENTIFIER = com.spm.ExampleUITests; 565 | PRODUCT_NAME = "$(TARGET_NAME)"; 566 | SDKROOT = auto; 567 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 568 | SWIFT_EMIT_LOC_STRINGS = NO; 569 | SWIFT_VERSION = 5.0; 570 | TARGETED_DEVICE_FAMILY = "1,2"; 571 | TEST_TARGET_NAME = Example; 572 | }; 573 | name = Debug; 574 | }; 575 | FAF25BC8294A025B005F9866 /* Release */ = { 576 | isa = XCBuildConfiguration; 577 | buildSettings = { 578 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 579 | CODE_SIGN_STYLE = Automatic; 580 | CURRENT_PROJECT_VERSION = 1; 581 | GENERATE_INFOPLIST_FILE = YES; 582 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 583 | MACOSX_DEPLOYMENT_TARGET = 13.0; 584 | MARKETING_VERSION = 1.0; 585 | PRODUCT_BUNDLE_IDENTIFIER = com.spm.ExampleUITests; 586 | PRODUCT_NAME = "$(TARGET_NAME)"; 587 | SDKROOT = auto; 588 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 589 | SWIFT_EMIT_LOC_STRINGS = NO; 590 | SWIFT_VERSION = 5.0; 591 | TARGETED_DEVICE_FAMILY = "1,2"; 592 | TEST_TARGET_NAME = Example; 593 | }; 594 | name = Release; 595 | }; 596 | /* End XCBuildConfiguration section */ 597 | 598 | /* Begin XCConfigurationList section */ 599 | FAF25B96294A025A005F9866 /* Build configuration list for PBXProject "Example" */ = { 600 | isa = XCConfigurationList; 601 | buildConfigurations = ( 602 | FAF25BBE294A025B005F9866 /* Debug */, 603 | FAF25BBF294A025B005F9866 /* Release */, 604 | ); 605 | defaultConfigurationIsVisible = 0; 606 | defaultConfigurationName = Release; 607 | }; 608 | FAF25BC0294A025B005F9866 /* Build configuration list for PBXNativeTarget "Example" */ = { 609 | isa = XCConfigurationList; 610 | buildConfigurations = ( 611 | FAF25BC1294A025B005F9866 /* Debug */, 612 | FAF25BC2294A025B005F9866 /* Release */, 613 | ); 614 | defaultConfigurationIsVisible = 0; 615 | defaultConfigurationName = Release; 616 | }; 617 | FAF25BC3294A025B005F9866 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { 618 | isa = XCConfigurationList; 619 | buildConfigurations = ( 620 | FAF25BC4294A025B005F9866 /* Debug */, 621 | FAF25BC5294A025B005F9866 /* Release */, 622 | ); 623 | defaultConfigurationIsVisible = 0; 624 | defaultConfigurationName = Release; 625 | }; 626 | FAF25BC6294A025B005F9866 /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { 627 | isa = XCConfigurationList; 628 | buildConfigurations = ( 629 | FAF25BC7294A025B005F9866 /* Debug */, 630 | FAF25BC8294A025B005F9866 /* Release */, 631 | ); 632 | defaultConfigurationIsVisible = 0; 633 | defaultConfigurationName = Release; 634 | }; 635 | /* End XCConfigurationList section */ 636 | 637 | /* Begin XCSwiftPackageProductDependency section */ 638 | FAE83D20294A091C00CD4060 /* SwiftyRedux */ = { 639 | isa = XCSwiftPackageProductDependency; 640 | productName = SwiftyRedux; 641 | }; 642 | FAE83D22294A091C00CD4060 /* SwiftyReduxEpics */ = { 643 | isa = XCSwiftPackageProductDependency; 644 | productName = SwiftyReduxEpics; 645 | }; 646 | FAE83D24294A091C00CD4060 /* SwiftyReduxReactiveExtensions */ = { 647 | isa = XCSwiftPackageProductDependency; 648 | productName = SwiftyReduxReactiveExtensions; 649 | }; 650 | /* End XCSwiftPackageProductDependency section */ 651 | }; 652 | rootObject = FAF25B93294A025A005F9866 /* Project object */; 653 | } 654 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ReactiveSwift", 6 | "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", 10 | "version": "6.7.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Example 4 | // 5 | // Created by Oleksandr Voronov on 12/14/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundColor(.accentColor) 16 | Text("Hello, world!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | struct ContentView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | ContentView() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Example/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleApp.swift 3 | // Example 4 | // 5 | // Created by Oleksandr Voronov on 12/14/22. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftyRedux 10 | 11 | // MARK: - State 12 | 13 | struct MainState { 14 | var counter: Int 15 | 16 | static let initial = MainState(counter: 0) 17 | } 18 | 19 | // MARK: - Actions 20 | 21 | enum Operations { 22 | struct Increment: SwiftyRedux.Action { } 23 | struct Decrement: SwiftyRedux.Action { } 24 | } 25 | 26 | // MARK: - Reducer 27 | 28 | let mainReducer: Reducer = { state, action in 29 | switch action { 30 | case is Operations.Increment: 31 | state.counter += 1 32 | case is Operations.Decrement: 33 | state.counter -= 1 34 | default: 35 | break 36 | } 37 | } 38 | 39 | // MARK: - Middleware 40 | 41 | let loggingMiddleware: Middleware = createMiddleware { getState, dispatch, next in 42 | return { action in 43 | guard let oldState = getState() else { return } 44 | print("[OLD ➡️]: \(oldState)") 45 | print("[MSG ✅]: \(action)") 46 | next(action) 47 | guard let newState = getState() else { return } 48 | print("[NEW ⬅️]: \(newState)\n") 49 | } 50 | } 51 | 52 | // MARK: - Store 53 | 54 | let store = Store(state: MainState.initial, reducer: mainReducer, middleware: [batchDispatchMiddleware(), loggingMiddleware]) 55 | 56 | @main 57 | struct ExampleApp: App { 58 | init() { 59 | // will send current and further state updates once started 60 | let uniqueEvenCounter = store.stateProducer() 61 | // focus on counter 62 | .map(\.counter) 63 | // filter only even values 64 | .filter { $0.isMultiple(of: 2) } 65 | // skip repeating in a row counter values 66 | .skipRepeats() 67 | 68 | // counter: 1 69 | store.dispatch(Operations.Increment()) 70 | 71 | // notice that we don't receive 0 counter here even though it's even value 72 | // that's because we start observing state after its counter value is already 1 73 | uniqueEvenCounter.start { counter in 74 | // see how this message is printed before "[NEW ...]" but after "[MSG ...]" 75 | // this is because the last `next` in middleware chain 76 | // is a function that applies reducer to the current state, updates state with its result and notifies observers, 77 | // and only then we get new updated state and log it with "[NEW ...]" message 78 | print("🎲 Unique Even Counter: \(counter)") 79 | } 80 | 81 | // counter: 2 82 | store.dispatch(Operations.Increment()) 83 | 84 | // counter: 3 85 | store.dispatch(Operations.Increment()) 86 | 87 | // counter: 2 88 | // see, we don't print "Unique Even Counter" as its previous even value was also 2 89 | store.dispatch(Operations.Decrement()) 90 | 91 | // notice how actions are split into single actions by `batchDispatchMiddleware`, 92 | // and BatchAction is logged afterwards and not handled by reducer 93 | store.dispatch(BatchAction( 94 | // counter: 3 95 | Operations.Increment(), 96 | // counter: 4 97 | Operations.Increment() 98 | )) 99 | 100 | // counter: 3 101 | store.dispatch(Operations.Decrement()) 102 | 103 | // counter: 2 104 | // now we receive 2 again, as previous counter value was 4, even though we've already had 2 few steps before 105 | store.dispatch(Operations.Decrement()) 106 | } 107 | 108 | var body: some Scene { 109 | WindowGroup { 110 | ContentView() 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ExampleTests/ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleTests.swift 3 | // ExampleTests 4 | // 5 | // Created by Oleksandr Voronov on 12/14/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ExampleTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | // Any test you write for XCTest can be annotated as throws and async. 24 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 25 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Example/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITests.swift 3 | // ExampleUITests 4 | // 5 | // Created by Oleksandr Voronov on 12/14/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ExampleUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/ExampleUITests/ExampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITestsLaunchTests.swift 3 | // ExampleUITests 4 | // 5 | // Created by Oleksandr Voronov on 12/14/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ExampleUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Voronov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ReactiveSwift", 6 | "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", 10 | "version": "6.7.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SwiftyRedux", 6 | platforms: [ 7 | .iOS(.v10), 8 | .macOS(.v10_12) 9 | ], 10 | products: [ 11 | .library(name: "SwiftyRedux", targets: ["SwiftyRedux"]), 12 | .library(name: "SwiftyReduxEpics", targets: ["SwiftyReduxEpics"]), 13 | .library(name: "SwiftyReduxReactiveExtensions", targets: ["SwiftyReduxReactiveExtensions"]) 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", .upToNextMajor(from: "6.0.0")) 17 | ], 18 | targets: [ 19 | .target( 20 | name: "SwiftyRedux", 21 | path: "SwiftyRedux/Sources", 22 | exclude: [ 23 | "Epics", 24 | "ReactiveExtensions" 25 | ], 26 | linkerSettings: [ 27 | .linkedFramework("Foundation") 28 | ] 29 | ), 30 | .target( 31 | name: "SwiftyReduxEpics", 32 | dependencies: [ 33 | "SwiftyRedux", 34 | .product(name: "ReactiveSwift", package: "ReactiveSwift") 35 | ], 36 | path: "SwiftyRedux/Sources/Epics" 37 | ), 38 | .target( 39 | name: "SwiftyReduxReactiveExtensions", 40 | dependencies: [ 41 | "SwiftyRedux", 42 | .product(name: "ReactiveSwift", package: "ReactiveSwift") 43 | ], 44 | path: "SwiftyRedux/Sources/ReactiveExtensions" 45 | ), 46 | .testTarget( 47 | name: "SwiftyReduxTests", 48 | dependencies: [ 49 | "SwiftyRedux", 50 | "SwiftyReduxEpics", 51 | "SwiftyReduxReactiveExtensions", 52 | .product(name: "ReactiveSwift", package: "ReactiveSwift") 53 | ], 54 | path: "SwiftyRedux/Tests" 55 | ) 56 | ], 57 | swiftLanguageVersions: [.v5] 58 | ) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swifty Redux 2 | [![Build Status](https://travis-ci.com/a-voronov/swifty-redux.svg?branch=master)](https://travis-ci.com/a-voronov/swifty-redux) 3 | 4 | Swift implementation of [Redux](https://redux.js.org) 5 | 6 | # About 7 | 8 | SwiftyRedux tries to stay as close to Redux.js with its ideas and API as possible. 9 | It should be easier to use this tool and understand approach within iOS platform by sharing same vocabulary and having such a great community in js world! There will be minor differences though due to the languages and platforms distinctions. 10 | 11 | SwiftyRedux has few core components: 12 | 13 | * **State** is a single data structure. It is a source of truth of our application. Store can be a struct or enum, it may contain other structs or enums as properties. Often it is a deeply nested object that describes your whole application state. Keep in mind that state should be easily serialized and deserialized, thus not contain any behavior. State is managed by the store. 14 | 15 | * **Store** is a place that contains state. It also allows subscribing to the state changes and dispatching actions to change its state. There's only single store in the whole Redux application. 16 | 17 | * **Action** is a plain object that represents an intention to change the state. Actions should not contain any behavior. All actions should conform to the empty `Action` protocol. The only way to change state is through action that will be handled in reducers. 18 | 19 | * **Reducer** is the only place that can change the state based on the current state and an acion. Reducers must be pure functions - functions that return the exact same output for given inputs, thus not performing any side effects. It makes them easy to test and reason about the logic that affects global application state. Store is responsible for delivering actions to the reducers and updating state with their results. 20 | 21 | * **Middleware** is a place where you keep async logic and side effects. It's the best way to connect IO with pure unidirectional flow application. They intercept actions after they were dispatched and before they reached reducers, but you can design your middlewares in such a way they don't slow down delivery of actions to the reducers. 22 | 23 | Please refer to https://redux.js.org/introduction/getting-started for more info about this approach in general and Redux.js community experience. 24 | 25 | # Flow Diagram 26 | 27 | ![redux](redux.jpg) 28 | 29 | Usually your flow will match current diagram: 30 | 31 | 1. You dispatch an Action via the Store. 32 | 1. Action travels to the Middleware. 33 | 1. Middlewares might dispatch a new Action (which will travel all they way from the very beginning) or propagates it further. 34 | 1. Reducers receive the Action and the current State. 35 | 1. They might apply the Action to the State to return a New State, or ignore it if no changes required. 36 | 1. After the state was changed, any Observers will receive it. 37 | 1. In case of UI observers, Presenters can transform state or its parts into Props and give it to their Views to render. 38 | 1. Any user action or program events can be turned into Actions and will be dispatched to the Store to start this cycle from the beginning. 39 | 40 | # Installation 41 | 42 | ## CocoaPods 43 | 44 | You can install SwiftyRedux via [CocoaPods](https://cocoapods.org) by adding it to your Podfile: 45 | ``` 46 | pod 'SwiftyRedux' 47 | ``` 48 | And running `pod install`. 49 | 50 | ## Carthage 51 | 52 | TBD 53 | 54 | ## Swift Package Manager 55 | 56 | ```swift 57 | .package(url: "https://github.com/a-voronov/swifty-redux.git", from: "0.4.0") 58 | ``` 59 | 60 | ## Example 61 | 62 | There's also an Example app, so you can play around with SwiftyRedux. In order to start using it: 63 | 64 | 1. Open `Example/Example.xcworkspace`. 65 | 1. Run `Example` scheme. 66 | 1. Check `Debug Area`. 67 | 68 | You can find sample code in the `ExampleApp.swift` file. 69 | 70 | # Project Structure 71 | 72 | SwiftyRedux is trying to be as minimalistic at its Core as possible, yet still open for extensions on top of it. 73 | Here are components included in this repo: 74 | 75 | ## ⚙️ Core 76 | Core is a basic component and contains minimal needed functionality to enjoy SwiftyRedux :) 77 | Such as `Action`, `Reducer`, `Middlerware`, `Store` and `Disposable`. 78 | ``` 79 | pod 'SwiftyRedux/Core' 80 | ``` 81 | 82 | ## 💊 Steroids 83 | Steroids is an extension that provides declarative API to transform state updates coming from the store. Its `Observable` and `ObservableProducer` abstractions look and behave similar to [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift) `Signal` and `SignalProducer`. 84 | ``` 85 | pod 'SwiftyRedux/Steroids' 86 | ``` 87 | 88 | ## 🚌 BatchedActions 89 | Batched Actions provide ability to send multiple actions at once and split them into single actions either with upgraded reducer or own middleware (not both though). Inspired by [redux-batched-actions](https://github.com/tshelburne/redux-batched-actions). 90 | ``` 91 | pod 'SwiftyRedux/BatchedActions' 92 | ``` 93 | 94 | ## 💥 SideEffects 95 | Side effects is a kind of middleware that is run **after** reducers have received and processed the action, so that, they can't "swallow" or delay it. Inspired by [redux-observable](https://redux-observable.js.org).[Epics](https://redux-observable.js.org/docs/basics/Epics.html) but this is an "imperative" epics. 96 | ``` 97 | pod 'SwiftyRedux/SideEffects' 98 | ``` 99 | 100 | ## 📦 Command 101 | Commands are simple wrappers over functions that contain additional callee meta information for easier debugging. 102 | ``` 103 | pod 'SwiftyRedux/Command' 104 | ``` 105 | 106 | ## 🚀 Epics 107 | Actually epics' implementation is close to [redux-observable](https://redux-observable.js.org).[Epics](https://redux-observable.js.org/docs/basics/Epics.html) but without fancy functionality like adding epics asynchronously/lazily yet. 108 | It is powered by ReactiveSwift to provide full reactive experience while transforming actions. 109 | ``` 110 | pod 'SwiftyRedux/Epics' 111 | ``` 112 | 113 | ## 🌉 ReactiveExtensions 114 | Bridge between ReactiveSwift and SwiftyRedux: `Observable` <> `Signal`, `ObservableProducer` <> `SignalProducer`. 115 | This one depends on ReactiveSwift and Steroids. 116 | ``` 117 | pod 'SwiftyRedux/ReactiveExtensions' 118 | ``` 119 | 120 | ## 🎨 Mix them together! 121 | 122 | All of these components already have **Core** as their dependecy, thus you can install any of them as shown above. 123 | You can also mix any of them together and install many at once, i.e.: 124 | ``` 125 | pod 'SwiftyRedux', :subspecs => ['Core', 'Steroids', 'SideEffects', 'BatchedActions'] 126 | ``` 127 | 128 | Note: since SPM doesn't support anything close to subspecs, there're only 3 main products available - `SwiftyRedux, SwiftyReduxEpics, SwiftyReduxReactiveExtensions`. 129 | 130 | # License 131 | 132 | SwiftyRedux is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 133 | -------------------------------------------------------------------------------- /SwiftyRedux.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SwiftyRedux' 3 | s.version = '0.4.0' 4 | s.summary = 'Swifty implementation of Redux' 5 | s.swift_version = '5.0' 6 | 7 | s.description = <<-DESC 8 | Swifty implementation of Redux with optional add-ons. 9 | DESC 10 | 11 | s.homepage = 'https://github.com/a-voronov/swifty-redux' 12 | s.license = { :type => 'MIT', :file => 'LICENSE' } 13 | s.author = { 'Oleksandr Voronov' => 'voronovaleksandr91@gmail.com' } 14 | s.source = { :git => 'https://github.com/a-voronov/swifty-redux.git', :tag => s.version.to_s } 15 | s.social_media_url = 'https://twitter.com/aleks_voronov' 16 | 17 | s.ios.deployment_target = '10.0' 18 | 19 | s.default_subspecs = 'Core', 'Steroids', 'Command', 'BatchedActions', 'SideEffects' 20 | 21 | s.subspec 'Core' do |ss| 22 | ss.source_files = 'SwiftyRedux/Sources/Core/**/*.{swift}' 23 | 24 | ss.test_spec 'Tests' do |ts| 25 | ts.source_files = 'SwiftyRedux/Tests/Core/**/*.{swift}' 26 | end 27 | end 28 | 29 | s.subspec 'All' do |ss| 30 | ss.dependency 'SwiftyRedux/Core' 31 | ss.dependency 'SwiftyRedux/Steroids' 32 | ss.dependency 'SwiftyRedux/Command' 33 | ss.dependency 'SwiftyRedux/BatchedActions' 34 | ss.dependency 'SwiftyRedux/SideEffects' 35 | ss.dependency 'SwiftyRedux/Epics' 36 | ss.dependency 'SwiftyRedux/ReactiveExtensions' 37 | 38 | ss.test_spec 'Tests' do |ts| 39 | ts.source_files = 'SwiftyRedux/Tests/**/*.{swift}' 40 | end 41 | end 42 | 43 | s.subspec 'Steroids' do |ss| 44 | ss.dependency 'SwiftyRedux/Core' 45 | ss.source_files = 'SwiftyRedux/Sources/Steroids/**/*.{swift}' 46 | 47 | ss.test_spec 'Tests' do |ts| 48 | ts.source_files = 'SwiftyRedux/Tests/Steroids/**/*.{swift}' 49 | end 50 | end 51 | 52 | s.subspec 'BatchedActions' do |ss| 53 | ss.dependency 'SwiftyRedux/Core' 54 | ss.source_files = 'SwiftyRedux/Sources/BatchedActions/**/*.{swift}' 55 | 56 | ss.test_spec 'Tests' do |ts| 57 | ts.source_files = 'SwiftyRedux/Tests/BatchedActions/**/*.{swift}' 58 | end 59 | end 60 | 61 | s.subspec 'Command' do |ss| 62 | ss.dependency 'SwiftyRedux/Core' 63 | ss.source_files = 'SwiftyRedux/Sources/Command/**/*.{swift}' 64 | 65 | ss.test_spec 'Tests' do |ts| 66 | ts.source_files = 'SwiftyRedux/Tests/Command/**/*.{swift}' 67 | end 68 | end 69 | 70 | s.subspec 'SideEffects' do |ss| 71 | ss.dependency 'SwiftyRedux/Core' 72 | ss.source_files = 'SwiftyRedux/Sources/SideEffects/**/*.{swift}' 73 | 74 | ss.test_spec 'Tests' do |ts| 75 | ts.source_files = 'SwiftyRedux/Tests/SideEffects/**/*.{swift}' 76 | end 77 | end 78 | 79 | s.subspec 'Epics' do |ss| 80 | ss.dependency 'SwiftyRedux/Core' 81 | ss.dependency 'ReactiveSwift', '~> 6.0' 82 | ss.source_files = 'SwiftyRedux/Sources/Epics/**/*.{swift}' 83 | 84 | ss.test_spec 'Tests' do |ts| 85 | ts.source_files = 'SwiftyRedux/Tests/Epics/**/*.{swift}' 86 | end 87 | end 88 | 89 | s.subspec 'ReactiveExtensions' do |ss| 90 | ss.dependency 'SwiftyRedux/Core' 91 | ss.dependency 'SwiftyRedux/Steroids' 92 | ss.dependency 'ReactiveSwift', '~> 6.0' 93 | ss.source_files = 'SwiftyRedux/Sources/ReactiveExtensions/**/*.{swift}' 94 | 95 | ss.test_spec 'Tests' do |ts| 96 | ts.source_files = 'SwiftyRedux/Tests/ReactiveExtensions/**/*.{swift}' 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/BatchedActions/BatchedActions.swift: -------------------------------------------------------------------------------- 1 | /// [Batched action](https://github.com/tshelburne/redux-batched-actions) is an action that combines multiple actions. 2 | /// Multiple batched actions can be combined as well as they conform to `Action` protocol. 3 | public protocol BatchedActions: Action { 4 | 5 | /// Array of any combined actions. 6 | var actions: [Action] { get } 7 | } 8 | 9 | /// `BatchedActions` implementation. Should be initialized with one or more actions. 10 | public struct BatchAction: BatchedActions { 11 | 12 | /// Array of any combined actions. 13 | public let actions: [Action] 14 | 15 | /// Initializes batch action with one or more actions. 16 | /// 17 | /// - Parameters: 18 | /// - first: First action. 19 | /// - rest: Rest of the actions variadic parameter. 20 | public init(_ first: Action, _ rest: Action...) { 21 | self.actions = [first] + rest 22 | } 23 | 24 | /// Initializes batch action with one or more actions. 25 | /// 26 | /// - Parameters: 27 | /// - first: First action. 28 | /// - rest: Rest of the actions array. 29 | public init(_ first: Action, _ rest: [Action]) { 30 | self.actions = [first] + rest 31 | } 32 | } 33 | 34 | /// Upgrades reducer to handle batched actions by splitting them into single actions no matter how deeply nested they are. 35 | /// 36 | /// Example: 37 | /// 38 | /// BatchAction( 39 | /// One(), 40 | /// BatchAction( 41 | /// BatchAction( 42 | /// Two(), 43 | /// Three() 44 | /// ), 45 | /// Four() 46 | /// ), 47 | /// Five() 48 | /// ) 49 | /// 50 | /// Will be handled in this order: 51 | /// 52 | /// One(), Two(), Three(), Four(), Five() 53 | /// 54 | /// - Parameter reducer: Reducer that should be upgraded. 55 | /// - Returns: Upgraded reducer. 56 | /// 57 | /// - Important: Note that `batchDispatchMiddleware` and `enableBatching` should not be used together 58 | /// as `batchDispatchMiddleware` calls next on the action it receives, whilst also dispatching each of the bundled actions. 59 | public func enableBatching(_ reducer: @escaping Reducer) -> Reducer { 60 | func batchingReducer(_ state: inout State, _ action: Action) { 61 | guard let batchAction = action as? BatchedActions else { 62 | return reducer(&state, action) 63 | } 64 | state = batchAction.actions.reduce(into: state, batchingReducer) 65 | } 66 | return batchingReducer 67 | } 68 | 69 | /// Middleware that dispatches batched actions by splitting them into single actions no matter how deeply nested they are. 70 | /// 71 | /// Example: 72 | /// 73 | /// BatchAction( 74 | /// One(), 75 | /// BatchAction( 76 | /// BatchAction( 77 | /// Two(), 78 | /// Three() 79 | /// ), 80 | /// Four() 81 | /// ), 82 | /// Five() 83 | /// ) 84 | /// 85 | /// Will dispatch actions in this order: 86 | /// 87 | /// One(), Two(), Three(), Four(), Five() 88 | /// 89 | /// - Returns: Middleware that intercepts batched actions and dispatch single actions out of them. 90 | /// 91 | /// - Important: Note that `batchDispatchMiddleware` and `enableBatching` should not be used together 92 | /// as `batchDispatchMiddleware` calls next on the action it receives, whilst also dispatching each of the bundled actions. 93 | public func batchDispatchMiddleware() -> Middleware { 94 | func dispatchChildActions(_ getState: @escaping GetState, _ dispatch: @escaping Dispatch, _ action: Action) { 95 | guard let batchAction = action as? BatchedActions else { 96 | return dispatch(action) 97 | } 98 | batchAction.actions.forEach { action in 99 | dispatchChildActions(getState, dispatch, action) 100 | } 101 | } 102 | 103 | return { getState, dispatch, next in 104 | return { action in 105 | if let batchAction = action as? BatchedActions { 106 | dispatchChildActions(getState, dispatch, batchAction) 107 | } 108 | return next(action) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Command/Command.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Command is a developer friendly wrapper around a closure. 4 | /// It helps to ease debugging by providing callee information. 5 | public final class Command { 6 | private let id: String 7 | private let file: StaticString 8 | private let function: StaticString 9 | private let line: UInt 10 | private let closure: (T) -> Void 11 | 12 | public init( 13 | id: String = "swifty-redux.command", 14 | file: StaticString = #file, 15 | function: StaticString = #function, 16 | line: UInt = #line, 17 | closure: @escaping (T) -> Void 18 | ) { 19 | self.id = id 20 | self.file = file 21 | self.function = function 22 | self.line = line 23 | self.closure = closure 24 | } 25 | 26 | /// Executes command with provided value. 27 | /// 28 | /// - Parameter value: Value to execute command with. 29 | public func execute(with value: T) { 30 | closure(value) 31 | } 32 | 33 | /// - Returns: No-op command with empty closure. 34 | public static func nop() -> Command { 35 | return Command(id: "swifty-redux.command.nop", closure: { _ in }) 36 | } 37 | 38 | /// Xcode quick look support. 39 | @objc 40 | func debugQuickLookObject() -> AnyObject? { 41 | return debugDescription as NSString 42 | } 43 | } 44 | 45 | extension Command where T == Void { 46 | 47 | /// Shortcut to execute command with `Void` value. 48 | public func execute() { 49 | execute(with: ()) 50 | } 51 | } 52 | 53 | extension Command { 54 | 55 | /// Bakes `value` into command by producing a new command that will be always executing with provided value. 56 | /// 57 | /// - Returns: `Void` command with `value` baked inside. 58 | public func with(value: T) -> Command { 59 | return Command { self.execute(with: value) } 60 | } 61 | } 62 | 63 | extension Command: Hashable { 64 | public func hash(into hasher: inout Hasher) { 65 | return hasher.combine(ObjectIdentifier(self)) 66 | } 67 | 68 | public static func == (lhs: Command, rhs: Command) -> Bool { 69 | return lhs === rhs 70 | } 71 | } 72 | 73 | extension Command: CustomDebugStringConvertible { 74 | public var debugDescription: String { 75 | return """ 76 | \(String(describing: type(of: self)))( 77 | id: \(id), 78 | file: \(file), 79 | function: \(function), 80 | line: \(line) 81 | ) 82 | """ 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Command/Redux+Command.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// Redux components extensions to receive Command instead of plain function for easier debugging. 4 | 5 | // MARK: - Store 6 | 7 | extension Store { 8 | @discardableResult 9 | public func subscribe(on queue: DispatchQueue? = nil, includingCurrentState: Bool = true, _ command: Command) -> Disposable { 10 | return subscribe(on: queue, includingCurrentState: includingCurrentState, observer: command.execute) 11 | } 12 | } 13 | 14 | // MARK: - Observable 15 | 16 | extension Observable { 17 | @discardableResult 18 | public func subscribe(on observingQueue: DispatchQueue? = nil, _ command: Command) -> Disposable { 19 | return subscribe(on: observingQueue, observer: command.execute) 20 | } 21 | } 22 | 23 | // MARK: - Observer 24 | 25 | extension Observer { 26 | public convenience init(queue: DispatchQueue? = nil, _ command: Command) { 27 | self.init(queue: queue, update: command.execute) 28 | } 29 | } 30 | 31 | // MARK: - Disposable 32 | 33 | extension Disposable { 34 | public convenience init(id: String? = nil, _ command: Command) { 35 | self.init(id: id, action: { command.execute() }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Action.swift: -------------------------------------------------------------------------------- 1 | /// [Actions](https://redux.js.org/basics/actions) are payloads of information that send data from your application to your store. 2 | /// They are the only source of information for the store. 3 | /// 4 | /// An action is a plain object that represents an intention to change the state. 5 | /// Actions are the only way to get data into the store. 6 | /// Any data, whether from UI events, network callbacks, or other sources such as WebSockets needs to eventually be dispatched as actions. 7 | /// 8 | /// You send them to the store using `store.dispatch(action)`. 9 | public protocol Action {} 10 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Concurrency/Atomic.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// [Atomic](https://www.objc.io/blog/2018/12/18/atomic-variables/) wrapper to provide synchronized access to a variable. 4 | /// 5 | /// However this one is analogous to read-write queue but without overhead of lookup by dispatch specific key. 6 | /// Reads are concurrent, while writes are serial (using barrier). Both operations perform synchronously. 7 | internal final class Atomic { 8 | 9 | /// Private wrapped value. 10 | private var _value: T 11 | 12 | /// Synchronization queue. 13 | private let queue: DispatchQueue 14 | 15 | /// Concurrent synchronized getter for wrapped value. 16 | internal var value: T { 17 | return queue.sync { _value } 18 | } 19 | 20 | /// Initializes wrapper with initial value and unique identifier. 21 | /// 22 | /// - Parameters: 23 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `"swifty-redux.atomic"` 24 | /// - value: Value to wrap. 25 | internal init(id: String = "swifty-redux.atomic", value: T) { 26 | _value = value 27 | queue = DispatchQueue(label: id + ".queue", attributes: .concurrent) 28 | } 29 | 30 | /// Serial mutating function for wrapped value. 31 | /// Next time you call `value` after `mutate` function it will have updated value. 32 | /// 33 | /// - Parameters: 34 | /// - transform: A function to transform inout value. 35 | /// - value: Inout value to transform. 36 | /// 37 | /// - Remark: Performs synchronously. 38 | internal func mutate(_ transform: (_ value: inout T) -> Void) { 39 | queue.sync(flags: .barrier) { 40 | transform(&self._value) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Concurrency/ReadWriteQueue.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// Queue whose purpose is to solve [Readers-writers problem](https://en.wikipedia.org/wiki/Readers–writers_problem). 4 | /// 5 | /// Any amount of readers can access data at a time, but only one writer is allowed at a time: 6 | /// * You read concurrently and synchronously for caller with `read` method. 7 | /// * You write serially both asynchronously and synchronously with `write` and `writeAndWait` methods respectively. 8 | /// 9 | /// It's fine to have async write operations, and sync read operations. Write ops block queue and read ops are executed synchronously, 10 | /// so if we want ro read after writing, we'll still be waiting (reads are sync) for write ops to finish and allow read ops to execute. 11 | /// 12 | /// - Note: It's safe to call `read` inside `write` and `write` inside `write`, etc. 13 | /// If you're trying to execute task while being already on this queue, it will safely execute this task without deadlocks. 14 | internal final class ReadWriteQueue { 15 | 16 | /// Unique key to identify internal queue among others. 17 | private let specificKey = DispatchSpecificKey() 18 | 19 | /// Internal concurrent queue. 20 | private let queue: DispatchQueue 21 | 22 | /// Computed flag that tells whether we're already executing work on internal queue. 23 | private var isAlreadyInQueue: Bool { 24 | return DispatchQueue.getSpecific(key: specificKey) == queue.label 25 | } 26 | 27 | /// Initializes read-write queue with specified label. 28 | /// 29 | /// - Parameter label: Label to identify queue. 30 | internal init(label: String = "swifty-redux.read-write.queue") { 31 | queue = DispatchQueue(label: label, attributes: .concurrent) 32 | queue.setSpecific(key: specificKey, value: label) 33 | } 34 | 35 | deinit { 36 | queue.setSpecific(key: specificKey, value: nil) 37 | } 38 | 39 | /// Performs 'write' operation asynchronously 40 | /// by blocking the queue so that no other operations can be executed concurrently until it finishes. 41 | /// 42 | /// Basically - any async work, that doesn't require waiting for its completion or returning any result. 43 | /// If we're already working on this queue, `work` won't be enqued in the end of the queue but will be executed immediately. 44 | /// 45 | /// - Parameter work: Work to be executed. 46 | internal func write(_ work: @escaping () -> Void) { 47 | if isAlreadyInQueue { 48 | work() 49 | } else { 50 | queue.async(flags: .barrier, execute: work) 51 | } 52 | } 53 | 54 | /// Performs 'write' operation synchronously 55 | /// by blocking the queue so that no other operations can be executed concurrently until it finishes. 56 | /// 57 | /// Caller will wait until work is completed. 58 | /// If we're already working on this queue, there won't be any deadlocks and `work` will be executed immediately on this queue. 59 | /// 60 | /// - Parameter work: Work to be executed. 61 | internal func writeAndWait(_ work: @escaping () -> Void) { 62 | if isAlreadyInQueue { 63 | work() 64 | } else { 65 | queue.sync(flags: .barrier, execute: work) 66 | } 67 | } 68 | 69 | /// Performs 'read' operation synchronously by not blocking the queue so that many 'read' operations can be executed concurrently. 70 | /// 71 | /// Caller will wait until work is completed. 72 | /// If we're already working on this queue, there won't be any deadlocks and `work` will be executed immediately on this queue. 73 | /// 74 | /// - Parameter work: Work that returns value after it's executed. 75 | /// - Returns: Result of `work`. 76 | internal func read(_ work: () throws -> T) rethrows -> T { 77 | if isAlreadyInQueue { 78 | return try work() 79 | } else { 80 | return try queue.sync(execute: work) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Disposing/CompositeDisposable.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// A disposable that will dispose of any number of other disposables. 4 | public final class CompositeDisposable { 5 | 6 | /// Synchronization queue. 7 | private let queue: DispatchQueue 8 | 9 | /// Set of disposables. Duplicates will be ignored. 10 | private var disposables: Set? 11 | 12 | /// Treats disposables `nil` value as a sign of composite disposable being disposed. 13 | private var _isDisposed: Bool { 14 | return disposables == nil 15 | } 16 | 17 | /// Whether composite disposable has been disposed already. 18 | /// 19 | /// - Remark: Thread-safe. 20 | public var isDisposed: Bool { 21 | return queue.sync { _isDisposed } 22 | } 23 | 24 | /// Initializes a composite disposable containing the given set of disposables. 25 | /// 26 | /// - Parameters: 27 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 28 | /// - disposables: A set of disposables to hook up with composite disposable. 29 | internal init(id: String? = nil, disposables: Set) { 30 | self.queue = DispatchQueue(label: (id ?? "swifty-redux.composite-disposable") + ".queue", attributes: .concurrent) 31 | self.disposables = disposables 32 | } 33 | 34 | /// Initializes an empty composite disposable 35 | /// 36 | /// - Parameters: 37 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 38 | public convenience init(id: String? = nil) { 39 | self.init(id: id, disposables: Set()) 40 | } 41 | 42 | /// Initializes a composite disposable containing the given variadic parameter of disposables. 43 | /// 44 | /// - Parameters: 45 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 46 | /// - disposables: A variadic parameter of disposables to hook up with composite disposable. 47 | public convenience init(id: String? = nil, disposing disposables: Disposable...) { 48 | self.init(id: id, disposables: Set(disposables)) 49 | } 50 | 51 | /// Initializes a composite disposable containing the given array of disposables. 52 | /// 53 | /// - Parameters: 54 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 55 | /// - disposables: An array of disposables to hook up with composite disposable. 56 | public convenience init(id: String? = nil, disposing disposables: [Disposable]) { 57 | self.init(id: id, disposables: Set(disposables)) 58 | } 59 | 60 | /// Add the given disposable to the composite. 61 | /// 62 | /// If composite is already disposed, will dispose `disposible` synchronously on a caller queue. 63 | /// 64 | /// - Parameter disposable: A disposable. 65 | /// 66 | /// - Remark: Thread-safe 67 | public func add(_ disposable: Disposable) { 68 | let toDispose: Disposable? = queue.sync(flags: .barrier) { 69 | guard !_isDisposed else { 70 | return disposable 71 | } 72 | disposables?.insert(disposable) 73 | return nil 74 | } 75 | toDispose?.dispose() 76 | } 77 | 78 | /// Add the given disposables to the composite. 79 | /// 80 | /// If composite is already disposed, will dispose each one from `disposibles` synchronously on a caller queue. 81 | /// 82 | /// - Parameter disposables: An array of disposable. 83 | /// 84 | /// - Remark: Thread-safe 85 | public func add(_ disposables: [Disposable]) { 86 | let toDispose: [Disposable]? = queue.sync(flags: .barrier) { 87 | guard !_isDisposed else { 88 | return disposables 89 | } 90 | self.disposables?.formUnion(Set(disposables)) 91 | return nil 92 | } 93 | toDispose?.forEach { $0.dispose() } 94 | } 95 | 96 | /// Removes given disposable from contained set if it's there. Does nothing otherwise. 97 | /// 98 | /// - Parameter disposable: Disposable to remove. 99 | public func remove(_ disposable: Disposable) { 100 | _ = queue.sync(flags: .barrier) { 101 | disposables?.remove(disposable) 102 | } 103 | } 104 | 105 | /// Disposes all contained disposables. 106 | /// 107 | /// Removes all contained disposables and disposes each of them synchronously on a caller queue. 108 | /// 109 | /// - Remark: Thread-safe 110 | public func dispose() { 111 | let currentDisposables: Set? = queue.sync(flags: .barrier) { 112 | let currentDisposables = disposables 113 | disposables?.removeAll(keepingCapacity: false) 114 | disposables = nil 115 | return currentDisposables 116 | } 117 | currentDisposables?.forEach { $0.dispose() } 118 | } 119 | } 120 | 121 | extension CompositeDisposable { 122 | /// Adds the right-hand-side composite disposable to the left-hand-side composite disposable 123 | /// by wrapping it into Disposable. 124 | /// 125 | /// - Parameters: 126 | /// - lhs: Composite disposable to add to. 127 | /// - rhs: Composite disposable to add. 128 | /// - Returns: Disposable that can be used to remove the disposable (that binds rhs) later. 129 | @discardableResult 130 | public static func += (lhs: CompositeDisposable, rhs: CompositeDisposable) -> Disposable { 131 | let disposable = Disposable(action: rhs.dispose) 132 | lhs.add(disposable) 133 | return disposable 134 | } 135 | 136 | /// Adds the right-hand-side disposable to the left-hand-side composite disposable. 137 | /// 138 | /// - Parameters: 139 | /// - lhs: Composite disposable to add to. 140 | /// - rhs: Disposable to add. 141 | /// - Returns: Disposable that can be used to remove the disposable later. 142 | @discardableResult 143 | public static func += (lhs: CompositeDisposable, rhs: Disposable) -> Disposable { 144 | lhs.add(rhs) 145 | return rhs 146 | } 147 | 148 | /// Adds the right-hand-side disposable if exists to the left-hand-side composite disposable. 149 | /// If right-hand-side disposable is `nil`, nothing happens and `nil` is returned. 150 | /// 151 | /// - Parameters: 152 | /// - lhs: Composite disposable to add to. 153 | /// - rhs: Disposable to add. 154 | /// - Returns: Disposable that can be used to remove the disposable later. 155 | @discardableResult 156 | public static func += (lhs: CompositeDisposable, rhs: Disposable?) -> Disposable? { 157 | rhs.map(lhs.add) 158 | return rhs 159 | } 160 | 161 | /// Adds the right-hand-side action to the left-hand-side composite disposable. 162 | /// 163 | /// - Parameters: 164 | /// - lhs: Composite disposable to add to. 165 | /// - rhs: A closure to be invoked when the composite is disposed of. 166 | /// - Returns: Disposable that can be used to remove the disposable later. 167 | @discardableResult 168 | public static func += (lhs: CompositeDisposable, rhs: @escaping () -> Void) -> Disposable { 169 | let disposable = Disposable(action: rhs) 170 | lhs.add(disposable) 171 | return disposable 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Disposing/Disposable.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// Represents something that can be disposed, usually associated with freeing resources or canceling work. 4 | /// 5 | /// Initialize it with action to execute once disposal is needed. 6 | /// You can see whether action has been already executed with `isDisposed` flag. And it won't be executed twice. 7 | public final class Disposable { 8 | private static func queue(id: String?) -> DispatchQueue { 9 | return DispatchQueue(label: (id ?? "swifty-redux.disposable") + ".queue", attributes: .concurrent) 10 | } 11 | 12 | /// Synchronization queue. 13 | private let queue: DispatchQueue 14 | 15 | /// Disposing action. 16 | private var action: (() -> Void)? 17 | 18 | private var _isDisposed: Bool = false 19 | 20 | /// Whether this disposable has been disposed already. 21 | /// 22 | /// - Remark: Thread-safe. 23 | public var isDisposed: Bool { 24 | return queue.sync { _isDisposed } 25 | } 26 | 27 | /// Initializes a disposable which runs the given `action` upon disposal. 28 | /// 29 | /// - Parameters: 30 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 31 | /// - action: A closure to run when calling `dispose()`. 32 | public init(id: String? = nil, action: @escaping () -> Void) { 33 | self.queue = Disposable.queue(id: id) 34 | self.action = action 35 | } 36 | 37 | /// Disposes from any resources if not already disposed. 38 | /// 39 | /// Sets `isDisposed` to `true` and calls `action` afterwards. Calls `action` on a caller queue. 40 | /// Will not call `action` twice if it was already called and `isDisposed` is `true`. 41 | /// 42 | /// - Remark: Thread-safe. 43 | public func dispose() { 44 | let shouldRunAction: Bool = queue.sync(flags: .barrier) { 45 | guard !self._isDisposed else { return false } 46 | self._isDisposed = true 47 | return true 48 | } 49 | if shouldRunAction { 50 | action?() 51 | action = nil 52 | } 53 | } 54 | 55 | /// Creates no-op disposable with empty action which is already disposed. 56 | public static func nop() -> Disposable { 57 | return Disposable() 58 | } 59 | 60 | private init() { 61 | self.queue = DispatchQueue(label: "swifty-redux.nop-disposable.queue", attributes: .concurrent) 62 | self.action = nil 63 | self._isDisposed = true 64 | } 65 | } 66 | 67 | extension Disposable: Hashable { 68 | public func hash(into hasher: inout Hasher) { 69 | return hasher.combine(ObjectIdentifier(self)) 70 | } 71 | 72 | public static func == (lhs: Disposable, rhs: Disposable) -> Bool { 73 | return lhs === rhs 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Middleware.swift: -------------------------------------------------------------------------------- 1 | /// Returns state. 2 | public typealias GetState = () -> State? 3 | 4 | /// Dispatches action. 5 | public typealias Dispatch = (Action) -> Void 6 | 7 | /// [Middleware](https://redux.js.org/advanced/middleware) provides a third-party extension point between dispatching an action, 8 | /// and the moment it reaches the reducer. 9 | /// 10 | /// It allows you to perform side-effects and async tasks. 11 | /// The best feature of middleware is that it's composable in a chain. 12 | /// You can use multiple independent middleware in a single store without one knowing about another. 13 | /// 14 | /// - Parameters: 15 | /// - getState: Returns current state if store is alive, otherwise - nil. 16 | /// - dispatch: Dispatches given action to the store. Performs asynchronously when used in store. 17 | /// The action will actually travel the whole middleware chain again, including the current middleware. 18 | /// - next: Dispatches given action further in the middleware chain. Performs synchronously when used in store. 19 | /// Middleware can decide not to propagate action further by not calling `next` function, 20 | /// however it's better if action travels the whole way back to the store. 21 | /// - Returns: Dispatch function for the next middleware. Here you can sniff all actions that come from the store to the reducer. 22 | /// And dispatch new ones based on what you receive. 23 | /// The very last participant of this chain is store's reducer. Store takes care of handling all of this. 24 | /// 25 | /// - Warning: Be careful, as this can cause an **infinite loop**: 26 | /// ``` 27 | /// let middleware: Middleware = createMiddleware { getState, dispatch, next in 28 | /// return { action in 29 | /// dispatch(action) 30 | /// } 31 | /// } 32 | /// ``` 33 | public typealias Middleware = ( 34 | _ getState: @escaping GetState, 35 | _ dispatch: @escaping Dispatch, 36 | _ next: @escaping Dispatch 37 | ) -> Dispatch 38 | 39 | /// Chains array of middlewares into single middleware. Each middleware will be processed in the same order as it's stored in the array. 40 | /// 41 | /// - Parameter middleware: Array of middleware to chain into single one. 42 | /// - Returns: Resulting middleware 43 | public func applyMiddleware(_ middleware: [Middleware]) -> Middleware { 44 | return { getState, dispatch, next in 45 | return middleware 46 | // array is reversed to form a call-stack. Thus who's put there first, will process action last. 47 | .reversed() 48 | .reduce(next) { result, current in 49 | current(getState, dispatch, result) 50 | } 51 | } 52 | } 53 | 54 | /// Creates middleware which propagates action to the next one automatically right after current one handles action. 55 | /// You'll need such behaviour in most cases. 56 | /// 57 | /// - Parameters: 58 | /// - middleware: Middleware without `next` argument. 59 | /// - getState: Returns current state if store is alive, otherwise - nil. 60 | /// - dispatch: Dispatches given action to the store. Performs asynchronously when used in store. 61 | /// The action will actually travel the whole middleware chain again, including the current middleware. 62 | /// - Returns: Resulting middleware 63 | public func createFallThroughMiddleware( 64 | _ middleware: @escaping (_ getState: @escaping GetState, _ dispatch: @escaping Dispatch) -> Dispatch 65 | ) -> Middleware { 66 | return { getState, dispatch, next in 67 | let current = middleware(getState, dispatch) 68 | return { action in 69 | current(action) 70 | return next(action) 71 | } 72 | } 73 | } 74 | 75 | /// Just and identity function for middleware. 76 | /// Can be used to keep code consistent when creating different kinds of middleware. 77 | /// 78 | /// - Parameter middleware: Any middleware. 79 | /// - Returns: Same middleware without any changes made to it. 80 | public func createMiddleware(_ middleware: @escaping Middleware) -> Middleware { 81 | return middleware 82 | } 83 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Observing/Observable.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// Observable represents a push style sequence. 4 | /// 5 | /// Subscribe to value updates using `subscribe` method and stop listening updates using disposable. 6 | /// All observers are removed and all disposables are disposed when observable dies. 7 | public final class Observable { 8 | 9 | /// Unique identifier, used to mark internal atomic observers set, composite disposable and observers' disposables. 10 | private let id: String 11 | 12 | /// Composite disposable to hold observers' disposables. 13 | private let disposables: CompositeDisposable 14 | 15 | /// Atomic set of observers. 16 | private let observers: Atomic>> 17 | 18 | /// Initializes with unique id and an observable manipulation function. 19 | /// 20 | /// - Parameters: 21 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 22 | /// - observable: A function to manipulate observable and send values here from the caller. 23 | /// Receives callback with value updates, returns disposable to stop observing value updates. 24 | /// Can be used for direct observable implementation, as well as for observables binding. 25 | /// - handler: A function to send values from the caller. 26 | /// - value: Value to send. 27 | public init(id: String? = nil, observable: (_ handler: @escaping (_ value: Value) -> Void) -> Disposable?) { 28 | self.id = id ?? "swifty-redux.observable" 29 | self.observers = Atomic(id: self.id, value: Set()) 30 | self.disposables = CompositeDisposable(id: "\(self.id).composite-disposable") 31 | self.disposables += observable { value in 32 | let currentObservers = self.observers.value 33 | currentObservers.forEach { observer in observer.update(value) } 34 | } 35 | } 36 | 37 | /// Initializes with unique id and an observable. 38 | /// 39 | /// - Parameters: 40 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 41 | /// - observable: Another observable to bind to. 42 | public convenience init(id: String? = nil, observable: Observable) { 43 | self.init(id: id, observable: { observable.subscribe(observer: $0) }) 44 | } 45 | 46 | /// Subscribes a value update observer. 47 | /// 48 | /// You can stop listening to updates by calling `dispose()` on returned disposable. 49 | /// Once disposable is disposed, it will be removed from composite disposable and observer will be removed as well. 50 | /// 51 | /// - Parameters: 52 | /// - observingQueue: A queue on which to asynchronously receive updates. Defaults to `nil`. 53 | /// - observer: Callback that will receive new values. 54 | /// - value: New value. 55 | @discardableResult 56 | public func subscribe(on observingQueue: DispatchQueue? = nil, observer: @escaping (_ value: Value) -> Void) -> Disposable { 57 | let observer = Observer(queue: observingQueue, update: observer) 58 | return subscribe(observer: observer) 59 | } 60 | 61 | /// Subscribes a value update observer. 62 | /// 63 | /// You can stop listening to updates by calling `dispose()` on returned disposable. 64 | /// Once disposable is disposed, it will be removed from composite disposable and observer will be removed as well. 65 | /// 66 | /// - Parameters: 67 | /// - observer: Value observer. 68 | @discardableResult 69 | public func subscribe(observer: Observer) -> Disposable { 70 | observers.mutate { $0.insert(observer) } 71 | var disposable: Disposable! 72 | disposable = Disposable(id: "\(id).disposable") { [weak self, weak observer] in 73 | guard let strongSelf = self, let observer = observer else { return } 74 | strongSelf.observers.mutate { $0.remove(observer) } 75 | // kinda optimization not to store all the disposables (in case this Observable is intended for Store) 76 | disposable.map(strongSelf.disposables.remove) 77 | } 78 | return disposables += disposable 79 | } 80 | 81 | deinit { 82 | disposables.dispose() 83 | observers.mutate { $0.removeAll() } 84 | } 85 | } 86 | 87 | extension Observable { 88 | 89 | /// Create an observable that will be controlled by sending values to an input observer. 90 | /// 91 | /// - Parameters: 92 | /// - id: Unique identifier. Mostly used for internal queue label and debugging purposes. Defaults to `nil`. 93 | /// - queue: A queue on which to asynchronously receive updates inside observable. Defaults to `nil`. 94 | /// - disposable: An optional disposable to associate with the observable, and to be disposed of when observable dies. 95 | /// - Returns: A 2-tuple of the output end of the pipe as `Observable`, and the input end of the pipe as `Observer`. 96 | /// 97 | /// - Note: It's recomended to use this method when creating `Observable` as it simplifies things 98 | /// by providing both input to send updates to and output to listen to these updates. 99 | public static func pipe(id: String? = nil, queue: DispatchQueue? = nil, disposable: Disposable? = nil) -> (output: Observable, input: Observer) { 100 | var observer: Observer! 101 | let observable = Observable(id: id) { action in 102 | observer = Observer(queue: queue, update: action) 103 | return disposable 104 | } 105 | return (observable, observer) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Observing/Observer.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// An Observer is a simple wrapper around a function which can receive Values on a given queue. 4 | public final class Observer { 5 | 6 | /// A handler to send values to the listener. 7 | public let update: (_ value: Value) -> Void 8 | 9 | /// Initializes observer with a `queue` to receive updates on, and `update` callback to receive values. 10 | /// 11 | /// - Parameters: 12 | /// - queue: A queue on which to asynchronously receive updates. 13 | /// If `nil`, updates will be received on a caller queue. Defaults to `nil`. 14 | /// - update: Callback that will receive new values. 15 | /// - value: New value. 16 | public init(queue: DispatchQueue? = nil, update: @escaping (_ value: Value) -> Void) { 17 | guard let queue = queue else { 18 | self.update = update 19 | return 20 | } 21 | self.update = { value in 22 | queue.async { 23 | update(value) 24 | } 25 | } 26 | } 27 | } 28 | 29 | extension Observer: Hashable { 30 | public func hash(into hasher: inout Hasher) { 31 | hasher.combine(ObjectIdentifier(self)) 32 | } 33 | 34 | public static func == (lhs: Observer, rhs: Observer) -> Bool { 35 | return lhs === rhs 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Reducer.swift: -------------------------------------------------------------------------------- 1 | /// [Reducers](https://redux.js.org/basics/reducers) specify how the application's state changes in response to actions sent to the store. 2 | /// Remember that actions only describe what happened, but don't describe how the application's state changes. 3 | /// 4 | /// It's the only place that can change application or domain state. 5 | /// Reducers are pure functions (there should be **no side effects**) that return new state depending on action and previous state. 6 | /// They can be nested and combined together. 7 | /// And it's better if they are split into smaller reducers that are focused on a small domain state. 8 | /// 9 | /// - Parameters: 10 | /// - state: Current state as `inout` argument. Will be modified after applying action to it. 11 | /// - action: Incoming action. 12 | public typealias Reducer = (_ state: inout State, _ action: Action) -> Void 13 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Core/Store.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// [Store](https://redux.js.org/basics/store) is an object that holds the application's state tree. 4 | /// 5 | /// Store has the following responsibilities: 6 | /// - Holds application state; 7 | /// - Allows access to state via `state`; 8 | /// - Allows state to be updated via `dispatch(action)` or `dispatchAndWait(action)`; 9 | /// - Registers listeners via `subscribe(observer)`; 10 | /// - Handles unregistering of listeners via the `Disposable` returned by `subscribe(observer)`. 11 | /// 12 | /// It's important to note that you'll only have a single store in a Redux application. 13 | /// When you want to split your data handling logic, you'll use reducer composition instead of many stores. 14 | /// 15 | /// There are many ways to share store inside your app: 16 | /// - Keep global reference to it 17 | /// - Inject it from top to bottom where it's needed 18 | /// - Share only some of its functions like `dispatch(action)` and `subscribe(observer)` 19 | /// 20 | /// You initialize store with initial state, main reducer and an array of middleware. 21 | /// Store has its own queue for managing observers and state changes through reducers in a serial way. 22 | public final class Store { 23 | 24 | /// Main reducer 25 | private let reducer: Reducer 26 | 27 | /// Synchronization read-write queue 28 | private let queue: ReadWriteQueue 29 | 30 | /// State updates observer and observable. 31 | /// - `Observer` is notified every time state changes and updates its `observable`. 32 | /// - `Observable` is used to update observers that subscribe to listen to state changes. 33 | private let (observable, observer): (Observable, Observer) 34 | 35 | /// Function that is used to dispatch an action. 36 | /// It passes action to the middleware, then - to the main reducer, and finally sets state provided by reducer and notifies observers. 37 | /// 38 | /// Action => Middleware => Reducers => Set State & Notify Observers 39 | private var dispatchFunction: Dispatch! 40 | 41 | /// Current state private property that we operate on inside store. 42 | private var currentState: State 43 | 44 | /// Current state. 45 | /// 46 | /// [State](https://redux.js.org/glossary#state) (also called the state tree) is a broad term, 47 | /// but in the Redux API it usually refers to the single state value that is managed by the store. 48 | /// It represents the entire state of a Redux application, which is often a deeply nested object. 49 | /// 50 | /// - Remark: Thread-safe. 51 | public var state: State { 52 | return queue.read { currentState } 53 | } 54 | 55 | /// Initializes store with initial state, main reducer and optionally - unique identifier and array of middleware. 56 | /// 57 | /// - Parameters: 58 | /// - id: Unique identifier. Mostly used for internal queues labels and debugging purposes. Defaults to `"swifty-redux.store"` 59 | /// - state: Initial state. 60 | /// - reducer: Main reducer. Unlike middleware, reducers are already nested, thus it's not an array. 61 | /// - middleware: Array of middlewares. No need to reduce them into single one before. Defaults to empty array. 62 | public init(id: String = "swifty-redux.store", state: State, reducer: @escaping Reducer, middleware: [Middleware] = []) { 63 | self.queue = ReadWriteQueue(label: "\(id).queue") 64 | self.currentState = state 65 | self.reducer = reducer 66 | 67 | (observable, observer) = Observable.pipe(id: "\(id).observable") 68 | 69 | dispatchFunction = applyMiddleware(middleware)( 70 | { [weak self] in self?.state }, 71 | { [weak self] in self?.dispatch($0) }, 72 | { [weak self] in self?.defaultDispatch(from: $0) } 73 | ) 74 | } 75 | 76 | /// Dispatches an action which travels through middleware to reducers and finally change state. 77 | /// This is the only way to trigger a state change. 78 | /// 79 | /// - Parameter action: Action to dispatch 80 | /// 81 | /// - Remark: Performs asynchronously. 82 | public func dispatch(_ action: Action) { 83 | queue.write { 84 | self.dispatchFunction(action) 85 | } 86 | } 87 | 88 | /// Dispatches an action which travels through middleware to reducers and finally change state. 89 | /// This is the only way to trigger a state change. 90 | /// 91 | /// - Parameter action: Action to dispatch 92 | /// 93 | /// - Remark: Performs synchronously. 94 | public func dispatchAndWait(_ action: Action) { 95 | queue.writeAndWait { 96 | self.dispatchFunction(action) 97 | } 98 | } 99 | 100 | /// Dispatch function that mutates state with main reducer and notifies observers 101 | /// 102 | /// - Parameter action: Action to dispatch 103 | /// 104 | /// - Remark: Performs synchronously. 105 | private func defaultDispatch(from action: Action) { 106 | queue.writeAndWait { 107 | self.reducer(&self.currentState, action) 108 | self.observer.update(self.currentState) 109 | } 110 | } 111 | 112 | /// Subscribes a state update observer. 113 | /// It will be called any time an action is dispatched, and some part of the state tree may potentially have changed. 114 | /// You can stop listening to updates by calling `dispose()` on returned disposable. 115 | /// 116 | /// - Parameters: 117 | /// - queue: A queue on which observer wants to receive updates. If `nil`, observer will be called on internal queue. Defaults to `nil`. 118 | /// - includingCurrentState: If `true`, observer will immediately receive current state 119 | /// (before creating and returning Disposable) and further updates as they appear. 120 | /// If `false`, observer will only receive further updates as they appear. Defaults to `true`. 121 | /// - observer: Observer callback that will receive new state after each update until it's manually disposed or store's dead. 122 | /// - state: Current state right after it's changed. 123 | /// - Returns: Disposable to stop listening to updates. 124 | /// Its `isDisposed` property will be `true` when store dies and cancels all subscriptions by itself. 125 | @discardableResult 126 | public func subscribe(on queue: DispatchQueue? = nil, includingCurrentState: Bool = true, observer: @escaping (_ state: State) -> Void) -> Disposable { 127 | let observer = Observer(queue: queue, update: observer) 128 | if includingCurrentState { 129 | observer.update(state) 130 | } 131 | return observable.subscribe(observer: observer) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Epics/Epics.swift: -------------------------------------------------------------------------------- 1 | import SwiftyRedux 2 | import ReactiveSwift 3 | 4 | /// [Epic](https://redux-observable.js.org/docs/basics/Epics.html) is a function which takes a stream of actions 5 | /// and returns a stream of actions. Basically - async pipe. **Actions in, actions out.** 6 | /// 7 | /// While you'll most commonly produce actions out in response to some action you received in, that's not actually a requirement. 8 | /// Once you're inside your Epic, use any Observable patterns you desire as long as anything output from the final, 9 | /// returned stream, is an action. 10 | /// 11 | /// Epics run alongside the normal Redux dispatch channel, *after* the reducers have already received them – 12 | /// so you cannot "swallow" an incoming action. Actions always run through your reducers **before** your Epics even receive them. 13 | /// 14 | /// Inspired by [redux-observable](https://redux-observable.js.org) and built on top of 15 | /// [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift). 16 | /// 17 | /// - Parameters: 18 | /// - actions: Signal of incoming actions that never fails. 19 | /// - state: State read-only reactive property. Basically - stream of state updates + its current value. 20 | /// - Returns: Signal of outcoming actions that never fails. 21 | /// 22 | /// - Attention: In order to not fall into stack overflow when synchronously producing output actions, 23 | /// we're using scheduler that does it asynchronously. 24 | /// Thus it can break order sometimes in which actions should have been dispatched naturally. 25 | /// 26 | /// For example, imagine we're sending these actions one after another in a row: 27 | /// ``` 28 | /// dispatch(One()) 29 | /// dispatch(Two()) 30 | /// dispatch(Five()) 31 | /// ``` 32 | /// If we had an epic that sends actions `Three()` and `Four()` when it receives action `Two()`, 33 | /// there would be no guarantee that they would be dispatched in a correct order: 34 | /// ``` 35 | /// One(), Two(), Three(), Four(), Five() 36 | /// ``` 37 | public typealias Epic = (_ actions: Signal, _ state: Property) -> Signal 38 | 39 | /// Creates middleware with a single epic. 40 | /// Epic will receive action only after it has travelled through other middlewares and reducers, 41 | /// so that it can't "swallow" or delay it. 42 | /// 43 | /// You'd basically use this function along with `combineEpics` to pass multiple epics as a middleware to the store. 44 | /// 45 | /// - Parameter epic: Epic that should be wrapped into middleware. 46 | /// - Returns: Middleware wrapping epic. 47 | public func createEpicMiddleware(_ epic: @escaping Epic) -> Middleware { 48 | return { getState, dispatch, next in 49 | guard let initialState = getState() else { return next } 50 | 51 | let queueScheduler = QueueScheduler(qos: .default, name: "swifty-redux.epic.queue-scheduler") 52 | let state = MutableProperty(initialState) 53 | let (actionsSignal, actionsObserver) = Signal.pipe() 54 | 55 | epic(actionsSignal, Property(state)) 56 | .observe(on: queueScheduler) 57 | .observeValues(dispatch) 58 | 59 | return { action in 60 | // Downstream middleware gets the action first, 61 | // which includes their reducers, so state is 62 | // updated before epics receive the action 63 | next(action) 64 | 65 | // It's important to update the `state` before we emit 66 | // the action because otherwise it would be stale 67 | guard let currentState = getState() else { return } 68 | 69 | state.value = currentState 70 | actionsObserver.send(value: action) 71 | } 72 | } 73 | } 74 | 75 | /// Combines many epics into a single one. 76 | /// 77 | /// You'd basically use this function along with `createEpicMiddleware` to pass multiple epics as a middleware to the store. 78 | /// 79 | /// - Parameter epics: Array of epics to combine. 80 | /// - Returns: Combined epic. 81 | public func combineEpics(_ epics: [Epic]) -> Epic { 82 | return { actions, state in 83 | Signal.merge(epics.map { $0(actions, state) }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/ReactiveExtensions/ReactiveExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftyRedux 2 | import ReactiveSwift 3 | 4 | public extension Signal where Value == SwiftyRedux.Action, Error == Never { 5 | func ofType(_ type: T.Type) -> Signal { 6 | return compactMap { value in 7 | value as? T 8 | } 9 | } 10 | } 11 | 12 | public extension Observable { 13 | 14 | /// - Returns: A ReactiveSwift.Signal which never fails that sends values of `self`. 15 | func toSignal() -> Signal { 16 | return Signal { observer, lifetime in 17 | let disposable = self.subscribe(observer: observer.send) 18 | lifetime.observeEnded(disposable.dispose) 19 | } 20 | } 21 | } 22 | 23 | public extension Signal where Error == Never { 24 | 25 | /// - Returns: An observable that sends values of `self`. 26 | func toObservable() -> Observable { 27 | return Observable { update in 28 | self.observeValues(update).map(Disposable.init) 29 | } 30 | } 31 | } 32 | 33 | public extension ObservableProducer { 34 | 35 | /// - Returns: A ReactiveSwift.SignalProducer which never fails that, when started, will send values of `self.` 36 | func toSignalProducer() -> SignalProducer { 37 | return SignalProducer { observer, lifetime in 38 | let disposable = self.start(observer: observer.send) 39 | lifetime.observeEnded(disposable.dispose) 40 | } 41 | } 42 | } 43 | 44 | public extension SignalProducer where Error == Never { 45 | 46 | /// - Returns: An ObservableProducer that, when started, will send values of `self.` 47 | func toObservableProducer() -> ObservableProducer { 48 | return ObservableProducer { observer, disposables in 49 | disposables += Disposable(disposable: self.startWithValues(observer.update)) 50 | } 51 | } 52 | } 53 | 54 | public extension SwiftyRedux.Disposable { 55 | 56 | /// Initializes a Disposable with ReactiveSwift.Disposable, that will be disposed when passed `disposable` is. 57 | /// If `disposable` is already disposed, it will be disposed as well. 58 | /// 59 | /// - Parameter disposable: ReactiveSwift.Disposable to hook up to. 60 | convenience init(disposable: ReactiveSwift.Disposable) { 61 | self.init(action: disposable.dispose) 62 | if disposable.isDisposed { 63 | dispose() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/SideEffects/SideEffects.swift: -------------------------------------------------------------------------------- 1 | /// Side effect is a function that allows you read store's state, dispatch actions and receive actions. 2 | /// You can perform any async logic here like making network calls, handling a socket connection or talking to a Database. 3 | /// You can combine many side effects in a single middleware. 4 | /// 5 | /// Inspired by [redux-observable](https://redux-observable.js.org).[Epics](https://redux-observable.js.org/docs/basics/Epics.html) 6 | /// but have more imperative nature. 7 | /// Same as Epics, side effects are run **after** reducers have received and processed the action, 8 | /// so that, they can't "swallow" or delay it. 9 | /// 10 | /// You'd use `combineSideEffects` along with `createSideEffectMiddleware` to pass multiple side effects as a middleware to the store. 11 | /// 12 | /// let combinedSideEffect = combineSideEffects( 13 | /// networkMonitoringSideEffect(), 14 | /// databaseSideEffect(), 15 | /// gRPCStreamingSideEffect() 16 | /// ) 17 | /// let middleware = createSideEffectMiddleware(combinedSideEffect) 18 | /// 19 | /// - Parameters: 20 | /// - getState: Returns current state if store is alive, otherwise - nil. 21 | /// - dispatch: Dispatches given action to the store. Performs asynchronously when used in store. 22 | /// The action will travel the whole middleware chain again, including the current middleware and all the side effects. 23 | /// - Returns: Dispatch function where side effect receives actions. 24 | public typealias SideEffect = (_ getState: @escaping GetState, _ dispatch: @escaping Dispatch) -> Dispatch 25 | 26 | /// Creates middleware with a single side effect. 27 | /// Side effect will receive action only after it has travelled through other middlewares and reducers, 28 | /// so that it can't "swallow" or delay it. 29 | /// 30 | /// You'd basically use this function along with `combineSideEffects` to pass multiple side effects as a middleware to the store. 31 | /// 32 | /// - Parameter sideEffect: Side effect that should be wrapped into middleware. 33 | /// - Returns: Middleware wrapping side effect. 34 | public func createSideEffectMiddleware(_ sideEffect: @escaping SideEffect) -> Middleware { 35 | return { getState, dispatch, next in 36 | let sideEffectDispatch = sideEffect(getState, dispatch) 37 | return { action in 38 | next(action) 39 | sideEffectDispatch(action) 40 | } 41 | } 42 | } 43 | 44 | /// Combines many side effects into single one. Accepts more than one side effects, otherwise it doesn't make sense. 45 | /// 46 | /// You'd basically use this function along with `createSideEffectMiddleware` to pass multiple side effects as a middleware to the store. 47 | /// 48 | /// - Parameters: 49 | /// - first: Initial side effect. 50 | /// - second: Second side effect. 51 | /// - rest: A variadic parameter of the rest of side effects to combine into one. 52 | /// - Returns: Combined side effect. 53 | public func combineSideEffects( 54 | _ first: @escaping SideEffect, 55 | _ second: @escaping SideEffect, 56 | _ rest: SideEffect... 57 | ) -> SideEffect { 58 | let sideEffects = [first, second] + rest 59 | return { getState, dispatch in 60 | let dispatches = sideEffects.map { $0(getState, dispatch) } 61 | return { action in 62 | dispatches.forEach { dispatch in dispatch(action) } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Steroids/Observable+Steroids.swift: -------------------------------------------------------------------------------- 1 | /// Observable extensions for value transformations. 2 | 3 | extension Observable { 4 | 5 | /// Maps each value in the observable to a new value. 6 | /// 7 | /// - Parameter transform: A closure that accepts a value and returns a new value. 8 | /// - Returns: An observable that will send new values. 9 | public func map(_ transform: @escaping (Value) -> T) -> Observable { 10 | return Observable { action in 11 | return self.subscribe { value in 12 | action(transform(value)) 13 | } 14 | } 15 | } 16 | 17 | /// Preserves only values which pass the given closure. 18 | /// 19 | /// - Parameter predicate: A closure to determine whether a value from `self` should be included in the returned observable. 20 | /// - Returns: An observable that forwards the values passing the given closure. 21 | public func filter(_ predicate: @escaping (Value) -> Bool) -> Observable { 22 | return Observable { action in 23 | return self.subscribe { value in 24 | if predicate(value) { 25 | action(value) 26 | } 27 | } 28 | } 29 | } 30 | 31 | /// Applies `transform` to values from observable and forwards values with non `nil` results unwrapped. 32 | /// 33 | /// - Parameter transform: A closure that accepts a value and returns a new optional value. 34 | /// - Returns: An observable that will send new values, that are non `nil` after the transformation. 35 | public func filterMap(_ transform: @escaping (Value) -> T?) -> Observable { 36 | return Observable { action in 37 | return self.subscribe { value in 38 | transform(value).map(action) 39 | } 40 | } 41 | } 42 | 43 | /// Forwards only values that are not considered equivalent to its immediately preceding value. 44 | /// 45 | /// - Parameter isEquivalent: A closure to determine whether two values (previous and current) are equivalent. 46 | /// - Returns: An observable which conditionally forwards values from `self`. 47 | /// 48 | /// - Note: The first value is always forwarded. 49 | public func skipRepeats(_ isEquivalent: @escaping (Value, Value) -> Bool) -> Observable { 50 | return Observable { action in 51 | var previous: Value? 52 | return self.subscribe { value in 53 | if let previous = previous, isEquivalent(previous, value) { 54 | return 55 | } 56 | previous = value 57 | action(value) 58 | } 59 | } 60 | } 61 | 62 | /// Skips first `count` number of values then act as usual. 63 | /// 64 | /// - Parameter count: A number of values to skip. 65 | /// - Returns: An observable that will skip the first `count` values, then forward everything afterward. 66 | /// 67 | /// - Precondition: `count` must be non-negative number. 68 | public func skip(first count: Int) -> Observable { 69 | precondition(count > 0) 70 | 71 | return Observable { action in 72 | var skipped = 0 73 | return self.subscribe { value in 74 | if skipped < count { 75 | skipped += 1 76 | } else { 77 | action(value) 78 | } 79 | } 80 | } 81 | } 82 | 83 | /// Does not forward any value from `self` until `predicate` returns `false`, 84 | /// at which point the returned observable starts to forward values from `self`, including the one leading to the toggling. 85 | /// 86 | /// - Parameter predicate: A closure to determine whether the skipping should continue. 87 | /// - Returns: An observable which conditionally forwards values from `self`. 88 | public func skip(while predicate: @escaping (Value) -> Bool) -> Observable { 89 | return Observable { action in 90 | var isSkipping = true 91 | return self.subscribe { value in 92 | isSkipping = isSkipping && predicate(value) 93 | if !isSkipping { 94 | action(value) 95 | } 96 | } 97 | } 98 | } 99 | 100 | /// Takes up to `n` values from the observable and then dispose. 101 | /// 102 | /// - Parameter count: A number of values to take from the observable. 103 | /// - Returns: An observable that will yield the first `count` values from `self` 104 | /// 105 | /// - Precondition: `count` must be non-negative number. 106 | public func take(first count: Int) -> Observable { 107 | precondition(count > 0) 108 | 109 | return Observable { action in 110 | var taken = 0 111 | var disposable: Disposable! 112 | disposable = self.subscribe { value in 113 | if taken < count { 114 | taken += 1 115 | action(value) 116 | } else { 117 | disposable.dispose() 118 | } 119 | } 120 | return disposable 121 | } 122 | } 123 | 124 | /// Forwards any values from `self` until `predicate` returns `false`, at which point the returned observable would be disposed. 125 | /// 126 | /// - Parameter predicate: A closure to determine whether the forwarding of values should continue. 127 | /// - Returns: An observable which conditionally forwards values from `self`. 128 | public func take(while predicate: @escaping (Value) -> Bool) -> Observable { 129 | return Observable { action in 130 | var disposable: Disposable! 131 | disposable = self.subscribe { value in 132 | if predicate(value) { 133 | action(value) 134 | } else { 135 | disposable.dispose() 136 | } 137 | } 138 | return disposable 139 | } 140 | } 141 | 142 | /// Forwards value from `self` with history: 143 | /// values of the returned observable are tuples whose first member is the previous value 144 | /// and whose second member is the current value. 145 | /// `initial` is supplied as the first member when `self` sends its first value. 146 | /// If `initial` is `nil` the returned observable would not emit any tuple until it has received at least two values. 147 | /// 148 | /// - Parameter initial: A value that will be combined with the first value sent by `self`. Defaults to nil. 149 | /// - Returns: An observable that sends tuples that contain previous and current sent values of `self`. 150 | public func combinePrevious(initial: Value? = nil) -> Observable<(Value, Value)> { 151 | return Observable<(Value, Value)> { action in 152 | var previous = initial 153 | return self.subscribe { value in 154 | if let previous = previous { 155 | action((previous, value)) 156 | } 157 | previous = value 158 | } 159 | } 160 | } 161 | } 162 | 163 | extension Observable where Value: Equatable { 164 | 165 | /// Forwards only values from `self` that are not equal to its immediately preceding value. 166 | /// 167 | /// - Returns: An observable which conditionally forwards values from `self`. 168 | /// 169 | /// - Note: The first value is always forwarded. 170 | public func skipRepeats() -> Observable { 171 | return skipRepeats(==) 172 | } 173 | } 174 | 175 | extension Observable { 176 | 177 | /// Maps each value in the observable to a new value by applying a key path. 178 | /// 179 | /// - Parameter keyPath: A key path relative to the observable's `Value` type. 180 | /// - Returns: An observable that will send new values. 181 | public func map(_ keyPath: KeyPath) -> Observable { 182 | return map { $0[keyPath: keyPath] } 183 | } 184 | 185 | /// Preserves only values by applying a key path whose value is `true`. 186 | /// 187 | /// - Parameter keyPath: A key path relative to the observable's `Value` type. 188 | /// - Returns: An observable that forwards the values passing the given closure. 189 | public func filter(_ keyPath: KeyPath) -> Observable { 190 | return filter { $0[keyPath: keyPath] } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Steroids/ObservableProducer.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /// An ObservableProducer creates Observables that can produce values of type `Value`. 4 | /// 5 | /// Observable producers do not do anything by themselves - work begins only when an observable is produced. 6 | /// They can be used to represent operations or tasks, like network requests, 7 | /// where each invocation of `start()` will create a new underlying operation. 8 | /// This ensures that consumers will receive the results, versus a plain Observable, 9 | /// where the results might be sent before any observers are attached. 10 | /// 11 | /// Inspired by [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift). 12 | /// [SignalProducer](https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/APIContracts.md#the-signalproducer-contract) 13 | public final class ObservableProducer { 14 | private let startHandler: (Observer, CompositeDisposable) -> Void 15 | 16 | /// Initializes an `ObservableProducer` which invokes the supplied starting side 17 | /// effect once upon the creation of every produced `Observable`, or in other 18 | /// words, for every invocation of `startWithObservable()`, `start()` and their convenience shorthands. 19 | /// 20 | /// The supplied starting side effect would be given: 21 | /// 1. an input `Observer` to emit values to the produced `Observable`; 22 | /// 2. a `CompositeDisposable` to bind resources to the lifetime of the produced `Observable`. 23 | /// 24 | /// - Parameter startHandler: The starting side effect. 25 | public init(_ startHandler: @escaping (Observer, CompositeDisposable) -> Void) { 26 | self.startHandler = startHandler 27 | } 28 | 29 | /// Initializes a producer for an observable that immediately sends one value and nothing more. 30 | /// 31 | /// This initializer differs from `init(value:)` in that its sole `value` is constructed lazily 32 | /// by invoking the supplied `action` when the `ObservableProducer` is started. 33 | /// 34 | /// - Parameter action: An action that yields a value to be sent by the `Observable`. 35 | public convenience init(_ action: @escaping () -> Value) { 36 | self.init { observer, disposables in 37 | observer.update(action()) 38 | } 39 | } 40 | 41 | /// Initializes a producer for an `Observable` that will immediately send one value and nothing more. 42 | /// 43 | /// - Parameter value: A value that should be sent by the `Observable`. 44 | public convenience init(_ value: Value) { 45 | self.init { observer, disposables in 46 | observer.update(value) 47 | } 48 | } 49 | 50 | /// Creates an `Observable` from `self`, and observe the `Observable` for all values being emitted. 51 | /// 52 | /// - Parameters: 53 | /// - observingQueue: A queue on which to asynchronously receive updates. Defaults to `nil`. 54 | /// - observer: A closure to be invoked with values from the produced `Observable`. 55 | /// - Returns: A disposable to interrupt the produced `Observable`. 56 | @discardableResult 57 | public func start(on observingQueue: DispatchQueue? = nil, observer: @escaping (Value) -> Void) -> Disposable { 58 | var disposable: Disposable! 59 | startWithObservable { observable, innerDisposables in 60 | innerDisposables += observable.subscribe(on: observingQueue, observer: observer) 61 | disposable = Disposable(action: innerDisposables.dispose) 62 | } 63 | return disposable 64 | } 65 | 66 | /// Creates an `Observable` from `self`, pass it into the given closure, and start the 67 | /// associated work on the produced `Observable` as the closure returns. 68 | /// 69 | /// - Parameter setup: A closure to be invoked before the work associated with the produced `Observable` commences. 70 | /// Both the produced `Observable` and an interrupt handle of the observable would be passed to the closure. 71 | public func startWithObservable(_ setup: (Observable, CompositeDisposable) -> Void) { 72 | let disposables = CompositeDisposable() 73 | let (observable, observer) = Observable.pipe(disposable: Disposable(action: disposables.dispose)) 74 | setup(observable, disposables) 75 | startHandler(observer, disposables) 76 | } 77 | 78 | /// Lifts an unary observable operator to operate upon observable producer instead. 79 | /// 80 | /// In other words, this will create a new `ObservableProducer` which will apply the given `Observable` operator 81 | /// to *every* created `Observable`, just as if the operator had been applied to each `Observable` yielded from `start()`. 82 | /// 83 | /// - Parameter transform: An unary operator to lift. 84 | /// - Returns: An observable producer that applies observable's operator to every created observable. 85 | public func lift(_ transform: @escaping (Observable) -> Observable) -> ObservableProducer { 86 | return ObservableProducer { observer, outerDisposables in 87 | self.startWithObservable { observable, innerDisposables in 88 | outerDisposables += innerDisposables 89 | transform(observable).subscribe(observer: observer.update) 90 | } 91 | } 92 | } 93 | } 94 | 95 | extension ObservableProducer { 96 | 97 | /// Maps each value in the producer to a new value. 98 | /// 99 | /// - Parameter transform: A closure that accepts a value and returns a different value. 100 | /// - Returns: An observable producer that, when started, will send a mapped value of `self.` 101 | public func map(_ transform: @escaping (Value) -> T) -> ObservableProducer { 102 | return lift { $0.map(transform) } 103 | } 104 | 105 | /// Preserves only values which pass the given closure. 106 | /// 107 | /// - Parameter predicate: A closure to determine whether a value from `self` should be included in the produced `Observable`. 108 | /// - Returns: A producer that, when started, forwards the values passing the given closure. 109 | public func filter(_ predicate: @escaping (Value) -> Bool) -> ObservableProducer { 110 | return lift { $0.filter(predicate) } 111 | } 112 | 113 | /// Applies `transform` to values from the producer and forwards values with non `nil` results unwrapped. 114 | /// 115 | /// - Parameter transform: A closure that accepts a value and returns a new optional value. 116 | /// - Returns: A producer that will send new values, that are non `nil` after the transformation. 117 | public func filterMap(_ transform: @escaping (Value) -> T?) -> ObservableProducer { 118 | return lift { $0.filterMap(transform) } 119 | } 120 | 121 | /// Forwards only values from `self` that are not considered equivalent to its immediately preceding value. 122 | /// 123 | /// - Parameter isEquivalent: A closure to determine whether two values are equivalent. 124 | /// - Returns: A producer which conditionally forwards values from `self` 125 | /// 126 | /// - Note: The first value is always forwarded. 127 | public func skipRepeats(_ isEquivalent: @escaping (Value, Value) -> Bool) -> ObservableProducer { 128 | return lift { $0.skipRepeats(isEquivalent) } 129 | } 130 | 131 | /// Skips the first `count` values, then forward everything afterward. 132 | /// 133 | /// - Parameter count: A number of values to skip. 134 | /// - Returns: A producer that, when started, will skip the first `count` values, then forward everything afterward. 135 | public func skip(first count: Int) -> ObservableProducer { 136 | return lift { $0.skip(first: count) } 137 | } 138 | 139 | /// Does not forward any value from `self` until `predicate` returns `false`, 140 | /// at which point the returned observable starts to forward values from `self`, including the one leading to the toggling. 141 | /// 142 | /// - Parameter predicate: A closure to determine whether the skipping should continue. 143 | /// - Returns: A producer which conditionally forwards values from `self`. 144 | public func skip(while predicate: @escaping (Value) -> Bool) -> ObservableProducer { 145 | return lift { $0.skip(while: predicate) } 146 | } 147 | 148 | /// Yields the first `count` values from the input producer. 149 | /// 150 | /// - Parameter count: A number of values to take from the observable. 151 | /// - Returns: A producer that, when started, will yield the first `count` values from `self`. 152 | /// 153 | /// - precondition: `count` must be non-negative number. 154 | public func take(first count: Int) -> ObservableProducer { 155 | return lift { $0.take(first: count) } 156 | } 157 | 158 | /// Forwards any values from `self` until `predicate` returns `false`, at which point the produced `Observable` would be disposed. 159 | /// 160 | /// - Parameter predicate: A closure to determine whether the forwarding of values should continue. 161 | /// - Returns: A producer which conditionally forwards values from `self`. 162 | public func take(while predicate: @escaping (Value) -> Bool) -> ObservableProducer { 163 | return lift { $0.take(while: predicate) } 164 | } 165 | 166 | /// Forwards events from `self` with history: 167 | /// values of the returned producer are tuples whose first member is the previous value 168 | /// and whose second member is the current value. 169 | /// `initial` is supplied as the first member when `self` sends its first value. 170 | /// If `initial` is `nil` the produced `Observable` would not emit any tuple until it has received at least two values. 171 | /// 172 | /// - Parameter initial: A value that will be combined with the first value sent by `self`. 173 | /// - Returns: A producer that sends tuples that contain previous and current sent values of `self`. 174 | public func combinePrevious(initial: Value? = nil) -> ObservableProducer<(Value, Value)> { 175 | return lift { $0.combinePrevious(initial: initial) } 176 | } 177 | } 178 | 179 | extension ObservableProducer where Value: Equatable { 180 | 181 | /// Forwards only values from `self` that are not equal to its immediately preceding value. 182 | /// 183 | /// - Returns: A producer which conditionally forwards values from `self`. 184 | /// 185 | /// - Note: The first value is always forwarded. 186 | public func skipRepeats() -> ObservableProducer { 187 | return skipRepeats(==) 188 | } 189 | } 190 | 191 | extension ObservableProducer { 192 | 193 | /// Maps each value in the producer to a new value by applying a key path. 194 | /// 195 | /// - Parameter keyPath: A key path relative to the producer's `Value` type. 196 | /// - Returns: A producer that will send new values. 197 | public func map(_ keyPath: KeyPath) -> ObservableProducer { 198 | return map { $0[keyPath: keyPath] } 199 | } 200 | 201 | /// Preserves only values by applying a key path whose value is `true`. 202 | /// 203 | /// - Parameter keyPath: A key path relative to the observable's `Value` type. 204 | /// - Returns: A producer that, when started, forwards the values passing the given closure. 205 | public func filter(_ keyPath: KeyPath) -> ObservableProducer { 206 | return filter { $0[keyPath: keyPath] } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /SwiftyRedux/Sources/Steroids/Store+Steroids.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | extension Store { 4 | 5 | /// Creates observable to receive state changing updates. 6 | /// Will send only further updates as they appear without current state at the moment of creating observer. 7 | /// Use it to declaratively transform state. 8 | /// 9 | /// Example: 10 | /// 11 | /// store.stateObservable() 12 | /// .map(\.networking) 13 | /// .filter(\.isReachable) 14 | /// .skipRepeats() 15 | /// .subscribe { networkingState in 16 | /// // ... 17 | /// } 18 | /// 19 | /// - Returns: An observable that will send new store's state. 20 | public func stateObservable() -> Observable { 21 | return Observable { action in 22 | return self.subscribe(includingCurrentState: false, observer: action) 23 | } 24 | } 25 | 26 | /// Creates observable producer to receive state changing updates once started. 27 | /// Will immediately send current state once started and then will send further updates as they appear. 28 | /// Use it to declaratively transform state. 29 | /// 30 | /// Example: 31 | /// 32 | /// store.stateProducer() 33 | /// .map(\.profiles) 34 | /// .filterMap { profilesState in 35 | /// profilesState[profileId] 36 | /// } 37 | /// .skipRepeats() 38 | /// .start { profileById in 39 | /// // ... 40 | /// } 41 | /// 42 | /// - Returns: An observable producer that, when started, will send store's state` 43 | public func stateProducer() -> ObservableProducer { 44 | return ObservableProducer { observer, disposables in 45 | disposables += self.subscribe(includingCurrentState: true, observer: observer.update) 46 | } 47 | } 48 | } 49 | 50 | extension Store where State: Equatable { 51 | 52 | /// Subscribes a unique state update observer. 53 | /// It will be called any time an action is dispatched, and some part of the state tree may potentially have changed, 54 | /// and new state doesn't equal to the previous one. Thus if no changes were made to the state, observer won't be called. 55 | /// You can stop listening to updates by calling `dispose()` on returned disposable. 56 | /// 57 | /// - Parameters: 58 | /// - queue: A queue on which observer wants to receive updates. If `nil`, observer will be called on internal queue. Defaults to `nil`. 59 | /// - includingCurrentState: If `true`, observer will immediately receive current state 60 | /// (before creating and returning Disposable) and further updates as they appear. 61 | /// If `false`, observer will only receive further updates as they appear. Defaults to `true`. 62 | /// - observer: Observer callback that will receive new unique state after each update until it's manually disposed or store's dead. 63 | /// - state: Current state right after it's changed and not equal to previous one. 64 | /// - Returns: Disposable to stop listening to updates. 65 | /// Its `isDisposed` property will be `true` when store dies and cancels all subscriptions by itself. 66 | @discardableResult 67 | public func subscribeUnique(on queue: DispatchQueue? = nil, includingCurrentState: Bool = true, observer: @escaping (State) -> Void) -> Disposable { 68 | if includingCurrentState { 69 | return stateProducer().skipRepeats().start(on: queue, observer: observer) 70 | } 71 | return stateObservable().skipRepeats().subscribe(on: queue, observer: observer) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/BatchedActions/BatchDispatchMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private typealias State = Int 5 | private enum AnyAction: SwiftyRedux.Action, Equatable { 6 | case one, two, three, four 7 | } 8 | 9 | class BatchDispatchMiddlewareTests: XCTestCase { 10 | var nextCalledWithAction: [SwiftyRedux.Action]! 11 | var dispatchCalledWithAction: [SwiftyRedux.Action]! 12 | var dispatchMiddleware: Dispatch! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | 17 | let middleware: Middleware = batchDispatchMiddleware() 18 | nextCalledWithAction = [] 19 | dispatchCalledWithAction = [] 20 | dispatchMiddleware = middleware( 21 | { 0 }, 22 | { action in self.dispatchCalledWithAction.append(action) }, 23 | { action in self.nextCalledWithAction.append(action) } 24 | ) 25 | } 26 | 27 | func testDispatchesAllBatchedActions() { 28 | dispatchMiddleware(BatchAction(AnyAction.one, AnyAction.two)) 29 | 30 | XCTAssertEqual(dispatchCalledWithAction as! [AnyAction], [.one, .two]) 31 | } 32 | 33 | func testCallsNextOnlyOnceOnBatchedActions() { 34 | dispatchMiddleware(BatchAction(AnyAction.one, AnyAction.two)) 35 | 36 | XCTAssertEqual(nextCalledWithAction.count, 1) 37 | XCTAssertTrue(nextCalledWithAction.first is BatchAction) 38 | } 39 | 40 | func testHandlesNestedBatchedActions() { 41 | dispatchMiddleware( 42 | BatchAction( 43 | AnyAction.one, 44 | BatchAction( 45 | AnyAction.two, 46 | AnyAction.three 47 | ), 48 | AnyAction.four 49 | ) 50 | ) 51 | 52 | XCTAssertEqual(nextCalledWithAction.count, 1) 53 | XCTAssertTrue(nextCalledWithAction.first is BatchAction) 54 | XCTAssertEqual(dispatchCalledWithAction as! [AnyAction], [.one, .two, .three, .four]) 55 | } 56 | 57 | func testCallsNextButNotDispatchForNonBatchedActions() { 58 | dispatchMiddleware(AnyAction.one) 59 | 60 | XCTAssertEqual(nextCalledWithAction as! [AnyAction], [.one]) 61 | XCTAssertEqual(dispatchCalledWithAction.count, 0) 62 | } 63 | 64 | func testDispatchesCustomBatchedActions() { 65 | struct CustomBatchAction: BatchedActions { 66 | let actions: [Action] 67 | } 68 | 69 | dispatchMiddleware(CustomBatchAction(actions: [AnyAction.one, AnyAction.two])) 70 | 71 | XCTAssertEqual(nextCalledWithAction.count, 1) 72 | XCTAssertTrue(nextCalledWithAction.first is CustomBatchAction) 73 | XCTAssertEqual(dispatchCalledWithAction as! [AnyAction], [.one, .two]) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/BatchedActions/EnableBatchingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private typealias State = Int 5 | private enum AnyAction: Action, Equatable { 6 | case one, two, three, four, five 7 | } 8 | 9 | private class MockReducer { 10 | private(set) var calledWithAction: [Action] = [] 11 | private(set) var reducer: Reducer! 12 | 13 | init() { 14 | reducer = { state, action in 15 | self.calledWithAction.append(action) 16 | } 17 | } 18 | } 19 | 20 | class EnableBatchingTests: XCTestCase { 21 | private var mock: MockReducer! 22 | private var batchedReducer: Reducer! 23 | 24 | override func setUp() { 25 | super.setUp() 26 | 27 | mock = MockReducer() 28 | batchedReducer = enableBatching(mock.reducer) 29 | } 30 | 31 | func testNonBatchedActionsArePassedThrough() { 32 | var state = 0 33 | batchedReducer(&state, AnyAction.one) 34 | batchedReducer(&state, AnyAction.two) 35 | 36 | XCTAssertEqual(mock.calledWithAction as! [AnyAction], [.one, .two]) 37 | } 38 | 39 | func testEachActionInsideBatchedActionIsPassedThroughSeparately() { 40 | var state = 0 41 | batchedReducer(&state, BatchAction(AnyAction.one, AnyAction.two)) 42 | 43 | XCTAssertEqual(mock.calledWithAction as! [AnyAction], [.one, .two]) 44 | } 45 | 46 | func testEachActionInsideNestedBatchedActionIsPassedThroughSeparatelyInCorrectOrder() { 47 | var state = 0 48 | batchedReducer( 49 | &state, 50 | BatchAction( 51 | AnyAction.one, 52 | BatchAction( 53 | BatchAction( 54 | AnyAction.two, 55 | AnyAction.three 56 | ), 57 | AnyAction.four 58 | ), 59 | AnyAction.five 60 | ) 61 | ) 62 | 63 | XCTAssertEqual(mock.calledWithAction as! [AnyAction], [.one, .two, .three, .four, .five]) 64 | } 65 | 66 | func testCustomBatchedActionsArePassedThrough() { 67 | struct CustomBatchAction: BatchedActions { 68 | let actions: [Action] 69 | } 70 | var state = 0 71 | batchedReducer(&state, CustomBatchAction(actions: [AnyAction.one, AnyAction.two])) 72 | 73 | XCTAssertEqual(mock.calledWithAction as! [AnyAction], [.one, .two]) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Command/CommandTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | class CommandTests: XCTestCase { 5 | func testCommandExecutesCorrectly() { 6 | var result: Int = 0 7 | let cmd = Command { x in result = x * 2 } 8 | 9 | cmd.execute(with: 21) 10 | 11 | XCTAssertEqual(result, 42) 12 | } 13 | 14 | func testCommandWithValueExecutesCorrectly() { 15 | var result: Int = 0 16 | let cmd = Command { x in result = x * 2 } 17 | 18 | cmd.with(value: 21).execute() 19 | 20 | XCTAssertEqual(result, 42) 21 | } 22 | 23 | func testCommandsEquality() { 24 | let cmd1 = Command.nop() 25 | let cmd2 = cmd1 26 | let cmd3 = Command.nop() 27 | 28 | XCTAssertEqual(cmd1, cmd2) 29 | XCTAssertNotEqual(cmd2, cmd3) 30 | } 31 | 32 | func testCommandsHashValues() { 33 | let cmd1 = Command.nop() 34 | let cmd2 = cmd1 35 | let cmd3 = Command.nop() 36 | 37 | XCTAssertEqual(cmd1.hashValue, cmd2.hashValue) 38 | XCTAssertNotEqual(cmd2.hashValue, cmd3.hashValue) 39 | } 40 | 41 | func testDebugQuickLook() { 42 | let cmd = Command.nop() 43 | 44 | XCTAssertEqual(cmd.debugDescription, cmd.debugQuickLookObject() as? String) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Command/Redux+CommandTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private typealias State = Int 5 | private struct AnyAction: SwiftyRedux.Action, Equatable {} 6 | 7 | class ReduxCommandTests: XCTestCase { 8 | private var initialState: State! 9 | private var nopReducer: Reducer! 10 | private var nopMiddleware: Middleware! 11 | 12 | override func setUp() { 13 | super.setUp() 14 | 15 | initialState = 0 16 | nopReducer = { state, action in } 17 | nopMiddleware = createFallThroughMiddleware { getState, dispatch in return { action in } } 18 | } 19 | 20 | // TODO: test subscribe includingCurrentState: true 21 | func testStore_whenSubscribingWithCommand_andIncludingCurrentState_shouldRedirectToOriginalMethod_byCallingCommandForCurrentStateAndEveryNextActionDispatched() { 22 | var result = [State]() 23 | let queue = DispatchQueue(label: "testStore_whenSubscribingWithCommand_shouldRedirectToOriginalMethod_byCallingCommandForEveryActionDispatched") 24 | let store = Store(state: initialState, reducer: { s, a in s += 1 }, middleware: [nopMiddleware]) 25 | 26 | store.subscribe(on: queue, includingCurrentState: true, Command { value in result.append(value) }) 27 | 28 | store.dispatch(AnyAction()) 29 | store.dispatch(AnyAction()) 30 | store.dispatchAndWait(AnyAction()) 31 | 32 | // wait for serial queue to finish executing previous async tasks 33 | queue.sync {} 34 | 35 | XCTAssertEqual(result, [0, 1, 2, 3]) 36 | } 37 | 38 | func testStore_whenSubscribingWithCommand_shouldRedirectToOriginalMethod_byCallingCommandOnSpecifiedQueueForActionDispatched() { 39 | let id = "testStore_whenSubscribingWithCommand_shouldRedirectToOriginalMethod_byCallingCommandOnSpecifiedQueueForActionDispatched" 40 | let key = DispatchSpecificKey() 41 | let queue = DispatchQueue(label: id) 42 | queue.setSpecific(key: key, value: id) 43 | let store = Store(state: initialState, reducer: nopReducer, middleware: [nopMiddleware]) 44 | 45 | store.subscribe(on: queue, includingCurrentState: false, Command { value in 46 | XCTAssertEqual(DispatchQueue.getSpecific(key: key), id) 47 | XCTAssertEqual(value, self.initialState) 48 | 49 | queue.setSpecific(key: key, value: nil) 50 | }) 51 | 52 | store.dispatch(AnyAction()) 53 | } 54 | 55 | func testStore_whenSubscribingWithCommand_shouldRedirectToOriginalMethod_byCallingCommandForEveryActionDispatched() { 56 | var result = [State]() 57 | let queue = DispatchQueue(label: "testStore_whenSubscribingWithCommand_shouldRedirectToOriginalMethod_byCallingCommandForEveryActionDispatched") 58 | let store = Store(state: initialState, reducer: { s, a in s += 1 }, middleware: [nopMiddleware]) 59 | 60 | store.subscribe(on: queue, includingCurrentState: false, Command { value in result.append(value) }) 61 | 62 | store.dispatch(AnyAction()) 63 | store.dispatch(AnyAction()) 64 | store.dispatchAndWait(AnyAction()) 65 | 66 | // wait for serial queue to finish executing previous async tasks 67 | queue.sync {} 68 | 69 | XCTAssertEqual(result, [1, 2, 3]) 70 | } 71 | 72 | func testObservable_whenSubscribingWithCommand_shouldRedirectToOriginalMethod() { 73 | let id = "testObservable_whenSubscribingWithCommand_shouldRedirectToOriginalMethod" 74 | let key = DispatchSpecificKey() 75 | let queue = DispatchQueue(label: id) 76 | queue.setSpecific(key: key, value: id) 77 | let (observable, observer) = Observable.pipe() 78 | 79 | observable.subscribe(on: queue, Command { value in 80 | XCTAssertEqual(DispatchQueue.getSpecific(key: key), id) 81 | XCTAssertEqual(value, self.initialState) 82 | 83 | queue.setSpecific(key: key, value: nil) 84 | }) 85 | 86 | observer.update(initialState) 87 | } 88 | 89 | func testObserver_whenInitializedWithCommand_shouldRedirectToDesignatedInitializer() { 90 | let id = "testObserver_whenInitializedWithCommand_shouldRedirectToDesignatedInitializer" 91 | let key = DispatchSpecificKey() 92 | let queue = DispatchQueue(label: id) 93 | queue.setSpecific(key: key, value: id) 94 | 95 | let observer = Observer(queue: queue, Command { value in 96 | XCTAssertEqual(DispatchQueue.getSpecific(key: key), id) 97 | XCTAssertEqual(value, self.initialState) 98 | 99 | queue.setSpecific(key: key, value: nil) 100 | }) 101 | 102 | observer.update(initialState) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Core/DisposableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | class DisposableTests: XCTestCase { 5 | func testNopDisposableIsAlreadyDisposed() { 6 | XCTAssertTrue(Disposable.nop().isDisposed) 7 | } 8 | 9 | func testDisposableIsDisposedOnce() { 10 | var result = 0 11 | let disposable = Disposable { result += 1 } 12 | 13 | let exp1 = expectation(description: "first") 14 | let exp2 = expectation(description: "second") 15 | let exp3 = expectation(description: "third") 16 | DispatchQueue.global().async { disposable.dispose(); exp1.fulfill() } 17 | DispatchQueue.global().async { disposable.dispose(); exp2.fulfill() } 18 | DispatchQueue.global().async { disposable.dispose(); exp3.fulfill() } 19 | 20 | waitForExpectations(timeout: 0.1) { e in 21 | XCTAssertEqual(result, 1) 22 | } 23 | } 24 | 25 | func testDisposable_whenDisposing_disposableIsMarkedAsDisposed() { 26 | var result = false 27 | let disposable = Disposable { result = true } 28 | 29 | disposable.dispose() 30 | 31 | XCTAssertEqual(result, disposable.isDisposed) 32 | } 33 | 34 | func testDisposable_whenDisposing_isDisposedFlagSetNotWaitingForDisposeToFinish() { 35 | let disposable = Disposable { 36 | DispatchQueue.global(qos: .background).async { Thread.sleep(forTimeInterval: 0.001) } 37 | } 38 | 39 | disposable.dispose() 40 | 41 | XCTAssertTrue(disposable.isDisposed) 42 | } 43 | 44 | func testCompositeDisposableDisposesAllAddedDisposables() { 45 | var result = 0 46 | let disposable1 = Disposable { result += 1 } 47 | let disposable2 = Disposable { result += 1 } 48 | let disposable3 = Disposable { result += 1 } 49 | let disposables = CompositeDisposable(disposing: disposable1, disposable2, disposable3) 50 | 51 | disposables.dispose() 52 | 53 | XCTAssertEqual(result, 3) 54 | XCTAssertTrue(disposables.isDisposed) 55 | } 56 | 57 | func testCompositeDisposableAddsAlreadyDisposedDisposable() { 58 | let disposables = CompositeDisposable() 59 | weak var nopDisposable: Disposable! 60 | 61 | autoreleasepool { 62 | let deinitNopDisposable = Disposable.nop() 63 | nopDisposable = deinitNopDisposable 64 | disposables.add(nopDisposable) 65 | } 66 | 67 | XCTAssertNotNil(nopDisposable) 68 | } 69 | 70 | func testCompositeDisposableAddsAlreadyDisposedDisposables() { 71 | let disposables = CompositeDisposable() 72 | weak var nopDisposable1: Disposable! 73 | weak var nopDisposable2: Disposable! 74 | weak var nopDisposable3: Disposable! 75 | 76 | autoreleasepool { 77 | let deinitNopDisposable1 = Disposable.nop() 78 | let deinitNopDisposable2 = Disposable.nop() 79 | let deinitNopDisposable3 = Disposable.nop() 80 | nopDisposable1 = deinitNopDisposable1 81 | nopDisposable2 = deinitNopDisposable2 82 | nopDisposable3 = deinitNopDisposable3 83 | disposables.add([nopDisposable1, nopDisposable2, nopDisposable3]) 84 | } 85 | 86 | XCTAssertNotNil(nopDisposable1) 87 | XCTAssertNotNil(nopDisposable2) 88 | XCTAssertNotNil(nopDisposable3) 89 | } 90 | 91 | func testCompositeDisposable_ifAlreadyDisposed_whenAddingDisposable_immediatelyDisposesIt() { 92 | let disposables = CompositeDisposable() 93 | let disposable = Disposable { } 94 | 95 | disposables.dispose() 96 | 97 | XCTAssertTrue(disposables.isDisposed) 98 | XCTAssertFalse(disposable.isDisposed) 99 | 100 | disposables.add(disposable) 101 | 102 | XCTAssertTrue(disposable.isDisposed) 103 | } 104 | 105 | func testCompositeDisposable_ifAlreadyDisposed_whenAddingMultipleDisposables_immediatelyDisposesThem() { 106 | let disposables = CompositeDisposable() 107 | let disposable1 = Disposable { } 108 | let disposable2 = Disposable { } 109 | let disposable3 = Disposable { } 110 | 111 | disposables.dispose() 112 | 113 | XCTAssertTrue(disposables.isDisposed) 114 | XCTAssertFalse([disposable1, disposable2, disposable3].allSatisfy { $0.isDisposed }) 115 | 116 | disposables.add([disposable1, disposable2, disposable3]) 117 | 118 | XCTAssertTrue([disposable1, disposable2, disposable3].allSatisfy { $0.isDisposed }) 119 | } 120 | 121 | func testCompositeDisposable_whenAddingAction_returnsDisposableCreatedWithIt() { 122 | var result = false 123 | let disposables = CompositeDisposable() 124 | let disposable = disposables += { result = true } 125 | 126 | disposable.dispose() 127 | 128 | XCTAssertTrue(result) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Core/MiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private typealias State = Int 5 | private struct StringAction: Action, Equatable { 6 | let value: String 7 | init(_ value: String) { self.value = value } 8 | } 9 | 10 | private func + (lhs: StringAction, rhs: String) -> StringAction { return StringAction(lhs.value + rhs) } 11 | private func + (lhs: String, rhs: StringAction) -> StringAction { return rhs + lhs } 12 | 13 | class MiddlewareTests: XCTestCase { 14 | private var initialState: State! 15 | private var nopAction: StringAction! 16 | private var nopReducer: Reducer! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | initialState = 0 22 | nopAction = StringAction("action") 23 | nopReducer = { state, action in } 24 | } 25 | 26 | func testAppliedMiddlewareIsChainedInCorrectOrder() { 27 | var result: StringAction! 28 | let middleware: Middleware = applyMiddleware([ 29 | createMiddleware { getState, dispatch, next in 30 | return { action in next(action as! StringAction + " first") } 31 | }, 32 | createMiddleware { getState, dispatch, next in 33 | return { action in next(action as! StringAction + " second") } 34 | }, 35 | createMiddleware { getState, dispatch, next in 36 | return { action in next(action as! StringAction + " third") } 37 | } 38 | ]) 39 | 40 | middleware({ self.initialState }, { _ in }, { action in result = action as? StringAction })(nopAction) 41 | 42 | XCTAssertEqual(result, nopAction + " first second third") 43 | } 44 | 45 | func testFallThroughMiddlewarePropagatesActionToTheNextOne() { 46 | var result = "" 47 | let middleware: Middleware = applyMiddleware([ 48 | createFallThroughMiddleware { getState, dispatch in 49 | return { action in result += (action as! StringAction).value + " first " } 50 | }, 51 | createFallThroughMiddleware { getState, dispatch in 52 | return { action in result += (action as! StringAction).value + " second" } 53 | } 54 | ]) 55 | 56 | middleware({ self.initialState }, { _ in }, { _ in })(nopAction) 57 | 58 | XCTAssertEqual(result, "\(nopAction.value) first \(nopAction.value) second") 59 | } 60 | 61 | func testCanGetState() { 62 | var result: State! 63 | let middleware: Middleware = createFallThroughMiddleware { getState, dispatch in 64 | return { action in result = getState() } 65 | } 66 | let store = Store(state: initialState, reducer: nopReducer, middleware: [middleware]) 67 | store.dispatchAndWait(nopAction) 68 | 69 | XCTAssertEqual(result, initialState) 70 | } 71 | 72 | func testCanDispatch() { 73 | var result: StringAction! 74 | let middleware: Middleware = createFallThroughMiddleware { getState, dispatch in 75 | return { action in 76 | if (action as! StringAction) == self.nopAction { 77 | dispatch("new " + (action as! StringAction)) 78 | } else { 79 | result = action as? StringAction 80 | } 81 | } 82 | } 83 | let store = Store(state: initialState, reducer: nopReducer, middleware: [middleware]) 84 | store.dispatchAndWait(nopAction) 85 | 86 | XCTAssertEqual(result, "new " + nopAction) 87 | } 88 | 89 | func testSkipsActionIfPreviousDontPropagateNext() { 90 | let store = Store(state: initialState, reducer: nopReducer, middleware: [ 91 | createMiddleware { getState, dispatch, next in 92 | return { action in } 93 | }, 94 | createFallThroughMiddleware { getState, dispatch in 95 | return { action in 96 | XCTFail() 97 | } 98 | } 99 | ]) 100 | store.dispatchAndWait(nopAction) 101 | } 102 | 103 | func testCanPropagateActionToNextMiddleware() { 104 | var result: StringAction! 105 | let store = Store(state: initialState, reducer: nopReducer, middleware: [ 106 | createMiddleware { getState, dispatch, next in 107 | return { action in next(action as! StringAction + " next") } 108 | }, 109 | createFallThroughMiddleware { getState, dispatch in 110 | return { action in result = action as? StringAction } 111 | } 112 | ]) 113 | store.dispatchAndWait(nopAction) 114 | 115 | XCTAssertEqual(result, nopAction + " next") 116 | } 117 | 118 | func testChangesStateAfterPropagatingToTheNextMiddleware() { 119 | let reducer: Reducer = { state, action in 120 | state += Int((action as! StringAction).value)! 121 | } 122 | let store = Store(state: initialState, reducer: reducer, middleware: [ 123 | createMiddleware { getState, dispatch, next in 124 | return { action in 125 | XCTAssertEqual(getState(), self.initialState) 126 | next(StringAction("42")) 127 | XCTAssertEqual(getState(), 42) 128 | } 129 | } 130 | ]) 131 | store.dispatchAndWait(nopAction) 132 | 133 | XCTAssertEqual(store.state, 42) 134 | } 135 | 136 | // // uncomment to test call stack overflow 137 | // func testInfiniteCall() { 138 | // let middleware: Middleware = createMiddleware { getState, dispatch, next in 139 | // return { action in 140 | // dispatch(action) 141 | // } 142 | // } 143 | // let store = Store(state: initialState, reducer: nopReducer, middleware: [middleware]) 144 | // 145 | // store.dispatch(nopAction) 146 | // } 147 | } 148 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Core/ObservableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | class ObservableTests: XCTestCase { 5 | func testSubscriberIsNotifiedOfNewUpdates() { 6 | var result: Int! 7 | let (observable, observer) = Observable.pipe() 8 | observable.subscribe { value in result = value } 9 | 10 | observer.update(42) 11 | 12 | XCTAssertEqual(result, 42) 13 | } 14 | 15 | func testNewSubscriberIsNotNotifiedOfOldUpdates() { 16 | var result: Int! 17 | let (observable, observer) = Observable.pipe() 18 | 19 | observer.update(0) 20 | observable.subscribe { value in result = value } 21 | observer.update(42) 22 | 23 | XCTAssertEqual(result, 42) 24 | } 25 | 26 | func testSubscriberIsNotifiedOnSpecifiedQueueAsynchronously() { 27 | let id = "testObserverIsNotifiedOnSpecifiedQueueAsynchronously" 28 | let queueId = DispatchSpecificKey() 29 | let queue = DispatchQueue(label: id) 30 | queue.setSpecific(key: queueId, value: id) 31 | 32 | var result: String! 33 | let (observable, observer) = Observable.pipe(queue: queue) 34 | let queueExpectation = expectation(description: id) 35 | 36 | observable.subscribe { value in 37 | result = DispatchQueue.getSpecific(key: queueId) 38 | queueExpectation.fulfill() 39 | } 40 | 41 | observer.update(42) 42 | 43 | waitForExpectations(timeout: 0.1) { e in 44 | queue.setSpecific(key: queueId, value: nil) 45 | 46 | XCTAssertEqual(result, id) 47 | } 48 | } 49 | 50 | func testSubscriberIsNotNotifiedAfterDisposing() { 51 | var result = [Int]() 52 | let (observable, observer) = Observable.pipe() 53 | let disposable = observable.subscribe { value in result.append(value) } 54 | 55 | observer.update(0) 56 | disposable.dispose() 57 | observer.update(42) 58 | 59 | XCTAssertEqual(result, [0]) 60 | } 61 | 62 | func testDisposableIsNotRetainedAfterItDisposes() { 63 | let observable = Observable { _ in nil } 64 | weak var disposable = observable.subscribe { value in } 65 | 66 | XCTAssertNotNil(disposable) 67 | XCTAssertFalse(disposable!.isDisposed) 68 | 69 | disposable!.dispose() 70 | 71 | XCTAssertNil(disposable) 72 | } 73 | 74 | func testAllSubscribersAreDisposedWhenObservableDies() { 75 | var observable: Observable? = .init { _ in nil } 76 | let disposable1 = observable!.subscribe { value in } 77 | let disposable2 = observable!.subscribe { value in } 78 | let disposable3 = observable!.subscribe { value in } 79 | 80 | observable = nil 81 | 82 | XCTAssertTrue([disposable1, disposable2, disposable3].allSatisfy { $0.isDisposed }) 83 | } 84 | 85 | func testPipe_whenSpecifyingObserverQueue_andSubscribingToObservableWithDifferentQueue_shouldNotifySubscribersOnSubscribingQueue() { 86 | let id = "testPipe_whenSpecifyingObserverQueue_andSubscribingToObservableWithDifferentQueue_shouldNotifySubscribersOnSubscribingQueue" 87 | 88 | let observerQueueId = DispatchSpecificKey() 89 | let observerQueue = DispatchQueue(label: id + "observer") 90 | observerQueue.setSpecific(key: observerQueueId, value: observerQueue.label) 91 | 92 | let subscribersQueueId = DispatchSpecificKey() 93 | let subscribersQueue = DispatchQueue(label: id + "subscribers") 94 | subscribersQueue.setSpecific(key: subscribersQueueId, value: subscribersQueue.label) 95 | 96 | var result: String! 97 | let (observable, observer) = Observable.pipe(queue: observerQueue) 98 | let queueExpectation = expectation(description: id) 99 | 100 | observable.subscribe(on: subscribersQueue) { value in 101 | result = DispatchQueue.getSpecific(key: subscribersQueueId) 102 | queueExpectation.fulfill() 103 | } 104 | 105 | observer.update(42) 106 | 107 | waitForExpectations(timeout: 0.1) { e in 108 | observerQueue.setSpecific(key: observerQueueId, value: nil) 109 | subscribersQueue.setSpecific(key: subscribersQueueId, value: nil) 110 | 111 | XCTAssertEqual(result, subscribersQueue.label) 112 | } 113 | } 114 | 115 | func testObservable_whenInitializedWithAnotherObservable_shouldSubscribeToItsUpdates() { 116 | var result = [Int]() 117 | let (sourceObservable, sourceObserver) = Observable.pipe() 118 | let observable = Observable(observable: sourceObservable) 119 | 120 | observable.subscribe { value in 121 | result.append(value) 122 | } 123 | 124 | sourceObserver.update(1) 125 | sourceObserver.update(2) 126 | sourceObserver.update(3) 127 | 128 | XCTAssertEqual(result, [1, 2, 3]) 129 | } 130 | 131 | func testObservable_whenInitializedWithAnotherObservable_shouldStopReceivingUpdatesAfterDisposing() { 132 | var result = [Int]() 133 | let (sourceObservable, sourceObserver) = Observable.pipe() 134 | let observable = Observable(observable: sourceObservable) 135 | let disposable = observable.subscribe { value in 136 | result.append(value) 137 | } 138 | 139 | sourceObserver.update(1) 140 | sourceObserver.update(2) 141 | disposable.dispose() 142 | sourceObserver.update(3) 143 | 144 | XCTAssertEqual(result, [1, 2]) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Core/PerformanceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private struct AnyAction: Action {} 5 | 6 | class PerformanceTests: XCTestCase { 7 | typealias State = Int 8 | 9 | var observers: [(State) -> Void]! 10 | var store: Store! 11 | 12 | override func setUp() { 13 | super.setUp() 14 | 15 | observers = (0..<3000).map { _ in { _ in } } 16 | store = Store(state: 0, reducer: { state, action in }) 17 | } 18 | 19 | func testNotify() { 20 | self.observers.forEach { self.store.subscribe(observer: $0) } 21 | self.measure { 22 | self.store.dispatchAndWait(AnyAction()) 23 | } 24 | } 25 | 26 | func testSubscribe() { 27 | self.measure { 28 | self.observers.forEach { self.store.subscribe(observer: $0) } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Core/StoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private typealias State = Int 5 | 6 | private enum AnyAction: Int, Action { case one = 1, two, three, four, five } 7 | private enum OpAction: Action, Equatable { case inc, mul } 8 | private struct StringAction: Action { 9 | let value: String 10 | init(_ value: String) { self.value = value } 11 | } 12 | 13 | private class MockMiddleware { 14 | private(set) var calledWithStoreCount: Int = 0 15 | private(set) var calledWithAction: [Action] = [] 16 | private(set) var middleware: Middleware! 17 | 18 | init() { 19 | middleware = createMiddleware { getState, dispatch, next in 20 | self.calledWithStoreCount += 1 21 | return { action in 22 | self.calledWithAction.append(action) 23 | next(action) 24 | } 25 | } 26 | } 27 | } 28 | private class MockFallThroughMiddleware { 29 | private(set) var calledWithStoreCount: Int = 0 30 | private(set) var calledWithAction: [Action] = [] 31 | private(set) var middleware: Middleware! 32 | 33 | init() { 34 | middleware = createFallThroughMiddleware { getState, dispatch in 35 | self.calledWithStoreCount += 1 36 | return { action in 37 | self.calledWithAction.append(action) 38 | } 39 | } 40 | } 41 | } 42 | 43 | class StoreTests: XCTestCase { 44 | private var initialState: State! 45 | private var nopReducer: Reducer! 46 | private var nopMiddleware: Middleware! 47 | 48 | override func setUp() { 49 | super.setUp() 50 | 51 | initialState = 0 52 | nopReducer = { state, action in } 53 | nopMiddleware = createFallThroughMiddleware { getState, dispatch in return { action in } } 54 | } 55 | 56 | func testMiddlewareIsExecutedOnlyOnceBeforeActionReceived() { 57 | let mock = MockMiddleware() 58 | let store = Store(state: initialState, reducer: nopReducer, middleware: [mock.middleware]) 59 | 60 | store.dispatch(AnyAction.one) 61 | store.dispatch(AnyAction.two) 62 | store.dispatchAndWait(AnyAction.three) 63 | 64 | XCTAssertEqual(mock.calledWithStoreCount, 1) 65 | } 66 | 67 | func testFallThroughMiddlewareIsExecutedOnlyOnceBeforeActionReceived() { 68 | let mock = MockFallThroughMiddleware() 69 | let store = Store(state: initialState, reducer: nopReducer, middleware: [mock.middleware]) 70 | 71 | store.dispatch(AnyAction.one) 72 | store.dispatch(AnyAction.two) 73 | store.dispatchAndWait(AnyAction.three) 74 | 75 | XCTAssertEqual(mock.calledWithStoreCount, 1) 76 | } 77 | 78 | func testMiddlewareExecutesActionBodyAsManyTimesAsActionsReceived() { 79 | let mock = MockMiddleware() 80 | let store = Store(state: initialState, reducer: nopReducer, middleware: [mock.middleware]) 81 | 82 | store.dispatch(AnyAction.one) 83 | store.dispatch(AnyAction.two) 84 | store.dispatchAndWait(AnyAction.three) 85 | 86 | XCTAssertEqual(mock.calledWithAction.count, 3) 87 | } 88 | 89 | func testFallThroughMiddlewareExecutesActionBodyAsManyTimesAsActionsReceived() { 90 | let mock = MockMiddleware() 91 | let store = Store(state: initialState, reducer: nopReducer, middleware: [mock.middleware]) 92 | 93 | store.dispatch(AnyAction.one) 94 | store.dispatch(AnyAction.two) 95 | store.dispatchAndWait(AnyAction.three) 96 | 97 | XCTAssertEqual(mock.calledWithAction.count, 3) 98 | } 99 | 100 | func testStore_whenDispatchingWithoutWaiting_shouldPerformAsynchronously() { 101 | var result: State = 0 102 | let asyncExpectation = expectation(description: "testStore_whenDispatchingWithoutWaiting_shouldPerformAsynchronously") 103 | let store = Store(state: 42, reducer: nopReducer, middleware: [nopMiddleware]) 104 | store.subscribe(includingCurrentState: false) { state in 105 | result = state 106 | asyncExpectation.fulfill() 107 | } 108 | 109 | store.dispatch(AnyAction.one) 110 | 111 | waitForExpectations(timeout: 0.1) { e in 112 | XCTAssertEqual(result, 42) 113 | } 114 | } 115 | 116 | func testStore_whenDispatchingAndWaiting_shouldPerformSynchronously() { 117 | var result: State = 0 118 | let store = Store(state: 42, reducer: nopReducer, middleware: [nopMiddleware]) 119 | store.subscribe(includingCurrentState: false) { state in 120 | result = state 121 | } 122 | 123 | store.dispatchAndWait(AnyAction.one) 124 | 125 | XCTAssertEqual(result, 42) 126 | } 127 | 128 | func testStore_afterSubscribeAndDispatchFlow_andItDeinits_allDisposablesShouldDispose() { 129 | weak var store: Store? 130 | var disposable: Disposable! 131 | 132 | autoreleasepool { 133 | let deinitStore = Store(state: initialState, reducer: nopReducer, middleware: [nopMiddleware]) 134 | store = deinitStore 135 | disposable = deinitStore.subscribe(includingCurrentState: false, observer: { state in }) 136 | deinitStore.dispatchAndWait(AnyAction.one) 137 | } 138 | 139 | XCTAssertTrue(disposable.isDisposed) 140 | XCTAssertNil(store) 141 | } 142 | 143 | func testMiddleware_whenRunOnDefaultQueue_shouldBeExecutedSequentiallyWithReducer() { 144 | var result = [String]() 145 | let middleware: Middleware = createMiddleware { getState, dispatch, next in 146 | return { action in 147 | result.append("m-\(action)") 148 | next(action) 149 | } 150 | } 151 | let reducer: Reducer = { state, action in 152 | result.append("r-\(action)") 153 | } 154 | let store = Store(state: initialState, reducer: reducer, middleware: [middleware]) 155 | 156 | store.dispatch(AnyAction.one) 157 | store.dispatch(AnyAction.two) 158 | store.dispatch(AnyAction.three) 159 | store.dispatchAndWait(AnyAction.four) 160 | 161 | XCTAssertEqual(result, ["m-one", "r-one", "m-two", "r-two", "m-three", "r-three", "m-four", "r-four"]) 162 | } 163 | 164 | func testMiddleware_evenIfRunOnDifferentQueues_shouldBeExecutedSequentially() { 165 | func asyncMiddleware(id: String, qos: DispatchQoS.QoSClass) -> Middleware { 166 | let asyncExpectation = expectation(description: "\(id) async middleware expectation") 167 | return createMiddleware { getState, dispatch, next in 168 | return { action in 169 | DispatchQueue.global(qos: qos).async { 170 | let action = (action as! StringAction).value 171 | next(StringAction("\(action) \(id)")) 172 | asyncExpectation.fulfill() 173 | } 174 | } 175 | } 176 | } 177 | 178 | var result = "" 179 | let reducer: Reducer = { state, action in 180 | let action = (action as! StringAction).value 181 | result += action 182 | } 183 | let middleware1 = asyncMiddleware(id: "first", qos: .default) 184 | let middleware2 = asyncMiddleware(id: "second", qos: .userInteractive) 185 | let middleware3 = asyncMiddleware(id: "third", qos: .background) 186 | let store = Store(state: initialState, reducer: reducer, middleware: [middleware1, middleware2, middleware3]) 187 | 188 | store.dispatch(StringAction("action")) 189 | 190 | waitForExpectations(timeout: 1) { e in 191 | XCTAssertEqual(result, "action first second third") 192 | } 193 | } 194 | 195 | func testStore_whenSubscribingNotIncludingCurrentState_shouldOnlyReceiveNextStateUpdates() { 196 | let reducer: Reducer = { state, action in 197 | switch action { 198 | case let action as OpAction where action == OpAction.mul: state *= 2 199 | case let action as OpAction where action == OpAction.inc: state += 3 200 | default: break 201 | } 202 | } 203 | let store = Store(state: 3, reducer: reducer) 204 | 205 | var result: [State] = [] 206 | store.subscribe(includingCurrentState: false) { state in 207 | result.append(state) 208 | } 209 | store.dispatch(OpAction.mul) 210 | store.dispatchAndWait(OpAction.inc) 211 | 212 | XCTAssertEqual(result, [6, 9]) 213 | } 214 | 215 | func testStore_whenSubscribingIncludingCurrentState_shouldImmediatelyReceiveCurrentStateAndKeepReceivingNextStateUpdates() { 216 | let reducer: Reducer = { state, action in 217 | switch action { 218 | case let action as OpAction where action == OpAction.mul: state *= 2 219 | case let action as OpAction where action == OpAction.inc: state += 3 220 | default: break 221 | } 222 | } 223 | let store = Store(state: 3, reducer: reducer) 224 | 225 | var result: [State] = [] 226 | store.subscribe(includingCurrentState: true) { state in 227 | result.append(state) 228 | } 229 | store.dispatch(OpAction.mul) 230 | store.dispatchAndWait(OpAction.inc) 231 | 232 | XCTAssertEqual(result, [3, 6, 9]) 233 | } 234 | 235 | func testStore_whenSubscribing_ReceiveStateUpdatesOnSelectedQueue() { 236 | let id = "testStore_whenSubscribing_ReceiveStateUpdatesOnSelectedQueue" 237 | let queueId = DispatchSpecificKey() 238 | let queue = DispatchQueue(label: id) 239 | queue.setSpecific(key: queueId, value: id) 240 | let store = Store(state: initialState, reducer: nopReducer) 241 | 242 | var result: String! 243 | let queueExpectation = expectation(description: id) 244 | store.subscribe(on: queue, includingCurrentState: false) { state in 245 | result = DispatchQueue.getSpecific(key: queueId) 246 | queueExpectation.fulfill() 247 | } 248 | store.dispatch(AnyAction.one) 249 | 250 | waitForExpectations(timeout: 0.1) { e in 251 | queue.setSpecific(key: queueId, value: nil) 252 | 253 | XCTAssertEqual(result, id) 254 | } 255 | } 256 | 257 | func testStore_whenSubscribingWithoutSelectedQueue_butDidSoBefore_receiveStateUpdatesOnDefaultQueue() { 258 | let id = "testStore_whenSubscribingWithoutSelectedQueue_butDidSoBefore_receiveStateUpdatesOnDefaultQueue" 259 | let queueId = DispatchSpecificKey() 260 | let queue = DispatchQueue(label: id) 261 | queue.setSpecific(key: queueId, value: id) 262 | let store = Store(state: initialState, reducer: nopReducer) 263 | 264 | var result: String! 265 | let onQueueExpectation = expectation(description: "\(id) on queue") 266 | let defaultQueueExpectation = expectation(description: "\(id) default queue") 267 | store.subscribe(on: queue, includingCurrentState: false) { state in 268 | onQueueExpectation.fulfill() 269 | } 270 | store.subscribe(includingCurrentState: false) { state in 271 | defaultQueueExpectation.fulfill() 272 | result = DispatchQueue.getSpecific(key: queueId) 273 | } 274 | store.dispatch(AnyAction.one) 275 | 276 | waitForExpectations(timeout: 0.1) { e in 277 | queue.setSpecific(key: queueId, value: nil) 278 | 279 | XCTAssertNotEqual(result, id) 280 | } 281 | } 282 | 283 | func testStore_whenUnsubscribing_stopReceivingStateUpdates() { 284 | let reducer: Reducer = { state, action in 285 | state = (action as! AnyAction).rawValue 286 | } 287 | let store = Store(state: initialState, reducer: reducer) 288 | 289 | var result: [State] = [] 290 | let disposable = store.subscribe(includingCurrentState: false) { state in 291 | result.append(state) 292 | } 293 | store.dispatch(AnyAction.one) 294 | store.dispatch(AnyAction.two) 295 | store.dispatchAndWait(AnyAction.three) 296 | 297 | disposable.dispose() 298 | store.dispatch(AnyAction.four) 299 | store.dispatchAndWait(AnyAction.five) 300 | 301 | XCTAssertEqual(result, [1, 2, 3]) 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Epics/EpicsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ReactiveSwift 3 | 4 | @testable import SwiftyRedux 5 | @testable import SwiftyReduxEpics 6 | 7 | private typealias State = Int 8 | private enum AnyAction: SwiftyRedux.Action, Equatable { 9 | case one, two, three 10 | } 11 | 12 | private class MockEpic { 13 | private(set) var calledSetupCount: Int = 0 14 | private(set) var calledWithAction: [SwiftyRedux.Action] = [] 15 | private(set) var epic: Epic! 16 | 17 | init() { 18 | epic = { actions, state in 19 | self.calledSetupCount += 1 20 | return actions.on(value: { action in 21 | self.calledWithAction.append(action) 22 | }) 23 | } 24 | } 25 | } 26 | 27 | class EpicsTests: XCTestCase { 28 | func testCombineEpicsCallsEachEpicOnce() { 29 | 30 | } 31 | 32 | func testEpicMiddlewareCanGetState() { 33 | 34 | } 35 | 36 | func testEpicMiddlewareCanDispatch() { 37 | 38 | } 39 | 40 | func testEpicMiddlewareStoreCallbackIsCalledOnce() { 41 | 42 | } 43 | 44 | func testEpicMiddlewareNextIsCalledBeforeEpic() { 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/ReactiveExtensions/ReactiveExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ReactiveSwift 3 | 4 | @testable import SwiftyReduxReactiveExtensions 5 | 6 | class ReactiveExtensionsTests: XCTestCase { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/SideEffects/SideEffectsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private typealias State = Int 5 | private enum AnyAction: SwiftyRedux.Action, Equatable { 6 | case one, two, three 7 | } 8 | 9 | private class MockSideEffect { 10 | private(set) var calledWithStoreCount: Int = 0 11 | private(set) var calledWithAction: [Action] = [] 12 | private(set) var sideEffect: SideEffect! 13 | 14 | init() { 15 | sideEffect = { getState, dispatch in 16 | self.calledWithStoreCount += 1 17 | return { action in 18 | self.calledWithAction.append(action) 19 | } 20 | } 21 | } 22 | } 23 | 24 | class SideEffectsTests: XCTestCase { 25 | func testCombineSideEffectCallsEachSideEffectOnce() { 26 | let action = AnyAction.one 27 | let mock1 = MockSideEffect() 28 | let mock2 = MockSideEffect() 29 | let mock3 = MockSideEffect() 30 | let sideEffect = combineSideEffects(mock1.sideEffect, mock2.sideEffect, mock3.sideEffect) 31 | 32 | _ = sideEffect({ 0 }, { _ in })(action) 33 | 34 | XCTAssertEqual(mock1.calledWithAction.count, 1) 35 | XCTAssertEqual(mock2.calledWithAction.count, 1) 36 | XCTAssertEqual(mock3.calledWithAction.count, 1) 37 | XCTAssertEqual(mock1.calledWithAction.first as! AnyAction, action) 38 | XCTAssertEqual(mock2.calledWithAction.first as! AnyAction, action) 39 | XCTAssertEqual(mock3.calledWithAction.first as! AnyAction, action) 40 | } 41 | 42 | func testSideEffectMiddlewareCanGetState() { 43 | var result: State? 44 | let middleware: Middleware = createSideEffectMiddleware { getState, dispatch in 45 | return { action in result = getState() } 46 | } 47 | let dispatch = middleware({ 42 }, { _ in }, { _ in }) 48 | 49 | dispatch(AnyAction.one) 50 | 51 | XCTAssertEqual(result, 42) 52 | } 53 | 54 | func testSideEffectMiddlewareCanDispatch() { 55 | var result: AnyAction? 56 | let middleware: Middleware = createSideEffectMiddleware { getState, dispatch in 57 | return { action in 58 | if action as! AnyAction == .one { 59 | dispatch(AnyAction.two) 60 | } 61 | } 62 | } 63 | let dispatch = middleware({ 0 }, { action in result = action as? AnyAction }, { _ in }) 64 | 65 | dispatch(AnyAction.one) 66 | 67 | XCTAssertEqual(result, .two) 68 | } 69 | 70 | func testSideEffectMiddlewareStoreCallbackIsCalledOnce() { 71 | let mock = MockSideEffect() 72 | let middleware = createSideEffectMiddleware(mock.sideEffect) 73 | let dispatch = middleware({ 0 }, { _ in }, { _ in }) 74 | 75 | dispatch(AnyAction.one) 76 | dispatch(AnyAction.two) 77 | dispatch(AnyAction.three) 78 | 79 | XCTAssertEqual(mock.calledWithStoreCount, 1) 80 | XCTAssertEqual(mock.calledWithAction.count, 3) 81 | } 82 | 83 | func testSideEffectMiddlewareNextIsCalledBeforeSideEffect() { 84 | enum Call: Equatable { case next, action } 85 | var result = [Call]() 86 | let middleware: Middleware = createSideEffectMiddleware { getState, dispatch in 87 | return { action in 88 | result.append(.action) 89 | } 90 | } 91 | let dispatch = middleware({ 0 }, { _ in }, { action in result.append(.next) }) 92 | 93 | dispatch(AnyAction.one) 94 | 95 | XCTAssertEqual(result, [.next, .action]) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Steroids/Observable+SteroidsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private struct Tuple: Equatable { 5 | let first: T 6 | let second: U 7 | 8 | init(_ first: T, _ second: U) { 9 | self.first = first 10 | self.second = second 11 | } 12 | } 13 | 14 | private extension Int { 15 | var string: String { 16 | return "\(self)" 17 | } 18 | } 19 | 20 | private extension Int { 21 | var isEven: Bool { 22 | return self % 2 == 0 23 | } 24 | } 25 | 26 | class ObservableSteroidsTests: XCTestCase { 27 | func testObservable_whenUsingMap_notifiesWithTransformedValues() { 28 | var result: String! 29 | let (observable, observer) = Observable.pipe() 30 | observable.map(String.init).subscribe { value in result = value } 31 | 32 | observer.update(42) 33 | 34 | XCTAssertEqual(result, "42") 35 | } 36 | 37 | func testObservable_whenUsingFilter_notifiesWithValuesThatPassPredicate() { 38 | var result = [Int]() 39 | let (observable, observer) = Observable.pipe() 40 | observable.filter { $0 % 2 == 0 }.subscribe { value in result.append(value) } 41 | 42 | observer.update(1) 43 | observer.update(2) 44 | observer.update(3) 45 | observer.update(4) 46 | 47 | XCTAssertEqual(result, [2, 4]) 48 | } 49 | 50 | func testObservable_whenUsingFilterMap_notifiesWithTransformedValuesIfTheyPassPredicate() { 51 | var result = [String]() 52 | let (observable, observer) = Observable.pipe() 53 | observable.filterMap { $0 % 2 == 0 ? "\($0)" : nil }.subscribe { value in result.append(value) } 54 | 55 | observer.update(1) 56 | observer.update(2) 57 | observer.update(3) 58 | observer.update(4) 59 | 60 | XCTAssertEqual(result, ["2", "4"]) 61 | } 62 | 63 | func testObservable_whenUsingSkipRepeats_notifiesWithOnlyUniqueValuesAccordingToPredicate() { 64 | var result = [Int]() 65 | let (observable, observer) = Observable.pipe() 66 | observable.skipRepeats { $0 == $1 }.subscribe { value in result.append(value) } 67 | 68 | observer.update(1) 69 | observer.update(1) 70 | observer.update(2) 71 | observer.update(4) 72 | observer.update(2) 73 | 74 | XCTAssertEqual(result, [1, 2, 4, 2]) 75 | } 76 | 77 | func testObservable_whenUsingSkipFirst_notifiesAfterSpecifiedNumberOfValuesPass() { 78 | var result = [Int]() 79 | let (observable, observer) = Observable.pipe() 80 | observable.skip(first: 2).subscribe { value in result.append(value) } 81 | 82 | observer.update(1) 83 | observer.update(2) 84 | observer.update(3) 85 | observer.update(4) 86 | 87 | XCTAssertEqual(result, [3, 4]) 88 | } 89 | 90 | func testObservable_whenUsingSkipWhile_notifiesWithValuesAfterOneFailsPredicate() { 91 | var result = [Int]() 92 | let (observable, observer) = Observable.pipe() 93 | observable.skip(while: { $0 != 3 }).subscribe { value in result.append(value) } 94 | 95 | observer.update(1) 96 | observer.update(2) 97 | observer.update(3) 98 | observer.update(4) 99 | 100 | XCTAssertEqual(result, [3, 4]) 101 | } 102 | 103 | func testObservable_whenUsingTakeFirst_notifiesWithOnlyFirstNumberOfValues() { 104 | var result = [Int]() 105 | let (observable, observer) = Observable.pipe() 106 | observable.take(first: 2).subscribe { value in result.append(value) } 107 | 108 | observer.update(1) 109 | observer.update(2) 110 | observer.update(3) 111 | observer.update(4) 112 | 113 | XCTAssertEqual(result, [1, 2]) 114 | } 115 | 116 | func testObservable_whenUsingTakeWhile_notifiesWithValuesUntilOneFailsPredicate() { 117 | var result = [Int]() 118 | let (observable, observer) = Observable.pipe() 119 | observable.take(while: { $0 != 3 }).subscribe { value in result.append(value) } 120 | 121 | observer.update(1) 122 | observer.update(2) 123 | observer.update(3) 124 | observer.update(4) 125 | 126 | XCTAssertEqual(result, [1, 2]) 127 | } 128 | 129 | func testObservable_whenUsingCombinePrevious_andInitialValueNotProvided_notifiesOnlyAfterSecondValueWasDispatched() { 130 | var result = [Tuple]() 131 | let (observable, observer) = Observable.pipe() 132 | observable.combinePrevious().subscribe { value in result.append(.init(value.0, value.1)) } 133 | 134 | observer.update(1) 135 | observer.update(2) 136 | observer.update(3) 137 | observer.update(4) 138 | 139 | XCTAssertEqual(result, [.init(1, 2), .init(2, 3), .init(3, 4)]) 140 | } 141 | 142 | func testObservable_whenUsingCombinePrevious_andInitialValueProvided_notifiesIncludingInitialValueWhileVeryFirstDispatch() { 143 | var result = [Tuple]() 144 | let (observable, observer) = Observable.pipe() 145 | observable.combinePrevious(initial: 0).subscribe { value in result.append(.init(value.0, value.1)) } 146 | 147 | observer.update(1) 148 | observer.update(2) 149 | observer.update(3) 150 | observer.update(4) 151 | 152 | XCTAssertEqual(result, [.init(0, 1), .init(1, 2), .init(2, 3), .init(3, 4)]) 153 | } 154 | 155 | func testObservable_whenUsingMapWithKeyPath_notifiesWithTransformedValues() { 156 | var result: String! 157 | let (observable, observer) = Observable.pipe() 158 | observable.map(\.string).subscribe { value in result = value } 159 | 160 | observer.update(42) 161 | 162 | XCTAssertEqual(result, "42") 163 | } 164 | 165 | func testObservable_whenUsingFilterWithKeyPath_notifiesWithValuesThatPassPredicate() { 166 | var result = [Int]() 167 | let (observable, observer) = Observable.pipe() 168 | observable.filter(\.isEven).subscribe { value in result.append(value) } 169 | 170 | observer.update(1) 171 | observer.update(2) 172 | observer.update(3) 173 | observer.update(4) 174 | 175 | XCTAssertEqual(result, [2, 4]) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Steroids/ObservableProducerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private struct Tuple: Equatable { 5 | let first: T 6 | let second: U 7 | 8 | init(_ first: T, _ second: U) { 9 | self.first = first 10 | self.second = second 11 | } 12 | } 13 | 14 | private extension Int { 15 | var string: String { 16 | return "\(self)" 17 | } 18 | } 19 | 20 | private extension Int { 21 | var isEven: Bool { 22 | return self % 2 == 0 23 | } 24 | } 25 | 26 | class ObservableProducerTests: XCTestCase { 27 | func testInitiatorIsNotifiedOfUpdates() { 28 | var result: Int! 29 | let producer = ObservableProducer { observer, disposables in 30 | observer.update(42) 31 | } 32 | producer.start { value in result = value } 33 | 34 | XCTAssertEqual(result, 42) 35 | } 36 | 37 | func testInitiatorIsNotifiedOnSpecifiedQueueAsynchronously() { 38 | var result: String! 39 | let id = "testInitiatorIsNotifiedOnSpecifiedQueueAsynchronously" 40 | let queueId = DispatchSpecificKey() 41 | let queue = DispatchQueue(label: id) 42 | let queueExpectation = expectation(description: id) 43 | queue.setSpecific(key: queueId, value: id) 44 | 45 | let producer = ObservableProducer { observer, disposables in 46 | observer.update(42) 47 | } 48 | 49 | producer.start(on: queue) { value in 50 | result = DispatchQueue.getSpecific(key: queueId) 51 | queueExpectation.fulfill() 52 | } 53 | 54 | waitForExpectations(timeout: 0.1) { e in 55 | queue.setSpecific(key: queueId, value: nil) 56 | 57 | XCTAssertEqual(result, id) 58 | } 59 | } 60 | 61 | func testInititatorIsNotNotifiedAfterDisposing() { 62 | var result = [Int]() 63 | let queue = DispatchQueue(label: "testInititatorIsNotNotifiedAfterDisposing") 64 | let queueExpectation = expectation(description: queue.label) 65 | let producer = ObservableProducer { observer, disposables in 66 | queue.asyncAfter(deadline: .now() + .milliseconds(5)) { 67 | (1...5).forEach(observer.update) 68 | queueExpectation.fulfill() 69 | } 70 | } 71 | 72 | var disposable: SwiftyRedux.Disposable! 73 | disposable = producer.start { value in 74 | result.append(value) 75 | if result.count == 3 { 76 | disposable?.dispose() 77 | } 78 | } 79 | 80 | waitForExpectations(timeout: 0.1) { e in 81 | XCTAssertEqual(result, [1, 2, 3]) 82 | } 83 | } 84 | 85 | func testInititatorIsNotNotifiedAfterInnerDisposing() { 86 | var result = [Int]() 87 | let producer = ObservableProducer { observer, disposables in 88 | observer.update(1) 89 | observer.update(2) 90 | observer.update(3) 91 | disposables.dispose() 92 | observer.update(4) 93 | observer.update(5) 94 | } 95 | 96 | producer.start { value in 97 | result.append(value) 98 | } 99 | 100 | XCTAssertEqual(result, [1, 2, 3]) 101 | } 102 | 103 | func testDisposableIsNotRetainedByAnyoneAtAll() { 104 | let producer = ObservableProducer { observer, disposables in } 105 | weak var disposable = producer.start { value in } 106 | 107 | XCTAssertNil(disposable) 108 | } 109 | 110 | func testNoInitiatorsAreDisposedWhenProducerDies() { 111 | var producer: ObservableProducer? = ObservableProducer { observer, disposables in } 112 | let disposable1 = producer!.start { value in } 113 | let disposable2 = producer!.start { value in } 114 | let disposable3 = producer!.start { value in } 115 | 116 | producer = nil 117 | 118 | XCTAssertFalse([disposable1, disposable2, disposable3].allSatisfy { $0.isDisposed }) 119 | } 120 | 121 | func testProducer_whenInitializedWithValue_shouldUpdateInitiatorsWithOnlyThisValue() { 122 | let initialValue = 42 123 | let producer = ObservableProducer(initialValue) 124 | 125 | producer.start { value in 126 | XCTAssertEqual(value, initialValue) 127 | } 128 | } 129 | 130 | func testProducer_whenInitializedWithAction_shouldUpdateInitiatorsByRunningThisActionEverytime() { 131 | var variable = 0 132 | let action: () -> Int = { variable } 133 | let producer = ObservableProducer(action) 134 | 135 | variable = 1 136 | producer.start { value in 137 | XCTAssertEqual(value, 1) 138 | } 139 | 140 | variable = 2 141 | producer.start { value in 142 | XCTAssertEqual(value, 2) 143 | } 144 | } 145 | } 146 | 147 | class ObservableProducerExtensionsTests: XCTestCase { 148 | func testProducer_whenUsingMap_notifiesWithTransformedValues() { 149 | var result: String! 150 | let producer = ObservableProducer { observer, disposables in 151 | observer.update(42) 152 | } 153 | 154 | producer.map(String.init).start { value in result = value } 155 | 156 | XCTAssertEqual(result, "42") 157 | } 158 | 159 | func testProducer_whenUsingFilter_notifiesWithValuesThatPassPredicate() { 160 | var result = [Int]() 161 | let producer = ObservableProducer { observer, disposables in 162 | observer.update(1) 163 | observer.update(2) 164 | observer.update(3) 165 | observer.update(4) 166 | } 167 | 168 | producer.filter { $0 % 2 == 0 }.start { value in result.append(value) } 169 | 170 | XCTAssertEqual(result, [2, 4]) 171 | } 172 | 173 | func testProducer_whenUsingFilterMap_notifiesWithTransformedValuesIfTheyPassPredicate() { 174 | var result = [String]() 175 | let producer = ObservableProducer { observer, disposables in 176 | observer.update(1) 177 | observer.update(2) 178 | observer.update(3) 179 | observer.update(4) 180 | } 181 | 182 | producer.filterMap { $0 % 2 == 0 ? "\($0)" : nil }.start { value in result.append(value) } 183 | 184 | XCTAssertEqual(result, ["2", "4"]) 185 | } 186 | 187 | func testProducer_whenUsingSkipRepeats_notifiesWithOnlyUniqueValuesAccordingToPredicate() { 188 | var result = [Int]() 189 | let producer = ObservableProducer { observer, disposables in 190 | observer.update(1) 191 | observer.update(1) 192 | observer.update(2) 193 | observer.update(4) 194 | observer.update(2) 195 | } 196 | 197 | producer.skipRepeats { $0 == $1 }.start { value in result.append(value) } 198 | 199 | XCTAssertEqual(result, [1, 2, 4, 2]) 200 | } 201 | 202 | func testProducer_whenUsingSkipFirst_notifiesAfterSpecifiedNumberOfValuesPass() { 203 | var result = [Int]() 204 | let producer = ObservableProducer { observer, disposables in 205 | observer.update(1) 206 | observer.update(2) 207 | observer.update(3) 208 | observer.update(4) 209 | } 210 | 211 | producer.skip(first: 2).start { value in result.append(value) } 212 | 213 | XCTAssertEqual(result, [3, 4]) 214 | } 215 | 216 | func testProducer_whenUsingSkipWhile_notifiesWithValuesAfterOneFailsPredicate() { 217 | var result = [Int]() 218 | let producer = ObservableProducer { observer, disposables in 219 | observer.update(1) 220 | observer.update(2) 221 | observer.update(3) 222 | observer.update(4) 223 | } 224 | 225 | producer.skip(while: { $0 != 3 }).start { value in result.append(value) } 226 | 227 | XCTAssertEqual(result, [3, 4]) 228 | } 229 | 230 | func testProducer_whenUsingTakeFirst_notifiesWithOnlyFirstNumberOfValues() { 231 | var result = [Int]() 232 | let producer = ObservableProducer { observer, disposables in 233 | observer.update(1) 234 | observer.update(2) 235 | observer.update(3) 236 | observer.update(4) 237 | } 238 | 239 | producer.take(first: 2).start { value in result.append(value) } 240 | 241 | XCTAssertEqual(result, [1, 2]) 242 | } 243 | 244 | func testProducer_whenUsingTakeWhile_notifiesWithValuesUntilOneFailsPredicate() { 245 | var result = [Int]() 246 | let producer = ObservableProducer { observer, disposables in 247 | observer.update(1) 248 | observer.update(2) 249 | observer.update(3) 250 | observer.update(4) 251 | } 252 | 253 | producer.take(while: { $0 != 3 }).start { value in result.append(value) } 254 | 255 | XCTAssertEqual(result, [1, 2]) 256 | } 257 | 258 | func testProducer_whenUsingCombinePrevious_andInitialValueNotProvided_notifiesOnlyAfterSecondValueWasDispatched() { 259 | var result = [Tuple]() 260 | let producer = ObservableProducer { observer, disposables in 261 | observer.update(1) 262 | observer.update(2) 263 | observer.update(3) 264 | observer.update(4) 265 | } 266 | 267 | producer.combinePrevious().start { value in result.append(.init(value.0, value.1)) } 268 | 269 | XCTAssertEqual(result, [.init(1, 2), .init(2, 3), .init(3, 4)]) 270 | } 271 | 272 | func testProducer_whenUsingCombinePrevious_andInitialValueProvided_notifiesIncludingInitialValueWhileVeryFirstDispatch() { 273 | var result = [Tuple]() 274 | let producer = ObservableProducer { observer, disposables in 275 | observer.update(1) 276 | observer.update(2) 277 | observer.update(3) 278 | observer.update(4) 279 | } 280 | 281 | producer.combinePrevious(initial: 0).start { value in result.append(.init(value.0, value.1)) } 282 | 283 | XCTAssertEqual(result, [.init(0, 1), .init(1, 2), .init(2, 3), .init(3, 4)]) 284 | } 285 | 286 | func testProducer_whenUsingMapWithKeyPath_notifiesWithTransformedValues() { 287 | var result: String! 288 | let producer = ObservableProducer { observer, disposables in 289 | observer.update(42) 290 | } 291 | 292 | producer.map(\.string).start { value in result = value } 293 | 294 | XCTAssertEqual(result, "42") 295 | } 296 | 297 | func testProducer_whenUsingFilterWithKeyPath_notifiesWithValuesThatPassPredicate() { 298 | var result = [Int]() 299 | let producer = ObservableProducer { observer, disposables in 300 | observer.update(1) 301 | observer.update(2) 302 | observer.update(3) 303 | observer.update(4) 304 | } 305 | 306 | producer.filter(\.isEven).start { value in result.append(value) } 307 | 308 | XCTAssertEqual(result, [2, 4]) 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /SwiftyRedux/Tests/Steroids/Store+SteroidsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyRedux 3 | 4 | private typealias State = Int 5 | 6 | private enum AnyAction: Int, SwiftyRedux.Action { case one = 1, two, three, four, five } 7 | private enum OpAction: SwiftyRedux.Action, Equatable { case inc, mul } 8 | 9 | class StoreSteroidsTests: XCTestCase { 10 | func testSubscribeToStore_whenSkippingRepeats_shouldReceiveUniqueStateUpdates() { 11 | let actions: [AnyAction] = [.one, .two, .one, .one, .three, .three, .five, .two] 12 | let reducer: Reducer = { state, action in 13 | state = (action as! AnyAction).rawValue 14 | } 15 | let store = Store(state: 0, reducer: reducer) 16 | 17 | var result: [State] = [] 18 | store.subscribeUnique(includingCurrentState: false) { state in 19 | result.append(state) 20 | } 21 | actions.forEach(store.dispatchAndWait) 22 | 23 | XCTAssertEqual(result, [1, 2, 1, 3, 5, 2]) 24 | } 25 | 26 | func testSubscribeToStore_whenSkippingRepeats_andIncludingCurrentState_shouldReceiveCurrentStateAndFurtherUniqueStateUpdatesWithoutFirstUpdate() { 27 | let actions: [AnyAction] = [.one, .two, .one, .one, .three, .three, .five, .two] 28 | let reducer: Reducer = { state, action in 29 | state = (action as! AnyAction).rawValue 30 | } 31 | let store = Store(state: 0, reducer: reducer) 32 | 33 | var result: [State] = [] 34 | store.subscribeUnique(includingCurrentState: true) { state in 35 | result.append(state) 36 | } 37 | actions.forEach(store.dispatchAndWait) 38 | 39 | XCTAssertEqual(result, [0, 1, 2, 1, 3, 5, 2]) 40 | } 41 | 42 | func testSubscribeToStore_whenSkippingRepeats_andIncludingCurrentState_andFirstUpdateEqualsToCurrentState_shouldReceiveCurrentStateAndFurtherUniqueStateUpdatesWithoutFirstUpdate() { 43 | let actions: [AnyAction] = [.one, .two, .one, .one, .three, .three, .five, .two] 44 | let reducer: Reducer = { state, action in 45 | state = (action as! AnyAction).rawValue 46 | } 47 | let store = Store(state: 1, reducer: reducer) 48 | 49 | var result: [State] = [] 50 | store.subscribeUnique(includingCurrentState: true) { state in 51 | result.append(state) 52 | } 53 | actions.forEach(store.dispatchAndWait) 54 | 55 | XCTAssertEqual(result, [1, 2, 1, 3, 5, 2]) 56 | } 57 | 58 | func testSubscribeToStore_whenNotSkippingRepeats_shouldReceiveDuplicatedStateUpdates() { 59 | let actions: [AnyAction] = [.one, .two, .one, .one, .three, .three, .five, .two] 60 | let reducer: Reducer = { state, action in 61 | state = (action as! AnyAction).rawValue 62 | } 63 | let store = Store(state: 0, reducer: reducer) 64 | 65 | var result: [State] = [] 66 | store.subscribe(includingCurrentState: false) { state in 67 | result.append(state) 68 | } 69 | actions.forEach(store.dispatchAndWait) 70 | 71 | XCTAssertEqual(result, [1, 2, 1, 1, 3, 3, 5, 2]) 72 | } 73 | 74 | func testStore_whenUnsubscribing_shouldStopReceivingStateUpdates() { 75 | let reducer: Reducer = { state, action in 76 | state = (action as! AnyAction).rawValue 77 | } 78 | let store = Store(state: 0, reducer: reducer) 79 | 80 | var result: [State] = [] 81 | let disposable = store.subscribe(includingCurrentState: false) { state in 82 | result.append(state) 83 | } 84 | store.dispatch(AnyAction.one) 85 | store.dispatch(AnyAction.two) 86 | store.dispatchAndWait(AnyAction.three) 87 | 88 | disposable.dispose() 89 | store.dispatch(AnyAction.four) 90 | store.dispatchAndWait(AnyAction.five) 91 | 92 | XCTAssertEqual(result, [1, 2, 3]) 93 | } 94 | 95 | func testStore_whenObserving_andSubscribingToObserver_shouldStartReceivingStateUpdates() { 96 | let reducer: Reducer = { state, action in 97 | switch action { 98 | case let action as OpAction where action == .mul: state *= 2 99 | case let action as OpAction where action == .inc: state += 3 100 | default: break 101 | } 102 | } 103 | let store = Store(state: 3, reducer: reducer) 104 | 105 | var result: [State] = [] 106 | store.stateObservable().subscribe { state in 107 | result.append(state) 108 | } 109 | store.dispatch(OpAction.mul) 110 | store.dispatchAndWait(OpAction.inc) 111 | 112 | XCTAssertEqual(result, [6, 9]) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /_Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /_Pods.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /_Pods.xcodeproj/xcshareddata/xcbaselines/76C5A9AE486FC6D6EABE3DC5363BA01C.xcbaseline/5646B978-E510-4B79-A411-46F1E1E64A24.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | PerformanceTests 8 | 9 | testNotify() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.000732 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | testSubscribe() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 0.0348 25 | baselineIntegrationDisplayName 26 | Local Baseline 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /_Pods.xcodeproj/xcshareddata/xcbaselines/76C5A9AE486FC6D6EABE3DC5363BA01C.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 5646B978-E510-4B79-A411-46F1E1E64A24 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2500 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro11,3 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone7,2 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /redux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-voronov/swifty-redux/b1d3eecda07b4a1471585130b9e46ddcb7258f32/redux.jpg --------------------------------------------------------------------------------