├── Cartfile ├── Cartfile.resolved ├── README.md ├── RealWorldReSwift.xcodeproj └── project.pbxproj ├── RealWorldReSwift ├── Actions.swift ├── AppDelegate.swift ├── AppReducer.swift ├── AppState.swift ├── AppStore.swift ├── Emitters & Observers │ ├── LocationEmitter.swift │ └── LocationObserver.swift ├── Middleware │ ├── Middleware.swift │ └── SimpleMiddleware.swift ├── Models │ ├── CLLocationCoordinate2D+Codable.swift │ ├── Geometry.swift │ ├── Place.swift │ ├── PlacesSearchResult.swift │ └── PriceLevel.swift ├── Networking │ ├── NetworkFetcher.swift │ ├── PlacesService.swift │ └── Result.swift ├── Util │ ├── CLAuthorizationStatus+IsAuthorized.swift │ ├── Info.plist │ ├── Loadable.swift │ └── LocationManager.swift └── Views │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── PlaceTableViewCell.swift │ └── ViewController.swift └── RealWorldReSwiftTests ├── AppReducerTests.swift ├── Emitters & Observers ├── LocationEmitterTests.swift └── LocationObserverTests.swift ├── Fakes ├── FakeAction.swift ├── FakeLocationManger.swift ├── FakeNetworkFetcher.swift ├── FakePlacesService.swift └── FakeReducer.swift ├── Info.plist ├── Middlewaqre ├── MiddlewareTests.swift └── SimpleMiddlewareTests.swift ├── Mocks └── MockURLSession.swift ├── Networking ├── NetworkFetcherTests.swift └── PlacesServiceTests.swift └── Util └── CLLocationCoordinate2DTests.swift /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReSwift/ReSwift" 2 | github "Quick/Nimble" 3 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v7.0.3" 2 | github "ReSwift/ReSwift" "4.0.1" 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReSwift in Practice 2 | 3 | Read the article about this project at [medium](https://medium.com/@tobi_86596/reswift-in-practice-1512e0f59eb5). 4 | 5 | ## Run on your machine 6 | 7 | In order to run this project on your machine you will need: 8 | 9 | * Xcode 9.3 (or newer) 10 | * An API for the [Google Places API](https://developers.google.com/places/web-service/intro) 11 | * You can get the get [here](https://developers.google.com/places/web-service/get-api-key) 12 | * Update this [line](https://github.com/t-unit/ReSwift-in-Practice/blob/master/RealWorldReSwift/AppDelegate.swift#L29) with the key 13 | -------------------------------------------------------------------------------- /RealWorldReSwift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D229798820710C3100026385 /* SimpleMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = D229798720710C3100026385 /* SimpleMiddleware.swift */; }; 11 | D229799720710D5700026385 /* SimpleMiddlewareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D229799620710D5700026385 /* SimpleMiddlewareTests.swift */; }; 12 | D22979992071156600026385 /* Middleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22979982071156600026385 /* Middleware.swift */; }; 13 | D24C5FF6207A67AB005C27BB /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C5FF5207A67AB005C27BB /* AppReducerTests.swift */; }; 14 | D2562EE1206E699600A54E13 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2562EE0206E699600A54E13 /* AppState.swift */; }; 15 | D2562EF0206E6A2F00A54E13 /* AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2562EEF206E6A2F00A54E13 /* AppStore.swift */; }; 16 | D2562EF2206E6A4600A54E13 /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2562EF1206E6A4600A54E13 /* AppReducer.swift */; }; 17 | D2562EF4206E7B2400A54E13 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2562EF3206E7B2400A54E13 /* Actions.swift */; }; 18 | D2562EF6206E847B00A54E13 /* PlaceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2562EF5206E847B00A54E13 /* PlaceTableViewCell.swift */; }; 19 | D273610D207A44BF00172F14 /* MiddlewareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D273610C207A44BF00172F14 /* MiddlewareTests.swift */; }; 20 | D273611C207A467B00172F14 /* FakePlacesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D273611B207A467B00172F14 /* FakePlacesService.swift */; }; 21 | D273611E207A61A800172F14 /* Loadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D273611D207A61A800172F14 /* Loadable.swift */; }; 22 | D2736122207A652700172F14 /* CLLocationCoordinate2DTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2736121207A652700172F14 /* CLLocationCoordinate2DTests.swift */; }; 23 | D2820ECE2095F1D8004F7F94 /* CLAuthorizationStatus+IsAuthorized.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2820ECD2095F1D8004F7F94 /* CLAuthorizationStatus+IsAuthorized.swift */; }; 24 | D2820EDE2095F3D4004F7F94 /* LocationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2820EDD2095F3D4004F7F94 /* LocationObserver.swift */; }; 25 | D2820EE020961264004F7F94 /* LocationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2820EDF20961264004F7F94 /* LocationObserverTests.swift */; }; 26 | D2820EE320961443004F7F94 /* FakeReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2820EE220961443004F7F94 /* FakeReducer.swift */; }; 27 | D2820EE5209615F2004F7F94 /* FakeAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2820EE4209615F2004F7F94 /* FakeAction.swift */; }; 28 | D287BA992067EFF500E79715 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BA982067EFF500E79715 /* AppDelegate.swift */; }; 29 | D287BA9B2067EFF500E79715 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BA9A2067EFF500E79715 /* ViewController.swift */; }; 30 | D287BA9E2067EFF500E79715 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D287BA9C2067EFF500E79715 /* Main.storyboard */; }; 31 | D287BAA02067EFF700E79715 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D287BA9F2067EFF700E79715 /* Assets.xcassets */; }; 32 | D287BAA32067EFF700E79715 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D287BAA12067EFF700E79715 /* LaunchScreen.storyboard */; }; 33 | D287BAD32067F0FA00E79715 /* ReSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D287BAC42067F0F300E79715 /* ReSwift.framework */; }; 34 | D287BAD42067F0FA00E79715 /* ReSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D287BAC42067F0F300E79715 /* ReSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 35 | D287BADB2067F1E400E79715 /* NetworkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BAD92067F1E400E79715 /* NetworkFetcher.swift */; }; 36 | D287BADC2067F1E400E79715 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BADA2067F1E400E79715 /* Result.swift */; }; 37 | D287BADF2067F27300E79715 /* PlacesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BADE2067F27300E79715 /* PlacesService.swift */; }; 38 | D287BAF42067F3CE00E79715 /* Place.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BAEE2067F3CE00E79715 /* Place.swift */; }; 39 | D287BAF52067F3CE00E79715 /* PriceLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BAEF2067F3CE00E79715 /* PriceLevel.swift */; }; 40 | D287BAF62067F3CF00E79715 /* PlacesSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BAF02067F3CE00E79715 /* PlacesSearchResult.swift */; }; 41 | D287BAF72067F3CF00E79715 /* CLLocationCoordinate2D+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BAF12067F3CE00E79715 /* CLLocationCoordinate2D+Codable.swift */; }; 42 | D287BAF82067F3CF00E79715 /* Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BAF22067F3CE00E79715 /* Geometry.swift */; }; 43 | D287BAFB2067F77400E79715 /* Cartfile in Resources */ = {isa = PBXBuildFile; fileRef = D287BAFA2067F77400E79715 /* Cartfile */; }; 44 | D287BB152067F80400E79715 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D287BB0B2067F7F000E79715 /* Nimble.framework */; }; 45 | D287BB172067F81F00E79715 /* Nimble.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D287BB0B2067F7F000E79715 /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 46 | D287BB192067F83E00E79715 /* NetworkFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BB182067F83E00E79715 /* NetworkFetcherTests.swift */; }; 47 | D287BB1D2067F8E600E79715 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BB1C2067F8E600E79715 /* MockURLSession.swift */; }; 48 | D287BB1F2068001A00E79715 /* PlacesServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BB1E2068001A00E79715 /* PlacesServiceTests.swift */; }; 49 | D287BB2120681B7500E79715 /* FakeNetworkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D287BB2020681B7500E79715 /* FakeNetworkFetcher.swift */; }; 50 | D2FFDF97208388EE00A4E27C /* LocationEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FFDF96208388EE00A4E27C /* LocationEmitter.swift */; }; 51 | D2FFDF99208392BF00A4E27C /* LocationEmitterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FFDF98208392BF00A4E27C /* LocationEmitterTests.swift */; }; 52 | D2FFDF9F2083945900A4E27C /* FakeLocationManger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FFDF9E2083945900A4E27C /* FakeLocationManger.swift */; }; 53 | D2FFDFA12083A68900A4E27C /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FFDFA02083A68900A4E27C /* LocationManager.swift */; }; 54 | /* End PBXBuildFile section */ 55 | 56 | /* Begin PBXContainerItemProxy section */ 57 | D287BAAA2067EFF700E79715 /* PBXContainerItemProxy */ = { 58 | isa = PBXContainerItemProxy; 59 | containerPortal = D287BA8D2067EFF500E79715 /* Project object */; 60 | proxyType = 1; 61 | remoteGlobalIDString = D287BA942067EFF500E79715; 62 | remoteInfo = RealWorldReSwift; 63 | }; 64 | D287BAC32067F0F300E79715 /* PBXContainerItemProxy */ = { 65 | isa = PBXContainerItemProxy; 66 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 67 | proxyType = 2; 68 | remoteGlobalIDString = 625E66831C1FF97E0027C288; 69 | remoteInfo = "ReSwift-iOS"; 70 | }; 71 | D287BAC52067F0F300E79715 /* PBXContainerItemProxy */ = { 72 | isa = PBXContainerItemProxy; 73 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 74 | proxyType = 2; 75 | remoteGlobalIDString = 625E669A1C1FFA3C0027C288; 76 | remoteInfo = "ReSwift-iOSTests"; 77 | }; 78 | D287BAC72067F0F300E79715 /* PBXContainerItemProxy */ = { 79 | isa = PBXContainerItemProxy; 80 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 81 | proxyType = 2; 82 | remoteGlobalIDString = 25DBCF7B1C30C4AA00D63A58; 83 | remoteInfo = "ReSwift-macOS"; 84 | }; 85 | D287BAC92067F0F300E79715 /* PBXContainerItemProxy */ = { 86 | isa = PBXContainerItemProxy; 87 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 88 | proxyType = 2; 89 | remoteGlobalIDString = 25DBCF871C30C4DB00D63A58; 90 | remoteInfo = "ReSwift-macOSTests"; 91 | }; 92 | D287BACB2067F0F300E79715 /* PBXContainerItemProxy */ = { 93 | isa = PBXContainerItemProxy; 94 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 95 | proxyType = 2; 96 | remoteGlobalIDString = 25DBCF4E1C30C18D00D63A58; 97 | remoteInfo = "ReSwift-tvOS"; 98 | }; 99 | D287BACD2067F0F300E79715 /* PBXContainerItemProxy */ = { 100 | isa = PBXContainerItemProxy; 101 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 102 | proxyType = 2; 103 | remoteGlobalIDString = 25DBCF641C30C1AC00D63A58; 104 | remoteInfo = "ReSwift-tvOSTests"; 105 | }; 106 | D287BACF2067F0F300E79715 /* PBXContainerItemProxy */ = { 107 | isa = PBXContainerItemProxy; 108 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 109 | proxyType = 2; 110 | remoteGlobalIDString = 25DBCF371C30BF2B00D63A58; 111 | remoteInfo = "ReSwift-watchOS"; 112 | }; 113 | D287BAD12067F0F300E79715 /* PBXContainerItemProxy */ = { 114 | isa = PBXContainerItemProxy; 115 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 116 | proxyType = 2; 117 | remoteGlobalIDString = 3F13C0E81E7B3E4000D8442C; 118 | remoteInfo = SwiftLintIntegration; 119 | }; 120 | D287BAD52067F0FA00E79715 /* PBXContainerItemProxy */ = { 121 | isa = PBXContainerItemProxy; 122 | containerPortal = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 123 | proxyType = 1; 124 | remoteGlobalIDString = 625E66821C1FF97E0027C288; 125 | remoteInfo = "ReSwift-iOS"; 126 | }; 127 | D287BB062067F7F000E79715 /* PBXContainerItemProxy */ = { 128 | isa = PBXContainerItemProxy; 129 | containerPortal = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 130 | proxyType = 2; 131 | remoteGlobalIDString = 1F925EAD195C0D6300ED456B; 132 | remoteInfo = "Nimble-macOS"; 133 | }; 134 | D287BB082067F7F000E79715 /* PBXContainerItemProxy */ = { 135 | isa = PBXContainerItemProxy; 136 | containerPortal = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 137 | proxyType = 2; 138 | remoteGlobalIDString = 1F925EB7195C0D6300ED456B; 139 | remoteInfo = "Nimble-macOSTests"; 140 | }; 141 | D287BB0A2067F7F000E79715 /* PBXContainerItemProxy */ = { 142 | isa = PBXContainerItemProxy; 143 | containerPortal = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 144 | proxyType = 2; 145 | remoteGlobalIDString = 1F1A74291940169200FFFC47; 146 | remoteInfo = "Nimble-iOS"; 147 | }; 148 | D287BB0C2067F7F000E79715 /* PBXContainerItemProxy */ = { 149 | isa = PBXContainerItemProxy; 150 | containerPortal = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 151 | proxyType = 2; 152 | remoteGlobalIDString = 1F1A74341940169200FFFC47; 153 | remoteInfo = "Nimble-iOSTests"; 154 | }; 155 | D287BB0E2067F7F000E79715 /* PBXContainerItemProxy */ = { 156 | isa = PBXContainerItemProxy; 157 | containerPortal = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 158 | proxyType = 2; 159 | remoteGlobalIDString = 1F5DF1551BDCA0CE00C3A531; 160 | remoteInfo = "Nimble-tvOS"; 161 | }; 162 | D287BB102067F7F000E79715 /* PBXContainerItemProxy */ = { 163 | isa = PBXContainerItemProxy; 164 | containerPortal = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 165 | proxyType = 2; 166 | remoteGlobalIDString = 1F5DF15E1BDCA0CE00C3A531; 167 | remoteInfo = "Nimble-tvOSTests"; 168 | }; 169 | D287BB122067F7F800E79715 /* PBXContainerItemProxy */ = { 170 | isa = PBXContainerItemProxy; 171 | containerPortal = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 172 | proxyType = 1; 173 | remoteGlobalIDString = 1F1A74281940169200FFFC47; 174 | remoteInfo = "Nimble-iOS"; 175 | }; 176 | /* End PBXContainerItemProxy section */ 177 | 178 | /* Begin PBXCopyFilesBuildPhase section */ 179 | D287BAD72067F0FA00E79715 /* Embed Frameworks */ = { 180 | isa = PBXCopyFilesBuildPhase; 181 | buildActionMask = 2147483647; 182 | dstPath = ""; 183 | dstSubfolderSpec = 10; 184 | files = ( 185 | D287BAD42067F0FA00E79715 /* ReSwift.framework in Embed Frameworks */, 186 | ); 187 | name = "Embed Frameworks"; 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | D287BB162067F80A00E79715 /* Copy Frameworks */ = { 191 | isa = PBXCopyFilesBuildPhase; 192 | buildActionMask = 2147483647; 193 | dstPath = ""; 194 | dstSubfolderSpec = 10; 195 | files = ( 196 | D287BB172067F81F00E79715 /* Nimble.framework in Copy Frameworks */, 197 | ); 198 | name = "Copy Frameworks"; 199 | runOnlyForDeploymentPostprocessing = 0; 200 | }; 201 | /* End PBXCopyFilesBuildPhase section */ 202 | 203 | /* Begin PBXFileReference section */ 204 | D229798720710C3100026385 /* SimpleMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleMiddleware.swift; sourceTree = ""; }; 205 | D229799620710D5700026385 /* SimpleMiddlewareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleMiddlewareTests.swift; sourceTree = ""; }; 206 | D22979982071156600026385 /* Middleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Middleware.swift; sourceTree = ""; }; 207 | D24C5FF5207A67AB005C27BB /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = ""; }; 208 | D2562EE0206E699600A54E13 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 209 | D2562EEF206E6A2F00A54E13 /* AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStore.swift; sourceTree = ""; }; 210 | D2562EF1206E6A4600A54E13 /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; 211 | D2562EF3206E7B2400A54E13 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; 212 | D2562EF5206E847B00A54E13 /* PlaceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceTableViewCell.swift; sourceTree = ""; }; 213 | D273610C207A44BF00172F14 /* MiddlewareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiddlewareTests.swift; sourceTree = ""; }; 214 | D273611B207A467B00172F14 /* FakePlacesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakePlacesService.swift; sourceTree = ""; }; 215 | D273611D207A61A800172F14 /* Loadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loadable.swift; sourceTree = ""; }; 216 | D2736121207A652700172F14 /* CLLocationCoordinate2DTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationCoordinate2DTests.swift; sourceTree = ""; }; 217 | D2820ECD2095F1D8004F7F94 /* CLAuthorizationStatus+IsAuthorized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = " CLAuthorizationStatus+IsAuthorized.swift"; sourceTree = ""; }; 218 | D2820EDD2095F3D4004F7F94 /* LocationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationObserver.swift; sourceTree = ""; }; 219 | D2820EDF20961264004F7F94 /* LocationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationObserverTests.swift; sourceTree = ""; }; 220 | D2820EE220961443004F7F94 /* FakeReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeReducer.swift; sourceTree = ""; }; 221 | D2820EE4209615F2004F7F94 /* FakeAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeAction.swift; sourceTree = ""; }; 222 | D287BA952067EFF500E79715 /* RealWorldReSwift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RealWorldReSwift.app; sourceTree = BUILT_PRODUCTS_DIR; }; 223 | D287BA982067EFF500E79715 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 224 | D287BA9A2067EFF500E79715 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 225 | D287BA9D2067EFF500E79715 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 226 | D287BA9F2067EFF700E79715 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 227 | D287BAA22067EFF700E79715 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 228 | D287BAA42067EFF700E79715 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 229 | D287BAA92067EFF700E79715 /* RealWorldReSwiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RealWorldReSwiftTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 230 | D287BAAF2067EFF700E79715 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 231 | D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReSwift.xcodeproj; path = Carthage/Checkouts/ReSwift/ReSwift.xcodeproj; sourceTree = ""; }; 232 | D287BAD92067F1E400E79715 /* NetworkFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkFetcher.swift; sourceTree = ""; }; 233 | D287BADA2067F1E400E79715 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 234 | D287BADE2067F27300E79715 /* PlacesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlacesService.swift; sourceTree = ""; }; 235 | D287BAEE2067F3CE00E79715 /* Place.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Place.swift; sourceTree = ""; }; 236 | D287BAEF2067F3CE00E79715 /* PriceLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PriceLevel.swift; sourceTree = ""; }; 237 | D287BAF02067F3CE00E79715 /* PlacesSearchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlacesSearchResult.swift; sourceTree = ""; }; 238 | D287BAF12067F3CE00E79715 /* CLLocationCoordinate2D+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CLLocationCoordinate2D+Codable.swift"; sourceTree = ""; }; 239 | D287BAF22067F3CE00E79715 /* Geometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Geometry.swift; sourceTree = ""; }; 240 | D287BAFA2067F77400E79715 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; 241 | D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Nimble.xcodeproj; path = Carthage/Checkouts/Nimble/Nimble.xcodeproj; sourceTree = ""; }; 242 | D287BB182067F83E00E79715 /* NetworkFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetcherTests.swift; sourceTree = ""; }; 243 | D287BB1C2067F8E600E79715 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; 244 | D287BB1E2068001A00E79715 /* PlacesServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesServiceTests.swift; sourceTree = ""; }; 245 | D287BB2020681B7500E79715 /* FakeNetworkFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeNetworkFetcher.swift; sourceTree = ""; }; 246 | D2FFDF96208388EE00A4E27C /* LocationEmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationEmitter.swift; sourceTree = ""; }; 247 | D2FFDF98208392BF00A4E27C /* LocationEmitterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEmitterTests.swift; sourceTree = ""; }; 248 | D2FFDF9E2083945900A4E27C /* FakeLocationManger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeLocationManger.swift; sourceTree = ""; }; 249 | D2FFDFA02083A68900A4E27C /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; 250 | /* End PBXFileReference section */ 251 | 252 | /* Begin PBXFrameworksBuildPhase section */ 253 | D287BA922067EFF500E79715 /* Frameworks */ = { 254 | isa = PBXFrameworksBuildPhase; 255 | buildActionMask = 2147483647; 256 | files = ( 257 | D287BAD32067F0FA00E79715 /* ReSwift.framework in Frameworks */, 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | D287BAA62067EFF700E79715 /* Frameworks */ = { 262 | isa = PBXFrameworksBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | D287BB152067F80400E79715 /* Nimble.framework in Frameworks */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXFrameworksBuildPhase section */ 270 | 271 | /* Begin PBXGroup section */ 272 | D2820EDC2095F3B9004F7F94 /* Util */ = { 273 | isa = PBXGroup; 274 | children = ( 275 | D2820ECD2095F1D8004F7F94 /* CLAuthorizationStatus+IsAuthorized.swift */, 276 | D287BAA42067EFF700E79715 /* Info.plist */, 277 | D273611D207A61A800172F14 /* Loadable.swift */, 278 | D2FFDFA02083A68900A4E27C /* LocationManager.swift */, 279 | ); 280 | path = Util; 281 | sourceTree = ""; 282 | }; 283 | D2820EE12096130F004F7F94 /* Emitters & Observers */ = { 284 | isa = PBXGroup; 285 | children = ( 286 | D2FFDF98208392BF00A4E27C /* LocationEmitterTests.swift */, 287 | D2820EDF20961264004F7F94 /* LocationObserverTests.swift */, 288 | ); 289 | path = "Emitters & Observers"; 290 | sourceTree = ""; 291 | }; 292 | D2820EE620961618004F7F94 /* Util */ = { 293 | isa = PBXGroup; 294 | children = ( 295 | D2736121207A652700172F14 /* CLLocationCoordinate2DTests.swift */, 296 | ); 297 | path = Util; 298 | sourceTree = ""; 299 | }; 300 | D2820EE720961624004F7F94 /* Middlewaqre */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | D273610C207A44BF00172F14 /* MiddlewareTests.swift */, 304 | D229799620710D5700026385 /* SimpleMiddlewareTests.swift */, 305 | ); 306 | path = Middlewaqre; 307 | sourceTree = ""; 308 | }; 309 | D2820EE820961642004F7F94 /* Networking */ = { 310 | isa = PBXGroup; 311 | children = ( 312 | D287BB182067F83E00E79715 /* NetworkFetcherTests.swift */, 313 | D287BB1E2068001A00E79715 /* PlacesServiceTests.swift */, 314 | ); 315 | path = Networking; 316 | sourceTree = ""; 317 | }; 318 | D287BA8C2067EFF500E79715 = { 319 | isa = PBXGroup; 320 | children = ( 321 | D287BAFA2067F77400E79715 /* Cartfile */, 322 | D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */, 323 | D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */, 324 | D287BA972067EFF500E79715 /* RealWorldReSwift */, 325 | D287BAAC2067EFF700E79715 /* RealWorldReSwiftTests */, 326 | D287BA962067EFF500E79715 /* Products */, 327 | D287BB142067F80400E79715 /* Frameworks */, 328 | ); 329 | sourceTree = ""; 330 | }; 331 | D287BA962067EFF500E79715 /* Products */ = { 332 | isa = PBXGroup; 333 | children = ( 334 | D287BA952067EFF500E79715 /* RealWorldReSwift.app */, 335 | D287BAA92067EFF700E79715 /* RealWorldReSwiftTests.xctest */, 336 | ); 337 | name = Products; 338 | sourceTree = ""; 339 | }; 340 | D287BA972067EFF500E79715 /* RealWorldReSwift */ = { 341 | isa = PBXGroup; 342 | children = ( 343 | D2562EF3206E7B2400A54E13 /* Actions.swift */, 344 | D287BA982067EFF500E79715 /* AppDelegate.swift */, 345 | D2562EF1206E6A4600A54E13 /* AppReducer.swift */, 346 | D2562EE0206E699600A54E13 /* AppState.swift */, 347 | D2562EEF206E6A2F00A54E13 /* AppStore.swift */, 348 | D2FFDF952083889600A4E27C /* Emitters & Observers */, 349 | D2FFDF942083849400A4E27C /* Middleware */, 350 | D287BAE02067F2E500E79715 /* Models */, 351 | D287BAD82067F1D600E79715 /* Networking */, 352 | D2820EDC2095F3B9004F7F94 /* Util */, 353 | D2FFDF862083848A00A4E27C /* Views */, 354 | ); 355 | path = RealWorldReSwift; 356 | sourceTree = ""; 357 | }; 358 | D287BAAC2067EFF700E79715 /* RealWorldReSwiftTests */ = { 359 | isa = PBXGroup; 360 | children = ( 361 | D24C5FF5207A67AB005C27BB /* AppReducerTests.swift */, 362 | D2820EE12096130F004F7F94 /* Emitters & Observers */, 363 | D287BB1A2067F8C000E79715 /* Fakes */, 364 | D287BAAF2067EFF700E79715 /* Info.plist */, 365 | D2820EE720961624004F7F94 /* Middlewaqre */, 366 | D287BB1B2067F8DA00E79715 /* Mocks */, 367 | D2820EE820961642004F7F94 /* Networking */, 368 | D2820EE620961618004F7F94 /* Util */, 369 | ); 370 | path = RealWorldReSwiftTests; 371 | sourceTree = ""; 372 | }; 373 | D287BAB92067F0F300E79715 /* Products */ = { 374 | isa = PBXGroup; 375 | children = ( 376 | D287BAC42067F0F300E79715 /* ReSwift.framework */, 377 | D287BAC62067F0F300E79715 /* ReSwift-iOSTests.xctest */, 378 | D287BAC82067F0F300E79715 /* ReSwift.framework */, 379 | D287BACA2067F0F300E79715 /* ReSwift-macOSTests.xctest */, 380 | D287BACC2067F0F300E79715 /* ReSwift.framework */, 381 | D287BACE2067F0F300E79715 /* ReSwift-tvOSTests.xctest */, 382 | D287BAD02067F0F300E79715 /* ReSwift.framework */, 383 | D287BAD22067F0F300E79715 /* SwiftLintIntegration.xctest */, 384 | ); 385 | name = Products; 386 | sourceTree = ""; 387 | }; 388 | D287BAD82067F1D600E79715 /* Networking */ = { 389 | isa = PBXGroup; 390 | children = ( 391 | D287BAD92067F1E400E79715 /* NetworkFetcher.swift */, 392 | D287BADE2067F27300E79715 /* PlacesService.swift */, 393 | D287BADA2067F1E400E79715 /* Result.swift */, 394 | ); 395 | path = Networking; 396 | sourceTree = ""; 397 | }; 398 | D287BAE02067F2E500E79715 /* Models */ = { 399 | isa = PBXGroup; 400 | children = ( 401 | D287BAF12067F3CE00E79715 /* CLLocationCoordinate2D+Codable.swift */, 402 | D287BAF22067F3CE00E79715 /* Geometry.swift */, 403 | D287BAEE2067F3CE00E79715 /* Place.swift */, 404 | D287BAF02067F3CE00E79715 /* PlacesSearchResult.swift */, 405 | D287BAEF2067F3CE00E79715 /* PriceLevel.swift */, 406 | ); 407 | path = Models; 408 | sourceTree = ""; 409 | }; 410 | D287BAFD2067F7F000E79715 /* Products */ = { 411 | isa = PBXGroup; 412 | children = ( 413 | D287BB072067F7F000E79715 /* Nimble.framework */, 414 | D287BB092067F7F000E79715 /* NimbleTests.xctest */, 415 | D287BB0B2067F7F000E79715 /* Nimble.framework */, 416 | D287BB0D2067F7F000E79715 /* NimbleTests.xctest */, 417 | D287BB0F2067F7F000E79715 /* Nimble.framework */, 418 | D287BB112067F7F000E79715 /* NimbleTests.xctest */, 419 | ); 420 | name = Products; 421 | sourceTree = ""; 422 | }; 423 | D287BB142067F80400E79715 /* Frameworks */ = { 424 | isa = PBXGroup; 425 | children = ( 426 | ); 427 | name = Frameworks; 428 | sourceTree = ""; 429 | }; 430 | D287BB1A2067F8C000E79715 /* Fakes */ = { 431 | isa = PBXGroup; 432 | children = ( 433 | D2820EE4209615F2004F7F94 /* FakeAction.swift */, 434 | D2FFDF9E2083945900A4E27C /* FakeLocationManger.swift */, 435 | D287BB2020681B7500E79715 /* FakeNetworkFetcher.swift */, 436 | D273611B207A467B00172F14 /* FakePlacesService.swift */, 437 | D2820EE220961443004F7F94 /* FakeReducer.swift */, 438 | ); 439 | path = Fakes; 440 | sourceTree = ""; 441 | }; 442 | D287BB1B2067F8DA00E79715 /* Mocks */ = { 443 | isa = PBXGroup; 444 | children = ( 445 | D287BB1C2067F8E600E79715 /* MockURLSession.swift */, 446 | ); 447 | path = Mocks; 448 | sourceTree = ""; 449 | }; 450 | D2FFDF862083848A00A4E27C /* Views */ = { 451 | isa = PBXGroup; 452 | children = ( 453 | D287BA9F2067EFF700E79715 /* Assets.xcassets */, 454 | D287BAA12067EFF700E79715 /* LaunchScreen.storyboard */, 455 | D287BA9C2067EFF500E79715 /* Main.storyboard */, 456 | D2562EF5206E847B00A54E13 /* PlaceTableViewCell.swift */, 457 | D287BA9A2067EFF500E79715 /* ViewController.swift */, 458 | ); 459 | path = Views; 460 | sourceTree = ""; 461 | }; 462 | D2FFDF942083849400A4E27C /* Middleware */ = { 463 | isa = PBXGroup; 464 | children = ( 465 | D22979982071156600026385 /* Middleware.swift */, 466 | D229798720710C3100026385 /* SimpleMiddleware.swift */, 467 | ); 468 | path = Middleware; 469 | sourceTree = ""; 470 | }; 471 | D2FFDF952083889600A4E27C /* Emitters & Observers */ = { 472 | isa = PBXGroup; 473 | children = ( 474 | D2FFDF96208388EE00A4E27C /* LocationEmitter.swift */, 475 | D2820EDD2095F3D4004F7F94 /* LocationObserver.swift */, 476 | ); 477 | path = "Emitters & Observers"; 478 | sourceTree = ""; 479 | }; 480 | /* End PBXGroup section */ 481 | 482 | /* Begin PBXNativeTarget section */ 483 | D287BA942067EFF500E79715 /* RealWorldReSwift */ = { 484 | isa = PBXNativeTarget; 485 | buildConfigurationList = D287BAB22067EFF700E79715 /* Build configuration list for PBXNativeTarget "RealWorldReSwift" */; 486 | buildPhases = ( 487 | D287BA912067EFF500E79715 /* Sources */, 488 | D287BA922067EFF500E79715 /* Frameworks */, 489 | D287BA932067EFF500E79715 /* Resources */, 490 | D287BAD72067F0FA00E79715 /* Embed Frameworks */, 491 | ); 492 | buildRules = ( 493 | ); 494 | dependencies = ( 495 | D287BAD62067F0FA00E79715 /* PBXTargetDependency */, 496 | ); 497 | name = RealWorldReSwift; 498 | productName = RealWorldReSwift; 499 | productReference = D287BA952067EFF500E79715 /* RealWorldReSwift.app */; 500 | productType = "com.apple.product-type.application"; 501 | }; 502 | D287BAA82067EFF700E79715 /* RealWorldReSwiftTests */ = { 503 | isa = PBXNativeTarget; 504 | buildConfigurationList = D287BAB52067EFF700E79715 /* Build configuration list for PBXNativeTarget "RealWorldReSwiftTests" */; 505 | buildPhases = ( 506 | D287BAA52067EFF700E79715 /* Sources */, 507 | D287BAA62067EFF700E79715 /* Frameworks */, 508 | D287BAA72067EFF700E79715 /* Resources */, 509 | D287BB162067F80A00E79715 /* Copy Frameworks */, 510 | ); 511 | buildRules = ( 512 | ); 513 | dependencies = ( 514 | D287BB132067F7F800E79715 /* PBXTargetDependency */, 515 | D287BAAB2067EFF700E79715 /* PBXTargetDependency */, 516 | ); 517 | name = RealWorldReSwiftTests; 518 | productName = RealWorldReSwiftTests; 519 | productReference = D287BAA92067EFF700E79715 /* RealWorldReSwiftTests.xctest */; 520 | productType = "com.apple.product-type.bundle.unit-test"; 521 | }; 522 | /* End PBXNativeTarget section */ 523 | 524 | /* Begin PBXProject section */ 525 | D287BA8D2067EFF500E79715 /* Project object */ = { 526 | isa = PBXProject; 527 | attributes = { 528 | LastSwiftUpdateCheck = 0930; 529 | LastUpgradeCheck = 0930; 530 | ORGANIZATIONNAME = "Tobias Ottenweller"; 531 | TargetAttributes = { 532 | D287BA942067EFF500E79715 = { 533 | CreatedOnToolsVersion = 9.3; 534 | }; 535 | D287BAA82067EFF700E79715 = { 536 | CreatedOnToolsVersion = 9.3; 537 | LastSwiftMigration = 0930; 538 | TestTargetID = D287BA942067EFF500E79715; 539 | }; 540 | }; 541 | }; 542 | buildConfigurationList = D287BA902067EFF500E79715 /* Build configuration list for PBXProject "RealWorldReSwift" */; 543 | compatibilityVersion = "Xcode 9.3"; 544 | developmentRegion = en; 545 | hasScannedForEncodings = 0; 546 | knownRegions = ( 547 | en, 548 | Base, 549 | ); 550 | mainGroup = D287BA8C2067EFF500E79715; 551 | productRefGroup = D287BA962067EFF500E79715 /* Products */; 552 | projectDirPath = ""; 553 | projectReferences = ( 554 | { 555 | ProductGroup = D287BAFD2067F7F000E79715 /* Products */; 556 | ProjectRef = D287BAFC2067F7F000E79715 /* Nimble.xcodeproj */; 557 | }, 558 | { 559 | ProductGroup = D287BAB92067F0F300E79715 /* Products */; 560 | ProjectRef = D287BAB82067F0F300E79715 /* ReSwift.xcodeproj */; 561 | }, 562 | ); 563 | projectRoot = ""; 564 | targets = ( 565 | D287BA942067EFF500E79715 /* RealWorldReSwift */, 566 | D287BAA82067EFF700E79715 /* RealWorldReSwiftTests */, 567 | ); 568 | }; 569 | /* End PBXProject section */ 570 | 571 | /* Begin PBXReferenceProxy section */ 572 | D287BAC42067F0F300E79715 /* ReSwift.framework */ = { 573 | isa = PBXReferenceProxy; 574 | fileType = wrapper.framework; 575 | path = ReSwift.framework; 576 | remoteRef = D287BAC32067F0F300E79715 /* PBXContainerItemProxy */; 577 | sourceTree = BUILT_PRODUCTS_DIR; 578 | }; 579 | D287BAC62067F0F300E79715 /* ReSwift-iOSTests.xctest */ = { 580 | isa = PBXReferenceProxy; 581 | fileType = wrapper.cfbundle; 582 | path = "ReSwift-iOSTests.xctest"; 583 | remoteRef = D287BAC52067F0F300E79715 /* PBXContainerItemProxy */; 584 | sourceTree = BUILT_PRODUCTS_DIR; 585 | }; 586 | D287BAC82067F0F300E79715 /* ReSwift.framework */ = { 587 | isa = PBXReferenceProxy; 588 | fileType = wrapper.framework; 589 | path = ReSwift.framework; 590 | remoteRef = D287BAC72067F0F300E79715 /* PBXContainerItemProxy */; 591 | sourceTree = BUILT_PRODUCTS_DIR; 592 | }; 593 | D287BACA2067F0F300E79715 /* ReSwift-macOSTests.xctest */ = { 594 | isa = PBXReferenceProxy; 595 | fileType = wrapper.cfbundle; 596 | path = "ReSwift-macOSTests.xctest"; 597 | remoteRef = D287BAC92067F0F300E79715 /* PBXContainerItemProxy */; 598 | sourceTree = BUILT_PRODUCTS_DIR; 599 | }; 600 | D287BACC2067F0F300E79715 /* ReSwift.framework */ = { 601 | isa = PBXReferenceProxy; 602 | fileType = wrapper.framework; 603 | path = ReSwift.framework; 604 | remoteRef = D287BACB2067F0F300E79715 /* PBXContainerItemProxy */; 605 | sourceTree = BUILT_PRODUCTS_DIR; 606 | }; 607 | D287BACE2067F0F300E79715 /* ReSwift-tvOSTests.xctest */ = { 608 | isa = PBXReferenceProxy; 609 | fileType = wrapper.cfbundle; 610 | path = "ReSwift-tvOSTests.xctest"; 611 | remoteRef = D287BACD2067F0F300E79715 /* PBXContainerItemProxy */; 612 | sourceTree = BUILT_PRODUCTS_DIR; 613 | }; 614 | D287BAD02067F0F300E79715 /* ReSwift.framework */ = { 615 | isa = PBXReferenceProxy; 616 | fileType = wrapper.framework; 617 | path = ReSwift.framework; 618 | remoteRef = D287BACF2067F0F300E79715 /* PBXContainerItemProxy */; 619 | sourceTree = BUILT_PRODUCTS_DIR; 620 | }; 621 | D287BAD22067F0F300E79715 /* SwiftLintIntegration.xctest */ = { 622 | isa = PBXReferenceProxy; 623 | fileType = wrapper.cfbundle; 624 | path = SwiftLintIntegration.xctest; 625 | remoteRef = D287BAD12067F0F300E79715 /* PBXContainerItemProxy */; 626 | sourceTree = BUILT_PRODUCTS_DIR; 627 | }; 628 | D287BB072067F7F000E79715 /* Nimble.framework */ = { 629 | isa = PBXReferenceProxy; 630 | fileType = wrapper.framework; 631 | path = Nimble.framework; 632 | remoteRef = D287BB062067F7F000E79715 /* PBXContainerItemProxy */; 633 | sourceTree = BUILT_PRODUCTS_DIR; 634 | }; 635 | D287BB092067F7F000E79715 /* NimbleTests.xctest */ = { 636 | isa = PBXReferenceProxy; 637 | fileType = wrapper.cfbundle; 638 | path = NimbleTests.xctest; 639 | remoteRef = D287BB082067F7F000E79715 /* PBXContainerItemProxy */; 640 | sourceTree = BUILT_PRODUCTS_DIR; 641 | }; 642 | D287BB0B2067F7F000E79715 /* Nimble.framework */ = { 643 | isa = PBXReferenceProxy; 644 | fileType = wrapper.framework; 645 | path = Nimble.framework; 646 | remoteRef = D287BB0A2067F7F000E79715 /* PBXContainerItemProxy */; 647 | sourceTree = BUILT_PRODUCTS_DIR; 648 | }; 649 | D287BB0D2067F7F000E79715 /* NimbleTests.xctest */ = { 650 | isa = PBXReferenceProxy; 651 | fileType = wrapper.cfbundle; 652 | path = NimbleTests.xctest; 653 | remoteRef = D287BB0C2067F7F000E79715 /* PBXContainerItemProxy */; 654 | sourceTree = BUILT_PRODUCTS_DIR; 655 | }; 656 | D287BB0F2067F7F000E79715 /* Nimble.framework */ = { 657 | isa = PBXReferenceProxy; 658 | fileType = wrapper.framework; 659 | path = Nimble.framework; 660 | remoteRef = D287BB0E2067F7F000E79715 /* PBXContainerItemProxy */; 661 | sourceTree = BUILT_PRODUCTS_DIR; 662 | }; 663 | D287BB112067F7F000E79715 /* NimbleTests.xctest */ = { 664 | isa = PBXReferenceProxy; 665 | fileType = wrapper.cfbundle; 666 | path = NimbleTests.xctest; 667 | remoteRef = D287BB102067F7F000E79715 /* PBXContainerItemProxy */; 668 | sourceTree = BUILT_PRODUCTS_DIR; 669 | }; 670 | /* End PBXReferenceProxy section */ 671 | 672 | /* Begin PBXResourcesBuildPhase section */ 673 | D287BA932067EFF500E79715 /* Resources */ = { 674 | isa = PBXResourcesBuildPhase; 675 | buildActionMask = 2147483647; 676 | files = ( 677 | D287BAA32067EFF700E79715 /* LaunchScreen.storyboard in Resources */, 678 | D287BAA02067EFF700E79715 /* Assets.xcassets in Resources */, 679 | D287BAFB2067F77400E79715 /* Cartfile in Resources */, 680 | D287BA9E2067EFF500E79715 /* Main.storyboard in Resources */, 681 | ); 682 | runOnlyForDeploymentPostprocessing = 0; 683 | }; 684 | D287BAA72067EFF700E79715 /* Resources */ = { 685 | isa = PBXResourcesBuildPhase; 686 | buildActionMask = 2147483647; 687 | files = ( 688 | ); 689 | runOnlyForDeploymentPostprocessing = 0; 690 | }; 691 | /* End PBXResourcesBuildPhase section */ 692 | 693 | /* Begin PBXSourcesBuildPhase section */ 694 | D287BA912067EFF500E79715 /* Sources */ = { 695 | isa = PBXSourcesBuildPhase; 696 | buildActionMask = 2147483647; 697 | files = ( 698 | D287BAF62067F3CF00E79715 /* PlacesSearchResult.swift in Sources */, 699 | D287BAF52067F3CE00E79715 /* PriceLevel.swift in Sources */, 700 | D287BAF72067F3CF00E79715 /* CLLocationCoordinate2D+Codable.swift in Sources */, 701 | D2562EF0206E6A2F00A54E13 /* AppStore.swift in Sources */, 702 | D287BA9B2067EFF500E79715 /* ViewController.swift in Sources */, 703 | D287BADF2067F27300E79715 /* PlacesService.swift in Sources */, 704 | D2FFDF97208388EE00A4E27C /* LocationEmitter.swift in Sources */, 705 | D287BADB2067F1E400E79715 /* NetworkFetcher.swift in Sources */, 706 | D2562EF2206E6A4600A54E13 /* AppReducer.swift in Sources */, 707 | D2562EE1206E699600A54E13 /* AppState.swift in Sources */, 708 | D229798820710C3100026385 /* SimpleMiddleware.swift in Sources */, 709 | D287BAF42067F3CE00E79715 /* Place.swift in Sources */, 710 | D2820EDE2095F3D4004F7F94 /* LocationObserver.swift in Sources */, 711 | D2FFDFA12083A68900A4E27C /* LocationManager.swift in Sources */, 712 | D273611E207A61A800172F14 /* Loadable.swift in Sources */, 713 | D287BADC2067F1E400E79715 /* Result.swift in Sources */, 714 | D2562EF4206E7B2400A54E13 /* Actions.swift in Sources */, 715 | D287BA992067EFF500E79715 /* AppDelegate.swift in Sources */, 716 | D2820ECE2095F1D8004F7F94 /* CLAuthorizationStatus+IsAuthorized.swift in Sources */, 717 | D287BAF82067F3CF00E79715 /* Geometry.swift in Sources */, 718 | D2562EF6206E847B00A54E13 /* PlaceTableViewCell.swift in Sources */, 719 | D22979992071156600026385 /* Middleware.swift in Sources */, 720 | ); 721 | runOnlyForDeploymentPostprocessing = 0; 722 | }; 723 | D287BAA52067EFF700E79715 /* Sources */ = { 724 | isa = PBXSourcesBuildPhase; 725 | buildActionMask = 2147483647; 726 | files = ( 727 | D287BB1D2067F8E600E79715 /* MockURLSession.swift in Sources */, 728 | D273611C207A467B00172F14 /* FakePlacesService.swift in Sources */, 729 | D2820EE020961264004F7F94 /* LocationObserverTests.swift in Sources */, 730 | D287BB2120681B7500E79715 /* FakeNetworkFetcher.swift in Sources */, 731 | D24C5FF6207A67AB005C27BB /* AppReducerTests.swift in Sources */, 732 | D287BB1F2068001A00E79715 /* PlacesServiceTests.swift in Sources */, 733 | D273610D207A44BF00172F14 /* MiddlewareTests.swift in Sources */, 734 | D229799720710D5700026385 /* SimpleMiddlewareTests.swift in Sources */, 735 | D2820EE320961443004F7F94 /* FakeReducer.swift in Sources */, 736 | D2FFDF9F2083945900A4E27C /* FakeLocationManger.swift in Sources */, 737 | D287BB192067F83E00E79715 /* NetworkFetcherTests.swift in Sources */, 738 | D2736122207A652700172F14 /* CLLocationCoordinate2DTests.swift in Sources */, 739 | D2820EE5209615F2004F7F94 /* FakeAction.swift in Sources */, 740 | D2FFDF99208392BF00A4E27C /* LocationEmitterTests.swift in Sources */, 741 | ); 742 | runOnlyForDeploymentPostprocessing = 0; 743 | }; 744 | /* End PBXSourcesBuildPhase section */ 745 | 746 | /* Begin PBXTargetDependency section */ 747 | D287BAAB2067EFF700E79715 /* PBXTargetDependency */ = { 748 | isa = PBXTargetDependency; 749 | target = D287BA942067EFF500E79715 /* RealWorldReSwift */; 750 | targetProxy = D287BAAA2067EFF700E79715 /* PBXContainerItemProxy */; 751 | }; 752 | D287BAD62067F0FA00E79715 /* PBXTargetDependency */ = { 753 | isa = PBXTargetDependency; 754 | name = "ReSwift-iOS"; 755 | targetProxy = D287BAD52067F0FA00E79715 /* PBXContainerItemProxy */; 756 | }; 757 | D287BB132067F7F800E79715 /* PBXTargetDependency */ = { 758 | isa = PBXTargetDependency; 759 | name = "Nimble-iOS"; 760 | targetProxy = D287BB122067F7F800E79715 /* PBXContainerItemProxy */; 761 | }; 762 | /* End PBXTargetDependency section */ 763 | 764 | /* Begin PBXVariantGroup section */ 765 | D287BA9C2067EFF500E79715 /* Main.storyboard */ = { 766 | isa = PBXVariantGroup; 767 | children = ( 768 | D287BA9D2067EFF500E79715 /* Base */, 769 | ); 770 | name = Main.storyboard; 771 | sourceTree = ""; 772 | }; 773 | D287BAA12067EFF700E79715 /* LaunchScreen.storyboard */ = { 774 | isa = PBXVariantGroup; 775 | children = ( 776 | D287BAA22067EFF700E79715 /* Base */, 777 | ); 778 | name = LaunchScreen.storyboard; 779 | sourceTree = ""; 780 | }; 781 | /* End PBXVariantGroup section */ 782 | 783 | /* Begin XCBuildConfiguration section */ 784 | D287BAB02067EFF700E79715 /* Debug */ = { 785 | isa = XCBuildConfiguration; 786 | buildSettings = { 787 | ALWAYS_SEARCH_USER_PATHS = NO; 788 | CLANG_ANALYZER_NONNULL = YES; 789 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 790 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 791 | CLANG_CXX_LIBRARY = "libc++"; 792 | CLANG_ENABLE_MODULES = YES; 793 | CLANG_ENABLE_OBJC_ARC = YES; 794 | CLANG_ENABLE_OBJC_WEAK = YES; 795 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 796 | CLANG_WARN_BOOL_CONVERSION = YES; 797 | CLANG_WARN_COMMA = YES; 798 | CLANG_WARN_CONSTANT_CONVERSION = YES; 799 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 800 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 801 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 802 | CLANG_WARN_EMPTY_BODY = YES; 803 | CLANG_WARN_ENUM_CONVERSION = YES; 804 | CLANG_WARN_INFINITE_RECURSION = YES; 805 | CLANG_WARN_INT_CONVERSION = YES; 806 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 807 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 808 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 809 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 810 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 811 | CLANG_WARN_STRICT_PROTOTYPES = YES; 812 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 813 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 814 | CLANG_WARN_UNREACHABLE_CODE = YES; 815 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 816 | CODE_SIGN_IDENTITY = "iPhone Developer"; 817 | COPY_PHASE_STRIP = NO; 818 | DEBUG_INFORMATION_FORMAT = dwarf; 819 | ENABLE_STRICT_OBJC_MSGSEND = YES; 820 | ENABLE_TESTABILITY = YES; 821 | GCC_C_LANGUAGE_STANDARD = gnu11; 822 | GCC_DYNAMIC_NO_PIC = NO; 823 | GCC_NO_COMMON_BLOCKS = YES; 824 | GCC_OPTIMIZATION_LEVEL = 0; 825 | GCC_PREPROCESSOR_DEFINITIONS = ( 826 | "DEBUG=1", 827 | "$(inherited)", 828 | ); 829 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 830 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 831 | GCC_WARN_UNDECLARED_SELECTOR = YES; 832 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 833 | GCC_WARN_UNUSED_FUNCTION = YES; 834 | GCC_WARN_UNUSED_VARIABLE = YES; 835 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 836 | MTL_ENABLE_DEBUG_INFO = YES; 837 | ONLY_ACTIVE_ARCH = YES; 838 | SDKROOT = iphoneos; 839 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 840 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 841 | }; 842 | name = Debug; 843 | }; 844 | D287BAB12067EFF700E79715 /* Release */ = { 845 | isa = XCBuildConfiguration; 846 | buildSettings = { 847 | ALWAYS_SEARCH_USER_PATHS = NO; 848 | CLANG_ANALYZER_NONNULL = YES; 849 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 850 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 851 | CLANG_CXX_LIBRARY = "libc++"; 852 | CLANG_ENABLE_MODULES = YES; 853 | CLANG_ENABLE_OBJC_ARC = YES; 854 | CLANG_ENABLE_OBJC_WEAK = YES; 855 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 856 | CLANG_WARN_BOOL_CONVERSION = YES; 857 | CLANG_WARN_COMMA = YES; 858 | CLANG_WARN_CONSTANT_CONVERSION = YES; 859 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 860 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 861 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 862 | CLANG_WARN_EMPTY_BODY = YES; 863 | CLANG_WARN_ENUM_CONVERSION = YES; 864 | CLANG_WARN_INFINITE_RECURSION = YES; 865 | CLANG_WARN_INT_CONVERSION = YES; 866 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 867 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 868 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 869 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 870 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 871 | CLANG_WARN_STRICT_PROTOTYPES = YES; 872 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 873 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 874 | CLANG_WARN_UNREACHABLE_CODE = YES; 875 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 876 | CODE_SIGN_IDENTITY = "iPhone Developer"; 877 | COPY_PHASE_STRIP = NO; 878 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 879 | ENABLE_NS_ASSERTIONS = NO; 880 | ENABLE_STRICT_OBJC_MSGSEND = YES; 881 | GCC_C_LANGUAGE_STANDARD = gnu11; 882 | GCC_NO_COMMON_BLOCKS = YES; 883 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 884 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 885 | GCC_WARN_UNDECLARED_SELECTOR = YES; 886 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 887 | GCC_WARN_UNUSED_FUNCTION = YES; 888 | GCC_WARN_UNUSED_VARIABLE = YES; 889 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 890 | MTL_ENABLE_DEBUG_INFO = NO; 891 | SDKROOT = iphoneos; 892 | SWIFT_COMPILATION_MODE = wholemodule; 893 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 894 | VALIDATE_PRODUCT = YES; 895 | }; 896 | name = Release; 897 | }; 898 | D287BAB32067EFF700E79715 /* Debug */ = { 899 | isa = XCBuildConfiguration; 900 | buildSettings = { 901 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 902 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 903 | CODE_SIGN_STYLE = Automatic; 904 | DEVELOPMENT_TEAM = DZ92W4V2G9; 905 | INFOPLIST_FILE = "$(SRCROOT)/RealWorldReSwift/Util/Info.plist"; 906 | LD_RUNPATH_SEARCH_PATHS = ( 907 | "$(inherited)", 908 | "@executable_path/Frameworks", 909 | ); 910 | PRODUCT_BUNDLE_IDENTIFIER = net.ottenweller.RealWorldReSwift; 911 | PRODUCT_NAME = "$(TARGET_NAME)"; 912 | SWIFT_VERSION = 4.0; 913 | TARGETED_DEVICE_FAMILY = "1,2"; 914 | }; 915 | name = Debug; 916 | }; 917 | D287BAB42067EFF700E79715 /* Release */ = { 918 | isa = XCBuildConfiguration; 919 | buildSettings = { 920 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 921 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 922 | CODE_SIGN_STYLE = Automatic; 923 | DEVELOPMENT_TEAM = DZ92W4V2G9; 924 | INFOPLIST_FILE = "$(SRCROOT)/RealWorldReSwift/Util/Info.plist"; 925 | LD_RUNPATH_SEARCH_PATHS = ( 926 | "$(inherited)", 927 | "@executable_path/Frameworks", 928 | ); 929 | PRODUCT_BUNDLE_IDENTIFIER = net.ottenweller.RealWorldReSwift; 930 | PRODUCT_NAME = "$(TARGET_NAME)"; 931 | SWIFT_VERSION = 4.0; 932 | TARGETED_DEVICE_FAMILY = "1,2"; 933 | }; 934 | name = Release; 935 | }; 936 | D287BAB62067EFF700E79715 /* Debug */ = { 937 | isa = XCBuildConfiguration; 938 | buildSettings = { 939 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 940 | BUNDLE_LOADER = "$(TEST_HOST)"; 941 | CLANG_ENABLE_MODULES = YES; 942 | CODE_SIGN_STYLE = Automatic; 943 | DEVELOPMENT_TEAM = DZ92W4V2G9; 944 | INFOPLIST_FILE = RealWorldReSwiftTests/Info.plist; 945 | LD_RUNPATH_SEARCH_PATHS = ( 946 | "$(inherited)", 947 | "@executable_path/Frameworks", 948 | "@loader_path/Frameworks", 949 | ); 950 | PRODUCT_BUNDLE_IDENTIFIER = net.ottenweller.RealWorldReSwiftTests; 951 | PRODUCT_NAME = "$(TARGET_NAME)"; 952 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 953 | SWIFT_VERSION = 4.0; 954 | TARGETED_DEVICE_FAMILY = "1,2"; 955 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealWorldReSwift.app/RealWorldReSwift"; 956 | }; 957 | name = Debug; 958 | }; 959 | D287BAB72067EFF700E79715 /* Release */ = { 960 | isa = XCBuildConfiguration; 961 | buildSettings = { 962 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 963 | BUNDLE_LOADER = "$(TEST_HOST)"; 964 | CLANG_ENABLE_MODULES = YES; 965 | CODE_SIGN_STYLE = Automatic; 966 | DEVELOPMENT_TEAM = DZ92W4V2G9; 967 | INFOPLIST_FILE = RealWorldReSwiftTests/Info.plist; 968 | LD_RUNPATH_SEARCH_PATHS = ( 969 | "$(inherited)", 970 | "@executable_path/Frameworks", 971 | "@loader_path/Frameworks", 972 | ); 973 | PRODUCT_BUNDLE_IDENTIFIER = net.ottenweller.RealWorldReSwiftTests; 974 | PRODUCT_NAME = "$(TARGET_NAME)"; 975 | SWIFT_VERSION = 4.0; 976 | TARGETED_DEVICE_FAMILY = "1,2"; 977 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealWorldReSwift.app/RealWorldReSwift"; 978 | }; 979 | name = Release; 980 | }; 981 | /* End XCBuildConfiguration section */ 982 | 983 | /* Begin XCConfigurationList section */ 984 | D287BA902067EFF500E79715 /* Build configuration list for PBXProject "RealWorldReSwift" */ = { 985 | isa = XCConfigurationList; 986 | buildConfigurations = ( 987 | D287BAB02067EFF700E79715 /* Debug */, 988 | D287BAB12067EFF700E79715 /* Release */, 989 | ); 990 | defaultConfigurationIsVisible = 0; 991 | defaultConfigurationName = Release; 992 | }; 993 | D287BAB22067EFF700E79715 /* Build configuration list for PBXNativeTarget "RealWorldReSwift" */ = { 994 | isa = XCConfigurationList; 995 | buildConfigurations = ( 996 | D287BAB32067EFF700E79715 /* Debug */, 997 | D287BAB42067EFF700E79715 /* Release */, 998 | ); 999 | defaultConfigurationIsVisible = 0; 1000 | defaultConfigurationName = Release; 1001 | }; 1002 | D287BAB52067EFF700E79715 /* Build configuration list for PBXNativeTarget "RealWorldReSwiftTests" */ = { 1003 | isa = XCConfigurationList; 1004 | buildConfigurations = ( 1005 | D287BAB62067EFF700E79715 /* Debug */, 1006 | D287BAB72067EFF700E79715 /* Release */, 1007 | ); 1008 | defaultConfigurationIsVisible = 0; 1009 | defaultConfigurationName = Release; 1010 | }; 1011 | /* End XCConfigurationList section */ 1012 | }; 1013 | rootObject = D287BA8D2067EFF500E79715 /* Project object */; 1014 | } 1015 | -------------------------------------------------------------------------------- /RealWorldReSwift/Actions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Actions.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 30.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreLocation 11 | import ReSwift 12 | 13 | enum PlacesAction: Action { 14 | 15 | case fetch 16 | case set(Loadable<[Place]>) 17 | } 18 | 19 | struct SetLocationAction: Action { 20 | 21 | let location: CLLocation 22 | } 23 | 24 | struct SetAuthorizationStatusAction: Action { 25 | 26 | let authorizationStatus: CLAuthorizationStatus 27 | } 28 | 29 | struct RequestAuthorizationAction: Action { } 30 | 31 | struct ApplicationDidBecomeActiveAction: Action { } 32 | -------------------------------------------------------------------------------- /RealWorldReSwift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import UIKit 11 | import ReSwift 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | private let placesService: PlacesServing 19 | private let appStore: AppStore 20 | private let locationEmitter: LocationEmitter 21 | private let locationObserver: LocationObserver 22 | 23 | override init() { 24 | 25 | let locationManager = CLLocationManager() 26 | 27 | placesService = PlacesService( 28 | locale: .current, 29 | apiKey: "", 30 | fetcher: NetworkFetcher(session: .shared, decoder: JSONDecoder()) 31 | ) 32 | 33 | appStore = AppStore( 34 | reducer: appReducer, 35 | state: nil, 36 | middleware: [ 37 | createMiddleware(fetchPlaces(service: placesService)), 38 | createMiddleware(requestAuthorization(locationManager: locationManager)), 39 | createMiddleware(startMonitoring(locationManager: locationManager)) 40 | ] 41 | ) 42 | 43 | locationEmitter = LocationEmitter(locationManager: locationManager, store: appStore) 44 | locationObserver = LocationObserver(store: appStore) 45 | 46 | super.init() 47 | } 48 | 49 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 50 | 51 | let viewController = window?.rootViewController as! ViewController 52 | viewController.store = appStore 53 | 54 | return true 55 | } 56 | 57 | func applicationDidBecomeActive(_ application: UIApplication) { 58 | appStore.dispatch(ApplicationDidBecomeActiveAction()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /RealWorldReSwift/AppReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppReducer.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 30.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import ReSwift 11 | 12 | func appReducer(action: Action, state: AppState?) -> AppState { 13 | 14 | return AppState( 15 | places: placesReducer(action: action, places: state?.places), 16 | lastKnownLocation: lastKnownLocationReducer(action: action, lastKnownLocation: state?.lastKnownLocation), 17 | authorizationStatus: authorizationStatusReducer(action: action, authorizationStatus: state?.authorizationStatus) 18 | ) 19 | } 20 | 21 | private func placesReducer(action: Action, places: Loadable<[Place]>?) -> Loadable<[Place]> { 22 | 23 | guard 24 | let placesAction = action as? PlacesAction, 25 | case .set(let state) = placesAction 26 | else { 27 | return places ?? .initial 28 | } 29 | return state 30 | } 31 | 32 | private func lastKnownLocationReducer(action: Action, lastKnownLocation: CLLocation?) -> CLLocation? { 33 | 34 | guard let action = action as? SetLocationAction else { 35 | return lastKnownLocation 36 | } 37 | return action.location 38 | } 39 | 40 | private func authorizationStatusReducer(action: Action, authorizationStatus: CLAuthorizationStatus?) -> CLAuthorizationStatus { 41 | 42 | guard let action = action as? SetAuthorizationStatusAction else { 43 | return authorizationStatus ?? .notDetermined 44 | } 45 | return action.authorizationStatus 46 | } 47 | -------------------------------------------------------------------------------- /RealWorldReSwift/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 30.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import ReSwift 11 | 12 | struct AppState: StateType { 13 | 14 | let places: Loadable<[Place]> 15 | let lastKnownLocation: CLLocation? 16 | let authorizationStatus: CLAuthorizationStatus 17 | } 18 | -------------------------------------------------------------------------------- /RealWorldReSwift/AppStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStore.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 30.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import ReSwift 10 | 11 | typealias AppStore = Store 12 | -------------------------------------------------------------------------------- /RealWorldReSwift/Emitters & Observers/LocationEmitter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStore.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 15.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import ReSwift 11 | 12 | final class LocationEmitter: NSObject { 13 | 14 | private let locationManager: LocationManager 15 | private let store: AppStore 16 | 17 | init(locationManager: LocationManager, store: AppStore) { 18 | 19 | self.locationManager = locationManager 20 | self.store = store 21 | super.init() 22 | 23 | locationManager.delegate = self 24 | dispatch(authorizationStatus: locationManager.authorizationStatus) 25 | dispatch(location: locationManager.location) 26 | } 27 | 28 | private func dispatch(authorizationStatus: CLAuthorizationStatus) { 29 | 30 | let action = SetAuthorizationStatusAction(authorizationStatus: authorizationStatus) 31 | store.dispatch(action) 32 | } 33 | 34 | private func dispatch(location: CLLocation?) { 35 | 36 | guard let location = location else { 37 | return 38 | } 39 | 40 | let action = SetLocationAction(location: location) 41 | store.dispatch(action) 42 | } 43 | } 44 | 45 | extension LocationEmitter: CLLocationManagerDelegate { 46 | 47 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 48 | dispatch(authorizationStatus: status) 49 | } 50 | 51 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 52 | dispatch(location: locations.last) 53 | } 54 | 55 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 56 | print(error) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /RealWorldReSwift/Emitters & Observers/LocationObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationObserver.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 29.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import ReSwift 11 | 12 | class LocationObserver { 13 | 14 | private let store: AppStore 15 | 16 | init(store: AppStore) { 17 | 18 | self.store = store 19 | 20 | store.subscribe(self) { subcription in 21 | subcription.select { state in state.lastKnownLocation } 22 | } 23 | } 24 | 25 | deinit { 26 | store.unsubscribe(self) 27 | } 28 | } 29 | 30 | extension LocationObserver: StoreSubscriber { 31 | 32 | typealias StoreSubscriberStateType = CLLocation? 33 | 34 | func newState(state: CLLocation?) { 35 | 36 | guard state != nil else { 37 | return 38 | } 39 | store.dispatch(PlacesAction.fetch) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /RealWorldReSwift/Middleware/Middleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Middleware.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 01.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import ReSwift 10 | import CoreLocation 11 | 12 | typealias Middleware = SimpleMiddleware 13 | 14 | func fetchPlaces(service: PlacesServing) -> Middleware { 15 | return { fetchPlaces(action: $0, context: $1, service: service) } 16 | } 17 | 18 | func requestAuthorization(locationManager: LocationManager) -> Middleware { 19 | return { requestAuthorization(action: $0, context: $1, locationManager: locationManager) } 20 | } 21 | 22 | func startMonitoring(locationManager: LocationManager) -> Middleware { 23 | return { startMonitoring(action: $0, context: $1, locationManager: locationManager) } 24 | } 25 | 26 | private func fetchPlaces(action: Action, context: MiddlewareContext, service: PlacesServing) -> Action? { 27 | 28 | guard 29 | let placesAction = action as? PlacesAction, 30 | case .fetch = placesAction 31 | else { 32 | return action 33 | } 34 | 35 | guard let coordinate = context.state?.lastKnownLocation?.coordinate else { 36 | return PlacesAction.set(Loadable.error(NoCoordinateError())) 37 | } 38 | 39 | service.search(coordinate: coordinate, radius: 2000.0) { result in 40 | 41 | DispatchQueue.main.async { 42 | let state: Loadable<[Place]> 43 | 44 | switch result { 45 | case .failure(let error): state = .error(error) 46 | case .success(let value): state = .value(value.results) 47 | } 48 | 49 | context.dispatch(PlacesAction.set(state)) 50 | } 51 | } 52 | 53 | return PlacesAction.set(.loading) 54 | } 55 | 56 | private func requestAuthorization(action: Action, context: MiddlewareContext, locationManager: LocationManager) -> Action? { 57 | 58 | if action is RequestAuthorizationAction { 59 | locationManager.requestWhenInUseAuthorization() 60 | } 61 | 62 | return action 63 | } 64 | 65 | private func startMonitoring(action: Action, context: MiddlewareContext, locationManager: LocationManager) -> Action? { 66 | 67 | let appDidBecomeActive = action is ApplicationDidBecomeActiveAction 68 | let isAuthorized = context.state?.authorizationStatus.isAuthorized == true 69 | let setAuthorizationStatusAction = action as? SetAuthorizationStatusAction 70 | let isSetAuthorized = setAuthorizationStatusAction?.authorizationStatus.isAuthorized == true 71 | 72 | if (appDidBecomeActive && isAuthorized) || (isSetAuthorized && !isAuthorized) { 73 | locationManager.startMonitoringSignificantLocationChanges() 74 | locationManager.requestLocation() 75 | } 76 | 77 | return action 78 | } 79 | 80 | private struct NoCoordinateError: Error {} 81 | 82 | -------------------------------------------------------------------------------- /RealWorldReSwift/Middleware/SimpleMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleMiddleware.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 01.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import ReSwift 10 | 11 | /** 12 | Examples: 13 | ``` 14 | func logMiddleware(action: Action, context: MiddlewareContext) -> Action? { 15 | 16 | print(action) 17 | return action 18 | } 19 | ``` 20 | ``` 21 | func asyncMiddleware(action: Action, context: MiddlewareContext) -> Action? { 22 | 23 | DispatchQueue.main.async { 24 | context.next(action) 25 | } 26 | return nil 27 | } 28 | ``` 29 | */ 30 | typealias SimpleMiddleware = (Action, MiddlewareContext) -> Action? 31 | 32 | struct MiddlewareContext { 33 | 34 | /// Closure that can be used to emit additional actions 35 | let dispatch: DispatchFunction 36 | let getState: () -> State? 37 | 38 | /// Closure that can be used forward your action if an async operation is performed. 39 | /// Just return `nil` in your middleware function in that case. 40 | let next: DispatchFunction 41 | 42 | var state: State? { 43 | return getState() 44 | } 45 | } 46 | 47 | /// Creates a middlewar function using SimpleMiddleware to create a ReSwift Middleware function. 48 | func createMiddleware(_ middleware: @escaping SimpleMiddleware) -> ReSwift.Middleware { 49 | 50 | return { dispatch, getState in 51 | return { next in 52 | return { action in 53 | 54 | let context = MiddlewareContext(dispatch: dispatch, getState: getState, next: next) 55 | if let newAction = middleware(action, context) { 56 | next(newAction) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /RealWorldReSwift/Models/CLLocationCoordinate2D+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+Codable.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | extension CLLocationCoordinate2D: Codable { 12 | 13 | enum CodingKeys: String, CodingKey { 14 | 15 | case longitude = "lng" 16 | case latitude = "lat" 17 | } 18 | 19 | public func encode(to encoder: Encoder) throws { 20 | var container = encoder.container(keyedBy: CodingKeys.self) 21 | try container.encode(longitude, forKey: .longitude) 22 | try container.encode(latitude, forKey: .latitude) 23 | } 24 | 25 | public init(from decoder: Decoder) throws { 26 | self.init() 27 | let container = try decoder.container(keyedBy: CodingKeys.self) 28 | longitude = try container.decode(Double.self, forKey: .longitude) 29 | latitude = try container.decode(Double.self, forKey: .latitude) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RealWorldReSwift/Models/Geometry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Geometry.swift 3 | // LunchMate 4 | // 5 | // Created by Tobias Ottenweller on 31.10.17. 6 | // Copyright © 2017 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | struct Geometry: Codable { 12 | 13 | enum CodingKeys: String, CodingKey { 14 | 15 | case coordinate = "location" 16 | } 17 | 18 | let coordinate: CLLocationCoordinate2D 19 | } 20 | -------------------------------------------------------------------------------- /RealWorldReSwift/Models/Place.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Place.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Place: Codable { 12 | 13 | enum CodingKeys: String, CodingKey { 14 | 15 | case geometry 16 | case id 17 | case name 18 | case priceLevel = "price_level" 19 | case rating 20 | } 21 | 22 | let geometry: Geometry 23 | let id: String 24 | let name: String 25 | let priceLevel: PriceLevel? 26 | let rating: Double 27 | } 28 | -------------------------------------------------------------------------------- /RealWorldReSwift/Models/PlacesSearchResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlacesSearchResult.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct PlacesSearchResultError: Error { 12 | 13 | let status: String 14 | } 15 | 16 | struct PlacesSearchResult: Decodable { 17 | 18 | let results: [Place] 19 | let status: String 20 | } 21 | -------------------------------------------------------------------------------- /RealWorldReSwift/Models/PriceLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PriceLevel.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum PriceLevel: Int, Codable { 12 | 13 | case free = 0 14 | case cheap = 1 15 | case moderat = 2 16 | case expensive = 3 17 | case veryExpensive = 4 18 | } 19 | -------------------------------------------------------------------------------- /RealWorldReSwift/Networking/NetworkFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFetcher.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum NetworkFetcherError: Error { 12 | 13 | case url 14 | case response 15 | case content 16 | } 17 | 18 | protocol NetworkFetching { 19 | 20 | @discardableResult 21 | func fetch(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask where T: Decodable 22 | } 23 | 24 | struct NetworkFetcher: NetworkFetching { 25 | 26 | let session: URLSession 27 | let decoder: JSONDecoder 28 | 29 | @discardableResult 30 | func fetch(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask where T: Decodable { 31 | 32 | let task = session.dataTask(with: request) { (optionalData, response, error) in 33 | 34 | guard let data = optionalData, error == nil else { 35 | completion(.failure(NetworkFetcherError.response)) 36 | return 37 | } 38 | 39 | do { 40 | let result = try self.decoder.decode(T.self, from: data) 41 | completion(.success(result)) 42 | } catch { 43 | completion(.failure(NetworkFetcherError.content)) 44 | } 45 | } 46 | 47 | task.resume() 48 | return task 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /RealWorldReSwift/Networking/PlacesService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlacesService.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | import CoreLocation 12 | 13 | protocol PlacesServing { 14 | 15 | func search(coordinate: CLLocationCoordinate2D, radius: Double, completion: @escaping (Result) -> Void) 16 | } 17 | 18 | struct PlacesService: PlacesServing { 19 | 20 | let locale: Locale 21 | let apiKey: String 22 | let fetcher: NetworkFetching 23 | 24 | func search(coordinate: CLLocationCoordinate2D, radius: Double, completion: @escaping (Result) -> Void) { 25 | 26 | do { 27 | let request = try RequestBuilder.build( 28 | forCoordinates: coordinate, 29 | radius: radius, 30 | locale: locale, 31 | apiKey: apiKey 32 | ) 33 | fetcher.fetch(request: request) { (result: Result) in 34 | 35 | switch result { 36 | case .failure: 37 | completion(result) 38 | case .success(let searchResult): 39 | 40 | if searchResult.status == "OK" { 41 | completion(.success(searchResult)) 42 | } else { 43 | let error = PlacesSearchResultError(status: searchResult.status) 44 | completion(.failure(error)) 45 | } 46 | } 47 | 48 | } 49 | } catch { 50 | completion(Result.failure(error)) 51 | } 52 | } 53 | } 54 | 55 | private struct RequestBuilder { 56 | 57 | static let baseUrlString = "https://maps.googleapis.com/maps/api/place/nearbysearch/json" 58 | 59 | static func build(forCoordinates coordinate: CLLocationCoordinate2D, radius: Double, locale: Locale, apiKey: String) throws -> URLRequest { 60 | 61 | guard var components = URLComponents(string: baseUrlString) else { 62 | throw NetworkFetcherError.url 63 | } 64 | 65 | components.queryItems = [ 66 | URLQueryItem(name: "key", value: apiKey), 67 | URLQueryItem(name: "location", value: "\(coordinate.latitude),\(coordinate.longitude)"), 68 | URLQueryItem(name: "radius", value: "\(radius)"), 69 | URLQueryItem(name: "language", value: locale.languageCode), 70 | URLQueryItem(name: "opennow", value: "1"), 71 | URLQueryItem(name: "type", value: "restaurant") 72 | ] 73 | 74 | guard let url = components.url else { 75 | throw NetworkFetcherError.url 76 | } 77 | 78 | return URLRequest(url: url) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /RealWorldReSwift/Networking/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import ReSwift 10 | 11 | enum Result { 12 | 13 | case success(T) 14 | case failure(Error) 15 | 16 | var error: Error? { 17 | 18 | switch self { 19 | case .failure(let error): return error 20 | default: return nil 21 | } 22 | } 23 | 24 | var value: T? { 25 | 26 | switch self { 27 | case .success(let value): return value 28 | default: return nil 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RealWorldReSwift/Util/ CLAuthorizationStatus+IsAuthorized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLAuthorizationStatus+IsAuthorized.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 29.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | extension CLAuthorizationStatus { 12 | 13 | var isAuthorized: Bool { 14 | 15 | guard self == .authorizedAlways || self == .authorizedWhenInUse else { 16 | return false 17 | } 18 | return true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RealWorldReSwift/Util/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | NSLocationWhenInUseUsageDescription 45 | some text here… 46 | 47 | 48 | -------------------------------------------------------------------------------- /RealWorldReSwift/Util/Loadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loadable.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 08.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | enum Loadable { 10 | 11 | case initial 12 | case loading 13 | case value(T) 14 | case error(Error) 15 | } 16 | -------------------------------------------------------------------------------- /RealWorldReSwift/Util/LocationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationManager.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 15.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | protocol LocationManager: class { 12 | 13 | var authorizationStatus: CLAuthorizationStatus { get } 14 | var location: CLLocation? { get } 15 | var delegate: CLLocationManagerDelegate? { get set } 16 | 17 | func requestWhenInUseAuthorization() 18 | func startMonitoringSignificantLocationChanges() 19 | func requestLocation() 20 | } 21 | 22 | extension CLLocationManager: LocationManager { 23 | 24 | var authorizationStatus: CLAuthorizationStatus { 25 | return CLLocationManager.authorizationStatus() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /RealWorldReSwift/Views/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 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /RealWorldReSwift/Views/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /RealWorldReSwift/Views/Base.lproj/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 | -------------------------------------------------------------------------------- /RealWorldReSwift/Views/Base.lproj/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 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 45 | 51 | 57 | 63 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 141 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /RealWorldReSwift/Views/PlaceTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaceTableViewCell.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 30.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PlaceTableViewCell: UITableViewCell { 12 | 13 | @IBOutlet weak var nameLabel: UILabel! 14 | @IBOutlet weak var priceLabel: UILabel! 15 | @IBOutlet weak var ratingLabel: UILabel! 16 | @IBOutlet weak var latLabel: UILabel! 17 | @IBOutlet weak var lonLabel: UILabel! 18 | } 19 | -------------------------------------------------------------------------------- /RealWorldReSwift/Views/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RealWorldReSwift 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ReSwift 11 | 12 | class ViewController: UIViewController { 13 | 14 | var store: AppStore! 15 | 16 | @IBOutlet private var tableView: UITableView! 17 | @IBOutlet private var authorizationNotDeterminedView: UIView! 18 | @IBOutlet private var authorizationDeniedView: UIView! 19 | 20 | private var data: [Place] = [] { 21 | didSet { 22 | tableView.reloadData() 23 | } 24 | } 25 | 26 | override func viewWillAppear(_ animated: Bool) { 27 | 28 | super.viewWillAppear(animated) 29 | store.subscribe(self) 30 | } 31 | 32 | override func viewWillDisappear(_ animated: Bool) { 33 | 34 | super.viewWillDisappear(animated) 35 | store.unsubscribe(self) 36 | } 37 | 38 | @IBAction private func requestAuthorizationButtonTouchUpInside(_ sender: Any) { 39 | store.dispatch(RequestAuthorizationAction()) 40 | } 41 | } 42 | 43 | extension ViewController: StoreSubscriber { 44 | 45 | typealias StoreSubscriberStateType = AppState 46 | 47 | func newState(state: AppState) { 48 | 49 | if case .value(let places) = state.places { 50 | data = places 51 | } 52 | 53 | authorizationNotDeterminedView.isHidden = state.authorizationStatus != .notDetermined 54 | authorizationDeniedView.isHidden = state.authorizationStatus.isAuthorized 55 | } 56 | } 57 | 58 | extension ViewController: UITableViewDataSource { 59 | 60 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 61 | return data.count 62 | } 63 | 64 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 65 | 66 | let cell = tableView.dequeueReusableCell(withIdentifier: "place", for: indexPath) as! PlaceTableViewCell 67 | let place = data[indexPath.row] 68 | 69 | cell.nameLabel.text = place.name 70 | cell.ratingLabel.text = place.rating.description 71 | cell.priceLabel.text = price(forLevel: place.priceLevel) 72 | cell.latLabel.text = place.geometry.coordinate.latitude.description 73 | cell.lonLabel.text = place.geometry.coordinate.longitude.description 74 | 75 | return cell 76 | } 77 | 78 | private func price(forLevel priceLevel: PriceLevel?) -> String { 79 | 80 | switch priceLevel { 81 | case .free?: return "free" 82 | case .cheap?: return "cheap" 83 | case .moderat?: return "moderat" 84 | case .expensive?: return "expensive" 85 | case .veryExpensive?: return "very expensive" 86 | default: return "" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/AppReducerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppReducerTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 08.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import XCTest 11 | import ReSwift 12 | import Nimble 13 | 14 | @testable 15 | import RealWorldReSwift 16 | 17 | // MARK: - places 18 | class AppReducerPlacesTests: XCTestCase { 19 | 20 | func testSetsIntial() { 21 | 22 | let state = appReducer(action: FakeAction(), state: nil) 23 | 24 | if case .initial = state.places { 25 | // success 26 | } else { 27 | XCTFail("Expected .initial got \(state.places)") 28 | } 29 | } 30 | 31 | func testSetsSuccesResult() { 32 | 33 | let state = appReducer(action: PlacesAction.set(.value([])), state: nil) 34 | 35 | if case .value = state.places { 36 | // sucess 37 | } else { 38 | XCTFail("Expected .value got \(state.places)") 39 | } 40 | } 41 | 42 | func testSetsFailureResult() { 43 | 44 | let error = NSError(domain: "", code: 0, userInfo: nil) 45 | let state = appReducer(action: PlacesAction.set(.error(error)), state: nil) 46 | 47 | if case .error = state.places { 48 | // sucess 49 | } else { 50 | XCTFail("Expected .error got \(state.places)") 51 | } 52 | } 53 | } 54 | 55 | // MARK: - last known location 56 | class AppReducerLastKnownLocationTests: XCTestCase { 57 | 58 | func testKeepsNil() { 59 | 60 | let state = appReducer(action: FakeAction(), state: nil) 61 | expect(state.lastKnownLocation).to(beNil()) 62 | } 63 | 64 | func testUpdates() { 65 | 66 | let location = CLLocation(latitude: 23, longitude: -23) 67 | let state = appReducer(action: SetLocationAction(location: location), state: nil) 68 | 69 | expect(state.lastKnownLocation) == location 70 | } 71 | } 72 | 73 | // MARK: - authorizationStatus 74 | class AppReducerAuthorizationStatusTests: XCTestCase { 75 | 76 | func testSetsInitialValue() { 77 | 78 | let state = appReducer(action: FakeAction(), state: nil) 79 | expect(state.authorizationStatus) == .notDetermined 80 | } 81 | 82 | func testUpdates() { 83 | 84 | let status: CLAuthorizationStatus = .authorizedAlways 85 | let state = appReducer(action: SetAuthorizationStatusAction(authorizationStatus: status), state: nil) 86 | expect(state.authorizationStatus) == status 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Emitters & Observers/LocationEmitterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationEmitterTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 15.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreLocation 11 | import Nimble 12 | import ReSwift 13 | 14 | @testable import RealWorldReSwift 15 | 16 | class LocationEmitterTests: XCTestCase { 17 | 18 | var locationManager: FakeLocationManager! 19 | var store: AppStore! 20 | var reducer: FakeReducer! 21 | var sut: LocationEmitter! 22 | 23 | let initial = CLLocation(latitude: 0, longitude: 0) 24 | 25 | override func setUp() { 26 | 27 | super.setUp() 28 | locationManager = FakeLocationManager() 29 | locationManager.location = initial 30 | locationManager.authorizationStatus = .restricted 31 | reducer = FakeReducer() 32 | store = AppStore(reducer: reducer.reduce, state: nil) 33 | sut = LocationEmitter(locationManager: locationManager, store: store) 34 | } 35 | 36 | override func tearDown() { 37 | 38 | locationManager = nil 39 | store = nil 40 | sut = nil 41 | reducer = nil 42 | super.tearDown() 43 | } 44 | 45 | func testSetsDelegate() { 46 | expect(self.locationManager.delegate) === sut 47 | } 48 | 49 | func testDispatchesInitialAuthorizationEvent() { 50 | 51 | let action = reducer.actions.first(where: { $0 is SetAuthorizationStatusAction }) as? SetAuthorizationStatusAction 52 | expect(action?.authorizationStatus) == .restricted 53 | } 54 | 55 | func testDispatchesAuthorizationEvent() { 56 | 57 | sut.locationManager(CLLocationManager(), didChangeAuthorization: .authorizedWhenInUse) 58 | 59 | let action = reducer.actions.last as? SetAuthorizationStatusAction 60 | expect(action?.authorizationStatus) == .authorizedWhenInUse 61 | } 62 | 63 | func testDispatchesInitialLocationEvent() { 64 | 65 | let action = reducer.actions.first(where: { $0 is SetLocationAction }) as? SetLocationAction 66 | expect(action?.location) == initial 67 | } 68 | 69 | func testDispatchesLocationEvent() { 70 | 71 | let location = CLLocation(latitude: 50, longitude: 50) 72 | sut.locationManager(CLLocationManager(), didUpdateLocations: [location]) 73 | 74 | let action = reducer.actions.last as? SetLocationAction 75 | expect(action?.location) == location 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Emitters & Observers/LocationObserverTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationObserverTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 29.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreLocation 11 | import Nimble 12 | import ReSwift 13 | 14 | @testable import RealWorldReSwift 15 | 16 | class LocationObserverTests: XCTestCase { 17 | 18 | var store: AppStore! 19 | var reducer: FakeReducer! 20 | var sut: LocationObserver! 21 | 22 | let initial = CLLocation(latitude: 0, longitude: 0) 23 | 24 | override func setUp() { 25 | 26 | super.setUp() 27 | reducer = FakeReducer() 28 | store = AppStore(reducer: reducer.reduce, state: nil) 29 | sut = LocationObserver(store: store) 30 | } 31 | 32 | override func tearDown() { 33 | 34 | store = nil 35 | sut = nil 36 | reducer = nil 37 | super.tearDown() 38 | } 39 | 40 | func testDispatchesActionWithLocation() { 41 | 42 | store.dispatch(FakeAction()) 43 | 44 | let placesActions = reducer.actions.filter { $0 is PlacesAction } 45 | expect(placesActions).to(beEmpty()) 46 | } 47 | 48 | func testDoesNothingWithoutLocation() { 49 | 50 | reducer.lastKnownLocation = CLLocation(latitude: 0, longitude: 0) 51 | store.dispatch(FakeAction()) 52 | 53 | let placesActions = reducer.actions.filter { $0 is PlacesAction } 54 | expect(placesActions.count) == 1 55 | 56 | switch reducer.actions.last as? PlacesAction { 57 | case .fetch?: break 58 | default: XCTFail("Expected .fetch got \(String(describing: reducer.actions.last))") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Fakes/FakeAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeAction.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 29.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import ReSwift 10 | 11 | struct FakeAction: Action { } 12 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Fakes/FakeLocationManger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeLocationManager.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 15.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | @testable import RealWorldReSwift 12 | 13 | class FakeLocationManager: LocationManager { 14 | 15 | var authorizationStatus: CLAuthorizationStatus = .notDetermined 16 | var location: CLLocation? = nil 17 | var delegate: CLLocationManagerDelegate? 18 | 19 | private(set) var requestWhenInUseAuthorizationCalled = false 20 | private(set) var startMonitoringSignificantLocationChangesCalled = false 21 | private(set) var requestLocationCalled = false 22 | 23 | func requestWhenInUseAuthorization() { 24 | requestWhenInUseAuthorizationCalled = true 25 | } 26 | 27 | func startMonitoringSignificantLocationChanges() { 28 | startMonitoringSignificantLocationChangesCalled = true 29 | } 30 | 31 | func requestLocation() { 32 | requestLocationCalled = true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Fakes/FakeNetworkFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeNetworkFetcher.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @testable 12 | import RealWorldReSwift 13 | 14 | class FakeNetworkFetcher: NetworkFetching { 15 | 16 | private(set) var receivedRequest: URLRequest? 17 | var result: Result = .failure(NSError(domain: "", code: 0, userInfo: nil)) 18 | 19 | func fetch(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask where T: Decodable { 20 | 21 | receivedRequest = request 22 | completion(result as! Result) 23 | return URLSessionDataTask() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Fakes/FakePlacesService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakePlacesService.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 08.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | @testable 12 | import RealWorldReSwift 13 | 14 | class FakePlacesService: PlacesServing { 15 | 16 | private(set) var receivedRadius: Double? 17 | private(set) var reveivedCoordinate: CLLocationCoordinate2D? 18 | 19 | var result: Result? 20 | 21 | func search(coordinate: CLLocationCoordinate2D, radius: Double, completion: @escaping (Result) -> Void) { 22 | 23 | reveivedCoordinate = coordinate 24 | receivedRadius = radius 25 | 26 | if let result = result { 27 | completion(result) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Fakes/FakeReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeReducer.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 29.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import ReSwift 11 | 12 | @testable import RealWorldReSwift 13 | 14 | class FakeReducer { 15 | 16 | var actions: [Action]! = [] 17 | 18 | var places: Loadable<[Place]> = .initial 19 | var lastKnownLocation: CLLocation? = nil 20 | var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse 21 | 22 | func reduce(action: Action, state: AppState?) -> AppState { 23 | 24 | actions.append(action) 25 | return AppState( 26 | places: places, 27 | lastKnownLocation: lastKnownLocation, 28 | authorizationStatus: authorizationStatus 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Middlewaqre/MiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiddlewareTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 08.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreLocation 11 | import Nimble 12 | import ReSwift 13 | 14 | @testable import RealWorldReSwift 15 | 16 | class BaseMiddlewareTests: XCTestCase { 17 | 18 | let location = CLLocation(latitude: 52.520008, longitude: 13.404954) 19 | 20 | var context: MiddlewareContext! 21 | var state: AppState! 22 | var dispatchedAction: Action? 23 | var sut: SimpleMiddleware! 24 | 25 | override func setUp() { 26 | 27 | super.setUp() 28 | 29 | state = AppState( 30 | places: .initial, 31 | lastKnownLocation: location, 32 | authorizationStatus: .notDetermined 33 | ) 34 | 35 | context = MiddlewareContext( 36 | dispatch: { self.dispatchedAction = $0 }, 37 | getState: { self.state }, 38 | next: { _ in } 39 | ) 40 | } 41 | 42 | override func tearDown() { 43 | 44 | context = nil 45 | dispatchedAction = nil 46 | sut = nil 47 | super.tearDown() 48 | } 49 | 50 | } 51 | 52 | class FetchPlacesMiddlewareTests: BaseMiddlewareTests { 53 | 54 | var fakeService: FakePlacesService! 55 | 56 | override func setUp() { 57 | 58 | super.setUp() 59 | fakeService = FakePlacesService() 60 | sut = fetchPlaces(service: fakeService) 61 | } 62 | 63 | override func tearDown() { 64 | 65 | fakeService = nil 66 | super.tearDown() 67 | } 68 | 69 | func testReturnsLoadAction() { 70 | 71 | let action = sut(PlacesAction.fetch, context) 72 | 73 | if case .set(let loadable)? = (action as? PlacesAction) { 74 | 75 | if case .loading = loadable { 76 | // success 77 | } else { 78 | XCTFail("Expected .loading got \(loadable)") 79 | } 80 | 81 | } else { 82 | XCTFail("Expected .set got \(action.debugDescription)") 83 | } 84 | } 85 | 86 | func testSuppliesRadius() { 87 | 88 | _ = sut(PlacesAction.fetch, context) 89 | expect(self.fakeService.receivedRadius) == 2000.0 90 | } 91 | 92 | func testSuppliesCoordinates() { 93 | 94 | _ = sut(PlacesAction.fetch, context) 95 | expect(self.fakeService.reveivedCoordinate?.latitude) == location.coordinate.latitude 96 | expect(self.fakeService.reveivedCoordinate?.longitude) == location.coordinate.longitude 97 | } 98 | 99 | func testDispatchesPlaces() { 100 | 101 | fakeService.result = .success(PlacesSearchResult(results: [], status: "BLUBB")) 102 | _ = sut(PlacesAction.fetch, context) 103 | 104 | expect(self.dispatchedAction as? PlacesAction).toEventuallyNot(beNil()) 105 | } 106 | } 107 | 108 | class RequestAuthorizationTests: BaseMiddlewareTests { 109 | 110 | var locationManager: FakeLocationManager! 111 | 112 | override func setUp() { 113 | 114 | super.setUp() 115 | locationManager = FakeLocationManager() 116 | sut = requestAuthorization(locationManager: locationManager) 117 | } 118 | 119 | override func tearDown() { 120 | 121 | locationManager = nil 122 | super.tearDown() 123 | } 124 | 125 | func testRequestsAuthorization() { 126 | 127 | _ = sut(RequestAuthorizationAction(), context) 128 | expect(self.locationManager.requestWhenInUseAuthorizationCalled) == true 129 | } 130 | } 131 | 132 | class StartMonitoringTests: BaseMiddlewareTests { 133 | 134 | var locationManager: FakeLocationManager! 135 | 136 | override func setUp() { 137 | 138 | super.setUp() 139 | locationManager = FakeLocationManager() 140 | sut = startMonitoring(locationManager: locationManager) 141 | } 142 | 143 | override func tearDown() { 144 | 145 | locationManager = nil 146 | super.tearDown() 147 | } 148 | 149 | func testStartsMonitoringDidBecomeActive() { 150 | 151 | state = AppState( 152 | places: .initial, 153 | lastKnownLocation: location, 154 | authorizationStatus: .authorizedAlways 155 | ) 156 | 157 | _ = sut(ApplicationDidBecomeActiveAction(), context) 158 | expect(self.locationManager.startMonitoringSignificantLocationChangesCalled) == true 159 | } 160 | 161 | func testStartsMonitoringSetAuthorizationStatus() { 162 | 163 | _ = sut(SetAuthorizationStatusAction(authorizationStatus: .authorizedAlways), context) 164 | expect(self.locationManager.startMonitoringSignificantLocationChangesCalled) == true 165 | } 166 | 167 | func testDoesNothingOnDidBecomeActiveForInvalidSetAuthorizationStatus() { 168 | 169 | _ = sut(ApplicationDidBecomeActiveAction(), context) 170 | expect(self.locationManager.startMonitoringSignificantLocationChangesCalled) == false 171 | } 172 | 173 | func testDoesNothingForInvalidSetAuthorizationStatus() { 174 | 175 | _ = sut(SetAuthorizationStatusAction(authorizationStatus: .denied), context) 176 | expect(self.locationManager.startMonitoringSignificantLocationChangesCalled) == false 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Middlewaqre/SimpleMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleMiddlewareTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 01.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Nimble 11 | import ReSwift 12 | 13 | @testable import RealWorldReSwift 14 | 15 | class SimpleMiddlewareTests: XCTestCase { 16 | 17 | struct State: StateType, Equatable { 18 | let value: String 19 | } 20 | 21 | enum Actions: Action { 22 | case one 23 | case two 24 | case three 25 | } 26 | 27 | let state = State(value: "test") 28 | var suppliedContext: MiddlewareContext? 29 | var forwaredAction: Action? 30 | var dispatchedAction: Action? 31 | 32 | override func setUp() { 33 | let simpleMiddleware: SimpleMiddleware = { (action, context) in 34 | self.suppliedContext = context 35 | context.dispatch(Actions.three) 36 | return Actions.two 37 | } 38 | 39 | let middleware = createMiddleware(simpleMiddleware) 40 | 41 | let dispatch = middleware({ self.dispatchedAction = $0 }, { self.state }) 42 | let next = dispatch { self.forwaredAction = $0 } 43 | next(Actions.one) 44 | 45 | super.setUp() 46 | } 47 | 48 | override func tearDown() { 49 | suppliedContext = nil 50 | forwaredAction = nil 51 | dispatchedAction = nil 52 | super.tearDown() 53 | } 54 | 55 | 56 | func testSuppliesState() { 57 | expect(self.suppliedContext?.state) == state 58 | } 59 | 60 | func testSuppliesNext() { 61 | expect(self.forwaredAction as? Actions) == .two 62 | } 63 | 64 | func testSuppliesDispatch() { 65 | expect(self.dispatchedAction as? Actions) == .three 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Mocks/MockURLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLSession.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MockURLSession: URLSession { 12 | 13 | let mockTask = MockSessionDataTask() 14 | private(set) var receivedRequest: URLRequest? 15 | 16 | var data: Data? 17 | var response: URLResponse? 18 | var error: Error? 19 | 20 | override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 21 | 22 | receivedRequest = request 23 | completionHandler(data, response, error) 24 | return mockTask 25 | } 26 | } 27 | 28 | class MockSessionDataTask: URLSessionDataTask { 29 | 30 | private(set) var resumeCalled = false 31 | 32 | override func resume() { 33 | resumeCalled = true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Networking/NetworkFetcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFetcherTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Nimble 11 | 12 | @testable import RealWorldReSwift 13 | 14 | class NetworkFetcherTests: XCTestCase { 15 | 16 | var sut: NetworkFetcher! 17 | var mockSession: MockURLSession! 18 | 19 | let request = URLRequest(url: URL(string: "localhost")!) 20 | let jsonData = try! JSONEncoder().encode(["abc"]) 21 | 22 | override func setUp() { 23 | super.setUp() 24 | mockSession = MockURLSession() 25 | sut = NetworkFetcher(session: mockSession, decoder: JSONDecoder()) 26 | } 27 | 28 | override func tearDown() { 29 | mockSession = nil 30 | sut = nil 31 | super.tearDown() 32 | } 33 | 34 | func testSuppliesRequest() { 35 | sut.fetch(request: request) { (result: Result) in } 36 | expect(self.mockSession.receivedRequest?.url) == request.url 37 | } 38 | 39 | func testResumes() { 40 | sut.fetch(request: request) { (result: Result) in } 41 | expect(self.mockSession.mockTask.resumeCalled) == true 42 | } 43 | 44 | func testHandlesResponseError() { 45 | mockSession.error = NSError(domain: "", code: 0, userInfo: nil) 46 | var error: Error? 47 | 48 | sut.fetch(request: request) { (result: Result) in 49 | error = result.error 50 | } 51 | expect(error as? NetworkFetcherError).toEventually(equal(.response)) 52 | } 53 | 54 | func testParsesCorrectData() { 55 | mockSession.data = jsonData 56 | var value: [String]? 57 | 58 | sut.fetch(request: request) { (result: Result<[String]>) in 59 | value = result.value 60 | } 61 | expect(value).toEventually(equal(["abc"])) 62 | } 63 | 64 | func testHandlesContentError() { 65 | mockSession.data = jsonData 66 | var error: Error? 67 | 68 | sut.fetch(request: request) { (result: Result<[Int]>) in 69 | error = result.error 70 | } 71 | expect(error as? NetworkFetcherError).toEventually(equal(.content)) 72 | } 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Networking/PlacesServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlacesServiceTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 25.03.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Nimble 11 | import CoreLocation 12 | 13 | @testable import RealWorldReSwift 14 | 15 | class PlacesServiceTests: XCTestCase { 16 | 17 | let coordinate = CLLocationCoordinate2D(latitude: 50.44, longitude: 10.123) 18 | 19 | var sut: PlacesService! 20 | var fakeNetworkFetcher: FakeNetworkFetcher! 21 | var query: String! 22 | 23 | override func setUp() { 24 | super.setUp() 25 | fakeNetworkFetcher = FakeNetworkFetcher() 26 | sut = PlacesService( 27 | locale: Locale(identifier: "en"), 28 | apiKey: "asdf-qwerty", 29 | fetcher: fakeNetworkFetcher 30 | ) 31 | 32 | sut.search( 33 | coordinate: coordinate, 34 | radius: 200, 35 | completion: { _ in } 36 | ) 37 | 38 | query = fakeNetworkFetcher.receivedRequest?.url?.query 39 | } 40 | 41 | override func tearDown() { 42 | sut = nil 43 | fakeNetworkFetcher = nil 44 | query = nil 45 | super.tearDown() 46 | } 47 | 48 | func testSuppliesStaticQueryParamters() { 49 | expect(self.query).to(contain("opennow=1")) 50 | expect(self.query).to(contain("type=restaurant")) 51 | } 52 | 53 | func testSuppliesBaseURL() { 54 | let urlString = fakeNetworkFetcher.receivedRequest?.url?.absoluteString 55 | expect(urlString).to(beginWith("https://maps.googleapis.com/maps/api/place/nearbysearch/json")) 56 | } 57 | 58 | func testSuppliesCoordinates() { 59 | expect(self.query).to(contain("location=50.44,10.123")) 60 | } 61 | 62 | func testSuppliesLocal() { 63 | expect(self.query).to(contain("language=en")) 64 | } 65 | 66 | func testSuppliesRadius() { 67 | expect(self.query).to(contain("radius=200.0")) 68 | } 69 | 70 | func testSuppliesApiKey() { 71 | expect(self.query).to(contain("key=asdf-qwerty")) 72 | } 73 | 74 | func testHandlesErrors() { 75 | 76 | let error = NSError(domain: "", code: 0, userInfo: nil) 77 | fakeNetworkFetcher.result = .failure(error) 78 | var result: Result? 79 | 80 | sut.search(coordinate: coordinate, radius: 0) { 81 | result = $0 82 | } 83 | 84 | expect(result).toEventuallyNot(beNil()) 85 | 86 | if case .failure(let receivedError)? = result { 87 | expect(receivedError as NSError) === error 88 | } else { 89 | XCTFail("expected .failure got \(result.debugDescription)") 90 | } 91 | } 92 | 93 | func testHandlesSuccess() { 94 | 95 | let value = PlacesSearchResult(results: [], status: "OK") 96 | fakeNetworkFetcher.result = .success(value) 97 | var result: Result? 98 | 99 | sut.search(coordinate: coordinate, radius: 0) { 100 | result = $0 101 | } 102 | 103 | expect(result).toEventuallyNot(beNil()) 104 | 105 | if case .success? = result { 106 | // success 107 | } else { 108 | XCTFail("expected .success got \(result.debugDescription)") 109 | } 110 | } 111 | 112 | func testHandlesInvalidStatus() { 113 | 114 | let value = PlacesSearchResult(results: [], status: "INVALID") 115 | fakeNetworkFetcher.result = .success(value) 116 | var result: Result? 117 | 118 | sut.search(coordinate: coordinate, radius: 0) { 119 | result = $0 120 | } 121 | 122 | expect(result).toEventuallyNot(beNil()) 123 | 124 | if case .failure(let error)? = result { 125 | let placesError = error as? PlacesSearchResultError 126 | expect(placesError?.status) == "INVALID" 127 | } else { 128 | XCTFail("expected .failure got \(result.debugDescription)") 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /RealWorldReSwiftTests/Util/CLLocationCoordinate2DTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2DTests.swift 3 | // RealWorldReSwiftTests 4 | // 5 | // Created by Tobias Ottenweller on 08.04.18. 6 | // Copyright © 2018 Tobias Ottenweller. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreLocation 11 | import Nimble 12 | 13 | @testable 14 | import RealWorldReSwift 15 | 16 | class CLLocationCoordinate2DTests: XCTestCase { 17 | 18 | let data = try! JSONSerialization.data( 19 | withJSONObject: ["lng": 12.0, "lat": 99.0], 20 | options: [] 21 | ) 22 | 23 | func testEncode() { 24 | 25 | let sut = CLLocationCoordinate2D(latitude: 77, longitude: 32) 26 | let data = try? JSONEncoder().encode(sut) 27 | let json = try? JSONSerialization.jsonObject(with: data ?? Data(), options: []) 28 | let dict = json as? [String: Any] 29 | 30 | expect(dict?["lng"] as? Double) == 32 31 | expect(dict?["lat"] as? Double) == 77 32 | } 33 | 34 | func testDecode() { 35 | 36 | let sut = try? JSONDecoder().decode(CLLocationCoordinate2D.self, from: data) 37 | 38 | expect(sut?.longitude) == 12.0 39 | expect(sut?.latitude) == 99.0 40 | } 41 | } 42 | --------------------------------------------------------------------------------