├── .gitignore
├── LICENSE
├── README.md
├── SwiftUI
├── .swiftpm
│ └── xcode
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── SearchRepositoriesFeature.xcscheme
├── App
│ ├── .swiftpm
│ │ └── xcode
│ │ │ └── package.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ ├── GithubApp.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ ├── GithubApp
│ │ ├── App.swift
│ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── GithubApp.entitlements
│ │ └── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ └── Package.swift
├── Package.swift
├── README.md
├── Sources
│ ├── ApiClient
│ │ ├── ApiClient.swift
│ │ └── BaseRequest.swift
│ ├── GithubClient
│ │ └── GithubClient.swift
│ ├── GithubClientLive
│ │ ├── GithubClient+Live.swift
│ │ ├── GithubRequest.swift
│ │ └── SearchReposRequest.swift
│ ├── RepositoryDetailFeature
│ │ ├── RepositoryDetailReducer.swift
│ │ └── RepositoryDetailView.swift
│ ├── SearchRepositoriesFeature
│ │ ├── RepositoryItemReducer.swift
│ │ ├── RepositoryItemView.swift
│ │ ├── SearchRepositoriesReducer.swift
│ │ └── SearchRepositoriesView.swift
│ └── SharedModel
│ │ ├── ApiError.swift
│ │ ├── Repository.swift
│ │ ├── SearchReposResponse+Mock.swift
│ │ └── SearchReposResponse.swift
└── Tests
│ └── SearchRepositoriesFeatureTests
│ └── SearchRepositoriesFeatureTests.swift
└── starter
├── App
├── GithubApp.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── GithubApp
│ ├── App.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── GithubApp.entitlements
│ └── Preview Content
│ │ └── Preview Assets.xcassets
│ │ └── Contents.json
└── Package.swift
├── Package.swift
├── README.md
└── Sources
└── AppFeature
└── ContentView.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Takehiro Kaneko
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 | # Github App built in the Composable Architecture
2 | A Github repositories search app built in [the Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture).
3 |
4 | 
5 |
6 | This practical sample application has the following features:
7 |
8 | - Search Github repositories
9 | - Paginate search results
10 | - Navigate to the repository details
11 | - Like repositories
12 | - Filter repositories with likes
13 |
14 | # Code
15 | - SwiftUI: [SwiftUI](SwiftUI)
16 | - UIKit: Coming soon...
17 |
18 | # Related Articles
19 | - [TCAでGithubリポジトリ検索アプリを作ってみよう①](https://qiita.com/takehilo/items/814319d4666fef402a41)
20 | - [TCAでGithubリポジトリ検索アプリを作ってみよう②](https://qiita.com/takehilo/items/c56fbfc92b462bc61b30)
21 |
--------------------------------------------------------------------------------
/SwiftUI/.swiftpm/xcode/xcshareddata/xcschemes/SearchRepositoriesFeature.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/SwiftUI/App/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 620B2AFE2B0E633F00090FAF /* GithubClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 620B2AFD2B0E633F00090FAF /* GithubClientLive */; };
11 | 62753F712AA8E671008CBC3D /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62753F702AA8E671008CBC3D /* App.swift */; };
12 | 62753F752AA8E672008CBC3D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 62753F742AA8E672008CBC3D /* Assets.xcassets */; };
13 | 62753F792AA8E672008CBC3D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 62753F782AA8E672008CBC3D /* Preview Assets.xcassets */; };
14 | 62B24E2D2AEAD18A00D95E3D /* SearchRepositoriesFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 62B24E2C2AEAD18A00D95E3D /* SearchRepositoriesFeature */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 620ED3A12AA8E73100F80622 /* GithubApp */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = GithubApp; path = ..; sourceTree = ""; };
19 | 62753F6D2AA8E671008CBC3D /* GithubApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | 62753F702AA8E671008CBC3D /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
21 | 62753F742AA8E672008CBC3D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
22 | 62753F762AA8E672008CBC3D /* GithubApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GithubApp.entitlements; sourceTree = ""; };
23 | 62753F782AA8E672008CBC3D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | /* End PBXFileReference section */
25 |
26 | /* Begin PBXFrameworksBuildPhase section */
27 | 62753F6A2AA8E671008CBC3D /* Frameworks */ = {
28 | isa = PBXFrameworksBuildPhase;
29 | buildActionMask = 2147483647;
30 | files = (
31 | 620B2AFE2B0E633F00090FAF /* GithubClientLive in Frameworks */,
32 | 62B24E2D2AEAD18A00D95E3D /* SearchRepositoriesFeature in Frameworks */,
33 | );
34 | runOnlyForDeploymentPostprocessing = 0;
35 | };
36 | /* End PBXFrameworksBuildPhase section */
37 |
38 | /* Begin PBXGroup section */
39 | 62753F642AA8E671008CBC3D = {
40 | isa = PBXGroup;
41 | children = (
42 | 62753F6F2AA8E671008CBC3D /* GithubApp */,
43 | 620ED3A12AA8E73100F80622 /* GithubApp */,
44 | 62753F6E2AA8E671008CBC3D /* Products */,
45 | 62D895CB2AA8E8E500A7A2D6 /* Frameworks */,
46 | );
47 | sourceTree = "";
48 | };
49 | 62753F6E2AA8E671008CBC3D /* Products */ = {
50 | isa = PBXGroup;
51 | children = (
52 | 62753F6D2AA8E671008CBC3D /* GithubApp.app */,
53 | );
54 | name = Products;
55 | sourceTree = "";
56 | };
57 | 62753F6F2AA8E671008CBC3D /* GithubApp */ = {
58 | isa = PBXGroup;
59 | children = (
60 | 62753F702AA8E671008CBC3D /* App.swift */,
61 | 62753F742AA8E672008CBC3D /* Assets.xcassets */,
62 | 62753F762AA8E672008CBC3D /* GithubApp.entitlements */,
63 | 62753F772AA8E672008CBC3D /* Preview Content */,
64 | );
65 | path = GithubApp;
66 | sourceTree = "";
67 | };
68 | 62753F772AA8E672008CBC3D /* Preview Content */ = {
69 | isa = PBXGroup;
70 | children = (
71 | 62753F782AA8E672008CBC3D /* Preview Assets.xcassets */,
72 | );
73 | path = "Preview Content";
74 | sourceTree = "";
75 | };
76 | 62D895CB2AA8E8E500A7A2D6 /* Frameworks */ = {
77 | isa = PBXGroup;
78 | children = (
79 | );
80 | name = Frameworks;
81 | sourceTree = "";
82 | };
83 | /* End PBXGroup section */
84 |
85 | /* Begin PBXNativeTarget section */
86 | 62753F6C2AA8E671008CBC3D /* GithubApp */ = {
87 | isa = PBXNativeTarget;
88 | buildConfigurationList = 62753F7C2AA8E672008CBC3D /* Build configuration list for PBXNativeTarget "GithubApp" */;
89 | buildPhases = (
90 | 62753F692AA8E671008CBC3D /* Sources */,
91 | 62753F6A2AA8E671008CBC3D /* Frameworks */,
92 | 62753F6B2AA8E671008CBC3D /* Resources */,
93 | );
94 | buildRules = (
95 | );
96 | dependencies = (
97 | );
98 | name = GithubApp;
99 | packageProductDependencies = (
100 | 62B24E2C2AEAD18A00D95E3D /* SearchRepositoriesFeature */,
101 | 620B2AFD2B0E633F00090FAF /* GithubClientLive */,
102 | );
103 | productName = GithubApp;
104 | productReference = 62753F6D2AA8E671008CBC3D /* GithubApp.app */;
105 | productType = "com.apple.product-type.application";
106 | };
107 | /* End PBXNativeTarget section */
108 |
109 | /* Begin PBXProject section */
110 | 62753F652AA8E671008CBC3D /* Project object */ = {
111 | isa = PBXProject;
112 | attributes = {
113 | BuildIndependentTargetsInParallel = 1;
114 | LastSwiftUpdateCheck = 1430;
115 | LastUpgradeCheck = 1430;
116 | TargetAttributes = {
117 | 62753F6C2AA8E671008CBC3D = {
118 | CreatedOnToolsVersion = 14.3.1;
119 | };
120 | };
121 | };
122 | buildConfigurationList = 62753F682AA8E671008CBC3D /* Build configuration list for PBXProject "GithubApp" */;
123 | compatibilityVersion = "Xcode 14.0";
124 | developmentRegion = en;
125 | hasScannedForEncodings = 0;
126 | knownRegions = (
127 | en,
128 | Base,
129 | );
130 | mainGroup = 62753F642AA8E671008CBC3D;
131 | packageReferences = (
132 | );
133 | productRefGroup = 62753F6E2AA8E671008CBC3D /* Products */;
134 | projectDirPath = "";
135 | projectRoot = "";
136 | targets = (
137 | 62753F6C2AA8E671008CBC3D /* GithubApp */,
138 | );
139 | };
140 | /* End PBXProject section */
141 |
142 | /* Begin PBXResourcesBuildPhase section */
143 | 62753F6B2AA8E671008CBC3D /* Resources */ = {
144 | isa = PBXResourcesBuildPhase;
145 | buildActionMask = 2147483647;
146 | files = (
147 | 62753F792AA8E672008CBC3D /* Preview Assets.xcassets in Resources */,
148 | 62753F752AA8E672008CBC3D /* Assets.xcassets in Resources */,
149 | );
150 | runOnlyForDeploymentPostprocessing = 0;
151 | };
152 | /* End PBXResourcesBuildPhase section */
153 |
154 | /* Begin PBXSourcesBuildPhase section */
155 | 62753F692AA8E671008CBC3D /* Sources */ = {
156 | isa = PBXSourcesBuildPhase;
157 | buildActionMask = 2147483647;
158 | files = (
159 | 62753F712AA8E671008CBC3D /* App.swift in Sources */,
160 | );
161 | runOnlyForDeploymentPostprocessing = 0;
162 | };
163 | /* End PBXSourcesBuildPhase section */
164 |
165 | /* Begin XCBuildConfiguration section */
166 | 62753F7A2AA8E672008CBC3D /* Debug */ = {
167 | isa = XCBuildConfiguration;
168 | buildSettings = {
169 | ALWAYS_SEARCH_USER_PATHS = NO;
170 | CLANG_ANALYZER_NONNULL = YES;
171 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
172 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
173 | CLANG_ENABLE_MODULES = YES;
174 | CLANG_ENABLE_OBJC_ARC = YES;
175 | CLANG_ENABLE_OBJC_WEAK = YES;
176 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
177 | CLANG_WARN_BOOL_CONVERSION = YES;
178 | CLANG_WARN_COMMA = YES;
179 | CLANG_WARN_CONSTANT_CONVERSION = YES;
180 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
181 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
182 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
183 | CLANG_WARN_EMPTY_BODY = YES;
184 | CLANG_WARN_ENUM_CONVERSION = YES;
185 | CLANG_WARN_INFINITE_RECURSION = YES;
186 | CLANG_WARN_INT_CONVERSION = YES;
187 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
188 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
189 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
190 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
191 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
192 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
193 | CLANG_WARN_STRICT_PROTOTYPES = YES;
194 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
195 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
196 | CLANG_WARN_UNREACHABLE_CODE = YES;
197 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
198 | COPY_PHASE_STRIP = NO;
199 | DEBUG_INFORMATION_FORMAT = dwarf;
200 | ENABLE_STRICT_OBJC_MSGSEND = YES;
201 | ENABLE_TESTABILITY = YES;
202 | GCC_C_LANGUAGE_STANDARD = gnu11;
203 | GCC_DYNAMIC_NO_PIC = NO;
204 | GCC_NO_COMMON_BLOCKS = YES;
205 | GCC_OPTIMIZATION_LEVEL = 0;
206 | GCC_PREPROCESSOR_DEFINITIONS = (
207 | "DEBUG=1",
208 | "$(inherited)",
209 | );
210 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
211 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
212 | GCC_WARN_UNDECLARED_SELECTOR = YES;
213 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
214 | GCC_WARN_UNUSED_FUNCTION = YES;
215 | GCC_WARN_UNUSED_VARIABLE = YES;
216 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
217 | MTL_FAST_MATH = YES;
218 | ONLY_ACTIVE_ARCH = YES;
219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
221 | };
222 | name = Debug;
223 | };
224 | 62753F7B2AA8E672008CBC3D /* Release */ = {
225 | isa = XCBuildConfiguration;
226 | buildSettings = {
227 | ALWAYS_SEARCH_USER_PATHS = NO;
228 | CLANG_ANALYZER_NONNULL = YES;
229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
231 | CLANG_ENABLE_MODULES = YES;
232 | CLANG_ENABLE_OBJC_ARC = YES;
233 | CLANG_ENABLE_OBJC_WEAK = YES;
234 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
235 | CLANG_WARN_BOOL_CONVERSION = YES;
236 | CLANG_WARN_COMMA = YES;
237 | CLANG_WARN_CONSTANT_CONVERSION = YES;
238 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
239 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
240 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
241 | CLANG_WARN_EMPTY_BODY = YES;
242 | CLANG_WARN_ENUM_CONVERSION = YES;
243 | CLANG_WARN_INFINITE_RECURSION = YES;
244 | CLANG_WARN_INT_CONVERSION = YES;
245 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
246 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
247 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
248 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
249 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
251 | CLANG_WARN_STRICT_PROTOTYPES = YES;
252 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
254 | CLANG_WARN_UNREACHABLE_CODE = YES;
255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
256 | COPY_PHASE_STRIP = NO;
257 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
258 | ENABLE_NS_ASSERTIONS = NO;
259 | ENABLE_STRICT_OBJC_MSGSEND = YES;
260 | GCC_C_LANGUAGE_STANDARD = gnu11;
261 | GCC_NO_COMMON_BLOCKS = YES;
262 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
263 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
264 | GCC_WARN_UNDECLARED_SELECTOR = YES;
265 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
266 | GCC_WARN_UNUSED_FUNCTION = YES;
267 | GCC_WARN_UNUSED_VARIABLE = YES;
268 | MTL_ENABLE_DEBUG_INFO = NO;
269 | MTL_FAST_MATH = YES;
270 | SWIFT_COMPILATION_MODE = wholemodule;
271 | SWIFT_OPTIMIZATION_LEVEL = "-O";
272 | };
273 | name = Release;
274 | };
275 | 62753F7D2AA8E672008CBC3D /* Debug */ = {
276 | isa = XCBuildConfiguration;
277 | buildSettings = {
278 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
279 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
280 | CODE_SIGN_ENTITLEMENTS = GithubApp/GithubApp.entitlements;
281 | CODE_SIGN_STYLE = Automatic;
282 | CURRENT_PROJECT_VERSION = 1;
283 | DEVELOPMENT_ASSET_PATHS = "\"GithubApp/Preview Content\"";
284 | DEVELOPMENT_TEAM = QEN3LWLX44;
285 | ENABLE_HARDENED_RUNTIME = YES;
286 | ENABLE_PREVIEWS = YES;
287 | GENERATE_INFOPLIST_FILE = YES;
288 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
289 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
290 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
291 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
292 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
293 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
294 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
295 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
296 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
297 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
298 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
299 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
300 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
301 | MACOSX_DEPLOYMENT_TARGET = 13.3;
302 | MARKETING_VERSION = 1.0;
303 | PRODUCT_BUNDLE_IDENTIFIER = com.takehilo.GithubApp;
304 | PRODUCT_NAME = "$(TARGET_NAME)";
305 | SDKROOT = auto;
306 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
307 | SWIFT_EMIT_LOC_STRINGS = YES;
308 | SWIFT_VERSION = 5.0;
309 | TARGETED_DEVICE_FAMILY = "1,2";
310 | };
311 | name = Debug;
312 | };
313 | 62753F7E2AA8E672008CBC3D /* Release */ = {
314 | isa = XCBuildConfiguration;
315 | buildSettings = {
316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
317 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
318 | CODE_SIGN_ENTITLEMENTS = GithubApp/GithubApp.entitlements;
319 | CODE_SIGN_STYLE = Automatic;
320 | CURRENT_PROJECT_VERSION = 1;
321 | DEVELOPMENT_ASSET_PATHS = "\"GithubApp/Preview Content\"";
322 | DEVELOPMENT_TEAM = QEN3LWLX44;
323 | ENABLE_HARDENED_RUNTIME = YES;
324 | ENABLE_PREVIEWS = YES;
325 | GENERATE_INFOPLIST_FILE = YES;
326 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
327 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
328 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
329 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
330 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
331 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
332 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
333 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
334 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
335 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
336 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
337 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
338 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
339 | MACOSX_DEPLOYMENT_TARGET = 13.3;
340 | MARKETING_VERSION = 1.0;
341 | PRODUCT_BUNDLE_IDENTIFIER = com.takehilo.GithubApp;
342 | PRODUCT_NAME = "$(TARGET_NAME)";
343 | SDKROOT = auto;
344 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
345 | SWIFT_EMIT_LOC_STRINGS = YES;
346 | SWIFT_VERSION = 5.0;
347 | TARGETED_DEVICE_FAMILY = "1,2";
348 | };
349 | name = Release;
350 | };
351 | /* End XCBuildConfiguration section */
352 |
353 | /* Begin XCConfigurationList section */
354 | 62753F682AA8E671008CBC3D /* Build configuration list for PBXProject "GithubApp" */ = {
355 | isa = XCConfigurationList;
356 | buildConfigurations = (
357 | 62753F7A2AA8E672008CBC3D /* Debug */,
358 | 62753F7B2AA8E672008CBC3D /* Release */,
359 | );
360 | defaultConfigurationIsVisible = 0;
361 | defaultConfigurationName = Release;
362 | };
363 | 62753F7C2AA8E672008CBC3D /* Build configuration list for PBXNativeTarget "GithubApp" */ = {
364 | isa = XCConfigurationList;
365 | buildConfigurations = (
366 | 62753F7D2AA8E672008CBC3D /* Debug */,
367 | 62753F7E2AA8E672008CBC3D /* Release */,
368 | );
369 | defaultConfigurationIsVisible = 0;
370 | defaultConfigurationName = Release;
371 | };
372 | /* End XCConfigurationList section */
373 |
374 | /* Begin XCSwiftPackageProductDependency section */
375 | 620B2AFD2B0E633F00090FAF /* GithubClientLive */ = {
376 | isa = XCSwiftPackageProductDependency;
377 | productName = GithubClientLive;
378 | };
379 | 62B24E2C2AEAD18A00D95E3D /* SearchRepositoriesFeature */ = {
380 | isa = XCSwiftPackageProductDependency;
381 | productName = SearchRepositoriesFeature;
382 | };
383 | /* End XCSwiftPackageProductDependency section */
384 | };
385 | rootObject = 62753F652AA8E671008CBC3D /* Project object */;
386 | }
387 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "apikit",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/ishkawa/APIKit",
7 | "state" : {
8 | "revision" : "b839e53b870104798035b279d2a6168b0a2227b1",
9 | "version" : "5.4.0"
10 | }
11 | },
12 | {
13 | "identity" : "combine-schedulers",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/pointfreeco/combine-schedulers",
16 | "state" : {
17 | "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb",
18 | "version" : "1.0.0"
19 | }
20 | },
21 | {
22 | "identity" : "swift-case-paths",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/pointfreeco/swift-case-paths",
25 | "state" : {
26 | "revision" : "ed7facdd4a361514b46e3bbc6238cd41c84be4ec",
27 | "version" : "1.1.1"
28 | }
29 | },
30 | {
31 | "identity" : "swift-clocks",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/pointfreeco/swift-clocks",
34 | "state" : {
35 | "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb",
36 | "version" : "1.0.0"
37 | }
38 | },
39 | {
40 | "identity" : "swift-collections",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/apple/swift-collections",
43 | "state" : {
44 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
45 | "version" : "1.0.4"
46 | }
47 | },
48 | {
49 | "identity" : "swift-composable-architecture",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/pointfreeco/swift-composable-architecture",
52 | "state" : {
53 | "revision" : "3f80eb9ed07c16a9f42ac4f23d4dafaa0494568c",
54 | "version" : "1.5.0"
55 | }
56 | },
57 | {
58 | "identity" : "swift-concurrency-extras",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras",
61 | "state" : {
62 | "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
63 | "version" : "1.1.0"
64 | }
65 | },
66 | {
67 | "identity" : "swift-custom-dump",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/pointfreeco/swift-custom-dump",
70 | "state" : {
71 | "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01",
72 | "version" : "1.0.0"
73 | }
74 | },
75 | {
76 | "identity" : "swift-dependencies",
77 | "kind" : "remoteSourceControl",
78 | "location" : "https://github.com/pointfreeco/swift-dependencies",
79 | "state" : {
80 | "revision" : "9783b58167f7618cb86011156e741cbc6f4cc864",
81 | "version" : "1.1.2"
82 | }
83 | },
84 | {
85 | "identity" : "swift-identified-collections",
86 | "kind" : "remoteSourceControl",
87 | "location" : "https://github.com/pointfreeco/swift-identified-collections",
88 | "state" : {
89 | "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8",
90 | "version" : "1.0.0"
91 | }
92 | },
93 | {
94 | "identity" : "swift-syntax",
95 | "kind" : "remoteSourceControl",
96 | "location" : "https://github.com/apple/swift-syntax",
97 | "state" : {
98 | "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
99 | "version" : "509.0.2"
100 | }
101 | },
102 | {
103 | "identity" : "swiftui-navigation",
104 | "kind" : "remoteSourceControl",
105 | "location" : "https://github.com/pointfreeco/swiftui-navigation",
106 | "state" : {
107 | "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc",
108 | "version" : "1.2.0"
109 | }
110 | },
111 | {
112 | "identity" : "xctest-dynamic-overlay",
113 | "kind" : "remoteSourceControl",
114 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
115 | "state" : {
116 | "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631",
117 | "version" : "1.0.2"
118 | }
119 | }
120 | ],
121 | "version" : 2
122 | }
123 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp/App.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SearchRepositoriesFeature
3 |
4 | @main
5 | struct GithubApp: App {
6 | var body: some Scene {
7 | WindowGroup {
8 | SearchRepositoriesView(store: .init(initialState: .init()) {
9 | SearchRepositoriesReducer()
10 | })
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp/GithubApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/SwiftUI/App/GithubApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUI/App/Package.swift:
--------------------------------------------------------------------------------
1 | import PackageDescription
2 |
3 | let package = Package(
4 | name: "",
5 | products: [],
6 | dependencies: [],
7 | targets: []
8 | )
9 |
--------------------------------------------------------------------------------
/SwiftUI/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "GithubApp",
8 | platforms: [.iOS(.v16)],
9 | products: [
10 | .library(name: "SharedModel", targets: ["SharedModel"]),
11 | .library(name: "SearchRepositoriesFeature", targets: ["SearchRepositoriesFeature"]),
12 | .library(name: "RepositoryDetailFeature", targets: ["RepositoryDetailFeature"]),
13 | .library(name: "ApiClient", targets: ["ApiClient"]),
14 | .library(name: "GithubClient", targets: ["GithubClient"]),
15 | .library(name: "GithubClientLive", targets: ["GithubClientLive"])
16 | ],
17 | dependencies: [
18 | .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.5.0"),
19 | .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.1.2"),
20 | .package(url: "https://github.com/ishkawa/APIKit", from: "5.4.0")
21 | ],
22 | targets: [
23 | .target(
24 | name: "SharedModel",
25 | dependencies: [
26 | ],
27 | swiftSettings: [
28 | .unsafeFlags([
29 | "-strict-concurrency=complete"
30 | ])
31 | ]
32 | ),
33 | .target(
34 | name: "SearchRepositoriesFeature",
35 | dependencies: [
36 | "SharedModel",
37 | "GithubClient",
38 | "RepositoryDetailFeature",
39 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture")
40 | ],
41 | swiftSettings: [
42 | .unsafeFlags([
43 | "-strict-concurrency=complete"
44 | ])
45 | ]
46 | ),
47 | .testTarget(
48 | name: "SearchRepositoriesFeatureTests",
49 | dependencies: [
50 | "SearchRepositoriesFeature",
51 | "RepositoryDetailFeature",
52 | ]
53 | ),
54 | .target(
55 | name: "RepositoryDetailFeature",
56 | dependencies: [
57 | "SharedModel",
58 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture")
59 | ],
60 | swiftSettings: [
61 | .unsafeFlags([
62 | "-strict-concurrency=complete"
63 | ])
64 | ]
65 | ),
66 | .target(
67 | name: "ApiClient",
68 | dependencies: [
69 | "SharedModel",
70 | .product(name: "APIKit", package: "APIKit")
71 | ],
72 | swiftSettings: [
73 | .unsafeFlags([
74 | "-strict-concurrency=complete"
75 | ])
76 | ]
77 | ),
78 | .target(
79 | name: "GithubClient",
80 | dependencies: [
81 | "SharedModel",
82 | .product(name: "Dependencies", package: "swift-dependencies"),
83 | .product(name: "DependenciesMacros", package: "swift-dependencies")
84 | ],
85 | swiftSettings: [
86 | .unsafeFlags([
87 | "-strict-concurrency=complete"
88 | ])
89 | ]
90 | ),
91 | .target(
92 | name: "GithubClientLive",
93 | dependencies: [
94 | "SharedModel",
95 | "ApiClient",
96 | "GithubClient",
97 | .product(name: "Dependencies", package: "swift-dependencies"),
98 | .product(name: "APIKit", package: "APIKit")
99 | ],
100 | swiftSettings: [
101 | .unsafeFlags([
102 | "-strict-concurrency=complete"
103 | ])
104 | ]
105 | )
106 | ]
107 | )
108 |
--------------------------------------------------------------------------------
/SwiftUI/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI example
2 | Just open `App/GithubApp.xocdeproj` and you can build the app.
3 |
4 | ```
5 | open App/GithubApp.xcodeproj
6 | ```
7 |
8 | [GitHub REST API uses rate limiting to control API traffic](https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limiting). To increase the rate limit, edit [GithubRequest.swift](Sources/GithubClientLive/GithubRequest.swift):
9 |
10 | ```swift
11 | params["Authorization"] = "Bearer " // replace with your personal access token
12 | ```
13 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/ApiClient/ApiClient.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SharedModel
3 | @preconcurrency import APIKit
4 |
5 | public final class ApiClient: Sendable {
6 | private let session: Session
7 |
8 | public init(session: Session) {
9 | self.session = session
10 | }
11 |
12 | public func send(request: T) async throws -> T.Response {
13 | do {
14 | return try await session.response(for: request)
15 | } catch let originalError as SessionTaskError {
16 | switch originalError {
17 | case let .connectionError(error), let .responseError(error), let .requestError(error):
18 | throw ApiError.unknown(error as NSError)
19 | case let .responseError(error as ApiError):
20 | throw error
21 | }
22 | }
23 | }
24 |
25 | public static let liveValue = ApiClient(session: Session.shared)
26 | public static let testValue = ApiClient(session: Session(adapter: NoopSessionAdapter()))
27 | }
28 |
29 | public final class NoopSessionTask: SessionTask {
30 | public func resume() {}
31 | public func cancel() {}
32 | }
33 |
34 | public struct NoopSessionAdapter: SessionAdapter {
35 | public func createTask(
36 | with URLRequest: URLRequest,
37 | handler: @escaping (Data?, URLResponse?, Error?) -> Void
38 | ) -> SessionTask {
39 | return NoopSessionTask()
40 | }
41 |
42 | public func getTasks(with handler: @escaping ([SessionTask]) -> Void) {
43 | handler([])
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/ApiClient/BaseRequest.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import APIKit
3 | import SharedModel
4 |
5 | public protocol BaseRequest: Request where Response: Decodable {
6 | var decoder: JSONDecoder { get }
7 | }
8 |
9 | public extension BaseRequest {
10 | func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any {
11 | guard 200..<300 ~= urlResponse.statusCode else {
12 | throw ApiError.unacceptableStatusCode(urlResponse.statusCode)
13 | }
14 | return object
15 | }
16 |
17 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
18 | let data = try JSONSerialization.data(withJSONObject: object, options: [])
19 | return try decoder.decode(Response.self, from: data)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/GithubClient/GithubClient.swift:
--------------------------------------------------------------------------------
1 | import Dependencies
2 | import SharedModel
3 | import DependenciesMacros
4 |
5 | @DependencyClient
6 | public struct GithubClient: Sendable {
7 | public var searchRepos: @Sendable (_ query: String, _ page: Int) async throws -> SearchReposResponse
8 | }
9 |
10 | extension GithubClient: TestDependencyKey {
11 | public static let testValue = Self()
12 | public static let previewValue = Self()
13 | }
14 |
15 | public extension DependencyValues {
16 | var githubClient: GithubClient {
17 | get { self[GithubClient.self] }
18 | set { self[GithubClient.self] = newValue }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/GithubClientLive/GithubClient+Live.swift:
--------------------------------------------------------------------------------
1 | import GithubClient
2 | import Dependencies
3 | import SharedModel
4 | import ApiClient
5 |
6 | extension GithubClient: DependencyKey {
7 | public static let liveValue: GithubClient = .live()
8 |
9 | static func live(apiClient: ApiClient = .liveValue) -> Self {
10 | .init(
11 | searchRepos: { query, page in
12 | try await apiClient.send(request: SearchReposRequest(query: query, page: page))
13 | }
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/GithubClientLive/GithubRequest.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import APIKit
3 | import ApiClient
4 |
5 | protocol GithubRequest: BaseRequest {
6 | }
7 |
8 | extension GithubRequest {
9 | var baseURL: URL { URL(string: "https://api.github.com")! }
10 | var headerFields: [String: String] { baseHeaders }
11 | var decoder: JSONDecoder { JSONDecoder() }
12 |
13 | var baseHeaders: [String: String] {
14 | var params: [String: String] = [:]
15 | params["Accept"] = "application/vnd.github+json"
16 | params["Authorization"] = "Bearer "
17 | return params
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/GithubClientLive/SearchReposRequest.swift:
--------------------------------------------------------------------------------
1 | import APIKit
2 | import SharedModel
3 |
4 | struct SearchReposRequest: GithubRequest {
5 | typealias Response = SearchReposResponse
6 | let method = APIKit.HTTPMethod.get
7 | let path = "/search/repositories"
8 | let queryParameters: [String: Any]?
9 |
10 | public init(
11 | query: String,
12 | page: Int
13 | ) {
14 | self.queryParameters = [
15 | "q": query,
16 | "page": page.description,
17 | "per_page": 10
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/RepositoryDetailFeature/RepositoryDetailReducer.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import Dependencies
3 | import Foundation
4 | import SharedModel
5 |
6 | @Reducer
7 | public struct RepositoryDetailReducer: Reducer, Sendable {
8 | // MARK: - State
9 | public struct State: Equatable, Sendable {
10 | public var id: Int { repository.id }
11 | public let repository: Repository
12 | @BindingState public var liked = false
13 |
14 | public init(
15 | repository: Repository,
16 | liked: Bool
17 | ) {
18 | self.repository = repository
19 | self.liked = liked
20 | }
21 | }
22 |
23 | public init() {}
24 |
25 | // MARK: - Action
26 | public enum Action: BindableAction, Sendable {
27 | case binding(BindingAction)
28 | }
29 |
30 | // MARK: - Dependencies
31 |
32 | // MARK: - Reducer
33 | public var body: some ReducerOf {
34 | BindingReducer()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/RepositoryDetailFeature/RepositoryDetailView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import ComposableArchitecture
3 | import SharedModel
4 |
5 | public struct RepositoryDetailView: View {
6 | let store: StoreOf
7 |
8 | public init(store: StoreOf) {
9 | self.store = store
10 | }
11 |
12 | public var body: some View {
13 | WithViewStore(store, observe: { $0 }) { viewStore in
14 | Form {
15 | HStack {
16 | VStack(alignment: .leading, spacing: 8) {
17 | AsyncImage(url: viewStore.repository.avatarUrl) { image in image.image?.resizable() }
18 | .frame(width: 40, height: 40)
19 |
20 | Text(viewStore.repository.name)
21 | .font(.system(size: 24, weight: .bold))
22 |
23 | if let description = viewStore.repository.description {
24 | Text(description)
25 | }
26 |
27 | Label {
28 | Text("\(viewStore.repository.stars)")
29 | .font(.system(size: 14, weight: .bold))
30 | } icon: {
31 | Image(systemName: "star.fill")
32 | .foregroundStyle(Color.yellow)
33 | }
34 | }
35 |
36 | Spacer(minLength: 16)
37 |
38 | Button {
39 | viewStore.$liked.wrappedValue.toggle()
40 | } label: {
41 | Image(systemName: viewStore.liked ? "heart.fill" : "heart")
42 | .resizable()
43 | .frame(width: 24, height: 24)
44 | .foregroundStyle(Color.pink)
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
52 | #Preview {
53 | RepositoryDetailView(
54 | store: .init(initialState: RepositoryDetailReducer.State(
55 | repository: .init(from: .mock(id: 100, name: "takehilo")),
56 | liked: true
57 | )) {
58 | RepositoryDetailReducer()
59 | }
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SearchRepositoriesFeature/RepositoryItemReducer.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import Dependencies
3 | import SharedModel
4 | import Foundation
5 |
6 | @Reducer
7 | public struct RepositoryItemReducer: Reducer, Sendable {
8 | // MARK: - State
9 | public struct State: Equatable, Identifiable, Sendable {
10 | public var id: Int { repository.id }
11 | let repository: Repository
12 | @BindingState var liked = false
13 |
14 | static func make(from item: SearchReposResponse.Item) -> Self {
15 | .init(repository: .init(from: item))
16 | }
17 | }
18 |
19 | // MARK: - Action
20 | public enum Action: BindableAction, Sendable {
21 | case binding(BindingAction)
22 | }
23 |
24 | // MARK: - Dependencies
25 |
26 | // MARK: - Reducer
27 | public var body: some ReducerOf {
28 | BindingReducer()
29 | }
30 | }
31 |
32 | extension IdentifiedArrayOf
33 | where Element == RepositoryItemReducer.State, ID == Int {
34 | init(response: SearchReposResponse) {
35 | self = IdentifiedArrayOf(uniqueElements: response.items.map { .make(from: $0) })
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SearchRepositoriesFeature/RepositoryItemView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import ComposableArchitecture
3 | import SharedModel
4 |
5 | struct RepositoryItemView: View {
6 | let store: StoreOf
7 |
8 | var body: some View {
9 | WithViewStore(store, observe: { $0 }) { viewStore in
10 | HStack {
11 | VStack(alignment: .leading, spacing: 8) {
12 | Text(viewStore.repository.name)
13 | .font(.system(size: 20, weight: .bold))
14 | .lineLimit(1)
15 |
16 | Label {
17 | Text("\(viewStore.repository.stars)")
18 | .font(.system(size: 14))
19 | } icon: {
20 | Image(systemName: "star.fill")
21 | .resizable()
22 | .frame(width: 20, height: 20)
23 | .foregroundStyle(Color.yellow)
24 | }
25 | }
26 |
27 | Spacer(minLength: 16)
28 |
29 | Button {
30 | viewStore.$liked.wrappedValue.toggle()
31 | } label: {
32 | Image(systemName: viewStore.liked ? "heart.fill" : "heart")
33 | .resizable()
34 | .frame(width: 20, height: 20)
35 | .foregroundStyle(Color.pink)
36 | }
37 | .buttonStyle(.plain)
38 | }
39 | }
40 | }
41 | }
42 |
43 | #Preview {
44 | Form {
45 | RepositoryItemView(
46 | store: .init(initialState: RepositoryItemReducer.State.make(from: .mock(id: 0, name: "Alice"))) {
47 | RepositoryItemReducer()
48 | }
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SearchRepositoriesFeature/SearchRepositoriesReducer.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import Dependencies
3 | import GithubClient
4 | import SharedModel
5 | import Foundation
6 | import RepositoryDetailFeature
7 |
8 | @Reducer
9 | public struct SearchRepositoriesReducer: Reducer, Sendable {
10 | // MARK: - State
11 | public struct State: Equatable, Sendable {
12 | var items = IdentifiedArrayOf()
13 | @BindingState var query = ""
14 | @BindingState var showFavoritesOnly = false
15 | var currentPage = 1
16 | var loadingState: LoadingState = .refreshing
17 | var hasMorePage = false
18 | var path = StackState()
19 |
20 | var filteredItems: IdentifiedArrayOf {
21 | items.filter {
22 | !showFavoritesOnly || $0.liked
23 | }
24 | }
25 |
26 | public init() {}
27 | }
28 |
29 | enum LoadingState: Equatable {
30 | case refreshing
31 | case loadingNext
32 | case none
33 | }
34 |
35 | private enum CancelId { case searchRepos }
36 |
37 | // MARK: - Action
38 | public enum Action: BindableAction, Sendable {
39 | case binding(BindingAction)
40 | case items(IdentifiedActionOf)
41 | case itemAppeared(id: Int)
42 | case searchReposResponse(Result)
43 | case path(StackAction)
44 | }
45 |
46 | // MARK: - Dependencies
47 | @Dependency(\.githubClient) var githubClient
48 | @Dependency(\.mainQueue) var mainQueue
49 |
50 | public init() {}
51 |
52 | // MARK: - Reducer
53 | public var body: some ReducerOf {
54 | BindingReducer()
55 | Reduce { state, action in
56 | switch action {
57 |
58 | case .binding(\.$query):
59 | guard !state.query.isEmpty else {
60 | state.hasMorePage = false
61 | state.items.removeAll()
62 | return .cancel(id: CancelId.searchRepos)
63 | }
64 |
65 | state.currentPage = 1
66 | state.loadingState = .refreshing
67 |
68 | return .run { [query = state.query, page = state.currentPage] send in
69 | await send(.searchReposResponse(Result {
70 | try await githubClient.searchRepos(query: query, page: page)
71 | }))
72 | }
73 | .debounce(id: CancelId.searchRepos, for: 0.3, scheduler: mainQueue)
74 |
75 | case .binding:
76 | return .none
77 |
78 | case let .searchReposResponse(.success(response)):
79 | switch state.loadingState {
80 | case .refreshing:
81 | state.items = .init(response: response)
82 | case .loadingNext:
83 | let newItems = IdentifiedArrayOf(response: response)
84 | state.items.append(contentsOf: newItems)
85 | case .none:
86 | break
87 | }
88 |
89 | state.hasMorePage = response.totalCount > state.items.count
90 | state.loadingState = .none
91 | return .none
92 |
93 | case .searchReposResponse(.failure):
94 | return .none
95 |
96 | case let .itemAppeared(id: id):
97 | if state.hasMorePage, state.items.index(id: id) == state.items.count - 1 {
98 | state.currentPage += 1
99 | state.loadingState = .loadingNext
100 |
101 | return .run { [query = state.query, page = state.currentPage] send in
102 | await send(.searchReposResponse(Result {
103 | try await githubClient.searchRepos(query: query, page: page)
104 | }))
105 | }
106 | } else {
107 | return .none
108 | }
109 |
110 | case .items:
111 | return .none
112 |
113 | case let .path(.element(id: id, action: .binding(\.$liked))):
114 | guard let repositoryDetail = state.path[id: id] else { return .none }
115 | state.items[id: repositoryDetail.id]?.liked = repositoryDetail.liked
116 | return .none
117 |
118 | case .path:
119 | return .none
120 |
121 | }
122 | }
123 | .forEach(\.items, action: \.items) {
124 | RepositoryItemReducer()
125 | }
126 | .forEach(\.path, action: \.path) {
127 | RepositoryDetailReducer()
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SearchRepositoriesFeature/SearchRepositoriesView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import ComposableArchitecture
3 | import RepositoryDetailFeature
4 |
5 | public struct SearchRepositoriesView: View {
6 | let store: StoreOf
7 |
8 | struct ViewState: Equatable {
9 | @BindingViewState var query: String
10 | @BindingViewState var showFavoritesOnly: Bool
11 | let hasMorePage: Bool
12 |
13 | init(store: BindingViewStore) {
14 | self._query = store.$query
15 | self._showFavoritesOnly = store.$showFavoritesOnly
16 | self.hasMorePage = store.hasMorePage
17 | }
18 | }
19 |
20 | public init(store: StoreOf) {
21 | self.store = store
22 | }
23 |
24 | public var body: some View {
25 | NavigationStackStore(store.scope(state: \.path, action: \.path)) {
26 | WithViewStore(store, observe: ViewState.init(store:)) { viewStore in
27 | List {
28 | Toggle(isOn: viewStore.$showFavoritesOnly) {
29 | Text("Favorites Only")
30 | }
31 |
32 | ForEachStore(store.scope(
33 | state: \.filteredItems,
34 | action: \.items
35 | )) { itemStore in
36 | WithViewStore(itemStore, observe: { $0 }) { itemViewStore in
37 | NavigationLink(
38 | state: RepositoryDetailReducer.State(
39 | repository: itemViewStore.repository,
40 | liked: itemViewStore.liked
41 | )
42 | ) {
43 | RepositoryItemView(store: itemStore)
44 | .onAppear {
45 | viewStore.send(.itemAppeared(id: itemStore.withState(\.id)))
46 | }
47 | }
48 | }
49 | }
50 |
51 | if viewStore.hasMorePage {
52 | ProgressView()
53 | .frame(maxWidth: .infinity)
54 | }
55 | }
56 | .searchable(text: viewStore.$query)
57 | }
58 | } destination: {
59 | RepositoryDetailView(store: $0)
60 | }
61 | }
62 | }
63 |
64 | #Preview {
65 | SearchRepositoriesView(
66 | store: .init(initialState: SearchRepositoriesReducer.State()) {
67 | SearchRepositoriesReducer()
68 | .dependency(
69 | \.githubClient,
70 | .init(searchRepos: { _, _ in .mock() })
71 | )
72 | }
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SharedModel/ApiError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum ApiError: Error, Equatable {
4 | case unacceptableStatusCode(Int)
5 | case unknown(NSError)
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SharedModel/Repository.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Repository: Equatable, Sendable {
4 | public let id: Int
5 | public let name: String
6 | public let avatarUrl: URL
7 | public let description: String?
8 | public let stars: Int
9 | }
10 |
11 | public extension Repository {
12 | init (from item: SearchReposResponse.Item) {
13 | self.id = item.id
14 | self.name = item.fullName
15 | self.avatarUrl = item.owner.avatarUrl
16 | self.description = item.description
17 | self.stars = item.stargazersCount
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SharedModel/SearchReposResponse+Mock.swift:
--------------------------------------------------------------------------------
1 | #if DEBUG
2 | import Foundation
3 |
4 | public extension SearchReposResponse {
5 | static func mock(totalCount: Int = 5) -> Self {
6 | .init(
7 | totalCount: totalCount,
8 | items: [
9 | .mock(id: 0, name: "Alice"),
10 | .mock(id: 1, name: "Bob"),
11 | .mock(id: 2, name: "Carol"),
12 | .mock(id: 3, name: "Dave"),
13 | .mock(id: 4, name: "Ellen"),
14 | ]
15 | )
16 | }
17 |
18 | static func mock2(totalCount: Int = 5) -> Self {
19 | .init(
20 | totalCount: totalCount,
21 | items: [
22 | .mock(id: 5, name: "Frank"),
23 | .mock(id: 6, name: "George"),
24 | .mock(id: 7, name: "Harry"),
25 | .mock(id: 8, name: "Ivan"),
26 | .mock(id: 9, name: "Justin")
27 | ]
28 | )
29 | }
30 |
31 | static func mockAll() -> Self {
32 | .init(
33 | totalCount: 10,
34 | items: [
35 | .mock(id: 0, name: "Alice"),
36 | .mock(id: 1, name: "Bob"),
37 | .mock(id: 2, name: "Carol"),
38 | .mock(id: 3, name: "Dave"),
39 | .mock(id: 4, name: "Ellen"),
40 | .mock(id: 5, name: "Frank"),
41 | .mock(id: 6, name: "George"),
42 | .mock(id: 7, name: "Harry"),
43 | .mock(id: 8, name: "Ivan"),
44 | .mock(id: 9, name: "Justin")
45 | ]
46 | )
47 | }
48 | }
49 |
50 | public extension SearchReposResponse.Item {
51 | static func mock(id: Int, name: String) -> Self {
52 | .init(
53 | id: id,
54 | name: name,
55 | fullName: "\(name)/awesome-repository",
56 | owner: .init(
57 | login: name,
58 | avatarUrl: URL(string: "https://github.com/\(name).png")!
59 | ),
60 | description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt",
61 | stargazersCount: id * 100
62 | )
63 | }
64 | }
65 | #endif
66 |
--------------------------------------------------------------------------------
/SwiftUI/Sources/SharedModel/SearchReposResponse.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct SearchReposResponse: Sendable, Decodable, Equatable {
4 | public let totalCount: Int
5 | public let items: [Item]
6 |
7 | public init(
8 | totalCount: Int,
9 | items: [Item]
10 | ) {
11 | self.totalCount = totalCount
12 | self.items = items
13 | }
14 |
15 | enum CodingKeys: String, CodingKey {
16 | case totalCount = "total_count"
17 | case items
18 | }
19 |
20 | public struct Item: Sendable, Decodable, Equatable {
21 | public let id: Int
22 | public let name: String
23 | public let fullName: String
24 | public let owner: Owner
25 | public let description: String?
26 | public let stargazersCount: Int
27 |
28 | public init(
29 | id: Int,
30 | name: String,
31 | fullName: String,
32 | owner: Owner,
33 | description: String?,
34 | stargazersCount: Int
35 | ) {
36 | self.id = id
37 | self.name = name
38 | self.fullName = fullName
39 | self.owner = owner
40 | self.description = description
41 | self.stargazersCount = stargazersCount
42 | }
43 |
44 | enum CodingKeys: String, CodingKey {
45 | case id
46 | case name
47 | case fullName = "full_name"
48 | case owner
49 | case description
50 | case stargazersCount = "stargazers_count"
51 | }
52 | }
53 |
54 | public struct Owner: Sendable, Decodable, Equatable {
55 | public let login: String
56 | public let avatarUrl: URL
57 |
58 | public init(
59 | login: String,
60 | avatarUrl: URL
61 | ) {
62 | self.login = login
63 | self.avatarUrl = avatarUrl
64 | }
65 |
66 | enum CodingKeys: String, CodingKey {
67 | case login
68 | case avatarUrl = "avatar_url"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/SwiftUI/Tests/SearchRepositoriesFeatureTests/SearchRepositoriesFeatureTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import ComposableArchitecture
3 | import SharedModel
4 | import RepositoryDetailFeature
5 |
6 | @testable import SearchRepositoriesFeature
7 |
8 | @MainActor
9 | class SearchRepositoriesFeatureTests: XCTestCase {
10 | func testSearchRepositories() async {
11 | let mainQueue = DispatchQueue.test
12 | let store = TestStore(initialState: SearchRepositoriesReducer.State()) {
13 | SearchRepositoriesReducer()
14 | } withDependencies: {
15 | $0.githubClient.searchRepos = { @Sendable _, _ in .mock(totalCount: 10) }
16 | $0.mainQueue = mainQueue.eraseToAnyScheduler()
17 | }
18 |
19 | await store.send(.set(\.$query, "t")) {
20 | $0.query = "t"
21 | $0.currentPage = 1
22 | $0.loadingState = .refreshing
23 | }
24 |
25 | await mainQueue.advance(by: .seconds(0.1))
26 |
27 | await store.send(.set(\.$query, "tca")) {
28 | $0.query = "tca"
29 | $0.currentPage = 1
30 | $0.loadingState = .refreshing
31 | }
32 |
33 | await mainQueue.advance(by: .seconds(0.3))
34 |
35 | await store.receive(\.searchReposResponse.success) {
36 | $0.items = .init(response: .mock(totalCount: 10))
37 | $0.hasMorePage = true
38 | $0.loadingState = .none
39 | }
40 |
41 | await store.send(.set(\.$query, "")) {
42 | $0.query = ""
43 | $0.hasMorePage = false
44 | $0.items = []
45 | }
46 | }
47 |
48 | func testPagination() async {
49 | var initialState = SearchRepositoriesReducer.State()
50 | initialState.items = .init(response: .mock())
51 | initialState.hasMorePage = true
52 |
53 | let store = TestStore(initialState: initialState) {
54 | SearchRepositoriesReducer()
55 | } withDependencies: {
56 | $0.githubClient.searchRepos = { @Sendable _, _ in .mock2(totalCount: 10) }
57 | }
58 |
59 | await store.send(.itemAppeared(id: 4)) {
60 | $0.currentPage = 2
61 | $0.loadingState = .loadingNext
62 | }
63 |
64 | await store.receive(\.searchReposResponse.success) {
65 | $0.items = .init(response: .mockAll())
66 | $0.hasMorePage = false
67 | $0.loadingState = .none
68 | }
69 | }
70 |
71 | func testSyncLikes() async {
72 | var initialState = SearchRepositoriesReducer.State()
73 | initialState.path = StackState([
74 | RepositoryDetailReducer.State(repository: .init(from: .mock(id: 1, name: "Alice")), liked: false)
75 | ])
76 |
77 | let store = TestStore(initialState: initialState) {
78 | SearchRepositoriesReducer()
79 | }
80 | store.exhaustivity = .off
81 |
82 | await store.send(.path(.element(id: 0, action: .set(\.$liked, true)))) {
83 | $0.items[id: 0]?.liked = true
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/starter/App/GithubApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 62753F712AA8E671008CBC3D /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62753F702AA8E671008CBC3D /* App.swift */; };
11 | 62753F752AA8E672008CBC3D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 62753F742AA8E672008CBC3D /* Assets.xcassets */; };
12 | 62753F792AA8E672008CBC3D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 62753F782AA8E672008CBC3D /* Preview Assets.xcassets */; };
13 | 627F6F202AA8ED08008BAA92 /* AppFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 627F6F1F2AA8ED08008BAA92 /* AppFeature */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | 620ED3A12AA8E73100F80622 /* GithubApp */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = GithubApp; path = ..; sourceTree = ""; };
18 | 62753F6D2AA8E671008CBC3D /* GithubApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
19 | 62753F702AA8E671008CBC3D /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
20 | 62753F742AA8E672008CBC3D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
21 | 62753F762AA8E672008CBC3D /* GithubApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GithubApp.entitlements; sourceTree = ""; };
22 | 62753F782AA8E672008CBC3D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
23 | /* End PBXFileReference section */
24 |
25 | /* Begin PBXFrameworksBuildPhase section */
26 | 62753F6A2AA8E671008CBC3D /* Frameworks */ = {
27 | isa = PBXFrameworksBuildPhase;
28 | buildActionMask = 2147483647;
29 | files = (
30 | 627F6F202AA8ED08008BAA92 /* AppFeature in Frameworks */,
31 | );
32 | runOnlyForDeploymentPostprocessing = 0;
33 | };
34 | /* End PBXFrameworksBuildPhase section */
35 |
36 | /* Begin PBXGroup section */
37 | 62753F642AA8E671008CBC3D = {
38 | isa = PBXGroup;
39 | children = (
40 | 62753F6F2AA8E671008CBC3D /* GithubApp */,
41 | 620ED3A12AA8E73100F80622 /* GithubApp */,
42 | 62753F6E2AA8E671008CBC3D /* Products */,
43 | 62D895CB2AA8E8E500A7A2D6 /* Frameworks */,
44 | );
45 | sourceTree = "";
46 | };
47 | 62753F6E2AA8E671008CBC3D /* Products */ = {
48 | isa = PBXGroup;
49 | children = (
50 | 62753F6D2AA8E671008CBC3D /* GithubApp.app */,
51 | );
52 | name = Products;
53 | sourceTree = "";
54 | };
55 | 62753F6F2AA8E671008CBC3D /* GithubApp */ = {
56 | isa = PBXGroup;
57 | children = (
58 | 62753F702AA8E671008CBC3D /* App.swift */,
59 | 62753F742AA8E672008CBC3D /* Assets.xcassets */,
60 | 62753F762AA8E672008CBC3D /* GithubApp.entitlements */,
61 | 62753F772AA8E672008CBC3D /* Preview Content */,
62 | );
63 | path = GithubApp;
64 | sourceTree = "";
65 | };
66 | 62753F772AA8E672008CBC3D /* Preview Content */ = {
67 | isa = PBXGroup;
68 | children = (
69 | 62753F782AA8E672008CBC3D /* Preview Assets.xcassets */,
70 | );
71 | path = "Preview Content";
72 | sourceTree = "";
73 | };
74 | 62D895CB2AA8E8E500A7A2D6 /* Frameworks */ = {
75 | isa = PBXGroup;
76 | children = (
77 | );
78 | name = Frameworks;
79 | sourceTree = "";
80 | };
81 | /* End PBXGroup section */
82 |
83 | /* Begin PBXNativeTarget section */
84 | 62753F6C2AA8E671008CBC3D /* GithubApp */ = {
85 | isa = PBXNativeTarget;
86 | buildConfigurationList = 62753F7C2AA8E672008CBC3D /* Build configuration list for PBXNativeTarget "GithubApp" */;
87 | buildPhases = (
88 | 62753F692AA8E671008CBC3D /* Sources */,
89 | 62753F6A2AA8E671008CBC3D /* Frameworks */,
90 | 62753F6B2AA8E671008CBC3D /* Resources */,
91 | );
92 | buildRules = (
93 | );
94 | dependencies = (
95 | );
96 | name = GithubApp;
97 | packageProductDependencies = (
98 | 627F6F1F2AA8ED08008BAA92 /* AppFeature */,
99 | );
100 | productName = GithubApp;
101 | productReference = 62753F6D2AA8E671008CBC3D /* GithubApp.app */;
102 | productType = "com.apple.product-type.application";
103 | };
104 | /* End PBXNativeTarget section */
105 |
106 | /* Begin PBXProject section */
107 | 62753F652AA8E671008CBC3D /* Project object */ = {
108 | isa = PBXProject;
109 | attributes = {
110 | BuildIndependentTargetsInParallel = 1;
111 | LastSwiftUpdateCheck = 1430;
112 | LastUpgradeCheck = 1430;
113 | TargetAttributes = {
114 | 62753F6C2AA8E671008CBC3D = {
115 | CreatedOnToolsVersion = 14.3.1;
116 | };
117 | };
118 | };
119 | buildConfigurationList = 62753F682AA8E671008CBC3D /* Build configuration list for PBXProject "GithubApp" */;
120 | compatibilityVersion = "Xcode 14.0";
121 | developmentRegion = en;
122 | hasScannedForEncodings = 0;
123 | knownRegions = (
124 | en,
125 | Base,
126 | );
127 | mainGroup = 62753F642AA8E671008CBC3D;
128 | productRefGroup = 62753F6E2AA8E671008CBC3D /* Products */;
129 | projectDirPath = "";
130 | projectRoot = "";
131 | targets = (
132 | 62753F6C2AA8E671008CBC3D /* GithubApp */,
133 | );
134 | };
135 | /* End PBXProject section */
136 |
137 | /* Begin PBXResourcesBuildPhase section */
138 | 62753F6B2AA8E671008CBC3D /* Resources */ = {
139 | isa = PBXResourcesBuildPhase;
140 | buildActionMask = 2147483647;
141 | files = (
142 | 62753F792AA8E672008CBC3D /* Preview Assets.xcassets in Resources */,
143 | 62753F752AA8E672008CBC3D /* Assets.xcassets in Resources */,
144 | );
145 | runOnlyForDeploymentPostprocessing = 0;
146 | };
147 | /* End PBXResourcesBuildPhase section */
148 |
149 | /* Begin PBXSourcesBuildPhase section */
150 | 62753F692AA8E671008CBC3D /* Sources */ = {
151 | isa = PBXSourcesBuildPhase;
152 | buildActionMask = 2147483647;
153 | files = (
154 | 62753F712AA8E671008CBC3D /* App.swift in Sources */,
155 | );
156 | runOnlyForDeploymentPostprocessing = 0;
157 | };
158 | /* End PBXSourcesBuildPhase section */
159 |
160 | /* Begin XCBuildConfiguration section */
161 | 62753F7A2AA8E672008CBC3D /* Debug */ = {
162 | isa = XCBuildConfiguration;
163 | buildSettings = {
164 | ALWAYS_SEARCH_USER_PATHS = NO;
165 | CLANG_ANALYZER_NONNULL = YES;
166 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
167 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
168 | CLANG_ENABLE_MODULES = YES;
169 | CLANG_ENABLE_OBJC_ARC = YES;
170 | CLANG_ENABLE_OBJC_WEAK = YES;
171 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
172 | CLANG_WARN_BOOL_CONVERSION = YES;
173 | CLANG_WARN_COMMA = YES;
174 | CLANG_WARN_CONSTANT_CONVERSION = YES;
175 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
177 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
178 | CLANG_WARN_EMPTY_BODY = YES;
179 | CLANG_WARN_ENUM_CONVERSION = YES;
180 | CLANG_WARN_INFINITE_RECURSION = YES;
181 | CLANG_WARN_INT_CONVERSION = YES;
182 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
183 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
184 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
186 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
187 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
188 | CLANG_WARN_STRICT_PROTOTYPES = YES;
189 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
190 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
191 | CLANG_WARN_UNREACHABLE_CODE = YES;
192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
193 | COPY_PHASE_STRIP = NO;
194 | DEBUG_INFORMATION_FORMAT = dwarf;
195 | ENABLE_STRICT_OBJC_MSGSEND = YES;
196 | ENABLE_TESTABILITY = YES;
197 | GCC_C_LANGUAGE_STANDARD = gnu11;
198 | GCC_DYNAMIC_NO_PIC = NO;
199 | GCC_NO_COMMON_BLOCKS = YES;
200 | GCC_OPTIMIZATION_LEVEL = 0;
201 | GCC_PREPROCESSOR_DEFINITIONS = (
202 | "DEBUG=1",
203 | "$(inherited)",
204 | );
205 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
206 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
207 | GCC_WARN_UNDECLARED_SELECTOR = YES;
208 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
209 | GCC_WARN_UNUSED_FUNCTION = YES;
210 | GCC_WARN_UNUSED_VARIABLE = YES;
211 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
212 | MTL_FAST_MATH = YES;
213 | ONLY_ACTIVE_ARCH = YES;
214 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
215 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
216 | };
217 | name = Debug;
218 | };
219 | 62753F7B2AA8E672008CBC3D /* Release */ = {
220 | isa = XCBuildConfiguration;
221 | buildSettings = {
222 | ALWAYS_SEARCH_USER_PATHS = NO;
223 | CLANG_ANALYZER_NONNULL = YES;
224 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
225 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
226 | CLANG_ENABLE_MODULES = YES;
227 | CLANG_ENABLE_OBJC_ARC = YES;
228 | CLANG_ENABLE_OBJC_WEAK = YES;
229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
230 | CLANG_WARN_BOOL_CONVERSION = YES;
231 | CLANG_WARN_COMMA = YES;
232 | CLANG_WARN_CONSTANT_CONVERSION = YES;
233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
236 | CLANG_WARN_EMPTY_BODY = YES;
237 | CLANG_WARN_ENUM_CONVERSION = YES;
238 | CLANG_WARN_INFINITE_RECURSION = YES;
239 | CLANG_WARN_INT_CONVERSION = YES;
240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
244 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
246 | CLANG_WARN_STRICT_PROTOTYPES = YES;
247 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
248 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
249 | CLANG_WARN_UNREACHABLE_CODE = YES;
250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
251 | COPY_PHASE_STRIP = NO;
252 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
253 | ENABLE_NS_ASSERTIONS = NO;
254 | ENABLE_STRICT_OBJC_MSGSEND = YES;
255 | GCC_C_LANGUAGE_STANDARD = gnu11;
256 | GCC_NO_COMMON_BLOCKS = YES;
257 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
258 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
259 | GCC_WARN_UNDECLARED_SELECTOR = YES;
260 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
261 | GCC_WARN_UNUSED_FUNCTION = YES;
262 | GCC_WARN_UNUSED_VARIABLE = YES;
263 | MTL_ENABLE_DEBUG_INFO = NO;
264 | MTL_FAST_MATH = YES;
265 | SWIFT_COMPILATION_MODE = wholemodule;
266 | SWIFT_OPTIMIZATION_LEVEL = "-O";
267 | };
268 | name = Release;
269 | };
270 | 62753F7D2AA8E672008CBC3D /* Debug */ = {
271 | isa = XCBuildConfiguration;
272 | buildSettings = {
273 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
274 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
275 | CODE_SIGN_ENTITLEMENTS = GithubApp/GithubApp.entitlements;
276 | CODE_SIGN_STYLE = Automatic;
277 | CURRENT_PROJECT_VERSION = 1;
278 | DEVELOPMENT_ASSET_PATHS = "\"GithubApp/Preview Content\"";
279 | DEVELOPMENT_TEAM = QEN3LWLX44;
280 | ENABLE_HARDENED_RUNTIME = YES;
281 | ENABLE_PREVIEWS = YES;
282 | GENERATE_INFOPLIST_FILE = YES;
283 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
284 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
285 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
286 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
287 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
288 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
289 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
290 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
291 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
292 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
293 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
294 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
295 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
296 | MACOSX_DEPLOYMENT_TARGET = 13.3;
297 | MARKETING_VERSION = 1.0;
298 | PRODUCT_BUNDLE_IDENTIFIER = com.takehilo.GithubApp;
299 | PRODUCT_NAME = "$(TARGET_NAME)";
300 | SDKROOT = auto;
301 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
302 | SWIFT_EMIT_LOC_STRINGS = YES;
303 | SWIFT_VERSION = 5.0;
304 | TARGETED_DEVICE_FAMILY = "1,2";
305 | };
306 | name = Debug;
307 | };
308 | 62753F7E2AA8E672008CBC3D /* Release */ = {
309 | isa = XCBuildConfiguration;
310 | buildSettings = {
311 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
312 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
313 | CODE_SIGN_ENTITLEMENTS = GithubApp/GithubApp.entitlements;
314 | CODE_SIGN_STYLE = Automatic;
315 | CURRENT_PROJECT_VERSION = 1;
316 | DEVELOPMENT_ASSET_PATHS = "\"GithubApp/Preview Content\"";
317 | DEVELOPMENT_TEAM = QEN3LWLX44;
318 | ENABLE_HARDENED_RUNTIME = YES;
319 | ENABLE_PREVIEWS = YES;
320 | GENERATE_INFOPLIST_FILE = YES;
321 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
322 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
323 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
324 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
325 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
326 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
327 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
328 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
329 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
330 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
331 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
332 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
333 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
334 | MACOSX_DEPLOYMENT_TARGET = 13.3;
335 | MARKETING_VERSION = 1.0;
336 | PRODUCT_BUNDLE_IDENTIFIER = com.takehilo.GithubApp;
337 | PRODUCT_NAME = "$(TARGET_NAME)";
338 | SDKROOT = auto;
339 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
340 | SWIFT_EMIT_LOC_STRINGS = YES;
341 | SWIFT_VERSION = 5.0;
342 | TARGETED_DEVICE_FAMILY = "1,2";
343 | };
344 | name = Release;
345 | };
346 | /* End XCBuildConfiguration section */
347 |
348 | /* Begin XCConfigurationList section */
349 | 62753F682AA8E671008CBC3D /* Build configuration list for PBXProject "GithubApp" */ = {
350 | isa = XCConfigurationList;
351 | buildConfigurations = (
352 | 62753F7A2AA8E672008CBC3D /* Debug */,
353 | 62753F7B2AA8E672008CBC3D /* Release */,
354 | );
355 | defaultConfigurationIsVisible = 0;
356 | defaultConfigurationName = Release;
357 | };
358 | 62753F7C2AA8E672008CBC3D /* Build configuration list for PBXNativeTarget "GithubApp" */ = {
359 | isa = XCConfigurationList;
360 | buildConfigurations = (
361 | 62753F7D2AA8E672008CBC3D /* Debug */,
362 | 62753F7E2AA8E672008CBC3D /* Release */,
363 | );
364 | defaultConfigurationIsVisible = 0;
365 | defaultConfigurationName = Release;
366 | };
367 | /* End XCConfigurationList section */
368 |
369 | /* Begin XCSwiftPackageProductDependency section */
370 | 627F6F1F2AA8ED08008BAA92 /* AppFeature */ = {
371 | isa = XCSwiftPackageProductDependency;
372 | productName = AppFeature;
373 | };
374 | /* End XCSwiftPackageProductDependency section */
375 | };
376 | rootObject = 62753F652AA8E671008CBC3D /* Project object */;
377 | }
378 |
--------------------------------------------------------------------------------
/starter/App/GithubApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/starter/App/GithubApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/starter/App/GithubApp/App.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import AppFeature
3 |
4 | @main
5 | struct GithubApp: App {
6 | var body: some Scene {
7 | WindowGroup {
8 | ContentView()
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/starter/App/GithubApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/starter/App/GithubApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/starter/App/GithubApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/starter/App/GithubApp/GithubApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/starter/App/GithubApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/starter/App/Package.swift:
--------------------------------------------------------------------------------
1 | import PackageDescription
2 |
3 | let package = Package(
4 | name: "",
5 | products: [],
6 | dependencies: [],
7 | targets: []
8 | )
9 |
--------------------------------------------------------------------------------
/starter/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "GithubApp",
8 | platforms: [.iOS(.v16)],
9 | products: [
10 | .library(
11 | name: "AppFeature",
12 | targets: ["AppFeature"]),
13 | ],
14 | targets: [
15 | .target(
16 | name: "AppFeature")
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/starter/README.md:
--------------------------------------------------------------------------------
1 | # Starter project
2 | Just open `App/GithubApp.xocdeproj` and you can build the app.
3 |
4 | ```
5 | open App/GithubApp.xcodeproj
6 | ```
7 |
8 | It only displays an initial template view.
9 |
10 |
11 |
--------------------------------------------------------------------------------
/starter/Sources/AppFeature/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct ContentView: View {
4 | public init() {}
5 |
6 | public var body: some View {
7 | VStack {
8 | Image(systemName: "globe")
9 | .imageScale(.large)
10 | .foregroundColor(.accentColor)
11 | Text("Hello, world!")
12 | }
13 | .padding()
14 | }
15 | }
16 |
17 | struct ContentView_Previews: PreviewProvider {
18 | static var previews: some View {
19 | ContentView()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------