├── .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 | ![app](https://github.com/takehilo/github-app-with-the-composable-architecture/assets/23430968/1c882930-bb88-4483-ba7a-137f56c35198) 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 | --------------------------------------------------------------------------------