├── .gitignore
├── LICENSE
├── README.md
├── SwiftUI-Flux.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── SwiftUI-Flux
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Flux
│ └── List
│ │ ├── RepositoryListAction.swift
│ │ ├── RepositoryListActionCreator.swift
│ │ ├── RepositoryListDispatcher.swift
│ │ └── RepositoryListStore.swift
├── Info.plist
├── Models
│ ├── AnySubscription.swift
│ ├── Repository.swift
│ ├── SearchRepositoryResponse.swift
│ └── User.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── SceneDelegate.swift
├── Services
│ ├── APIService.swift
│ ├── APIServiceError.swift
│ ├── ExperimentService.swift
│ ├── SearchRepositoryRequest.swift
│ └── TrackerService.swift
└── Views
│ ├── RepositoryDetailView.swift
│ ├── RepositoryListRow.swift
│ └── RepositoryListView.swift
└── SwiftUI-FluxTests
├── Info.plist
├── MockAPISearvice.swift
├── MockExperimentService.swift
├── MockTrackerService.swift
├── RepositoryListActionCreatorTests.swift
└── SwiftUI_FluxTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Yusuke Kita
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI-Flux
2 |
3 | Flux enables us to have unidirectional data flow and make it testable. It's used to be implemented using RxSwift or ReactiveSwift in the past, but I use Combine this time. This is an experimental project using SwiftUI + Flux.
4 |
5 |
6 |
7 | ## Requirements
8 |
9 | Xcode 11.0 Beta 5+
10 | Swift 5.1+
11 |
12 | ## More examples
13 |
14 | See other architectures as well
15 |
16 | - MVVM: https://github.com/kitasuke/SwiftUI-MVVM
17 | - Redux: https://github.com/kitasuke/SwiftUI-Redux
18 |
--------------------------------------------------------------------------------
/SwiftUI-Flux.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | EC139DC922B4C525003B138A /* SearchRepositoryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DC522B4C525003B138A /* SearchRepositoryResponse.swift */; };
11 | EC139DCA22B4C525003B138A /* AnySubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DC622B4C525003B138A /* AnySubscription.swift */; };
12 | EC139DCB22B4C525003B138A /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DC722B4C525003B138A /* User.swift */; };
13 | EC139DCC22B4C525003B138A /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DC822B4C525003B138A /* Repository.swift */; };
14 | EC139DD822B4C531003B138A /* TrackerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DCE22B4C530003B138A /* TrackerService.swift */; };
15 | EC139DDA22B4C531003B138A /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DD022B4C530003B138A /* APIService.swift */; };
16 | EC139DDB22B4C531003B138A /* APIServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DD122B4C530003B138A /* APIServiceError.swift */; };
17 | EC139DDC22B4C531003B138A /* SearchRepositoryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DD222B4C530003B138A /* SearchRepositoryRequest.swift */; };
18 | EC139DDD22B4C531003B138A /* ExperimentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DD322B4C530003B138A /* ExperimentService.swift */; };
19 | EC139DDE22B4C531003B138A /* RepositoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DD522B4C531003B138A /* RepositoryListView.swift */; };
20 | EC139DDF22B4C531003B138A /* RepositoryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DD622B4C531003B138A /* RepositoryListRow.swift */; };
21 | EC139DE022B4C531003B138A /* RepositoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DD722B4C531003B138A /* RepositoryDetailView.swift */; };
22 | EC139DE422B4C53D003B138A /* MockTrackerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DE122B4C53D003B138A /* MockTrackerService.swift */; };
23 | EC139DE522B4C53D003B138A /* MockAPISearvice.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DE222B4C53D003B138A /* MockAPISearvice.swift */; };
24 | EC139DE622B4C53D003B138A /* MockExperimentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DE322B4C53D003B138A /* MockExperimentService.swift */; };
25 | EC139DF022B4C5CF003B138A /* RepositoryListAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DEF22B4C5CF003B138A /* RepositoryListAction.swift */; };
26 | EC139DF222B4C6AA003B138A /* RepositoryListActionCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DF122B4C6AA003B138A /* RepositoryListActionCreator.swift */; };
27 | EC139DF422B4C6F0003B138A /* RepositoryListDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DF322B4C6F0003B138A /* RepositoryListDispatcher.swift */; };
28 | EC139DF622B4CBC4003B138A /* RepositoryListStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DF522B4CBC4003B138A /* RepositoryListStore.swift */; };
29 | EC139DF822B4D217003B138A /* RepositoryListActionCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC139DF722B4D217003B138A /* RepositoryListActionCreatorTests.swift */; };
30 | ECFE06C122B4C498001C2F76 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFE06C022B4C498001C2F76 /* AppDelegate.swift */; };
31 | ECFE06C322B4C498001C2F76 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFE06C222B4C498001C2F76 /* SceneDelegate.swift */; };
32 | ECFE06C722B4C499001C2F76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ECFE06C622B4C499001C2F76 /* Assets.xcassets */; };
33 | ECFE06CA22B4C499001C2F76 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ECFE06C922B4C499001C2F76 /* Preview Assets.xcassets */; };
34 | ECFE06CD22B4C499001C2F76 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ECFE06CB22B4C499001C2F76 /* LaunchScreen.storyboard */; };
35 | ECFE06D822B4C499001C2F76 /* SwiftUI_FluxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFE06D722B4C499001C2F76 /* SwiftUI_FluxTests.swift */; };
36 | /* End PBXBuildFile section */
37 |
38 | /* Begin PBXContainerItemProxy section */
39 | ECFE06D422B4C499001C2F76 /* PBXContainerItemProxy */ = {
40 | isa = PBXContainerItemProxy;
41 | containerPortal = ECFE06B522B4C498001C2F76 /* Project object */;
42 | proxyType = 1;
43 | remoteGlobalIDString = ECFE06BC22B4C498001C2F76;
44 | remoteInfo = "SwiftUI-Flux";
45 | };
46 | /* End PBXContainerItemProxy section */
47 |
48 | /* Begin PBXFileReference section */
49 | EC139DC522B4C525003B138A /* SearchRepositoryResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchRepositoryResponse.swift; sourceTree = ""; };
50 | EC139DC622B4C525003B138A /* AnySubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnySubscription.swift; sourceTree = ""; };
51 | EC139DC722B4C525003B138A /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
52 | EC139DC822B4C525003B138A /* Repository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; };
53 | EC139DCE22B4C530003B138A /* TrackerService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackerService.swift; sourceTree = ""; };
54 | EC139DD022B4C530003B138A /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; };
55 | EC139DD122B4C530003B138A /* APIServiceError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIServiceError.swift; sourceTree = ""; };
56 | EC139DD222B4C530003B138A /* SearchRepositoryRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchRepositoryRequest.swift; sourceTree = ""; };
57 | EC139DD322B4C530003B138A /* ExperimentService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperimentService.swift; sourceTree = ""; };
58 | EC139DD522B4C531003B138A /* RepositoryListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryListView.swift; sourceTree = ""; };
59 | EC139DD622B4C531003B138A /* RepositoryListRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryListRow.swift; sourceTree = ""; };
60 | EC139DD722B4C531003B138A /* RepositoryDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryDetailView.swift; sourceTree = ""; };
61 | EC139DE122B4C53D003B138A /* MockTrackerService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTrackerService.swift; sourceTree = ""; };
62 | EC139DE222B4C53D003B138A /* MockAPISearvice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockAPISearvice.swift; sourceTree = ""; };
63 | EC139DE322B4C53D003B138A /* MockExperimentService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockExperimentService.swift; sourceTree = ""; };
64 | EC139DEF22B4C5CF003B138A /* RepositoryListAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListAction.swift; sourceTree = ""; };
65 | EC139DF122B4C6AA003B138A /* RepositoryListActionCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListActionCreator.swift; sourceTree = ""; };
66 | EC139DF322B4C6F0003B138A /* RepositoryListDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListDispatcher.swift; sourceTree = ""; };
67 | EC139DF522B4CBC4003B138A /* RepositoryListStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListStore.swift; sourceTree = ""; };
68 | EC139DF722B4D217003B138A /* RepositoryListActionCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListActionCreatorTests.swift; sourceTree = ""; };
69 | ECFE06BD22B4C498001C2F76 /* SwiftUI-Flux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI-Flux.app"; sourceTree = BUILT_PRODUCTS_DIR; };
70 | ECFE06C022B4C498001C2F76 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
71 | ECFE06C222B4C498001C2F76 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
72 | ECFE06C622B4C499001C2F76 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
73 | ECFE06C922B4C499001C2F76 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
74 | ECFE06CC22B4C499001C2F76 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
75 | ECFE06CE22B4C499001C2F76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
76 | ECFE06D322B4C499001C2F76 /* SwiftUI-FluxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftUI-FluxTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
77 | ECFE06D722B4C499001C2F76 /* SwiftUI_FluxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI_FluxTests.swift; sourceTree = ""; };
78 | ECFE06D922B4C499001C2F76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
79 | /* End PBXFileReference section */
80 |
81 | /* Begin PBXFrameworksBuildPhase section */
82 | ECFE06BA22B4C498001C2F76 /* Frameworks */ = {
83 | isa = PBXFrameworksBuildPhase;
84 | buildActionMask = 2147483647;
85 | files = (
86 | );
87 | runOnlyForDeploymentPostprocessing = 0;
88 | };
89 | ECFE06D022B4C499001C2F76 /* Frameworks */ = {
90 | isa = PBXFrameworksBuildPhase;
91 | buildActionMask = 2147483647;
92 | files = (
93 | );
94 | runOnlyForDeploymentPostprocessing = 0;
95 | };
96 | /* End PBXFrameworksBuildPhase section */
97 |
98 | /* Begin PBXGroup section */
99 | EC139DC422B4C525003B138A /* Models */ = {
100 | isa = PBXGroup;
101 | children = (
102 | EC139DC522B4C525003B138A /* SearchRepositoryResponse.swift */,
103 | EC139DC622B4C525003B138A /* AnySubscription.swift */,
104 | EC139DC722B4C525003B138A /* User.swift */,
105 | EC139DC822B4C525003B138A /* Repository.swift */,
106 | );
107 | path = Models;
108 | sourceTree = "";
109 | };
110 | EC139DCD22B4C530003B138A /* Services */ = {
111 | isa = PBXGroup;
112 | children = (
113 | EC139DCE22B4C530003B138A /* TrackerService.swift */,
114 | EC139DD022B4C530003B138A /* APIService.swift */,
115 | EC139DD122B4C530003B138A /* APIServiceError.swift */,
116 | EC139DD222B4C530003B138A /* SearchRepositoryRequest.swift */,
117 | EC139DD322B4C530003B138A /* ExperimentService.swift */,
118 | );
119 | path = Services;
120 | sourceTree = "";
121 | };
122 | EC139DD422B4C531003B138A /* Views */ = {
123 | isa = PBXGroup;
124 | children = (
125 | EC139DD522B4C531003B138A /* RepositoryListView.swift */,
126 | EC139DD622B4C531003B138A /* RepositoryListRow.swift */,
127 | EC139DD722B4C531003B138A /* RepositoryDetailView.swift */,
128 | );
129 | path = Views;
130 | sourceTree = "";
131 | };
132 | EC139DEC22B4C5A8003B138A /* Flux */ = {
133 | isa = PBXGroup;
134 | children = (
135 | EC139DEE22B4C5BE003B138A /* Detail */,
136 | EC139DED22B4C5B9003B138A /* List */,
137 | );
138 | path = Flux;
139 | sourceTree = "";
140 | };
141 | EC139DED22B4C5B9003B138A /* List */ = {
142 | isa = PBXGroup;
143 | children = (
144 | EC139DEF22B4C5CF003B138A /* RepositoryListAction.swift */,
145 | EC139DF122B4C6AA003B138A /* RepositoryListActionCreator.swift */,
146 | EC139DF322B4C6F0003B138A /* RepositoryListDispatcher.swift */,
147 | EC139DF522B4CBC4003B138A /* RepositoryListStore.swift */,
148 | );
149 | path = List;
150 | sourceTree = "";
151 | };
152 | EC139DEE22B4C5BE003B138A /* Detail */ = {
153 | isa = PBXGroup;
154 | children = (
155 | );
156 | path = Detail;
157 | sourceTree = "";
158 | };
159 | ECFE06B422B4C498001C2F76 = {
160 | isa = PBXGroup;
161 | children = (
162 | ECFE06BF22B4C498001C2F76 /* SwiftUI-Flux */,
163 | ECFE06D622B4C499001C2F76 /* SwiftUI-FluxTests */,
164 | ECFE06BE22B4C498001C2F76 /* Products */,
165 | );
166 | sourceTree = "";
167 | };
168 | ECFE06BE22B4C498001C2F76 /* Products */ = {
169 | isa = PBXGroup;
170 | children = (
171 | ECFE06BD22B4C498001C2F76 /* SwiftUI-Flux.app */,
172 | ECFE06D322B4C499001C2F76 /* SwiftUI-FluxTests.xctest */,
173 | );
174 | name = Products;
175 | sourceTree = "";
176 | };
177 | ECFE06BF22B4C498001C2F76 /* SwiftUI-Flux */ = {
178 | isa = PBXGroup;
179 | children = (
180 | EC139DEC22B4C5A8003B138A /* Flux */,
181 | EC139DCD22B4C530003B138A /* Services */,
182 | EC139DD422B4C531003B138A /* Views */,
183 | EC139DC422B4C525003B138A /* Models */,
184 | ECFE06C022B4C498001C2F76 /* AppDelegate.swift */,
185 | ECFE06C222B4C498001C2F76 /* SceneDelegate.swift */,
186 | ECFE06C622B4C499001C2F76 /* Assets.xcassets */,
187 | ECFE06CB22B4C499001C2F76 /* LaunchScreen.storyboard */,
188 | ECFE06CE22B4C499001C2F76 /* Info.plist */,
189 | ECFE06C822B4C499001C2F76 /* Preview Content */,
190 | );
191 | path = "SwiftUI-Flux";
192 | sourceTree = "";
193 | };
194 | ECFE06C822B4C499001C2F76 /* Preview Content */ = {
195 | isa = PBXGroup;
196 | children = (
197 | ECFE06C922B4C499001C2F76 /* Preview Assets.xcassets */,
198 | );
199 | path = "Preview Content";
200 | sourceTree = "";
201 | };
202 | ECFE06D622B4C499001C2F76 /* SwiftUI-FluxTests */ = {
203 | isa = PBXGroup;
204 | children = (
205 | EC139DE222B4C53D003B138A /* MockAPISearvice.swift */,
206 | EC139DE322B4C53D003B138A /* MockExperimentService.swift */,
207 | EC139DE122B4C53D003B138A /* MockTrackerService.swift */,
208 | ECFE06D722B4C499001C2F76 /* SwiftUI_FluxTests.swift */,
209 | ECFE06D922B4C499001C2F76 /* Info.plist */,
210 | EC139DF722B4D217003B138A /* RepositoryListActionCreatorTests.swift */,
211 | );
212 | path = "SwiftUI-FluxTests";
213 | sourceTree = "";
214 | };
215 | /* End PBXGroup section */
216 |
217 | /* Begin PBXNativeTarget section */
218 | ECFE06BC22B4C498001C2F76 /* SwiftUI-Flux */ = {
219 | isa = PBXNativeTarget;
220 | buildConfigurationList = ECFE06DC22B4C499001C2F76 /* Build configuration list for PBXNativeTarget "SwiftUI-Flux" */;
221 | buildPhases = (
222 | ECFE06B922B4C498001C2F76 /* Sources */,
223 | ECFE06BA22B4C498001C2F76 /* Frameworks */,
224 | ECFE06BB22B4C498001C2F76 /* Resources */,
225 | );
226 | buildRules = (
227 | );
228 | dependencies = (
229 | );
230 | name = "SwiftUI-Flux";
231 | productName = "SwiftUI-Flux";
232 | productReference = ECFE06BD22B4C498001C2F76 /* SwiftUI-Flux.app */;
233 | productType = "com.apple.product-type.application";
234 | };
235 | ECFE06D222B4C499001C2F76 /* SwiftUI-FluxTests */ = {
236 | isa = PBXNativeTarget;
237 | buildConfigurationList = ECFE06DF22B4C499001C2F76 /* Build configuration list for PBXNativeTarget "SwiftUI-FluxTests" */;
238 | buildPhases = (
239 | ECFE06CF22B4C499001C2F76 /* Sources */,
240 | ECFE06D022B4C499001C2F76 /* Frameworks */,
241 | ECFE06D122B4C499001C2F76 /* Resources */,
242 | );
243 | buildRules = (
244 | );
245 | dependencies = (
246 | ECFE06D522B4C499001C2F76 /* PBXTargetDependency */,
247 | );
248 | name = "SwiftUI-FluxTests";
249 | productName = "SwiftUI-FluxTests";
250 | productReference = ECFE06D322B4C499001C2F76 /* SwiftUI-FluxTests.xctest */;
251 | productType = "com.apple.product-type.bundle.unit-test";
252 | };
253 | /* End PBXNativeTarget section */
254 |
255 | /* Begin PBXProject section */
256 | ECFE06B522B4C498001C2F76 /* Project object */ = {
257 | isa = PBXProject;
258 | attributes = {
259 | LastSwiftUpdateCheck = 1100;
260 | LastUpgradeCheck = 1100;
261 | ORGANIZATIONNAME = "Yusuke Kita";
262 | TargetAttributes = {
263 | ECFE06BC22B4C498001C2F76 = {
264 | CreatedOnToolsVersion = 11.0;
265 | };
266 | ECFE06D222B4C499001C2F76 = {
267 | CreatedOnToolsVersion = 11.0;
268 | TestTargetID = ECFE06BC22B4C498001C2F76;
269 | };
270 | };
271 | };
272 | buildConfigurationList = ECFE06B822B4C498001C2F76 /* Build configuration list for PBXProject "SwiftUI-Flux" */;
273 | compatibilityVersion = "Xcode 9.3";
274 | developmentRegion = en;
275 | hasScannedForEncodings = 0;
276 | knownRegions = (
277 | en,
278 | Base,
279 | );
280 | mainGroup = ECFE06B422B4C498001C2F76;
281 | productRefGroup = ECFE06BE22B4C498001C2F76 /* Products */;
282 | projectDirPath = "";
283 | projectRoot = "";
284 | targets = (
285 | ECFE06BC22B4C498001C2F76 /* SwiftUI-Flux */,
286 | ECFE06D222B4C499001C2F76 /* SwiftUI-FluxTests */,
287 | );
288 | };
289 | /* End PBXProject section */
290 |
291 | /* Begin PBXResourcesBuildPhase section */
292 | ECFE06BB22B4C498001C2F76 /* Resources */ = {
293 | isa = PBXResourcesBuildPhase;
294 | buildActionMask = 2147483647;
295 | files = (
296 | ECFE06CD22B4C499001C2F76 /* LaunchScreen.storyboard in Resources */,
297 | ECFE06CA22B4C499001C2F76 /* Preview Assets.xcassets in Resources */,
298 | ECFE06C722B4C499001C2F76 /* Assets.xcassets in Resources */,
299 | );
300 | runOnlyForDeploymentPostprocessing = 0;
301 | };
302 | ECFE06D122B4C499001C2F76 /* Resources */ = {
303 | isa = PBXResourcesBuildPhase;
304 | buildActionMask = 2147483647;
305 | files = (
306 | );
307 | runOnlyForDeploymentPostprocessing = 0;
308 | };
309 | /* End PBXResourcesBuildPhase section */
310 |
311 | /* Begin PBXSourcesBuildPhase section */
312 | ECFE06B922B4C498001C2F76 /* Sources */ = {
313 | isa = PBXSourcesBuildPhase;
314 | buildActionMask = 2147483647;
315 | files = (
316 | EC139DDD22B4C531003B138A /* ExperimentService.swift in Sources */,
317 | EC139DDC22B4C531003B138A /* SearchRepositoryRequest.swift in Sources */,
318 | EC139DF422B4C6F0003B138A /* RepositoryListDispatcher.swift in Sources */,
319 | EC139DD822B4C531003B138A /* TrackerService.swift in Sources */,
320 | EC139DF022B4C5CF003B138A /* RepositoryListAction.swift in Sources */,
321 | EC139DDB22B4C531003B138A /* APIServiceError.swift in Sources */,
322 | EC139DC922B4C525003B138A /* SearchRepositoryResponse.swift in Sources */,
323 | EC139DF222B4C6AA003B138A /* RepositoryListActionCreator.swift in Sources */,
324 | EC139DCA22B4C525003B138A /* AnySubscription.swift in Sources */,
325 | ECFE06C122B4C498001C2F76 /* AppDelegate.swift in Sources */,
326 | EC139DF622B4CBC4003B138A /* RepositoryListStore.swift in Sources */,
327 | EC139DDE22B4C531003B138A /* RepositoryListView.swift in Sources */,
328 | EC139DCB22B4C525003B138A /* User.swift in Sources */,
329 | ECFE06C322B4C498001C2F76 /* SceneDelegate.swift in Sources */,
330 | EC139DDA22B4C531003B138A /* APIService.swift in Sources */,
331 | EC139DE022B4C531003B138A /* RepositoryDetailView.swift in Sources */,
332 | EC139DDF22B4C531003B138A /* RepositoryListRow.swift in Sources */,
333 | EC139DCC22B4C525003B138A /* Repository.swift in Sources */,
334 | );
335 | runOnlyForDeploymentPostprocessing = 0;
336 | };
337 | ECFE06CF22B4C499001C2F76 /* Sources */ = {
338 | isa = PBXSourcesBuildPhase;
339 | buildActionMask = 2147483647;
340 | files = (
341 | EC139DF822B4D217003B138A /* RepositoryListActionCreatorTests.swift in Sources */,
342 | EC139DE422B4C53D003B138A /* MockTrackerService.swift in Sources */,
343 | EC139DE522B4C53D003B138A /* MockAPISearvice.swift in Sources */,
344 | EC139DE622B4C53D003B138A /* MockExperimentService.swift in Sources */,
345 | ECFE06D822B4C499001C2F76 /* SwiftUI_FluxTests.swift in Sources */,
346 | );
347 | runOnlyForDeploymentPostprocessing = 0;
348 | };
349 | /* End PBXSourcesBuildPhase section */
350 |
351 | /* Begin PBXTargetDependency section */
352 | ECFE06D522B4C499001C2F76 /* PBXTargetDependency */ = {
353 | isa = PBXTargetDependency;
354 | target = ECFE06BC22B4C498001C2F76 /* SwiftUI-Flux */;
355 | targetProxy = ECFE06D422B4C499001C2F76 /* PBXContainerItemProxy */;
356 | };
357 | /* End PBXTargetDependency section */
358 |
359 | /* Begin PBXVariantGroup section */
360 | ECFE06CB22B4C499001C2F76 /* LaunchScreen.storyboard */ = {
361 | isa = PBXVariantGroup;
362 | children = (
363 | ECFE06CC22B4C499001C2F76 /* Base */,
364 | );
365 | name = LaunchScreen.storyboard;
366 | sourceTree = "";
367 | };
368 | /* End PBXVariantGroup section */
369 |
370 | /* Begin XCBuildConfiguration section */
371 | ECFE06DA22B4C499001C2F76 /* Debug */ = {
372 | isa = XCBuildConfiguration;
373 | buildSettings = {
374 | ALWAYS_SEARCH_USER_PATHS = NO;
375 | CLANG_ANALYZER_NONNULL = YES;
376 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
377 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
378 | CLANG_CXX_LIBRARY = "libc++";
379 | CLANG_ENABLE_MODULES = YES;
380 | CLANG_ENABLE_OBJC_ARC = YES;
381 | CLANG_ENABLE_OBJC_WEAK = YES;
382 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
383 | CLANG_WARN_BOOL_CONVERSION = YES;
384 | CLANG_WARN_COMMA = YES;
385 | CLANG_WARN_CONSTANT_CONVERSION = YES;
386 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
387 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
388 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
389 | CLANG_WARN_EMPTY_BODY = YES;
390 | CLANG_WARN_ENUM_CONVERSION = YES;
391 | CLANG_WARN_INFINITE_RECURSION = YES;
392 | CLANG_WARN_INT_CONVERSION = YES;
393 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
394 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
395 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
396 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
397 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
398 | CLANG_WARN_STRICT_PROTOTYPES = YES;
399 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
400 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
401 | CLANG_WARN_UNREACHABLE_CODE = YES;
402 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
403 | COPY_PHASE_STRIP = NO;
404 | DEBUG_INFORMATION_FORMAT = dwarf;
405 | ENABLE_STRICT_OBJC_MSGSEND = YES;
406 | ENABLE_TESTABILITY = YES;
407 | GCC_C_LANGUAGE_STANDARD = gnu11;
408 | GCC_DYNAMIC_NO_PIC = NO;
409 | GCC_NO_COMMON_BLOCKS = YES;
410 | GCC_OPTIMIZATION_LEVEL = 0;
411 | GCC_PREPROCESSOR_DEFINITIONS = (
412 | "DEBUG=1",
413 | "$(inherited)",
414 | );
415 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
416 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
417 | GCC_WARN_UNDECLARED_SELECTOR = YES;
418 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
419 | GCC_WARN_UNUSED_FUNCTION = YES;
420 | GCC_WARN_UNUSED_VARIABLE = YES;
421 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
422 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
423 | MTL_FAST_MATH = YES;
424 | ONLY_ACTIVE_ARCH = YES;
425 | SDKROOT = iphoneos;
426 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
427 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
428 | };
429 | name = Debug;
430 | };
431 | ECFE06DB22B4C499001C2F76 /* Release */ = {
432 | isa = XCBuildConfiguration;
433 | buildSettings = {
434 | ALWAYS_SEARCH_USER_PATHS = NO;
435 | CLANG_ANALYZER_NONNULL = YES;
436 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
437 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
438 | CLANG_CXX_LIBRARY = "libc++";
439 | CLANG_ENABLE_MODULES = YES;
440 | CLANG_ENABLE_OBJC_ARC = YES;
441 | CLANG_ENABLE_OBJC_WEAK = YES;
442 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
443 | CLANG_WARN_BOOL_CONVERSION = YES;
444 | CLANG_WARN_COMMA = YES;
445 | CLANG_WARN_CONSTANT_CONVERSION = YES;
446 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
447 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
448 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
449 | CLANG_WARN_EMPTY_BODY = YES;
450 | CLANG_WARN_ENUM_CONVERSION = YES;
451 | CLANG_WARN_INFINITE_RECURSION = YES;
452 | CLANG_WARN_INT_CONVERSION = YES;
453 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
454 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
455 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
456 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
457 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
458 | CLANG_WARN_STRICT_PROTOTYPES = YES;
459 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
460 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
461 | CLANG_WARN_UNREACHABLE_CODE = YES;
462 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
463 | COPY_PHASE_STRIP = NO;
464 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
465 | ENABLE_NS_ASSERTIONS = NO;
466 | ENABLE_STRICT_OBJC_MSGSEND = YES;
467 | GCC_C_LANGUAGE_STANDARD = gnu11;
468 | GCC_NO_COMMON_BLOCKS = YES;
469 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
470 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
471 | GCC_WARN_UNDECLARED_SELECTOR = YES;
472 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
473 | GCC_WARN_UNUSED_FUNCTION = YES;
474 | GCC_WARN_UNUSED_VARIABLE = YES;
475 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
476 | MTL_ENABLE_DEBUG_INFO = NO;
477 | MTL_FAST_MATH = YES;
478 | SDKROOT = iphoneos;
479 | SWIFT_COMPILATION_MODE = wholemodule;
480 | SWIFT_OPTIMIZATION_LEVEL = "-O";
481 | VALIDATE_PRODUCT = YES;
482 | };
483 | name = Release;
484 | };
485 | ECFE06DD22B4C499001C2F76 /* Debug */ = {
486 | isa = XCBuildConfiguration;
487 | buildSettings = {
488 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
489 | CODE_SIGN_STYLE = Automatic;
490 | DEVELOPMENT_ASSET_PATHS = "SwiftUI-Flux/Preview\\ Content";
491 | DEVELOPMENT_TEAM = 34P5LXX69P;
492 | ENABLE_PREVIEWS = YES;
493 | INFOPLIST_FILE = "SwiftUI-Flux/Info.plist";
494 | LD_RUNPATH_SEARCH_PATHS = (
495 | "$(inherited)",
496 | "@executable_path/Frameworks",
497 | );
498 | PRODUCT_BUNDLE_IDENTIFIER = "com.kitasuke.SwiftUI-Flux";
499 | PRODUCT_NAME = "$(TARGET_NAME)";
500 | SWIFT_VERSION = 5.0;
501 | TARGETED_DEVICE_FAMILY = "1,2";
502 | };
503 | name = Debug;
504 | };
505 | ECFE06DE22B4C499001C2F76 /* Release */ = {
506 | isa = XCBuildConfiguration;
507 | buildSettings = {
508 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
509 | CODE_SIGN_STYLE = Automatic;
510 | DEVELOPMENT_ASSET_PATHS = "SwiftUI-Flux/Preview\\ Content";
511 | DEVELOPMENT_TEAM = 34P5LXX69P;
512 | ENABLE_PREVIEWS = YES;
513 | INFOPLIST_FILE = "SwiftUI-Flux/Info.plist";
514 | LD_RUNPATH_SEARCH_PATHS = (
515 | "$(inherited)",
516 | "@executable_path/Frameworks",
517 | );
518 | PRODUCT_BUNDLE_IDENTIFIER = "com.kitasuke.SwiftUI-Flux";
519 | PRODUCT_NAME = "$(TARGET_NAME)";
520 | SWIFT_VERSION = 5.0;
521 | TARGETED_DEVICE_FAMILY = "1,2";
522 | };
523 | name = Release;
524 | };
525 | ECFE06E022B4C499001C2F76 /* Debug */ = {
526 | isa = XCBuildConfiguration;
527 | buildSettings = {
528 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
529 | BUNDLE_LOADER = "$(TEST_HOST)";
530 | CODE_SIGN_STYLE = Automatic;
531 | DEVELOPMENT_TEAM = 34P5LXX69P;
532 | INFOPLIST_FILE = "SwiftUI-FluxTests/Info.plist";
533 | LD_RUNPATH_SEARCH_PATHS = (
534 | "$(inherited)",
535 | "@executable_path/Frameworks",
536 | "@loader_path/Frameworks",
537 | );
538 | PRODUCT_BUNDLE_IDENTIFIER = "com.kitasuke.SwiftUI-FluxTests";
539 | PRODUCT_NAME = "$(TARGET_NAME)";
540 | SWIFT_VERSION = 5.0;
541 | TARGETED_DEVICE_FAMILY = "1,2";
542 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUI-Flux.app/SwiftUI-Flux";
543 | };
544 | name = Debug;
545 | };
546 | ECFE06E122B4C499001C2F76 /* Release */ = {
547 | isa = XCBuildConfiguration;
548 | buildSettings = {
549 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
550 | BUNDLE_LOADER = "$(TEST_HOST)";
551 | CODE_SIGN_STYLE = Automatic;
552 | DEVELOPMENT_TEAM = 34P5LXX69P;
553 | INFOPLIST_FILE = "SwiftUI-FluxTests/Info.plist";
554 | LD_RUNPATH_SEARCH_PATHS = (
555 | "$(inherited)",
556 | "@executable_path/Frameworks",
557 | "@loader_path/Frameworks",
558 | );
559 | PRODUCT_BUNDLE_IDENTIFIER = "com.kitasuke.SwiftUI-FluxTests";
560 | PRODUCT_NAME = "$(TARGET_NAME)";
561 | SWIFT_VERSION = 5.0;
562 | TARGETED_DEVICE_FAMILY = "1,2";
563 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUI-Flux.app/SwiftUI-Flux";
564 | };
565 | name = Release;
566 | };
567 | /* End XCBuildConfiguration section */
568 |
569 | /* Begin XCConfigurationList section */
570 | ECFE06B822B4C498001C2F76 /* Build configuration list for PBXProject "SwiftUI-Flux" */ = {
571 | isa = XCConfigurationList;
572 | buildConfigurations = (
573 | ECFE06DA22B4C499001C2F76 /* Debug */,
574 | ECFE06DB22B4C499001C2F76 /* Release */,
575 | );
576 | defaultConfigurationIsVisible = 0;
577 | defaultConfigurationName = Release;
578 | };
579 | ECFE06DC22B4C499001C2F76 /* Build configuration list for PBXNativeTarget "SwiftUI-Flux" */ = {
580 | isa = XCConfigurationList;
581 | buildConfigurations = (
582 | ECFE06DD22B4C499001C2F76 /* Debug */,
583 | ECFE06DE22B4C499001C2F76 /* Release */,
584 | );
585 | defaultConfigurationIsVisible = 0;
586 | defaultConfigurationName = Release;
587 | };
588 | ECFE06DF22B4C499001C2F76 /* Build configuration list for PBXNativeTarget "SwiftUI-FluxTests" */ = {
589 | isa = XCConfigurationList;
590 | buildConfigurations = (
591 | ECFE06E022B4C499001C2F76 /* Debug */,
592 | ECFE06E122B4C499001C2F76 /* Release */,
593 | );
594 | defaultConfigurationIsVisible = 0;
595 | defaultConfigurationName = Release;
596 | };
597 | /* End XCConfigurationList section */
598 | };
599 | rootObject = ECFE06B522B4C498001C2F76 /* Project object */;
600 | }
601 |
--------------------------------------------------------------------------------
/SwiftUI-Flux.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftUI-Flux.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | func applicationWillTerminate(_ application: UIApplication) {
22 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
23 | }
24 |
25 | // MARK: UISceneSession Lifecycle
26 |
27 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
28 | // Called when a new scene session is being created.
29 | // Use this method to select a configuration to create the new scene with.
30 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
31 | }
32 |
33 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
34 | // Called when the user discards a scene session.
35 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
36 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
37 | }
38 |
39 |
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/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 | }
--------------------------------------------------------------------------------
/SwiftUI-Flux/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SwiftUI-Flux/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 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Flux/List/RepositoryListAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryListAction.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum RepositoryListAction {
12 | case updateRepositories([Repository])
13 | case updateErrorMessage(String)
14 | case showError
15 | case showIcon
16 | }
17 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Flux/List/RepositoryListActionCreator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryListActionCreator.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 | final class RepositoryListActionCreator {
13 | private let dispatcher: RepositoryListDispatcher
14 | private let apiService: APIServiceType
15 | private let trackerService: TrackerType
16 | private let experimentService: ExperimentServiceType
17 | private let onAppearSubject = PassthroughSubject()
18 | private let responseSubject = PassthroughSubject()
19 | private let errorSubject = PassthroughSubject()
20 | private let trackingSubject = PassthroughSubject()
21 | private var cancellables: [AnyCancellable] = []
22 |
23 | init(dispatcher: RepositoryListDispatcher = .shared,
24 | apiService: APIServiceType = APIService(),
25 | trackerService: TrackerType = TrackerService(),
26 | experimentService: ExperimentServiceType = ExperimentService()) {
27 | self.dispatcher = dispatcher
28 | self.apiService = apiService
29 | self.trackerService = trackerService
30 | self.experimentService = experimentService
31 |
32 | bindData()
33 | bindActions()
34 | }
35 |
36 | func bindData() {
37 | let request = SearchRepositoryRequest()
38 | let responsePublisher = onAppearSubject
39 | .flatMap { [apiService] _ in
40 | apiService.response(from: request)
41 | .catch { [weak self] error -> Empty in
42 | self?.errorSubject.send(error)
43 | return .init()
44 | }
45 | }
46 |
47 | let responseStream = responsePublisher
48 | .share()
49 | .subscribe(responseSubject)
50 |
51 | let trackingDataStream = trackingSubject
52 | .sink(receiveValue: trackerService.log)
53 |
54 | let trackingStream = onAppearSubject
55 | .map { .listView }
56 | .subscribe(trackingSubject)
57 |
58 | cancellables += [
59 | responseStream,
60 | trackingDataStream,
61 | trackingStream,
62 | ]
63 | }
64 |
65 | func bindActions() {
66 | let responseDataStream = responseSubject
67 | .map { $0.items }
68 | .sink(receiveValue: { [dispatcher] in dispatcher.dispatch(.updateRepositories($0)) })
69 |
70 | let errorDataStream = errorSubject
71 | .map { error -> String in
72 | switch error {
73 | case .responseError: return "network error"
74 | case .parseError: return "parse error"
75 | }
76 | }
77 | .sink(receiveValue: { [dispatcher] in dispatcher.dispatch(.updateErrorMessage($0)) })
78 |
79 | let errorStream = errorSubject
80 | .map { _ in }
81 | .sink(receiveValue: { [dispatcher] in dispatcher.dispatch(.showError) })
82 |
83 | let experimentStream = onAppearSubject
84 | .filter { [experimentService] _ in
85 | experimentService.experiment(for: .showIcon)
86 | }
87 | .sink(receiveValue: { [dispatcher] in dispatcher.dispatch(.showIcon) })
88 |
89 | cancellables += [
90 | responseDataStream,
91 | errorDataStream,
92 | errorStream,
93 | experimentStream,
94 | ]
95 | }
96 |
97 | func onAppear() {
98 | onAppearSubject.send(())
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Flux/List/RepositoryListDispatcher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryListDispatcher.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 | final class RepositoryListDispatcher {
13 | static let shared = RepositoryListDispatcher()
14 |
15 | private let actionSubject = PassthroughSubject()
16 | private var cancellables: [AnyCancellable] = []
17 |
18 | func register(callback: @escaping (RepositoryListAction) -> ()) {
19 | let actionStream = actionSubject.sink(receiveValue: callback)
20 | cancellables += [actionStream]
21 | }
22 |
23 | func dispatch(_ action: RepositoryListAction) {
24 | actionSubject.send(action)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Flux/List/RepositoryListStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryListStore.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 | import SwiftUI
12 |
13 | final class RepositoryListStore: ObservableObject {
14 | static let shared = RepositoryListStore()
15 |
16 | @Published private(set) var repositories: [Repository] = []
17 | @Published var isErrorShown = false
18 | @Published var errorMessage = ""
19 | @Published private(set) var shouldShowIcon = false
20 |
21 | init(dispatcher: RepositoryListDispatcher = .shared) {
22 | dispatcher.register { [weak self] (action) in
23 | guard let strongSelf = self else { return }
24 |
25 | switch action {
26 | case .updateRepositories(let repositories): strongSelf.repositories = repositories
27 | case .updateErrorMessage(let message): strongSelf.errorMessage = message
28 | case .showError: strongSelf.isErrorShown = true
29 | case .showIcon: break
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UILaunchStoryboardName
33 | LaunchScreen
34 | UISceneConfigurationName
35 | Default Configuration
36 | UISceneDelegateClassName
37 | $(PRODUCT_MODULE_NAME).SceneDelegate
38 |
39 |
40 |
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | UISupportedInterfaceOrientations~ipad
55 |
56 | UIInterfaceOrientationPortrait
57 | UIInterfaceOrientationPortraitUpsideDown
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationLandscapeRight
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Models/AnySubscription.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnySubscription.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/9/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 | final class AnySubscription: Subscription {
13 |
14 | private let cancellable: AnyCancellable
15 |
16 | init(_ cancel: @escaping () -> Void) {
17 | self.cancellable = AnyCancellable(cancel)
18 | }
19 |
20 | func request(_ demand: Subscribers.Demand) {}
21 |
22 | func cancel() {
23 | cancellable.cancel()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Models/Repository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Repository.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/5/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct Repository: Decodable, Hashable, Identifiable {
13 | var id: Int64
14 | var fullName: String
15 | var description: String?
16 | var stargazersCount: Int = 0
17 | var language: String?
18 | var owner: User
19 | }
20 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Models/SearchRepositoryResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchRepositoryResponse.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/5/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct SearchRepositoryResponse: Decodable {
12 | var items: [Repository]
13 | }
14 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/5/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct User: Decodable, Hashable, Identifiable {
13 | var id: Int64
14 | var login: String
15 | var avatarUrl: URL
16 | }
17 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SwiftUI-Flux/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20 |
21 | // Use a UIHostingController as window root view controller
22 | if let windowScene = scene as? UIWindowScene {
23 | let window = UIWindow(windowScene: windowScene)
24 | window.rootViewController = UIHostingController(rootView: RepositoryListView())
25 | self.window = window
26 | window.makeKeyAndVisible()
27 | }
28 | }
29 |
30 | func sceneDidDisconnect(_ scene: UIScene) {
31 | // Called as the scene is being released by the system.
32 | // This occurs shortly after the scene enters the background, or when its session is discarded.
33 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
34 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
35 | }
36 |
37 | func sceneDidBecomeActive(_ scene: UIScene) {
38 | // Called when the scene has moved from an inactive state to an active state.
39 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
40 | }
41 |
42 | func sceneWillResignActive(_ scene: UIScene) {
43 | // Called when the scene will move from an active state to an inactive state.
44 | // This may occur due to temporary interruptions (ex. an incoming phone call).
45 | }
46 |
47 | func sceneWillEnterForeground(_ scene: UIScene) {
48 | // Called as the scene transitions from the background to the foreground.
49 | // Use this method to undo the changes made on entering the background.
50 | }
51 |
52 | func sceneDidEnterBackground(_ scene: UIScene) {
53 | // Called as the scene transitions from the foreground to the background.
54 | // Use this method to save data, release shared resources, and store enough scene-specific state information
55 | // to restore the scene back to its current state.
56 | }
57 |
58 |
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Services/APIService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIService.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/6/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 | protocol APIRequestType {
13 | associatedtype Response: Decodable
14 |
15 | var path: String { get }
16 | var queryItems: [URLQueryItem]? { get }
17 | }
18 |
19 | protocol APIServiceType {
20 | func response(from request: Request) -> AnyPublisher where Request: APIRequestType
21 | }
22 |
23 | final class APIService: APIServiceType {
24 |
25 | private let baseURL: URL
26 | init(baseURL: URL = URL(string: "https://api.github.com")!) {
27 | self.baseURL = baseURL
28 | }
29 |
30 | func response(from request: Request) -> AnyPublisher where Request: APIRequestType {
31 |
32 | let pathURL = URL(string: request.path, relativeTo: baseURL)!
33 |
34 | var urlComponents = URLComponents(url: pathURL, resolvingAgainstBaseURL: true)!
35 | urlComponents.queryItems = request.queryItems
36 | var request = URLRequest(url: urlComponents.url!)
37 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
38 |
39 | let decorder = JSONDecoder()
40 | decorder.keyDecodingStrategy = .convertFromSnakeCase
41 | return URLSession.shared.dataTaskPublisher(for: request)
42 | .map { data, urlResponse in data }
43 | .mapError { _ in APIServiceError.responseError }
44 | .decode(type: Request.Response.self, decoder: decorder)
45 | .mapError(APIServiceError.parseError)
46 | .receive(on: RunLoop.main)
47 | .eraseToAnyPublisher()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Services/APIServiceError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIServiceError.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/6/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum APIServiceError: Error {
12 | case responseError
13 | case parseError(Error)
14 | }
15 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Services/ExperimentService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExperimentService.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/8/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum ExperimentKey: String {
12 | case showIcon
13 | }
14 |
15 | protocol ExperimentServiceType {
16 | func experiment(for key: ExperimentKey) -> Bool
17 | }
18 |
19 | final class ExperimentService: ExperimentServiceType {
20 | func experiment(for key: ExperimentKey) -> Bool {
21 | // call api to get variant for the key
22 | return true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Services/SearchRepositoryRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchRepositoryRequest.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/9/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct SearchRepositoryRequest: APIRequestType {
12 | typealias Response = SearchRepositoryResponse
13 |
14 | var path: String { return "/search/repositories" }
15 | var queryItems: [URLQueryItem]? {
16 | return [
17 | .init(name: "q", value: "SwiftUI"),
18 | .init(name: "order", value: "desc")
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Services/TrackerService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrackerService.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/8/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum TrackEventType {
12 | case listView
13 | }
14 |
15 | protocol TrackerType {
16 | func log(type: TrackEventType)
17 | }
18 |
19 | final class TrackerService: TrackerType {
20 |
21 | func log(type: TrackEventType) {
22 | // do something
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Views/RepositoryDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryDetailView.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/5/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct RepositoryDetailView: View {
13 | var text: String
14 |
15 | var body: some View {
16 | Text(text)
17 | }
18 | }
19 |
20 | #if DEBUG
21 | struct RepositoryDetailView_Previews : PreviewProvider {
22 | static var previews: some View {
23 | RepositoryDetailView(text: "foo")
24 | }
25 | }
26 | #endif
27 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Views/RepositoryListRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryListRow.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/5/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct RepositoryListRow: View {
13 |
14 | @State var repository: Repository
15 |
16 | var body: some View {
17 | NavigationLink(destination: RepositoryDetailView(text: repository.fullName)) {
18 | Text(repository.fullName)
19 | }
20 | }
21 | }
22 |
23 | #if DEBUG
24 | struct RepositoryListRow_Previews : PreviewProvider {
25 | static var previews: some View {
26 | RepositoryListRow(repository:
27 | Repository(
28 | id: 1,
29 | fullName: "foo",
30 | owner: User(id: 1, login: "bar", avatarUrl: URL(string: "baz")!)
31 | )
32 | )
33 | }
34 | }
35 | #endif
36 |
--------------------------------------------------------------------------------
/SwiftUI-Flux/Views/RepositoryListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwiftUI-Flux
4 | //
5 | // Created by Yusuke Kita on 6/5/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct RepositoryListView : View {
12 | @ObservedObject var store: RepositoryListStore = .shared
13 | private var actionCreator: RepositoryListActionCreator
14 |
15 | init(actionCreator: RepositoryListActionCreator = .init()) {
16 | self.actionCreator = actionCreator
17 | }
18 |
19 | var body: some View {
20 | NavigationView {
21 | List(store.repositories) { repository in
22 | RepositoryListRow(repository: repository)
23 | }
24 | .alert(isPresented: $store.isErrorShown) { () -> Alert in
25 | Alert(title: Text("Error"), message: Text(store.errorMessage))
26 | }
27 | .navigationBarTitle(Text("Repositories"))
28 | }
29 | .onAppear(perform: { self.actionCreator.onAppear() })
30 | }
31 | }
32 |
33 | #if DEBUG
34 | struct RepositoryListView_Previews : PreviewProvider {
35 | static var previews: some View {
36 | RepositoryListView()
37 | }
38 | }
39 | #endif
40 |
--------------------------------------------------------------------------------
/SwiftUI-FluxTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SwiftUI-FluxTests/MockAPISearvice.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockAPISearvice.swift
3 | // SwiftUI-FluxTests
4 | //
5 | // Created by Yusuke Kita on 6/7/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 | @testable import SwiftUI_Flux
12 |
13 | final class MockAPIService: APIServiceType {
14 | var stubs: [Any] = []
15 |
16 | func stub(for type: Request.Type, response: @escaping ((Request) -> AnyPublisher)) where Request: APIRequestType {
17 | stubs.append(response)
18 | }
19 |
20 | func response(from request: Request) -> AnyPublisher where Request: APIRequestType {
21 |
22 | let response = stubs.compactMap { stub -> AnyPublisher? in
23 | let stub = stub as? ((Request) -> AnyPublisher)
24 | return stub?(request)
25 | }.last
26 |
27 | return response ?? Empty()
28 | .eraseToAnyPublisher()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SwiftUI-FluxTests/MockExperimentService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockExperimentService.swift
3 | // SwiftUI-FluxTests
4 | //
5 | // Created by Yusuke Kita on 6/9/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import SwiftUI_Flux
11 |
12 | final class MockExperimentService: ExperimentServiceType {
13 |
14 | var stubs: [ExperimentKey: Bool] = [:]
15 | func experiment(for key: ExperimentKey, value: Bool) {
16 | stubs[key] = value
17 | }
18 |
19 | func experiment(for key: ExperimentKey) -> Bool {
20 | return stubs[key] ?? false
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/SwiftUI-FluxTests/MockTrackerService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockTrackerService.swift
3 | // SwiftUI-FluxTests
4 | //
5 | // Created by Yusuke Kita on 6/8/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import SwiftUI_Flux
11 |
12 | final class MockTrackerService: TrackerType {
13 |
14 | private(set) var loggedTypes: [TrackEventType] = []
15 |
16 | func log(type: TrackEventType) {
17 | loggedTypes.append(type)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SwiftUI-FluxTests/RepositoryListActionCreatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryListActionCreatorTests.swift
3 | // SwiftUI-FluxTests
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 | import XCTest
12 | @testable import SwiftUI_Flux
13 |
14 | final class RepositoryListActionCreatorTests: XCTestCase {
15 | private let dispatcher: RepositoryListDispatcher = .shared
16 |
17 | func test_updateRepositoriesWhenOnAppear() {
18 | let apiService = MockAPIService()
19 | apiService.stub(for: SearchRepositoryRequest.self) { _ in
20 | Result.Publisher(
21 | SearchRepositoryResponse(
22 | items: [.init(id: 1, fullName: "foo", owner: .init(id: 2, login: "bar", avatarUrl: URL(string: "baz")!))]
23 | )
24 | )
25 | .eraseToAnyPublisher()
26 | }
27 | let actionCreator = makeActionCreator(apiService: apiService)
28 | var repositories: [Repository] = []
29 | dispatcher.register { (action) in
30 | switch action {
31 | case .updateRepositories(let value): repositories.append(contentsOf: value)
32 | default: break
33 | }
34 | }
35 |
36 | actionCreator.onAppear()
37 | XCTAssertTrue(!repositories.isEmpty)
38 | }
39 |
40 | func test_serviceErrorWhenOnAppear() {
41 | let apiService = MockAPIService()
42 | apiService.stub(for: SearchRepositoryRequest.self) { _ in
43 | Result.Publisher(
44 | APIServiceError.responseError
45 | ).eraseToAnyPublisher()
46 | }
47 | let actionCreator = makeActionCreator(apiService: apiService)
48 | let expectation = self.expectation(description: "error")
49 | var errorShown = false
50 | dispatcher.register { (action) in
51 | switch action {
52 | case .showError:
53 | errorShown = true
54 | XCTAssertTrue(errorShown)
55 | expectation.fulfill()
56 | default: break
57 | }
58 | }
59 |
60 | actionCreator.onAppear()
61 | wait(for: [expectation], timeout: 3.0)
62 | }
63 |
64 | func test_logListViewWhenOnAppear() {
65 | let trackerService = MockTrackerService()
66 | let actionCreator = makeActionCreator(trackerService: trackerService)
67 |
68 | actionCreator.onAppear()
69 | XCTAssertTrue(trackerService.loggedTypes.contains(.listView))
70 | }
71 |
72 | func test_showIconEnabledWhenOnAppear() {
73 | let experimentService = MockExperimentService()
74 | experimentService.stubs[.showIcon] = true
75 | let actionCreator = makeActionCreator(experimentService: experimentService)
76 | var iconShown = false
77 | dispatcher.register { (action) in
78 | switch action {
79 | case .showIcon: iconShown = true
80 | default: break
81 | }
82 | }
83 |
84 | actionCreator.onAppear()
85 | XCTAssertTrue(iconShown)
86 | }
87 |
88 | func test_showIconDisabledWhenOnAppear() {
89 | let experimentService = MockExperimentService()
90 | experimentService.stubs[.showIcon] = false
91 | let actionCreator = makeActionCreator(experimentService: experimentService)
92 | var iconShown = false
93 | dispatcher.register { (action) in
94 | switch action {
95 | case .showError: iconShown = true
96 | default: break
97 | }
98 | }
99 |
100 | actionCreator.onAppear()
101 | XCTAssertFalse(iconShown)
102 | }
103 |
104 | private func makeActionCreator(
105 | apiService: APIServiceType = MockAPIService(),
106 | trackerService: TrackerType = MockTrackerService(),
107 | experimentService: ExperimentServiceType = MockExperimentService()
108 | ) -> RepositoryListActionCreator {
109 | return .init(
110 | dispatcher: dispatcher,
111 | apiService: apiService,
112 | trackerService: trackerService,
113 | experimentService: experimentService
114 | )
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/SwiftUI-FluxTests/SwiftUI_FluxTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUI_FluxTests.swift
3 | // SwiftUI-FluxTests
4 | //
5 | // Created by Yusuke Kita on 6/15/19.
6 | // Copyright © 2019 Yusuke Kita. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import SwiftUI_Flux
11 |
12 | class SwiftUI_FluxTests: XCTestCase {
13 |
14 | override func setUp() {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDown() {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------