├── .gitignore ├── Podfile ├── Podfile.lock ├── README.md ├── ReSwiftAsyncMiddlewarePattern.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── ReSwiftAsyncMiddlewarePattern.xcworkspace └── contents.xcworkspacedata ├── ReSwiftAsyncMiddlewarePattern ├── Actions │ └── Actions.swift ├── AppDelegate.swift ├── AppState.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── Middleware │ ├── CreateMiddleware.swift │ ├── InjectService.swift │ └── MiddlewareItem.swift ├── Model │ ├── Post.swift │ └── User.swift ├── Reducers │ ├── AppReducer.swift │ ├── PostsReducer.swift │ └── UsersReducer.swift ├── Services │ ├── DataService.swift │ └── RemoteDataService.swift ├── SideEffects │ ├── CreatePost.swift │ ├── FetchPosts.swift │ ├── FetchUsers.swift │ └── SideEffects.swift └── View │ ├── LaunchScreen.storyboard │ ├── Main.storyboard │ └── ViewController.swift └── ReSwiftAsyncMiddlewarePatternTests ├── AppReducerSpec.swift ├── CreateMiddlewareSpec.swift ├── CreatePostSpec.swift ├── FetchPostsSpec.swift ├── FetchUsersSpec.swift ├── Info.plist ├── InjectServiceSpec.swift ├── PostsReducerSpec.swift ├── UsersReducerSpec.swift └── Utils ├── EquatableExtensions.swift ├── RandomStringExtension.swift ├── TestData.swift └── TestDataService.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '10.0' 3 | 4 | target 'ReSwiftAsyncMiddlewarePattern' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for ReSwiftAsyncMiddlewarePattern 9 | pod 'ReSwift' 10 | pod "PromiseKit", "~> 4.0" 11 | 12 | target 'ReSwiftAsyncMiddlewarePatternTests' do 13 | inherit! :search_paths 14 | # Pods for testing 15 | pod 'Quick' 16 | pod 'Nimble', :inhibit_warnings => true 17 | 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Nimble (7.0.1) 3 | - PromiseKit (4.2.2): 4 | - PromiseKit/Foundation (= 4.2.2) 5 | - PromiseKit/QuartzCore (= 4.2.2) 6 | - PromiseKit/UIKit (= 4.2.2) 7 | - PromiseKit/CorePromise (4.2.2) 8 | - PromiseKit/Foundation (4.2.2): 9 | - PromiseKit/CorePromise 10 | - PromiseKit/QuartzCore (4.2.2): 11 | - PromiseKit/CorePromise 12 | - PromiseKit/UIKit (4.2.2): 13 | - PromiseKit/CorePromise 14 | - Quick (1.1.0) 15 | - ReSwift (4.0.0) 16 | 17 | DEPENDENCIES: 18 | - Nimble 19 | - PromiseKit (~> 4.0) 20 | - Quick 21 | - ReSwift 22 | 23 | SPEC CHECKSUMS: 24 | Nimble: 657d000e11df8aebe27cdaf9d244de7f30ed87f7 25 | PromiseKit: 00e8886881f151c7e573d06b437915b0bb2970ec 26 | Quick: dafc587e21eed9f4cab3249b9f9015b0b7a7f71d 27 | ReSwift: f058ef13b46ae5f463f10605ccbbb7fb04424d8d 28 | 29 | PODFILE CHECKSUM: af0c053997a17e6abd7a0b14461e683ba44e337d 30 | 31 | COCOAPODS: 1.2.1 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReSwiftAsyncMiddlewarePattern 2 | Example of writing fully unit testable asynchronous requests using [ReSwift](https://github.com/reswift/reswift) using middleware. In this pattern the side effects are handled outside of core application logic, so action creators and reducers are side effect free. 3 | 4 | *In this context "side effect" means any call to an external service, be it Alamofire, UserDefaults or UIKit.* 5 | 6 | While this is an example project with bare minimum code, you can also see a full real-life app that was built using this pattern: https://github.com/timojaask/AwesomeQuotesReSwift 7 | 8 | ## Project 9 | This demo project consists of ReSwift reducers, actions as well as asynchronous action handlers in a form of ReSwift middleware. It includes tests for all of the core app functionality. 10 | 11 | There is no real user interface here, instead there's a test function that dispatches various actions and a store listener that prints state into console whenever it changes. 12 | 13 | ## Problem 14 | ReSwift documentation suggests to fire asynchronous operations directcly from within action creators: 15 | 16 | ```swift 17 | func fetchGitHubRepositories(state: State, store: Store) -> Action? { 18 | guard case let .LoggedIn(configuration) = state.authenticationState.loggedInState else { return nil } 19 | 20 | Octokit(configuration).repositories { response in 21 | dispatch_async(dispatch_get_main_queue()) { 22 | store.dispatch(SetRepostories(repositories: .Repositories(response))) 23 | } 24 | } 25 | 26 | return SetRepositories(repositories: .Loading) 27 | } 28 | ``` 29 | 30 | This makes it difficult to test action creators, as it is not clear how to replace `Octokit` object with a test stub. You could inject dependencies by wrapping action creators in a function that provides the required service, however then it would still feel like the action is doing too many things. It would be preferable for an `Action` to have only one purpose - describing an action. So ideally we want to call third party services somewhere outside of our actions and reducers. 31 | 32 | ## Solution 33 | Instead of firing asynchronous operations from within action creators, we can use a ReSwift middleware, whose only job will be handling side effects: 34 | 35 | ```swift 36 | // SideEffects/fetchUsers.swift 37 | func fetchUsers(action: Action, dispatch: @escaping DispatchFunction) { 38 | guard let action = action as? FetchUsers, 39 | case .request = action else { return } 40 | 41 | let dataService = RemoteDataService() 42 | dataService.fetchUsers() 43 | .then { dispatch(FetchUsers.success(users: $0)) } 44 | .catch { dispatch(FetchUsers.failure(error: $0)) } 45 | } 46 | 47 | // AppDelegate.swift 48 | let sideEffects = [ 49 | fetchUsers, 50 | fetchPosts, 51 | createPost 52 | ] 53 | let middleware = createMiddleware(items: sideEffects) 54 | let store = Store(reducer: appReducer, state: nil, middleware: [middleware]) 55 | ``` 56 | 57 | In order to make it testable, we can wrap it in a function that provides the dependency: 58 | 59 | ```swift 60 | // SideEffects/fetchUsers.swift 61 | func fetchUsers(dataService: DataService) -> MiddlewareItem { 62 | return { (action: Action, dispatch: @escaping DispatchFunction) in 63 | guard let action = action as? FetchUsers, 64 | case .request = action else { return } 65 | 66 | dataService.fetchUsers() 67 | .then { dispatch(FetchUsers.success(users: $0)) } 68 | .catch { dispatch(FetchUsers.failure(error: $0)) } 69 | } 70 | } 71 | 72 | // AppDelegate.swift 73 | let sideEffects = [ 74 | fetchUsers(RemoteDataService()), 75 | fetchPosts(RemoteDataService()), 76 | createPost(RemoteDataService()), 77 | ] 78 | let middleware = createMiddleware(items: sideEffects) 79 | let store = Store(reducer: appReducer, state: nil, middleware: [middleware]) 80 | ``` 81 | 82 | Now your actions that perform an asynchronous operation can be simple enums: 83 | 84 | ```swift 85 | enum FetchUsers: Action { 86 | case request 87 | case success(users: [User]) 88 | case failure(error: Error) 89 | } 90 | ``` 91 | 92 | And your reducers simple pure functions: 93 | 94 | ```swift 95 | func appReducer(action: Action, state: AppState?) -> AppState { 96 | return AppState( 97 | users: usersReducer(action: action, state: state?.users), 98 | posts: postsReducer(action: action, state: state?.posts) 99 | ) 100 | } 101 | 102 | func usersReducer(action: Action, state: [User]?) -> [User] { 103 | let state = state ?? [] 104 | 105 | guard let action = action as? FetchUsers, 106 | case .success(let fetchedUsers) = action else { 107 | return state 108 | } 109 | 110 | return fetchedUsers 111 | } 112 | ``` 113 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2F3F3C681F1E2AFD0048C501 /* CreateMiddlewareSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3F3C671F1E2AFD0048C501 /* CreateMiddlewareSpec.swift */; }; 11 | 2F3F3C6A1F1E38AB0048C501 /* InjectServiceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3F3C691F1E38AB0048C501 /* InjectServiceSpec.swift */; }; 12 | 2FC4448E1F1A438A009B051E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC4448D1F1A438A009B051E /* AppDelegate.swift */; }; 13 | 2FC444901F1A438A009B051E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC4448F1F1A438A009B051E /* ViewController.swift */; }; 14 | 2FC444951F1A438A009B051E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2FC444941F1A438A009B051E /* Assets.xcassets */; }; 15 | 2FC444A31F1A438A009B051E /* AppReducerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444A21F1A438A009B051E /* AppReducerSpec.swift */; }; 16 | 2FC444AE1F1A4495009B051E /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444AD1F1A4495009B051E /* Actions.swift */; }; 17 | 2FC444B01F1A44A5009B051E /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444AF1F1A44A5009B051E /* AppReducer.swift */; }; 18 | 2FC444B21F1A44B3009B051E /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444B11F1A44B3009B051E /* AppState.swift */; }; 19 | 2FC444B41F1A44C2009B051E /* DataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444B31F1A44C2009B051E /* DataService.swift */; }; 20 | 2FC444BA1F1A45CC009B051E /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444B91F1A45CC009B051E /* User.swift */; }; 21 | 2FC444BC1F1A45F1009B051E /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444BB1F1A45F1009B051E /* Post.swift */; }; 22 | 2FC444BE1F1A89F2009B051E /* UsersReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444BD1F1A89F2009B051E /* UsersReducer.swift */; }; 23 | 2FC444C01F1A8A0C009B051E /* PostsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444BF1F1A8A0C009B051E /* PostsReducer.swift */; }; 24 | 2FC444C41F1A8AD2009B051E /* SideEffects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444C31F1A8AD2009B051E /* SideEffects.swift */; }; 25 | 2FC444C61F1A8B18009B051E /* FetchUsers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444C51F1A8B18009B051E /* FetchUsers.swift */; }; 26 | 2FC444C81F1A8FA5009B051E /* FetchPosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444C71F1A8FA5009B051E /* FetchPosts.swift */; }; 27 | 2FC444CA1F1A9052009B051E /* CreatePost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444C91F1A9052009B051E /* CreatePost.swift */; }; 28 | 2FC444CD1F1A971A009B051E /* RemoteDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC444CC1F1A971A009B051E /* RemoteDataService.swift */; }; 29 | 2FC444D11F1AA211009B051E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2FC444CF1F1AA211009B051E /* LaunchScreen.storyboard */; }; 30 | 2FC444D21F1AA211009B051E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2FC444D01F1AA211009B051E /* Main.storyboard */; }; 31 | 2FC99F251F1CDE71002BCC98 /* CreateMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F221F1CDE71002BCC98 /* CreateMiddleware.swift */; }; 32 | 2FC99F261F1CDE71002BCC98 /* InjectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F231F1CDE71002BCC98 /* InjectService.swift */; }; 33 | 2FC99F271F1CDE71002BCC98 /* MiddlewareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F241F1CDE71002BCC98 /* MiddlewareItem.swift */; }; 34 | 2FC99F3A1F1D0A3F002BCC98 /* EquatableExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F391F1D0A3F002BCC98 /* EquatableExtensions.swift */; }; 35 | 2FC99F3C1F1D0A5E002BCC98 /* RandomStringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F3B1F1D0A5E002BCC98 /* RandomStringExtension.swift */; }; 36 | 2FC99F3E1F1D0A7C002BCC98 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F3D1F1D0A7C002BCC98 /* TestData.swift */; }; 37 | 2FC99F401F1D34DB002BCC98 /* UsersReducerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F3F1F1D34DB002BCC98 /* UsersReducerSpec.swift */; }; 38 | 2FC99F421F1D3F71002BCC98 /* PostsReducerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F411F1D3F71002BCC98 /* PostsReducerSpec.swift */; }; 39 | 2FC99F441F1D45F2002BCC98 /* FetchUsersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F431F1D45F2002BCC98 /* FetchUsersSpec.swift */; }; 40 | 2FC99F461F1D8794002BCC98 /* TestDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F451F1D8794002BCC98 /* TestDataService.swift */; }; 41 | 2FC99F481F1D87FB002BCC98 /* FetchPostsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F471F1D87FB002BCC98 /* FetchPostsSpec.swift */; }; 42 | 2FC99F4A1F1D99AB002BCC98 /* CreatePostSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC99F491F1D99AB002BCC98 /* CreatePostSpec.swift */; }; 43 | 4477C38973D03A8FA02FEAFC /* Pods_ReSwiftAsyncMiddlewarePattern.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5790D2E9D0185E8E0E5046EC /* Pods_ReSwiftAsyncMiddlewarePattern.framework */; }; 44 | 50EA6B2B8CF1C9414606F766 /* Pods_ReSwiftAsyncMiddlewarePatternTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 204407718DD5BFB38DF3686B /* Pods_ReSwiftAsyncMiddlewarePatternTests.framework */; }; 45 | /* End PBXBuildFile section */ 46 | 47 | /* Begin PBXContainerItemProxy section */ 48 | 2FC4449F1F1A438A009B051E /* PBXContainerItemProxy */ = { 49 | isa = PBXContainerItemProxy; 50 | containerPortal = 2FC444821F1A438A009B051E /* Project object */; 51 | proxyType = 1; 52 | remoteGlobalIDString = 2FC444891F1A438A009B051E; 53 | remoteInfo = ReSwiftAsyncMiddlewarePattern; 54 | }; 55 | /* End PBXContainerItemProxy section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | 204407718DD5BFB38DF3686B /* Pods_ReSwiftAsyncMiddlewarePatternTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReSwiftAsyncMiddlewarePatternTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | 2F3F3C671F1E2AFD0048C501 /* CreateMiddlewareSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateMiddlewareSpec.swift; sourceTree = ""; }; 60 | 2F3F3C691F1E38AB0048C501 /* InjectServiceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InjectServiceSpec.swift; sourceTree = ""; }; 61 | 2FC4448A1F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReSwiftAsyncMiddlewarePattern.app; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 2FC4448D1F1A438A009B051E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 63 | 2FC4448F1F1A438A009B051E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 64 | 2FC444941F1A438A009B051E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 65 | 2FC444991F1A438A009B051E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 2FC4449E1F1A438A009B051E /* ReSwiftAsyncMiddlewarePatternTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReSwiftAsyncMiddlewarePatternTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | 2FC444A21F1A438A009B051E /* AppReducerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerSpec.swift; sourceTree = ""; }; 68 | 2FC444A41F1A438A009B051E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | 2FC444AD1F1A4495009B051E /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; 70 | 2FC444AF1F1A44A5009B051E /* AppReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; 71 | 2FC444B11F1A44B3009B051E /* AppState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 72 | 2FC444B31F1A44C2009B051E /* DataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataService.swift; sourceTree = ""; }; 73 | 2FC444B91F1A45CC009B051E /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 74 | 2FC444BB1F1A45F1009B051E /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 75 | 2FC444BD1F1A89F2009B051E /* UsersReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersReducer.swift; sourceTree = ""; }; 76 | 2FC444BF1F1A8A0C009B051E /* PostsReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsReducer.swift; sourceTree = ""; }; 77 | 2FC444C31F1A8AD2009B051E /* SideEffects.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideEffects.swift; sourceTree = ""; }; 78 | 2FC444C51F1A8B18009B051E /* FetchUsers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchUsers.swift; sourceTree = ""; }; 79 | 2FC444C71F1A8FA5009B051E /* FetchPosts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPosts.swift; sourceTree = ""; }; 80 | 2FC444C91F1A9052009B051E /* CreatePost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePost.swift; sourceTree = ""; }; 81 | 2FC444CC1F1A971A009B051E /* RemoteDataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteDataService.swift; sourceTree = ""; }; 82 | 2FC444CF1F1AA211009B051E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 83 | 2FC444D01F1AA211009B051E /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 84 | 2FC99F221F1CDE71002BCC98 /* CreateMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateMiddleware.swift; sourceTree = ""; }; 85 | 2FC99F231F1CDE71002BCC98 /* InjectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InjectService.swift; sourceTree = ""; }; 86 | 2FC99F241F1CDE71002BCC98 /* MiddlewareItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiddlewareItem.swift; sourceTree = ""; }; 87 | 2FC99F391F1D0A3F002BCC98 /* EquatableExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EquatableExtensions.swift; sourceTree = ""; }; 88 | 2FC99F3B1F1D0A5E002BCC98 /* RandomStringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RandomStringExtension.swift; sourceTree = ""; }; 89 | 2FC99F3D1F1D0A7C002BCC98 /* TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; 90 | 2FC99F3F1F1D34DB002BCC98 /* UsersReducerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersReducerSpec.swift; sourceTree = ""; }; 91 | 2FC99F411F1D3F71002BCC98 /* PostsReducerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostsReducerSpec.swift; sourceTree = ""; }; 92 | 2FC99F431F1D45F2002BCC98 /* FetchUsersSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchUsersSpec.swift; sourceTree = ""; }; 93 | 2FC99F451F1D8794002BCC98 /* TestDataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDataService.swift; sourceTree = ""; }; 94 | 2FC99F471F1D87FB002BCC98 /* FetchPostsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPostsSpec.swift; sourceTree = ""; }; 95 | 2FC99F491F1D99AB002BCC98 /* CreatePostSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostSpec.swift; sourceTree = ""; }; 96 | 39ACB2CE797BE8210B6FA831 /* Pods-ReSwiftAsyncMiddlewarePatternTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReSwiftAsyncMiddlewarePatternTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePatternTests/Pods-ReSwiftAsyncMiddlewarePatternTests.debug.xcconfig"; sourceTree = ""; }; 97 | 446B0A90EBEA0E12CF6F73B0 /* Pods-ReSwiftAsyncMiddlewarePattern.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReSwiftAsyncMiddlewarePattern.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePattern/Pods-ReSwiftAsyncMiddlewarePattern.debug.xcconfig"; sourceTree = ""; }; 98 | 5790D2E9D0185E8E0E5046EC /* Pods_ReSwiftAsyncMiddlewarePattern.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReSwiftAsyncMiddlewarePattern.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 99 | 734C3A6B3F8EFE4EC0F66403 /* Pods-ReSwiftAsyncMiddlewarePattern.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReSwiftAsyncMiddlewarePattern.release.xcconfig"; path = "Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePattern/Pods-ReSwiftAsyncMiddlewarePattern.release.xcconfig"; sourceTree = ""; }; 100 | E3EC00B6B260A57505AB0261 /* Pods-ReSwiftAsyncMiddlewarePatternTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReSwiftAsyncMiddlewarePatternTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePatternTests/Pods-ReSwiftAsyncMiddlewarePatternTests.release.xcconfig"; sourceTree = ""; }; 101 | /* End PBXFileReference section */ 102 | 103 | /* Begin PBXFrameworksBuildPhase section */ 104 | 2FC444871F1A438A009B051E /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | 4477C38973D03A8FA02FEAFC /* Pods_ReSwiftAsyncMiddlewarePattern.framework in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | 2FC4449B1F1A438A009B051E /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | 50EA6B2B8CF1C9414606F766 /* Pods_ReSwiftAsyncMiddlewarePatternTests.framework in Frameworks */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | /* End PBXFrameworksBuildPhase section */ 121 | 122 | /* Begin PBXGroup section */ 123 | 10E155830A2602CF41FE8E73 /* Pods */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 446B0A90EBEA0E12CF6F73B0 /* Pods-ReSwiftAsyncMiddlewarePattern.debug.xcconfig */, 127 | 734C3A6B3F8EFE4EC0F66403 /* Pods-ReSwiftAsyncMiddlewarePattern.release.xcconfig */, 128 | 39ACB2CE797BE8210B6FA831 /* Pods-ReSwiftAsyncMiddlewarePatternTests.debug.xcconfig */, 129 | E3EC00B6B260A57505AB0261 /* Pods-ReSwiftAsyncMiddlewarePatternTests.release.xcconfig */, 130 | ); 131 | name = Pods; 132 | sourceTree = ""; 133 | }; 134 | 2FC444811F1A438A009B051E = { 135 | isa = PBXGroup; 136 | children = ( 137 | 2FC4448C1F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern */, 138 | 2FC444A11F1A438A009B051E /* ReSwiftAsyncMiddlewarePatternTests */, 139 | 2FC4448B1F1A438A009B051E /* Products */, 140 | 10E155830A2602CF41FE8E73 /* Pods */, 141 | 5C546DEE60EB2215975F4D48 /* Frameworks */, 142 | ); 143 | sourceTree = ""; 144 | }; 145 | 2FC4448B1F1A438A009B051E /* Products */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 2FC4448A1F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern.app */, 149 | 2FC4449E1F1A438A009B051E /* ReSwiftAsyncMiddlewarePatternTests.xctest */, 150 | ); 151 | name = Products; 152 | sourceTree = ""; 153 | }; 154 | 2FC4448C1F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 2FC4448D1F1A438A009B051E /* AppDelegate.swift */, 158 | 2FC444B11F1A44B3009B051E /* AppState.swift */, 159 | 2FC444B51F1A456C009B051E /* Actions */, 160 | 2FC444B61F1A4579009B051E /* Reducers */, 161 | 2FC444B71F1A457F009B051E /* SideEffects */, 162 | 2FC99F211F1CDD61002BCC98 /* Middleware */, 163 | 2FC444B81F1A45C4009B051E /* Model */, 164 | 2FC444CB1F1A96F2009B051E /* Services */, 165 | 2FC444CE1F1AA125009B051E /* View */, 166 | 2FC444991F1A438A009B051E /* Info.plist */, 167 | 2FC444941F1A438A009B051E /* Assets.xcassets */, 168 | ); 169 | path = ReSwiftAsyncMiddlewarePattern; 170 | sourceTree = ""; 171 | }; 172 | 2FC444A11F1A438A009B051E /* ReSwiftAsyncMiddlewarePatternTests */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 2FC444A21F1A438A009B051E /* AppReducerSpec.swift */, 176 | 2FC99F3F1F1D34DB002BCC98 /* UsersReducerSpec.swift */, 177 | 2FC99F411F1D3F71002BCC98 /* PostsReducerSpec.swift */, 178 | 2FC99F431F1D45F2002BCC98 /* FetchUsersSpec.swift */, 179 | 2FC99F471F1D87FB002BCC98 /* FetchPostsSpec.swift */, 180 | 2FC99F491F1D99AB002BCC98 /* CreatePostSpec.swift */, 181 | 2F3F3C671F1E2AFD0048C501 /* CreateMiddlewareSpec.swift */, 182 | 2F3F3C691F1E38AB0048C501 /* InjectServiceSpec.swift */, 183 | 2FC99F381F1D0A18002BCC98 /* Utils */, 184 | 2FC444A41F1A438A009B051E /* Info.plist */, 185 | ); 186 | path = ReSwiftAsyncMiddlewarePatternTests; 187 | sourceTree = ""; 188 | }; 189 | 2FC444B51F1A456C009B051E /* Actions */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 2FC444AD1F1A4495009B051E /* Actions.swift */, 193 | ); 194 | path = Actions; 195 | sourceTree = ""; 196 | }; 197 | 2FC444B61F1A4579009B051E /* Reducers */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | 2FC444AF1F1A44A5009B051E /* AppReducer.swift */, 201 | 2FC444BD1F1A89F2009B051E /* UsersReducer.swift */, 202 | 2FC444BF1F1A8A0C009B051E /* PostsReducer.swift */, 203 | ); 204 | path = Reducers; 205 | sourceTree = ""; 206 | }; 207 | 2FC444B71F1A457F009B051E /* SideEffects */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | 2FC444C31F1A8AD2009B051E /* SideEffects.swift */, 211 | 2FC444C51F1A8B18009B051E /* FetchUsers.swift */, 212 | 2FC444C71F1A8FA5009B051E /* FetchPosts.swift */, 213 | 2FC444C91F1A9052009B051E /* CreatePost.swift */, 214 | ); 215 | path = SideEffects; 216 | sourceTree = ""; 217 | }; 218 | 2FC444B81F1A45C4009B051E /* Model */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | 2FC444B91F1A45CC009B051E /* User.swift */, 222 | 2FC444BB1F1A45F1009B051E /* Post.swift */, 223 | ); 224 | path = Model; 225 | sourceTree = ""; 226 | }; 227 | 2FC444CB1F1A96F2009B051E /* Services */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 2FC444B31F1A44C2009B051E /* DataService.swift */, 231 | 2FC444CC1F1A971A009B051E /* RemoteDataService.swift */, 232 | ); 233 | path = Services; 234 | sourceTree = ""; 235 | }; 236 | 2FC444CE1F1AA125009B051E /* View */ = { 237 | isa = PBXGroup; 238 | children = ( 239 | 2FC444CF1F1AA211009B051E /* LaunchScreen.storyboard */, 240 | 2FC444D01F1AA211009B051E /* Main.storyboard */, 241 | 2FC4448F1F1A438A009B051E /* ViewController.swift */, 242 | ); 243 | path = View; 244 | sourceTree = ""; 245 | }; 246 | 2FC99F211F1CDD61002BCC98 /* Middleware */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | 2FC99F221F1CDE71002BCC98 /* CreateMiddleware.swift */, 250 | 2FC99F231F1CDE71002BCC98 /* InjectService.swift */, 251 | 2FC99F241F1CDE71002BCC98 /* MiddlewareItem.swift */, 252 | ); 253 | path = Middleware; 254 | sourceTree = ""; 255 | }; 256 | 2FC99F381F1D0A18002BCC98 /* Utils */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | 2FC99F391F1D0A3F002BCC98 /* EquatableExtensions.swift */, 260 | 2FC99F3B1F1D0A5E002BCC98 /* RandomStringExtension.swift */, 261 | 2FC99F3D1F1D0A7C002BCC98 /* TestData.swift */, 262 | 2FC99F451F1D8794002BCC98 /* TestDataService.swift */, 263 | ); 264 | path = Utils; 265 | sourceTree = ""; 266 | }; 267 | 5C546DEE60EB2215975F4D48 /* Frameworks */ = { 268 | isa = PBXGroup; 269 | children = ( 270 | 5790D2E9D0185E8E0E5046EC /* Pods_ReSwiftAsyncMiddlewarePattern.framework */, 271 | 204407718DD5BFB38DF3686B /* Pods_ReSwiftAsyncMiddlewarePatternTests.framework */, 272 | ); 273 | name = Frameworks; 274 | sourceTree = ""; 275 | }; 276 | /* End PBXGroup section */ 277 | 278 | /* Begin PBXNativeTarget section */ 279 | 2FC444891F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern */ = { 280 | isa = PBXNativeTarget; 281 | buildConfigurationList = 2FC444A71F1A438A009B051E /* Build configuration list for PBXNativeTarget "ReSwiftAsyncMiddlewarePattern" */; 282 | buildPhases = ( 283 | D1A336226E36459770BF6C88 /* [CP] Check Pods Manifest.lock */, 284 | 2FC444861F1A438A009B051E /* Sources */, 285 | 2FC444871F1A438A009B051E /* Frameworks */, 286 | 2FC444881F1A438A009B051E /* Resources */, 287 | 430174461FA7A60905DD0689 /* [CP] Embed Pods Frameworks */, 288 | 2A39E6E87406CC311E81A873 /* [CP] Copy Pods Resources */, 289 | ); 290 | buildRules = ( 291 | ); 292 | dependencies = ( 293 | ); 294 | name = ReSwiftAsyncMiddlewarePattern; 295 | productName = ReSwiftAsyncMiddlewarePattern; 296 | productReference = 2FC4448A1F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern.app */; 297 | productType = "com.apple.product-type.application"; 298 | }; 299 | 2FC4449D1F1A438A009B051E /* ReSwiftAsyncMiddlewarePatternTests */ = { 300 | isa = PBXNativeTarget; 301 | buildConfigurationList = 2FC444AA1F1A438A009B051E /* Build configuration list for PBXNativeTarget "ReSwiftAsyncMiddlewarePatternTests" */; 302 | buildPhases = ( 303 | 7328E57AA26B6DC03F18EBF4 /* [CP] Check Pods Manifest.lock */, 304 | 2FC4449A1F1A438A009B051E /* Sources */, 305 | 2FC4449B1F1A438A009B051E /* Frameworks */, 306 | 2FC4449C1F1A438A009B051E /* Resources */, 307 | DFDB4AE5E04A89D9CBB8E088 /* [CP] Embed Pods Frameworks */, 308 | E6D7BA242C0C2AF56A2CA22F /* [CP] Copy Pods Resources */, 309 | ); 310 | buildRules = ( 311 | ); 312 | dependencies = ( 313 | 2FC444A01F1A438A009B051E /* PBXTargetDependency */, 314 | ); 315 | name = ReSwiftAsyncMiddlewarePatternTests; 316 | productName = ReSwiftAsyncMiddlewarePatternTests; 317 | productReference = 2FC4449E1F1A438A009B051E /* ReSwiftAsyncMiddlewarePatternTests.xctest */; 318 | productType = "com.apple.product-type.bundle.unit-test"; 319 | }; 320 | /* End PBXNativeTarget section */ 321 | 322 | /* Begin PBXProject section */ 323 | 2FC444821F1A438A009B051E /* Project object */ = { 324 | isa = PBXProject; 325 | attributes = { 326 | LastSwiftUpdateCheck = 0830; 327 | LastUpgradeCheck = 0830; 328 | ORGANIZATIONNAME = timojaask; 329 | TargetAttributes = { 330 | 2FC444891F1A438A009B051E = { 331 | CreatedOnToolsVersion = 8.3.3; 332 | DevelopmentTeam = 6W4GZQ5Z99; 333 | ProvisioningStyle = Automatic; 334 | }; 335 | 2FC4449D1F1A438A009B051E = { 336 | CreatedOnToolsVersion = 8.3.3; 337 | DevelopmentTeam = 6W4GZQ5Z99; 338 | ProvisioningStyle = Automatic; 339 | TestTargetID = 2FC444891F1A438A009B051E; 340 | }; 341 | }; 342 | }; 343 | buildConfigurationList = 2FC444851F1A438A009B051E /* Build configuration list for PBXProject "ReSwiftAsyncMiddlewarePattern" */; 344 | compatibilityVersion = "Xcode 3.2"; 345 | developmentRegion = English; 346 | hasScannedForEncodings = 0; 347 | knownRegions = ( 348 | en, 349 | Base, 350 | ); 351 | mainGroup = 2FC444811F1A438A009B051E; 352 | productRefGroup = 2FC4448B1F1A438A009B051E /* Products */; 353 | projectDirPath = ""; 354 | projectRoot = ""; 355 | targets = ( 356 | 2FC444891F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern */, 357 | 2FC4449D1F1A438A009B051E /* ReSwiftAsyncMiddlewarePatternTests */, 358 | ); 359 | }; 360 | /* End PBXProject section */ 361 | 362 | /* Begin PBXResourcesBuildPhase section */ 363 | 2FC444881F1A438A009B051E /* Resources */ = { 364 | isa = PBXResourcesBuildPhase; 365 | buildActionMask = 2147483647; 366 | files = ( 367 | 2FC444D21F1AA211009B051E /* Main.storyboard in Resources */, 368 | 2FC444951F1A438A009B051E /* Assets.xcassets in Resources */, 369 | 2FC444D11F1AA211009B051E /* LaunchScreen.storyboard in Resources */, 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | }; 373 | 2FC4449C1F1A438A009B051E /* Resources */ = { 374 | isa = PBXResourcesBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | /* End PBXResourcesBuildPhase section */ 381 | 382 | /* Begin PBXShellScriptBuildPhase section */ 383 | 2A39E6E87406CC311E81A873 /* [CP] Copy Pods Resources */ = { 384 | isa = PBXShellScriptBuildPhase; 385 | buildActionMask = 2147483647; 386 | files = ( 387 | ); 388 | inputPaths = ( 389 | ); 390 | name = "[CP] Copy Pods Resources"; 391 | outputPaths = ( 392 | ); 393 | runOnlyForDeploymentPostprocessing = 0; 394 | shellPath = /bin/sh; 395 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePattern/Pods-ReSwiftAsyncMiddlewarePattern-resources.sh\"\n"; 396 | showEnvVarsInLog = 0; 397 | }; 398 | 430174461FA7A60905DD0689 /* [CP] Embed Pods Frameworks */ = { 399 | isa = PBXShellScriptBuildPhase; 400 | buildActionMask = 2147483647; 401 | files = ( 402 | ); 403 | inputPaths = ( 404 | ); 405 | name = "[CP] Embed Pods Frameworks"; 406 | outputPaths = ( 407 | ); 408 | runOnlyForDeploymentPostprocessing = 0; 409 | shellPath = /bin/sh; 410 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePattern/Pods-ReSwiftAsyncMiddlewarePattern-frameworks.sh\"\n"; 411 | showEnvVarsInLog = 0; 412 | }; 413 | 7328E57AA26B6DC03F18EBF4 /* [CP] Check Pods Manifest.lock */ = { 414 | isa = PBXShellScriptBuildPhase; 415 | buildActionMask = 2147483647; 416 | files = ( 417 | ); 418 | inputPaths = ( 419 | ); 420 | name = "[CP] Check Pods Manifest.lock"; 421 | outputPaths = ( 422 | ); 423 | runOnlyForDeploymentPostprocessing = 0; 424 | shellPath = /bin/sh; 425 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 426 | showEnvVarsInLog = 0; 427 | }; 428 | D1A336226E36459770BF6C88 /* [CP] Check Pods Manifest.lock */ = { 429 | isa = PBXShellScriptBuildPhase; 430 | buildActionMask = 2147483647; 431 | files = ( 432 | ); 433 | inputPaths = ( 434 | ); 435 | name = "[CP] Check Pods Manifest.lock"; 436 | outputPaths = ( 437 | ); 438 | runOnlyForDeploymentPostprocessing = 0; 439 | shellPath = /bin/sh; 440 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 441 | showEnvVarsInLog = 0; 442 | }; 443 | DFDB4AE5E04A89D9CBB8E088 /* [CP] Embed Pods Frameworks */ = { 444 | isa = PBXShellScriptBuildPhase; 445 | buildActionMask = 2147483647; 446 | files = ( 447 | ); 448 | inputPaths = ( 449 | ); 450 | name = "[CP] Embed Pods Frameworks"; 451 | outputPaths = ( 452 | ); 453 | runOnlyForDeploymentPostprocessing = 0; 454 | shellPath = /bin/sh; 455 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePatternTests/Pods-ReSwiftAsyncMiddlewarePatternTests-frameworks.sh\"\n"; 456 | showEnvVarsInLog = 0; 457 | }; 458 | E6D7BA242C0C2AF56A2CA22F /* [CP] Copy Pods Resources */ = { 459 | isa = PBXShellScriptBuildPhase; 460 | buildActionMask = 2147483647; 461 | files = ( 462 | ); 463 | inputPaths = ( 464 | ); 465 | name = "[CP] Copy Pods Resources"; 466 | outputPaths = ( 467 | ); 468 | runOnlyForDeploymentPostprocessing = 0; 469 | shellPath = /bin/sh; 470 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ReSwiftAsyncMiddlewarePatternTests/Pods-ReSwiftAsyncMiddlewarePatternTests-resources.sh\"\n"; 471 | showEnvVarsInLog = 0; 472 | }; 473 | /* End PBXShellScriptBuildPhase section */ 474 | 475 | /* Begin PBXSourcesBuildPhase section */ 476 | 2FC444861F1A438A009B051E /* Sources */ = { 477 | isa = PBXSourcesBuildPhase; 478 | buildActionMask = 2147483647; 479 | files = ( 480 | 2FC444AE1F1A4495009B051E /* Actions.swift in Sources */, 481 | 2FC444901F1A438A009B051E /* ViewController.swift in Sources */, 482 | 2FC99F261F1CDE71002BCC98 /* InjectService.swift in Sources */, 483 | 2FC444C01F1A8A0C009B051E /* PostsReducer.swift in Sources */, 484 | 2FC99F251F1CDE71002BCC98 /* CreateMiddleware.swift in Sources */, 485 | 2FC444CA1F1A9052009B051E /* CreatePost.swift in Sources */, 486 | 2FC99F271F1CDE71002BCC98 /* MiddlewareItem.swift in Sources */, 487 | 2FC444B01F1A44A5009B051E /* AppReducer.swift in Sources */, 488 | 2FC444BC1F1A45F1009B051E /* Post.swift in Sources */, 489 | 2FC444BE1F1A89F2009B051E /* UsersReducer.swift in Sources */, 490 | 2FC444C81F1A8FA5009B051E /* FetchPosts.swift in Sources */, 491 | 2FC444B41F1A44C2009B051E /* DataService.swift in Sources */, 492 | 2FC444CD1F1A971A009B051E /* RemoteDataService.swift in Sources */, 493 | 2FC444B21F1A44B3009B051E /* AppState.swift in Sources */, 494 | 2FC444C41F1A8AD2009B051E /* SideEffects.swift in Sources */, 495 | 2FC444C61F1A8B18009B051E /* FetchUsers.swift in Sources */, 496 | 2FC4448E1F1A438A009B051E /* AppDelegate.swift in Sources */, 497 | 2FC444BA1F1A45CC009B051E /* User.swift in Sources */, 498 | ); 499 | runOnlyForDeploymentPostprocessing = 0; 500 | }; 501 | 2FC4449A1F1A438A009B051E /* Sources */ = { 502 | isa = PBXSourcesBuildPhase; 503 | buildActionMask = 2147483647; 504 | files = ( 505 | 2F3F3C681F1E2AFD0048C501 /* CreateMiddlewareSpec.swift in Sources */, 506 | 2FC99F421F1D3F71002BCC98 /* PostsReducerSpec.swift in Sources */, 507 | 2FC444A31F1A438A009B051E /* AppReducerSpec.swift in Sources */, 508 | 2F3F3C6A1F1E38AB0048C501 /* InjectServiceSpec.swift in Sources */, 509 | 2FC99F3E1F1D0A7C002BCC98 /* TestData.swift in Sources */, 510 | 2FC99F441F1D45F2002BCC98 /* FetchUsersSpec.swift in Sources */, 511 | 2FC99F4A1F1D99AB002BCC98 /* CreatePostSpec.swift in Sources */, 512 | 2FC99F401F1D34DB002BCC98 /* UsersReducerSpec.swift in Sources */, 513 | 2FC99F3C1F1D0A5E002BCC98 /* RandomStringExtension.swift in Sources */, 514 | 2FC99F461F1D8794002BCC98 /* TestDataService.swift in Sources */, 515 | 2FC99F481F1D87FB002BCC98 /* FetchPostsSpec.swift in Sources */, 516 | 2FC99F3A1F1D0A3F002BCC98 /* EquatableExtensions.swift in Sources */, 517 | ); 518 | runOnlyForDeploymentPostprocessing = 0; 519 | }; 520 | /* End PBXSourcesBuildPhase section */ 521 | 522 | /* Begin PBXTargetDependency section */ 523 | 2FC444A01F1A438A009B051E /* PBXTargetDependency */ = { 524 | isa = PBXTargetDependency; 525 | target = 2FC444891F1A438A009B051E /* ReSwiftAsyncMiddlewarePattern */; 526 | targetProxy = 2FC4449F1F1A438A009B051E /* PBXContainerItemProxy */; 527 | }; 528 | /* End PBXTargetDependency section */ 529 | 530 | /* Begin XCBuildConfiguration section */ 531 | 2FC444A51F1A438A009B051E /* Debug */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | ALWAYS_SEARCH_USER_PATHS = NO; 535 | CLANG_ANALYZER_NONNULL = YES; 536 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 537 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 538 | CLANG_CXX_LIBRARY = "libc++"; 539 | CLANG_ENABLE_MODULES = YES; 540 | CLANG_ENABLE_OBJC_ARC = YES; 541 | CLANG_WARN_BOOL_CONVERSION = YES; 542 | CLANG_WARN_CONSTANT_CONVERSION = YES; 543 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 544 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 545 | CLANG_WARN_EMPTY_BODY = YES; 546 | CLANG_WARN_ENUM_CONVERSION = YES; 547 | CLANG_WARN_INFINITE_RECURSION = YES; 548 | CLANG_WARN_INT_CONVERSION = YES; 549 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 550 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 551 | CLANG_WARN_UNREACHABLE_CODE = YES; 552 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 553 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 554 | COPY_PHASE_STRIP = NO; 555 | DEBUG_INFORMATION_FORMAT = dwarf; 556 | ENABLE_STRICT_OBJC_MSGSEND = YES; 557 | ENABLE_TESTABILITY = YES; 558 | GCC_C_LANGUAGE_STANDARD = gnu99; 559 | GCC_DYNAMIC_NO_PIC = NO; 560 | GCC_NO_COMMON_BLOCKS = YES; 561 | GCC_OPTIMIZATION_LEVEL = 0; 562 | GCC_PREPROCESSOR_DEFINITIONS = ( 563 | "DEBUG=1", 564 | "$(inherited)", 565 | ); 566 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 567 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 568 | GCC_WARN_UNDECLARED_SELECTOR = YES; 569 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 570 | GCC_WARN_UNUSED_FUNCTION = YES; 571 | GCC_WARN_UNUSED_VARIABLE = YES; 572 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 573 | MTL_ENABLE_DEBUG_INFO = YES; 574 | ONLY_ACTIVE_ARCH = YES; 575 | SDKROOT = iphoneos; 576 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 577 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 578 | TARGETED_DEVICE_FAMILY = "1,2"; 579 | }; 580 | name = Debug; 581 | }; 582 | 2FC444A61F1A438A009B051E /* Release */ = { 583 | isa = XCBuildConfiguration; 584 | buildSettings = { 585 | ALWAYS_SEARCH_USER_PATHS = NO; 586 | CLANG_ANALYZER_NONNULL = YES; 587 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 588 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 589 | CLANG_CXX_LIBRARY = "libc++"; 590 | CLANG_ENABLE_MODULES = YES; 591 | CLANG_ENABLE_OBJC_ARC = YES; 592 | CLANG_WARN_BOOL_CONVERSION = YES; 593 | CLANG_WARN_CONSTANT_CONVERSION = YES; 594 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 595 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 596 | CLANG_WARN_EMPTY_BODY = YES; 597 | CLANG_WARN_ENUM_CONVERSION = YES; 598 | CLANG_WARN_INFINITE_RECURSION = YES; 599 | CLANG_WARN_INT_CONVERSION = YES; 600 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 601 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 602 | CLANG_WARN_UNREACHABLE_CODE = YES; 603 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 604 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 605 | COPY_PHASE_STRIP = NO; 606 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 607 | ENABLE_NS_ASSERTIONS = NO; 608 | ENABLE_STRICT_OBJC_MSGSEND = YES; 609 | GCC_C_LANGUAGE_STANDARD = gnu99; 610 | GCC_NO_COMMON_BLOCKS = YES; 611 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 612 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 613 | GCC_WARN_UNDECLARED_SELECTOR = YES; 614 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 615 | GCC_WARN_UNUSED_FUNCTION = YES; 616 | GCC_WARN_UNUSED_VARIABLE = YES; 617 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 618 | MTL_ENABLE_DEBUG_INFO = NO; 619 | SDKROOT = iphoneos; 620 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 621 | TARGETED_DEVICE_FAMILY = "1,2"; 622 | VALIDATE_PRODUCT = YES; 623 | }; 624 | name = Release; 625 | }; 626 | 2FC444A81F1A438A009B051E /* Debug */ = { 627 | isa = XCBuildConfiguration; 628 | baseConfigurationReference = 446B0A90EBEA0E12CF6F73B0 /* Pods-ReSwiftAsyncMiddlewarePattern.debug.xcconfig */; 629 | buildSettings = { 630 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 631 | DEVELOPMENT_TEAM = 6W4GZQ5Z99; 632 | INFOPLIST_FILE = ReSwiftAsyncMiddlewarePattern/Info.plist; 633 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 634 | PRODUCT_BUNDLE_IDENTIFIER = com.timojaask.ReSwiftAsyncMiddlewarePattern; 635 | PRODUCT_NAME = "$(TARGET_NAME)"; 636 | SWIFT_VERSION = 3.0; 637 | }; 638 | name = Debug; 639 | }; 640 | 2FC444A91F1A438A009B051E /* Release */ = { 641 | isa = XCBuildConfiguration; 642 | baseConfigurationReference = 734C3A6B3F8EFE4EC0F66403 /* Pods-ReSwiftAsyncMiddlewarePattern.release.xcconfig */; 643 | buildSettings = { 644 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 645 | DEVELOPMENT_TEAM = 6W4GZQ5Z99; 646 | INFOPLIST_FILE = ReSwiftAsyncMiddlewarePattern/Info.plist; 647 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 648 | PRODUCT_BUNDLE_IDENTIFIER = com.timojaask.ReSwiftAsyncMiddlewarePattern; 649 | PRODUCT_NAME = "$(TARGET_NAME)"; 650 | SWIFT_VERSION = 3.0; 651 | }; 652 | name = Release; 653 | }; 654 | 2FC444AB1F1A438A009B051E /* Debug */ = { 655 | isa = XCBuildConfiguration; 656 | baseConfigurationReference = 39ACB2CE797BE8210B6FA831 /* Pods-ReSwiftAsyncMiddlewarePatternTests.debug.xcconfig */; 657 | buildSettings = { 658 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; 659 | BUNDLE_LOADER = "$(TEST_HOST)"; 660 | DEVELOPMENT_TEAM = 6W4GZQ5Z99; 661 | INFOPLIST_FILE = ReSwiftAsyncMiddlewarePatternTests/Info.plist; 662 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 663 | PRODUCT_BUNDLE_IDENTIFIER = com.timojaask.ReSwiftAsyncMiddlewarePatternTests; 664 | PRODUCT_NAME = "$(TARGET_NAME)"; 665 | SWIFT_VERSION = 3.0; 666 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReSwiftAsyncMiddlewarePattern.app/ReSwiftAsyncMiddlewarePattern"; 667 | }; 668 | name = Debug; 669 | }; 670 | 2FC444AC1F1A438A009B051E /* Release */ = { 671 | isa = XCBuildConfiguration; 672 | baseConfigurationReference = E3EC00B6B260A57505AB0261 /* Pods-ReSwiftAsyncMiddlewarePatternTests.release.xcconfig */; 673 | buildSettings = { 674 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; 675 | BUNDLE_LOADER = "$(TEST_HOST)"; 676 | DEVELOPMENT_TEAM = 6W4GZQ5Z99; 677 | INFOPLIST_FILE = ReSwiftAsyncMiddlewarePatternTests/Info.plist; 678 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 679 | PRODUCT_BUNDLE_IDENTIFIER = com.timojaask.ReSwiftAsyncMiddlewarePatternTests; 680 | PRODUCT_NAME = "$(TARGET_NAME)"; 681 | SWIFT_VERSION = 3.0; 682 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReSwiftAsyncMiddlewarePattern.app/ReSwiftAsyncMiddlewarePattern"; 683 | }; 684 | name = Release; 685 | }; 686 | /* End XCBuildConfiguration section */ 687 | 688 | /* Begin XCConfigurationList section */ 689 | 2FC444851F1A438A009B051E /* Build configuration list for PBXProject "ReSwiftAsyncMiddlewarePattern" */ = { 690 | isa = XCConfigurationList; 691 | buildConfigurations = ( 692 | 2FC444A51F1A438A009B051E /* Debug */, 693 | 2FC444A61F1A438A009B051E /* Release */, 694 | ); 695 | defaultConfigurationIsVisible = 0; 696 | defaultConfigurationName = Release; 697 | }; 698 | 2FC444A71F1A438A009B051E /* Build configuration list for PBXNativeTarget "ReSwiftAsyncMiddlewarePattern" */ = { 699 | isa = XCConfigurationList; 700 | buildConfigurations = ( 701 | 2FC444A81F1A438A009B051E /* Debug */, 702 | 2FC444A91F1A438A009B051E /* Release */, 703 | ); 704 | defaultConfigurationIsVisible = 0; 705 | defaultConfigurationName = Release; 706 | }; 707 | 2FC444AA1F1A438A009B051E /* Build configuration list for PBXNativeTarget "ReSwiftAsyncMiddlewarePatternTests" */ = { 708 | isa = XCConfigurationList; 709 | buildConfigurations = ( 710 | 2FC444AB1F1A438A009B051E /* Debug */, 711 | 2FC444AC1F1A438A009B051E /* Release */, 712 | ); 713 | defaultConfigurationIsVisible = 0; 714 | defaultConfigurationName = Release; 715 | }; 716 | /* End XCConfigurationList section */ 717 | }; 718 | rootObject = 2FC444821F1A438A009B051E /* Project object */; 719 | } 720 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Actions/Actions.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | enum FetchUsers: Action { 4 | case request 5 | case success(users: [User]) 6 | case failure(error: Error) 7 | } 8 | 9 | enum FetchPosts: Action { 10 | case request 11 | case success(posts: [Post]) 12 | case failure(error: Error) 13 | } 14 | 15 | enum CreatePost: Action { 16 | case request(post: Post) 17 | case success 18 | case failure(error: Error) 19 | } 20 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ReSwift 3 | 4 | @UIApplicationMain 5 | class AppDelegate: UIResponder, UIApplicationDelegate { 6 | 7 | var window: UIWindow? 8 | 9 | var store: Store! 10 | let debugStoreSubscriber = DebugStoreSubscriber() 11 | 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 13 | let sideEffects = injectService(service: RemoteDataService(), receivers: dataServiceSideEffects) 14 | let middleware = createMiddleware(items: sideEffects) 15 | store = Store(reducer: appReducer, state: nil, middleware: [middleware]) 16 | store.subscribe(debugStoreSubscriber) 17 | 18 | testPostingAndFetchingStuff() 19 | return true 20 | } 21 | 22 | func testPostingAndFetchingStuff() { 23 | let testPost = Post(title: "New Post", body: "Body of the new post") 24 | store.dispatch(CreatePost.request(post: testPost)) 25 | store.dispatch(FetchPosts.request) 26 | } 27 | } 28 | 29 | class DebugStoreSubscriber: StoreSubscriber { 30 | func newState(state: AppState) { 31 | print("State changed") 32 | print(" -- posts: \(state.posts)") 33 | print(" -- users: \(state.users)") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/AppState.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | struct AppState: StateType { 4 | var users: [User] = [] 5 | var posts: [Post] = [] 6 | } 7 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Middleware/CreateMiddleware.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | func createMiddleware(items: [MiddlewareItem]) -> Middleware { 4 | return { dispatch, getState in 5 | return { next in 6 | return { action in 7 | items.forEach { $0(action, dispatch) } 8 | return next(action) 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Middleware/InjectService.swift: -------------------------------------------------------------------------------- 1 | func injectService(service: T, receivers: [(T) -> (U)]) -> [U] { 2 | return receivers.map { $0(service) } 3 | } 4 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Middleware/MiddlewareItem.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | typealias MiddlewareItem = (Action, @escaping DispatchFunction) -> () 4 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Model/Post.swift: -------------------------------------------------------------------------------- 1 | struct Post { 2 | let title: String 3 | let body: String 4 | } 5 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Model/User.swift: -------------------------------------------------------------------------------- 1 | struct User { 2 | let firstName: String 3 | let lastName: String 4 | } 5 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Reducers/AppReducer.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | func appReducer(action: Action, state: AppState?) -> AppState { 4 | print("appReducer with action: \(action)") 5 | return AppState( 6 | users: usersReducer(action: action, state: state?.users), 7 | posts: postsReducer(action: action, state: state?.posts) 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Reducers/PostsReducer.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | func postsReducer(action: Action, state: [Post]?) -> [Post] { 4 | let state = state ?? [] 5 | 6 | guard let action = action as? FetchPosts, 7 | case .success(let fetchedPosts) = action else { 8 | return state 9 | } 10 | 11 | print("postsReducer reducer") 12 | 13 | return fetchedPosts 14 | } 15 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Reducers/UsersReducer.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | func usersReducer(action: Action, state: [User]?) -> [User] { 4 | let state = state ?? [] 5 | 6 | guard let action = action as? FetchUsers, 7 | case .success(let fetchedUsers) = action else { 8 | return state 9 | } 10 | 11 | print("usersReducer reducer") 12 | 13 | return fetchedUsers 14 | } 15 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Services/DataService.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | 3 | protocol DataService { 4 | func fetchUsers() -> Promise<[User]> 5 | func fetchPosts() -> Promise<[Post]> 6 | func createPost(post: Post) -> Promise 7 | } 8 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/Services/RemoteDataService.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | 3 | struct RemoteDataService: DataService { 4 | func fetchUsers() -> Promise<[User]> { 5 | return Promise<[User]> { fulfill, reject in 6 | FakeRemoteService.executeWithDelay { 7 | fulfill(FakeRemoteService.users) 8 | } 9 | } 10 | } 11 | 12 | func fetchPosts() -> Promise<[Post]> { 13 | return Promise<[Post]> { fulfill, reject in 14 | FakeRemoteService.executeWithDelay { 15 | fulfill(FakeRemoteService.posts) 16 | } 17 | } 18 | } 19 | 20 | func createPost(post: Post) -> Promise { 21 | return Promise { fulfill, reject in 22 | FakeRemoteService.executeWithDelay { 23 | FakeRemoteService.posts.append(post) 24 | fulfill() 25 | } 26 | } 27 | } 28 | } 29 | 30 | fileprivate struct FakeRemoteService { 31 | static var posts = [ 32 | Post(title: "Post One", body: "Body of post one."), 33 | Post(title: "Post Two", body: "Body of post two."), 34 | Post(title: "Post Three", body: "Body of post three."), 35 | ] 36 | 37 | static let users = [ 38 | User(firstName: "User", lastName: "One"), 39 | User(firstName: "User", lastName: "Two"), 40 | User(firstName: "User", lastName: "Three"), 41 | ] 42 | 43 | static func executeWithDelay(closure: @escaping () -> ()) { 44 | let artificialDelaySeconds = 2.0 45 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + artificialDelaySeconds) { 46 | closure() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/SideEffects/CreatePost.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | func createPost(dataService: DataService) -> MiddlewareItem { 4 | return { (action: Action, dispatch: @escaping DispatchFunction) in 5 | guard let action = action as? CreatePost, 6 | case .request(let post) = action else { return } 7 | 8 | print("createPost side effect") 9 | 10 | dataService.createPost(post: post) 11 | .then { dispatch(CreatePost.success) } 12 | .catch { dispatch(CreatePost.failure(error: $0)) } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/SideEffects/FetchPosts.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | func fetchPosts(dataService: DataService) -> MiddlewareItem { 4 | return { (action: Action, dispatch: @escaping DispatchFunction) in 5 | guard let action = action as? FetchPosts, 6 | case .request = action else { return } 7 | 8 | print("fetchPosts side effect") 9 | 10 | dataService.fetchPosts() 11 | .then { dispatch(FetchPosts.success(posts: $0)) } 12 | .catch { dispatch(FetchPosts.failure(error: $0)) } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/SideEffects/FetchUsers.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | func fetchUsers(dataService: DataService) -> MiddlewareItem { 4 | return { (action: Action, dispatch: @escaping DispatchFunction) in 5 | guard let action = action as? FetchUsers, 6 | case .request = action else { return } 7 | 8 | print("fetchUsers side effect") 9 | 10 | dataService.fetchUsers() 11 | .then { dispatch(FetchUsers.success(users: $0)) } 12 | .catch { dispatch(FetchUsers.failure(error: $0)) } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/SideEffects/SideEffects.swift: -------------------------------------------------------------------------------- 1 | let dataServiceSideEffects = [ 2 | fetchUsers, 3 | fetchPosts, 4 | createPost 5 | ] 6 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/View/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/View/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePattern/View/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewController: UIViewController { 4 | 5 | override func viewDidLoad() { 6 | super.viewDidLoad() 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/AppReducerSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | @testable import ReSwiftAsyncMiddlewarePattern 4 | 5 | class AppReducerSpec: QuickSpec { 6 | 7 | override func spec() { 8 | describe("Reducer") { 9 | 10 | it("Sets users array when FetchUsers.success action is called, keeping other state unchanged") { 11 | let newUsers = randomUsers() 12 | let fetchUsersSuccessAction = FetchUsers.success(users: newUsers) 13 | 14 | let initialState = randomAppState() 15 | let expectedState = AppState(users: newUsers, posts: initialState.posts) 16 | let actualState = appReducer(action: fetchUsersSuccessAction, state: initialState) 17 | 18 | expect(actualState).to(equal(expectedState)) 19 | } 20 | 21 | it("Sets posts array when FetchPosts.success action is called, keeping other state unchanged") { 22 | let newPosts = randomPosts() 23 | let fetchPostsSuccessAction = FetchPosts.success(posts: newPosts) 24 | 25 | let initialState = randomAppState() 26 | let expectedState = AppState(users: initialState.users, posts: newPosts) 27 | let actualState = appReducer(action: fetchPostsSuccessAction, state: initialState) 28 | 29 | expect(actualState).to(equal(expectedState)) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/CreateMiddlewareSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import ReSwift 4 | @testable import ReSwiftAsyncMiddlewarePattern 5 | 6 | class CreateMiddlewareSpec: QuickSpec { 7 | 8 | override func spec() { 9 | describe("createMiddleware") { 10 | it("calls all middleware items when action is dispatched") { 11 | var middlewareItem1Called = false 12 | var middlewareItem2Called = false 13 | 14 | let middlewareItems = [ 15 | { (action: Action, dispatch: @escaping DispatchFunction) -> Void in 16 | middlewareItem1Called = true 17 | }, 18 | { (action: Action, dispatch: @escaping DispatchFunction) -> Void in 19 | middlewareItem2Called = true 20 | } 21 | ] 22 | 23 | self.callMiddleware(action: FetchPosts.request, items: middlewareItems) 24 | 25 | expect(middlewareItem1Called).toEventually(beTrue(), timeout: 1) 26 | expect(middlewareItem2Called).toEventually(beTrue(), timeout: 1) 27 | } 28 | 29 | it("passes correct action to all middleware items") { 30 | let expectedPosts = randomPosts() 31 | var actualPosts1: [Post]? 32 | var actualPosts2: [Post]? 33 | 34 | let middlewareItems = [ 35 | { (action: Action, dispatch: @escaping DispatchFunction) -> Void in 36 | guard let action = action as? FetchPosts, 37 | case .success(let fetchedPosts) = action else { return } 38 | actualPosts1 = fetchedPosts 39 | }, 40 | { (action: Action, dispatch: @escaping DispatchFunction) -> Void in 41 | guard let action = action as? FetchPosts, 42 | case .success(let fetchedPosts) = action else { return } 43 | actualPosts2 = fetchedPosts 44 | } 45 | ] 46 | 47 | let action = FetchPosts.success(posts: expectedPosts) 48 | self.callMiddleware(action: action, items: middlewareItems) 49 | 50 | expect(actualPosts1).toEventually(equal(expectedPosts), timeout: 1) 51 | expect(actualPosts2).toEventually(equal(expectedPosts), timeout: 1) 52 | } 53 | 54 | it("passes correct dispatch function to all middleware items") { 55 | let expectedPosts = randomPosts() 56 | let expectedDispatchCalls = 2 57 | var actualDispatchCalls = 0 58 | 59 | let middlewareItems = [ 60 | { (action: Action, dispatch: @escaping DispatchFunction) -> Void in 61 | dispatch(FetchPosts.success(posts: expectedPosts)) 62 | }, 63 | { (action: Action, dispatch: @escaping DispatchFunction) -> Void in 64 | dispatch(FetchPosts.success(posts: expectedPosts)) 65 | } 66 | ] 67 | 68 | self.callMiddleware(action: FetchPosts.request, items: middlewareItems) { action in 69 | guard let action = action as? FetchPosts, 70 | case .success(let actualPosts) = action else { return } 71 | 72 | actualDispatchCalls += 1 73 | expect(actualPosts).to(equal(expectedPosts)) 74 | } 75 | 76 | expect(actualDispatchCalls).toEventually(equal(expectedDispatchCalls)) 77 | } 78 | } 79 | } 80 | 81 | func callMiddleware(action: Action, items: [MiddlewareItem], dispatchFunction: ((Action) -> Void)? = nil) { 82 | let middleware = createMiddleware(items: items) 83 | let dispatchFunction = dispatchFunction ?? { (_: Action) in } 84 | let getState = { () -> AppState? in AppState() } 85 | middleware(dispatchFunction, getState)(dispatchFunction)(action) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/CreatePostSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import ReSwift 4 | @testable import ReSwiftAsyncMiddlewarePattern 5 | 6 | class CreatePostSpec: QuickSpec { 7 | 8 | override func spec() { 9 | describe("createPost") { 10 | 11 | it("Does not call createPost service when action is not CreatePost") { 12 | let testDataService = TestDataService(createPostCallback: { 13 | fail("The test was not supposed to call createPost DataService method") 14 | }) 15 | let middlewareItem = createPost(dataService: testDataService) 16 | middlewareItem(FetchPosts.request) { _ in } 17 | } 18 | 19 | it("Does not call createPost service when action is CreatePost and type is not request") { 20 | let testDataService = TestDataService(createPostCallback: { 21 | fail("The test was not supposed to call createPost DataService method") 22 | }) 23 | let middlewareItem = createPost(dataService: testDataService) 24 | middlewareItem(CreatePost.success) { _ in } 25 | } 26 | 27 | it("Dispatches CreatePost.success action after CreatePost.request is passed") { 28 | let testDataService = TestDataService(posts: [], users: []) 29 | let middlewareItem = createPost(dataService: testDataService) 30 | let newPost = randomPost() 31 | 32 | var successActionDispatched = false 33 | middlewareItem(CreatePost.request(post: newPost)) { action in 34 | guard let action = action as? CreatePost, 35 | case .success = action else { 36 | fail("Expected middleware to dispatch CreatePost.success action") 37 | return 38 | } 39 | successActionDispatched = true 40 | } 41 | expect(successActionDispatched).toEventually(beTrue(), timeout: 1) 42 | } 43 | 44 | it("Dispatches CreatePost.error action if error occurs") { 45 | let testDataService = TestDataService(shouldFail: true) 46 | let middlewareItem = createPost(dataService: testDataService) 47 | let newPost = randomPost() 48 | 49 | let expected = TestDataServiceError.someError 50 | var actual: Error? 51 | middlewareItem(CreatePost.request(post: newPost)) { action in 52 | guard let action = action as? CreatePost, 53 | case .failure(let error) = action else { 54 | fail("Expected middleware to dispatch CreatePost.failure action") 55 | return 56 | } 57 | actual = error 58 | } 59 | expect(actual).toEventually(matchError(expected), timeout: 1) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/FetchPostsSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import ReSwift 4 | @testable import ReSwiftAsyncMiddlewarePattern 5 | 6 | class FetchPostsSpec: QuickSpec { 7 | 8 | override func spec() { 9 | describe("fetchPosts") { 10 | 11 | it("Does not call fetchPosts service when action is not FetchPosts") { 12 | let testDataService = TestDataService(fetchPostsCallback: { 13 | fail("The test was not supposed to call fetchPosts DataService method") 14 | }) 15 | let middlewareItem = fetchPosts(dataService: testDataService) 16 | middlewareItem(FetchUsers.request) { _ in } 17 | } 18 | 19 | it("Does not call fetchPosts service when action is FetchPosts and type is not request") { 20 | let testDataService = TestDataService(fetchPostsCallback: { 21 | fail("The test was not supposed to call fetchPosts DataService method") 22 | }) 23 | let middlewareItem = fetchPosts(dataService: testDataService) 24 | middlewareItem(FetchPosts.success(posts: [])) { _ in } 25 | } 26 | 27 | it("Dispatches FetchPosts.success action with correct posts after FetchPosts.request is passed") { 28 | let expected = randomPosts() 29 | let testDataService = TestDataService(posts: expected, users: []) 30 | let middlewareItem = fetchPosts(dataService: testDataService) 31 | 32 | var actual: [Post] = [] 33 | middlewareItem(FetchPosts.request) { action in 34 | guard let action = action as? FetchPosts, 35 | case .success(let fetchedPosts) = action else { 36 | fail("Expected middleware to dispatch FetchPosts.success action") 37 | return 38 | } 39 | actual = fetchedPosts 40 | } 41 | expect(actual).toEventually(equal(expected), timeout: 1) 42 | } 43 | 44 | it("Dispatches FetchPosts.error action if error occurs") { 45 | let testDataService = TestDataService(shouldFail: true) 46 | let middlewareItem = fetchPosts(dataService: testDataService) 47 | 48 | let expected = TestDataServiceError.someError 49 | var actual: Error? 50 | middlewareItem(FetchPosts.request) { action in 51 | guard let action = action as? FetchPosts, 52 | case .failure(let error) = action else { 53 | fail("Expected middleware to dispatch FetchPosts.failure action") 54 | return 55 | } 56 | actual = error 57 | } 58 | expect(actual).toEventually(matchError(expected), timeout: 1) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/FetchUsersSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import ReSwift 4 | @testable import ReSwiftAsyncMiddlewarePattern 5 | 6 | class FetchUsersSpec: QuickSpec { 7 | 8 | override func spec() { 9 | describe("fetchUsers") { 10 | 11 | it("Does not call fetchUsers service when action is not FetchUsers") { 12 | let testDataService = TestDataService(fetchUsersCallback: { 13 | fail("The test was not supposed to call fetchUsers DataService method") 14 | }) 15 | let middlewareItem = fetchUsers(dataService: testDataService) 16 | middlewareItem(FetchPosts.request) { _ in } 17 | } 18 | 19 | it("Does not call fetchUsers service when action is FetchUsers and type is not request") { 20 | let testDataService = TestDataService(fetchUsersCallback: { 21 | fail("The test was not supposed to call fetchUsers DataService method") 22 | }) 23 | let middlewareItem = fetchUsers(dataService: testDataService) 24 | middlewareItem(FetchUsers.success(users: [])) { _ in } 25 | } 26 | 27 | it("Dispatches FetchUsers.success action with correct users after FetchUsers.request is passed") { 28 | let expected = randomUsers() 29 | let testDataService = TestDataService(posts: [], users: expected) 30 | let middlewareItem = fetchUsers(dataService: testDataService) 31 | 32 | var actual: [User] = [] 33 | middlewareItem(FetchUsers.request) { action in 34 | guard let action = action as? FetchUsers, 35 | case .success(let fetchedUsers) = action else { 36 | fail("Expected middleware to dispatch FetchUsers.success action") 37 | return 38 | } 39 | actual = fetchedUsers 40 | } 41 | expect(actual).toEventually(equal(expected), timeout: 1) 42 | } 43 | 44 | it("Dispatches FetchUsers.error action if error occurs") { 45 | let testDataService = TestDataService(shouldFail: true) 46 | let middlewareItem = fetchUsers(dataService: testDataService) 47 | 48 | let expected = TestDataServiceError.someError 49 | var actual: Error? 50 | middlewareItem(FetchUsers.request) { action in 51 | guard let action = action as? FetchUsers, 52 | case .failure(let error) = action else { 53 | fail("Expected middleware to dispatch FetchUsers.failure action") 54 | return 55 | } 56 | actual = error 57 | } 58 | expect(actual).toEventually(matchError(expected), timeout: 1) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/InjectServiceSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import ReSwift 4 | @testable import ReSwiftAsyncMiddlewarePattern 5 | 6 | class InjectServiceSpec: QuickSpec { 7 | 8 | override func spec() { 9 | describe("injectService") { 10 | it("injects service into receivers") { 11 | 12 | let receivers = [ 13 | { (service: Int) -> (Int) in return service * 2 }, 14 | { (service: Int) -> (Int) in return service * 2 } 15 | ] 16 | let service: Int = randomNumber() 17 | 18 | let expected = [ service * 2, service * 2] 19 | let actual = injectService(service: service, receivers: receivers) 20 | 21 | expect(actual).to(equal(expected)) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/PostsReducerSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | @testable import ReSwiftAsyncMiddlewarePattern 4 | 5 | class PostsReducerSpec: QuickSpec { 6 | 7 | override func spec() { 8 | describe("Posts reducer") { 9 | 10 | it("Instantiates [Post] when passed state is nil") { 11 | let initialState: [Post]? = nil 12 | let expectedState: [Post] = [] 13 | let actualState = postsReducer(action: FetchUsers.request, state: initialState) 14 | 15 | expect(actualState).to(equal(expectedState)) 16 | } 17 | 18 | it("Leaves state unchanged if action is not FetchPosts") { 19 | let initialState = randomPosts() 20 | let expectedState = initialState 21 | let actualState = postsReducer(action: FetchUsers.success(users: []), state: initialState) 22 | 23 | expect(actualState).to(equal(expectedState)) 24 | } 25 | 26 | it("Leaves state unchanged if action is FetchPosts but not .success") { 27 | let initialState = randomPosts() 28 | let expectedState = initialState 29 | let actualState = postsReducer(action: FetchPosts.request, state: initialState) 30 | 31 | expect(actualState).to(equal(expectedState)) 32 | } 33 | 34 | it("Returns new posts on FetchPosts.success action") { 35 | let newPosts = randomPosts() 36 | let fetchPostsSuccessAction = FetchPosts.success(posts: newPosts) 37 | 38 | let initialState = randomPosts() 39 | let expectedState = newPosts 40 | let actualState = postsReducer(action: fetchPostsSuccessAction, state: initialState) 41 | 42 | expect(actualState).to(equal(expectedState)) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/UsersReducerSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | @testable import ReSwiftAsyncMiddlewarePattern 4 | 5 | class UsersReducerSpec: QuickSpec { 6 | 7 | override func spec() { 8 | describe("Users reducer") { 9 | 10 | it("Instantiates [User] state when passed state is nil") { 11 | let initialState: [User]? = nil 12 | let expectedState: [User] = [] 13 | let actualState = usersReducer(action: FetchPosts.request, state: initialState) 14 | 15 | expect(actualState).to(equal(expectedState)) 16 | } 17 | 18 | it("Leaves state unchanged if action is not FetchUsers") { 19 | let initialState = randomUsers() 20 | let expectedState = initialState 21 | let actualState = usersReducer(action: FetchPosts.success(posts: []), state: initialState) 22 | 23 | expect(actualState).to(equal(expectedState)) 24 | } 25 | 26 | it("Leaves state unchanged if action is FetchUsers but not .success") { 27 | let initialState = randomUsers() 28 | let expectedState = initialState 29 | let actualState = usersReducer(action: FetchUsers.request, state: initialState) 30 | 31 | expect(actualState).to(equal(expectedState)) 32 | } 33 | 34 | it("Returns new users on FetchUsers.success action") { 35 | let newUsers = randomUsers() 36 | let fetchUsersSuccessAction = FetchUsers.success(users: newUsers) 37 | 38 | let initialState = randomUsers() 39 | let expectedState = newUsers 40 | let actualState = usersReducer(action: fetchUsersSuccessAction, state: initialState) 41 | 42 | expect(actualState).to(equal(expectedState)) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/Utils/EquatableExtensions.swift: -------------------------------------------------------------------------------- 1 | @testable import ReSwiftAsyncMiddlewarePattern 2 | 3 | extension AppState: Equatable { } 4 | public func ==(lhs: AppState, rhs: AppState) -> Bool { 5 | return lhs.posts == rhs.posts && lhs.users == rhs.users 6 | } 7 | 8 | extension User: Equatable { } 9 | public func ==(lhs: User, rhs: User) -> Bool { 10 | return lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName 11 | } 12 | 13 | extension Post: Equatable { } 14 | public func ==(lhs: Post, rhs: Post) -> Bool { 15 | return lhs.body == rhs.body && lhs.title == rhs.title 16 | } 17 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/Utils/RandomStringExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import ReSwiftAsyncMiddlewarePattern 3 | 4 | func randomNumber() -> Int { return randomNumber(min: 0, max: 100000000) } 5 | 6 | func randomNumber(max: Int) -> Int { return randomNumber(min: 0, max: max) } 7 | 8 | func randomNumber(min: Int, max: Int) -> Int { 9 | let upperBound = UInt32(max - min); 10 | return min + Int(arc4random_uniform(upperBound)) 11 | } 12 | 13 | extension String { 14 | static func random(length: Int) -> String { 15 | let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 16 | 17 | return (0 ..< length).reduce(String()) { accum, _ in 18 | let randomOffset = randomNumber(max: letters.characters.count) 19 | let randomIndex = letters.index(letters.startIndex, offsetBy: Int(randomOffset)) 20 | return accum.appending(String(letters[randomIndex])) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/Utils/TestData.swift: -------------------------------------------------------------------------------- 1 | @testable import ReSwiftAsyncMiddlewarePattern 2 | 3 | func randomPost() -> Post { 4 | return Post(title: String.random(length: 5), body: String.random(length: 5)) 5 | } 6 | 7 | func randomUser() -> User { 8 | return User(firstName: String.random(length: 5), lastName: String.random(length: 5)) 9 | } 10 | 11 | func randomUsers(_ number: Int = 4) -> [User] { 12 | return (0.. [Post] { 16 | return (0.. AppState { 20 | return AppState(users: randomUsers(), posts: randomPosts()) 21 | } 22 | -------------------------------------------------------------------------------- /ReSwiftAsyncMiddlewarePatternTests/Utils/TestDataService.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | @testable import ReSwiftAsyncMiddlewarePattern 3 | 4 | enum TestDataServiceError: Error { 5 | case someError 6 | } 7 | 8 | struct TestDataService: DataService { 9 | let posts: [Post] 10 | let users: [User] 11 | let shouldFail: Bool 12 | let fetchUsersCallback: (() -> Void)? 13 | let fetchPostsCallback: (() -> Void)? 14 | let createPostCallback: (() -> Void)? 15 | 16 | init(shouldFail: Bool) { 17 | self.init(posts: [], users: [], shouldFail: shouldFail, fetchUsersCallback: nil, fetchPostsCallback: nil, createPostCallback: nil) 18 | } 19 | 20 | init(posts: [Post], users: [User]) { 21 | self.init(posts: posts, users: users, shouldFail: false, fetchUsersCallback: nil, fetchPostsCallback: nil, createPostCallback: nil) 22 | } 23 | 24 | init(fetchUsersCallback: (() -> Void)?) { 25 | self.init(posts: [], users: [], shouldFail: false, fetchUsersCallback: fetchUsersCallback, fetchPostsCallback: nil, createPostCallback: nil) 26 | } 27 | 28 | init(fetchPostsCallback: (() -> Void)?) { 29 | self.init(posts: [], users: [], shouldFail: false, fetchUsersCallback: nil, fetchPostsCallback: fetchPostsCallback, createPostCallback: nil) 30 | } 31 | 32 | init(createPostCallback: (() -> Void)?) { 33 | self.init(posts: [], users: [], shouldFail: false, fetchUsersCallback: nil, fetchPostsCallback: nil, createPostCallback: createPostCallback) 34 | } 35 | 36 | init(posts: [Post], users: [User], shouldFail: Bool, fetchUsersCallback: (() -> Void)?, fetchPostsCallback: (() -> Void)?, createPostCallback: (() -> Void)?) { 37 | self.posts = posts 38 | self.users = users 39 | self.shouldFail = shouldFail 40 | self.fetchUsersCallback = fetchUsersCallback 41 | self.fetchPostsCallback = fetchPostsCallback 42 | self.createPostCallback = createPostCallback 43 | } 44 | 45 | func fetchPosts() -> Promise<[Post]> { 46 | self.fetchPostsCallback?() 47 | return promise(shouldFail: self.shouldFail) { $0(self.posts) } 48 | } 49 | 50 | func fetchUsers() -> Promise<[User]> { 51 | self.fetchUsersCallback?() 52 | return promise(shouldFail: self.shouldFail) { $0(self.users) } 53 | } 54 | 55 | func createPost(post: Post) -> Promise { 56 | self.createPostCallback?() 57 | return promise(shouldFail: self.shouldFail) { $0() } 58 | } 59 | 60 | private func promise(shouldFail: Bool, _ fulfillCall: (_ fulfill: (T) -> Void) -> Void) -> Promise { 61 | return Promise { (fulfill, reject) in 62 | guard !shouldFail else { 63 | reject(TestDataServiceError.someError) 64 | return 65 | } 66 | fulfillCall(fulfill) 67 | } 68 | } 69 | } 70 | --------------------------------------------------------------------------------