├── .gitignore ├── LICENSE ├── README.md ├── SVVSSample.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── SVVSSample ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SVVSSampleApp.swift ├── User.swift ├── UserRepository.swift ├── UserStore.swift ├── UserView.swift └── UserViewState.swift ├── SVVSSampleTests └── SVVSSampleTests.swift └── SVVSSampleUITests ├── SVVSSampleUITests.swift └── SVVSSampleUITestsLaunchTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | /*.xcodeproj/xcuserdata 2 | /*.xcodeproj/project.xcworkspace/xcuserdata 3 | /*.xcworkspace/xcuserdata 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chatwork Co., Ltd. 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 | # svvs-sample 2 | [iOSDC Japan 2023登壇の際の資料](https://speakerdeck.com/ryunakayama/swiftuinishi-sitaxin-akitekutiyanodao-ru-nitiao-mu) 3 | -------------------------------------------------------------------------------- /SVVSSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D68407EB2A90D7300098B6AE /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = D68407EA2A90D7300098B6AE /* OrderedCollections */; }; 11 | D6D652C22A8B160100D0D338 /* SVVSSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652C12A8B160100D0D338 /* SVVSSampleApp.swift */; }; 12 | D6D652C42A8B160100D0D338 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652C32A8B160100D0D338 /* ContentView.swift */; }; 13 | D6D652C62A8B160400D0D338 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D652C52A8B160400D0D338 /* Assets.xcassets */; }; 14 | D6D652C92A8B160400D0D338 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D652C82A8B160400D0D338 /* Preview Assets.xcassets */; }; 15 | D6D652D32A8B160400D0D338 /* SVVSSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652D22A8B160400D0D338 /* SVVSSampleTests.swift */; }; 16 | D6D652DD2A8B160400D0D338 /* SVVSSampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652DC2A8B160400D0D338 /* SVVSSampleUITests.swift */; }; 17 | D6D652DF2A8B160400D0D338 /* SVVSSampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652DE2A8B160400D0D338 /* SVVSSampleUITestsLaunchTests.swift */; }; 18 | D6D652EC2A8B199300D0D338 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652EB2A8B199300D0D338 /* User.swift */; }; 19 | D6D652EF2A8B19E100D0D338 /* SwiftID in Frameworks */ = {isa = PBXBuildFile; productRef = D6D652EE2A8B19E100D0D338 /* SwiftID */; }; 20 | D6D652F12A8B1A5600D0D338 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652F02A8B1A5600D0D338 /* UserView.swift */; }; 21 | D6D652F32A8B20D700D0D338 /* UserViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652F22A8B20D700D0D338 /* UserViewState.swift */; }; 22 | D6D652F52A8B229800D0D338 /* UserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652F42A8B229800D0D338 /* UserStore.swift */; }; 23 | D6D652F72A8B241C00D0D338 /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D652F62A8B241C00D0D338 /* UserRepository.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | D6D652CF2A8B160400D0D338 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = D6D652B62A8B160100D0D338 /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = D6D652BD2A8B160100D0D338; 32 | remoteInfo = SVVSSample; 33 | }; 34 | D6D652D92A8B160400D0D338 /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = D6D652B62A8B160100D0D338 /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = D6D652BD2A8B160100D0D338; 39 | remoteInfo = SVVSSample; 40 | }; 41 | /* End PBXContainerItemProxy section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | D6D652BE2A8B160100D0D338 /* SVVSSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SVVSSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | D6D652C12A8B160100D0D338 /* SVVSSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVVSSampleApp.swift; sourceTree = ""; }; 46 | D6D652C32A8B160100D0D338 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 47 | D6D652C52A8B160400D0D338 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | D6D652C82A8B160400D0D338 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 49 | D6D652CE2A8B160400D0D338 /* SVVSSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SVVSSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | D6D652D22A8B160400D0D338 /* SVVSSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVVSSampleTests.swift; sourceTree = ""; }; 51 | D6D652D82A8B160400D0D338 /* SVVSSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SVVSSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | D6D652DC2A8B160400D0D338 /* SVVSSampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVVSSampleUITests.swift; sourceTree = ""; }; 53 | D6D652DE2A8B160400D0D338 /* SVVSSampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVVSSampleUITestsLaunchTests.swift; sourceTree = ""; }; 54 | D6D652EB2A8B199300D0D338 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 55 | D6D652F02A8B1A5600D0D338 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = ""; }; 56 | D6D652F22A8B20D700D0D338 /* UserViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserViewState.swift; sourceTree = ""; }; 57 | D6D652F42A8B229800D0D338 /* UserStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStore.swift; sourceTree = ""; }; 58 | D6D652F62A8B241C00D0D338 /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | D6D652BB2A8B160100D0D338 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | D68407EB2A90D7300098B6AE /* OrderedCollections in Frameworks */, 67 | D6D652EF2A8B19E100D0D338 /* SwiftID in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | D6D652CB2A8B160400D0D338 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | D6D652D52A8B160400D0D338 /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | /* End PBXFrameworksBuildPhase section */ 86 | 87 | /* Begin PBXGroup section */ 88 | D6D652B52A8B160100D0D338 = { 89 | isa = PBXGroup; 90 | children = ( 91 | D6D652C02A8B160100D0D338 /* SVVSSample */, 92 | D6D652D12A8B160400D0D338 /* SVVSSampleTests */, 93 | D6D652DB2A8B160400D0D338 /* SVVSSampleUITests */, 94 | D6D652BF2A8B160100D0D338 /* Products */, 95 | ); 96 | sourceTree = ""; 97 | }; 98 | D6D652BF2A8B160100D0D338 /* Products */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | D6D652BE2A8B160100D0D338 /* SVVSSample.app */, 102 | D6D652CE2A8B160400D0D338 /* SVVSSampleTests.xctest */, 103 | D6D652D82A8B160400D0D338 /* SVVSSampleUITests.xctest */, 104 | ); 105 | name = Products; 106 | sourceTree = ""; 107 | }; 108 | D6D652C02A8B160100D0D338 /* SVVSSample */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | D6D652C12A8B160100D0D338 /* SVVSSampleApp.swift */, 112 | D6D652C32A8B160100D0D338 /* ContentView.swift */, 113 | D6D652C52A8B160400D0D338 /* Assets.xcassets */, 114 | D6D652C72A8B160400D0D338 /* Preview Content */, 115 | D6D652EB2A8B199300D0D338 /* User.swift */, 116 | D6D652F02A8B1A5600D0D338 /* UserView.swift */, 117 | D6D652F22A8B20D700D0D338 /* UserViewState.swift */, 118 | D6D652F42A8B229800D0D338 /* UserStore.swift */, 119 | D6D652F62A8B241C00D0D338 /* UserRepository.swift */, 120 | ); 121 | path = SVVSSample; 122 | sourceTree = ""; 123 | }; 124 | D6D652C72A8B160400D0D338 /* Preview Content */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | D6D652C82A8B160400D0D338 /* Preview Assets.xcassets */, 128 | ); 129 | path = "Preview Content"; 130 | sourceTree = ""; 131 | }; 132 | D6D652D12A8B160400D0D338 /* SVVSSampleTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | D6D652D22A8B160400D0D338 /* SVVSSampleTests.swift */, 136 | ); 137 | path = SVVSSampleTests; 138 | sourceTree = ""; 139 | }; 140 | D6D652DB2A8B160400D0D338 /* SVVSSampleUITests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | D6D652DC2A8B160400D0D338 /* SVVSSampleUITests.swift */, 144 | D6D652DE2A8B160400D0D338 /* SVVSSampleUITestsLaunchTests.swift */, 145 | ); 146 | path = SVVSSampleUITests; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | D6D652BD2A8B160100D0D338 /* SVVSSample */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = D6D652E22A8B160400D0D338 /* Build configuration list for PBXNativeTarget "SVVSSample" */; 155 | buildPhases = ( 156 | D6D652BA2A8B160100D0D338 /* Sources */, 157 | D6D652BB2A8B160100D0D338 /* Frameworks */, 158 | D6D652BC2A8B160100D0D338 /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = SVVSSample; 165 | packageProductDependencies = ( 166 | D6D652EE2A8B19E100D0D338 /* SwiftID */, 167 | D68407EA2A90D7300098B6AE /* OrderedCollections */, 168 | ); 169 | productName = SVVSSample; 170 | productReference = D6D652BE2A8B160100D0D338 /* SVVSSample.app */; 171 | productType = "com.apple.product-type.application"; 172 | }; 173 | D6D652CD2A8B160400D0D338 /* SVVSSampleTests */ = { 174 | isa = PBXNativeTarget; 175 | buildConfigurationList = D6D652E52A8B160400D0D338 /* Build configuration list for PBXNativeTarget "SVVSSampleTests" */; 176 | buildPhases = ( 177 | D6D652CA2A8B160400D0D338 /* Sources */, 178 | D6D652CB2A8B160400D0D338 /* Frameworks */, 179 | D6D652CC2A8B160400D0D338 /* Resources */, 180 | ); 181 | buildRules = ( 182 | ); 183 | dependencies = ( 184 | D6D652D02A8B160400D0D338 /* PBXTargetDependency */, 185 | ); 186 | name = SVVSSampleTests; 187 | productName = SVVSSampleTests; 188 | productReference = D6D652CE2A8B160400D0D338 /* SVVSSampleTests.xctest */; 189 | productType = "com.apple.product-type.bundle.unit-test"; 190 | }; 191 | D6D652D72A8B160400D0D338 /* SVVSSampleUITests */ = { 192 | isa = PBXNativeTarget; 193 | buildConfigurationList = D6D652E82A8B160400D0D338 /* Build configuration list for PBXNativeTarget "SVVSSampleUITests" */; 194 | buildPhases = ( 195 | D6D652D42A8B160400D0D338 /* Sources */, 196 | D6D652D52A8B160400D0D338 /* Frameworks */, 197 | D6D652D62A8B160400D0D338 /* Resources */, 198 | ); 199 | buildRules = ( 200 | ); 201 | dependencies = ( 202 | D6D652DA2A8B160400D0D338 /* PBXTargetDependency */, 203 | ); 204 | name = SVVSSampleUITests; 205 | productName = SVVSSampleUITests; 206 | productReference = D6D652D82A8B160400D0D338 /* SVVSSampleUITests.xctest */; 207 | productType = "com.apple.product-type.bundle.ui-testing"; 208 | }; 209 | /* End PBXNativeTarget section */ 210 | 211 | /* Begin PBXProject section */ 212 | D6D652B62A8B160100D0D338 /* Project object */ = { 213 | isa = PBXProject; 214 | attributes = { 215 | BuildIndependentTargetsInParallel = 1; 216 | LastSwiftUpdateCheck = 1430; 217 | LastUpgradeCheck = 1430; 218 | TargetAttributes = { 219 | D6D652BD2A8B160100D0D338 = { 220 | CreatedOnToolsVersion = 14.3.1; 221 | }; 222 | D6D652CD2A8B160400D0D338 = { 223 | CreatedOnToolsVersion = 14.3.1; 224 | TestTargetID = D6D652BD2A8B160100D0D338; 225 | }; 226 | D6D652D72A8B160400D0D338 = { 227 | CreatedOnToolsVersion = 14.3.1; 228 | TestTargetID = D6D652BD2A8B160100D0D338; 229 | }; 230 | }; 231 | }; 232 | buildConfigurationList = D6D652B92A8B160100D0D338 /* Build configuration list for PBXProject "SVVSSample" */; 233 | compatibilityVersion = "Xcode 14.0"; 234 | developmentRegion = en; 235 | hasScannedForEncodings = 0; 236 | knownRegions = ( 237 | en, 238 | Base, 239 | ); 240 | mainGroup = D6D652B52A8B160100D0D338; 241 | packageReferences = ( 242 | D6D652ED2A8B19E100D0D338 /* XCRemoteSwiftPackageReference "swift-id" */, 243 | D68407E92A90D7300098B6AE /* XCRemoteSwiftPackageReference "swift-collections" */, 244 | ); 245 | productRefGroup = D6D652BF2A8B160100D0D338 /* Products */; 246 | projectDirPath = ""; 247 | projectRoot = ""; 248 | targets = ( 249 | D6D652BD2A8B160100D0D338 /* SVVSSample */, 250 | D6D652CD2A8B160400D0D338 /* SVVSSampleTests */, 251 | D6D652D72A8B160400D0D338 /* SVVSSampleUITests */, 252 | ); 253 | }; 254 | /* End PBXProject section */ 255 | 256 | /* Begin PBXResourcesBuildPhase section */ 257 | D6D652BC2A8B160100D0D338 /* Resources */ = { 258 | isa = PBXResourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | D6D652C92A8B160400D0D338 /* Preview Assets.xcassets in Resources */, 262 | D6D652C62A8B160400D0D338 /* Assets.xcassets in Resources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | D6D652CC2A8B160400D0D338 /* Resources */ = { 267 | isa = PBXResourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | D6D652D62A8B160400D0D338 /* Resources */ = { 274 | isa = PBXResourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | /* End PBXResourcesBuildPhase section */ 281 | 282 | /* Begin PBXSourcesBuildPhase section */ 283 | D6D652BA2A8B160100D0D338 /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | D6D652F52A8B229800D0D338 /* UserStore.swift in Sources */, 288 | D6D652F32A8B20D700D0D338 /* UserViewState.swift in Sources */, 289 | D6D652C42A8B160100D0D338 /* ContentView.swift in Sources */, 290 | D6D652F72A8B241C00D0D338 /* UserRepository.swift in Sources */, 291 | D6D652C22A8B160100D0D338 /* SVVSSampleApp.swift in Sources */, 292 | D6D652EC2A8B199300D0D338 /* User.swift in Sources */, 293 | D6D652F12A8B1A5600D0D338 /* UserView.swift in Sources */, 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | }; 297 | D6D652CA2A8B160400D0D338 /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | D6D652D32A8B160400D0D338 /* SVVSSampleTests.swift in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | D6D652D42A8B160400D0D338 /* Sources */ = { 306 | isa = PBXSourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | D6D652DF2A8B160400D0D338 /* SVVSSampleUITestsLaunchTests.swift in Sources */, 310 | D6D652DD2A8B160400D0D338 /* SVVSSampleUITests.swift in Sources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXSourcesBuildPhase section */ 315 | 316 | /* Begin PBXTargetDependency section */ 317 | D6D652D02A8B160400D0D338 /* PBXTargetDependency */ = { 318 | isa = PBXTargetDependency; 319 | target = D6D652BD2A8B160100D0D338 /* SVVSSample */; 320 | targetProxy = D6D652CF2A8B160400D0D338 /* PBXContainerItemProxy */; 321 | }; 322 | D6D652DA2A8B160400D0D338 /* PBXTargetDependency */ = { 323 | isa = PBXTargetDependency; 324 | target = D6D652BD2A8B160100D0D338 /* SVVSSample */; 325 | targetProxy = D6D652D92A8B160400D0D338 /* PBXContainerItemProxy */; 326 | }; 327 | /* End PBXTargetDependency section */ 328 | 329 | /* Begin XCBuildConfiguration section */ 330 | D6D652E02A8B160400D0D338 /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_ANALYZER_NONNULL = YES; 335 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 356 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 357 | CLANG_WARN_STRICT_PROTOTYPES = YES; 358 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 359 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 360 | CLANG_WARN_UNREACHABLE_CODE = YES; 361 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 362 | COPY_PHASE_STRIP = NO; 363 | DEBUG_INFORMATION_FORMAT = dwarf; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | ENABLE_TESTABILITY = YES; 366 | GCC_C_LANGUAGE_STANDARD = gnu11; 367 | GCC_DYNAMIC_NO_PIC = NO; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_OPTIMIZATION_LEVEL = 0; 370 | GCC_PREPROCESSOR_DEFINITIONS = ( 371 | "DEBUG=1", 372 | "$(inherited)", 373 | ); 374 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 375 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 376 | GCC_WARN_UNDECLARED_SELECTOR = YES; 377 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 378 | GCC_WARN_UNUSED_FUNCTION = YES; 379 | GCC_WARN_UNUSED_VARIABLE = YES; 380 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 381 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 382 | MTL_FAST_MATH = YES; 383 | ONLY_ACTIVE_ARCH = YES; 384 | SDKROOT = iphoneos; 385 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 386 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 387 | SWIFT_STRICT_CONCURRENCY = complete; 388 | }; 389 | name = Debug; 390 | }; 391 | D6D652E12A8B160400D0D338 /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_NONNULL = YES; 396 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 398 | CLANG_ENABLE_MODULES = YES; 399 | CLANG_ENABLE_OBJC_ARC = YES; 400 | CLANG_ENABLE_OBJC_WEAK = YES; 401 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 402 | CLANG_WARN_BOOL_CONVERSION = YES; 403 | CLANG_WARN_COMMA = YES; 404 | CLANG_WARN_CONSTANT_CONVERSION = YES; 405 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 406 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 407 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 408 | CLANG_WARN_EMPTY_BODY = YES; 409 | CLANG_WARN_ENUM_CONVERSION = YES; 410 | CLANG_WARN_INFINITE_RECURSION = YES; 411 | CLANG_WARN_INT_CONVERSION = YES; 412 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 413 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 414 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 415 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 416 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 417 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 418 | CLANG_WARN_STRICT_PROTOTYPES = YES; 419 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 420 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | COPY_PHASE_STRIP = NO; 424 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 425 | ENABLE_NS_ASSERTIONS = NO; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | GCC_C_LANGUAGE_STANDARD = gnu11; 428 | GCC_NO_COMMON_BLOCKS = YES; 429 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 430 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 431 | GCC_WARN_UNDECLARED_SELECTOR = YES; 432 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 433 | GCC_WARN_UNUSED_FUNCTION = YES; 434 | GCC_WARN_UNUSED_VARIABLE = YES; 435 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 436 | MTL_ENABLE_DEBUG_INFO = NO; 437 | MTL_FAST_MATH = YES; 438 | SDKROOT = iphoneos; 439 | SWIFT_COMPILATION_MODE = wholemodule; 440 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 441 | SWIFT_STRICT_CONCURRENCY = complete; 442 | VALIDATE_PRODUCT = YES; 443 | }; 444 | name = Release; 445 | }; 446 | D6D652E32A8B160400D0D338 /* Debug */ = { 447 | isa = XCBuildConfiguration; 448 | buildSettings = { 449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 450 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 451 | CODE_SIGN_STYLE = Automatic; 452 | CURRENT_PROJECT_VERSION = 1; 453 | DEVELOPMENT_ASSET_PATHS = "\"SVVSSample/Preview Content\""; 454 | ENABLE_PREVIEWS = YES; 455 | GENERATE_INFOPLIST_FILE = YES; 456 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 457 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 458 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 459 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 460 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@executable_path/Frameworks", 464 | ); 465 | MARKETING_VERSION = 1.0; 466 | PRODUCT_BUNDLE_IDENTIFIER = com.chatwork.SVVSSample; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SWIFT_EMIT_LOC_STRINGS = YES; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Debug; 473 | }; 474 | D6D652E42A8B160400D0D338 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 479 | CODE_SIGN_STYLE = Automatic; 480 | CURRENT_PROJECT_VERSION = 1; 481 | DEVELOPMENT_ASSET_PATHS = "\"SVVSSample/Preview Content\""; 482 | ENABLE_PREVIEWS = YES; 483 | GENERATE_INFOPLIST_FILE = YES; 484 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 485 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 486 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 487 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 488 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 489 | LD_RUNPATH_SEARCH_PATHS = ( 490 | "$(inherited)", 491 | "@executable_path/Frameworks", 492 | ); 493 | MARKETING_VERSION = 1.0; 494 | PRODUCT_BUNDLE_IDENTIFIER = com.chatwork.SVVSSample; 495 | PRODUCT_NAME = "$(TARGET_NAME)"; 496 | SWIFT_EMIT_LOC_STRINGS = YES; 497 | SWIFT_VERSION = 5.0; 498 | TARGETED_DEVICE_FAMILY = "1,2"; 499 | }; 500 | name = Release; 501 | }; 502 | D6D652E62A8B160400D0D338 /* Debug */ = { 503 | isa = XCBuildConfiguration; 504 | buildSettings = { 505 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 506 | BUNDLE_LOADER = "$(TEST_HOST)"; 507 | CODE_SIGN_STYLE = Automatic; 508 | CURRENT_PROJECT_VERSION = 1; 509 | GENERATE_INFOPLIST_FILE = YES; 510 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 511 | MARKETING_VERSION = 1.0; 512 | PRODUCT_BUNDLE_IDENTIFIER = com.chatwork.SVVSSampleTests; 513 | PRODUCT_NAME = "$(TARGET_NAME)"; 514 | SWIFT_EMIT_LOC_STRINGS = NO; 515 | SWIFT_VERSION = 5.0; 516 | TARGETED_DEVICE_FAMILY = "1,2"; 517 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SVVSSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SVVSSample"; 518 | }; 519 | name = Debug; 520 | }; 521 | D6D652E72A8B160400D0D338 /* Release */ = { 522 | isa = XCBuildConfiguration; 523 | buildSettings = { 524 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 525 | BUNDLE_LOADER = "$(TEST_HOST)"; 526 | CODE_SIGN_STYLE = Automatic; 527 | CURRENT_PROJECT_VERSION = 1; 528 | GENERATE_INFOPLIST_FILE = YES; 529 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 530 | MARKETING_VERSION = 1.0; 531 | PRODUCT_BUNDLE_IDENTIFIER = com.chatwork.SVVSSampleTests; 532 | PRODUCT_NAME = "$(TARGET_NAME)"; 533 | SWIFT_EMIT_LOC_STRINGS = NO; 534 | SWIFT_VERSION = 5.0; 535 | TARGETED_DEVICE_FAMILY = "1,2"; 536 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SVVSSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SVVSSample"; 537 | }; 538 | name = Release; 539 | }; 540 | D6D652E92A8B160400D0D338 /* Debug */ = { 541 | isa = XCBuildConfiguration; 542 | buildSettings = { 543 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 544 | CODE_SIGN_STYLE = Automatic; 545 | CURRENT_PROJECT_VERSION = 1; 546 | GENERATE_INFOPLIST_FILE = YES; 547 | MARKETING_VERSION = 1.0; 548 | PRODUCT_BUNDLE_IDENTIFIER = com.chatwork.SVVSSampleUITests; 549 | PRODUCT_NAME = "$(TARGET_NAME)"; 550 | SWIFT_EMIT_LOC_STRINGS = NO; 551 | SWIFT_VERSION = 5.0; 552 | TARGETED_DEVICE_FAMILY = "1,2"; 553 | TEST_TARGET_NAME = SVVSSample; 554 | }; 555 | name = Debug; 556 | }; 557 | D6D652EA2A8B160400D0D338 /* Release */ = { 558 | isa = XCBuildConfiguration; 559 | buildSettings = { 560 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 561 | CODE_SIGN_STYLE = Automatic; 562 | CURRENT_PROJECT_VERSION = 1; 563 | GENERATE_INFOPLIST_FILE = YES; 564 | MARKETING_VERSION = 1.0; 565 | PRODUCT_BUNDLE_IDENTIFIER = com.chatwork.SVVSSampleUITests; 566 | PRODUCT_NAME = "$(TARGET_NAME)"; 567 | SWIFT_EMIT_LOC_STRINGS = NO; 568 | SWIFT_VERSION = 5.0; 569 | TARGETED_DEVICE_FAMILY = "1,2"; 570 | TEST_TARGET_NAME = SVVSSample; 571 | }; 572 | name = Release; 573 | }; 574 | /* End XCBuildConfiguration section */ 575 | 576 | /* Begin XCConfigurationList section */ 577 | D6D652B92A8B160100D0D338 /* Build configuration list for PBXProject "SVVSSample" */ = { 578 | isa = XCConfigurationList; 579 | buildConfigurations = ( 580 | D6D652E02A8B160400D0D338 /* Debug */, 581 | D6D652E12A8B160400D0D338 /* Release */, 582 | ); 583 | defaultConfigurationIsVisible = 0; 584 | defaultConfigurationName = Release; 585 | }; 586 | D6D652E22A8B160400D0D338 /* Build configuration list for PBXNativeTarget "SVVSSample" */ = { 587 | isa = XCConfigurationList; 588 | buildConfigurations = ( 589 | D6D652E32A8B160400D0D338 /* Debug */, 590 | D6D652E42A8B160400D0D338 /* Release */, 591 | ); 592 | defaultConfigurationIsVisible = 0; 593 | defaultConfigurationName = Release; 594 | }; 595 | D6D652E52A8B160400D0D338 /* Build configuration list for PBXNativeTarget "SVVSSampleTests" */ = { 596 | isa = XCConfigurationList; 597 | buildConfigurations = ( 598 | D6D652E62A8B160400D0D338 /* Debug */, 599 | D6D652E72A8B160400D0D338 /* Release */, 600 | ); 601 | defaultConfigurationIsVisible = 0; 602 | defaultConfigurationName = Release; 603 | }; 604 | D6D652E82A8B160400D0D338 /* Build configuration list for PBXNativeTarget "SVVSSampleUITests" */ = { 605 | isa = XCConfigurationList; 606 | buildConfigurations = ( 607 | D6D652E92A8B160400D0D338 /* Debug */, 608 | D6D652EA2A8B160400D0D338 /* Release */, 609 | ); 610 | defaultConfigurationIsVisible = 0; 611 | defaultConfigurationName = Release; 612 | }; 613 | /* End XCConfigurationList section */ 614 | 615 | /* Begin XCRemoteSwiftPackageReference section */ 616 | D68407E92A90D7300098B6AE /* XCRemoteSwiftPackageReference "swift-collections" */ = { 617 | isa = XCRemoteSwiftPackageReference; 618 | repositoryURL = "https://github.com/apple/swift-collections.git"; 619 | requirement = { 620 | kind = upToNextMajorVersion; 621 | minimumVersion = 1.0.0; 622 | }; 623 | }; 624 | D6D652ED2A8B19E100D0D338 /* XCRemoteSwiftPackageReference "swift-id" */ = { 625 | isa = XCRemoteSwiftPackageReference; 626 | repositoryURL = "https://github.com/koher/swift-id.git"; 627 | requirement = { 628 | kind = upToNextMajorVersion; 629 | minimumVersion = 1.0.0; 630 | }; 631 | }; 632 | /* End XCRemoteSwiftPackageReference section */ 633 | 634 | /* Begin XCSwiftPackageProductDependency section */ 635 | D68407EA2A90D7300098B6AE /* OrderedCollections */ = { 636 | isa = XCSwiftPackageProductDependency; 637 | package = D68407E92A90D7300098B6AE /* XCRemoteSwiftPackageReference "swift-collections" */; 638 | productName = OrderedCollections; 639 | }; 640 | D6D652EE2A8B19E100D0D338 /* SwiftID */ = { 641 | isa = XCSwiftPackageProductDependency; 642 | package = D6D652ED2A8B19E100D0D338 /* XCRemoteSwiftPackageReference "swift-id" */; 643 | productName = SwiftID; 644 | }; 645 | /* End XCSwiftPackageProductDependency section */ 646 | }; 647 | rootObject = D6D652B62A8B160100D0D338 /* Project object */; 648 | } 649 | -------------------------------------------------------------------------------- /SVVSSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SVVSSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SVVSSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-collections", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-collections.git", 7 | "state" : { 8 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 9 | "version" : "1.0.4" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-id", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/koher/swift-id.git", 16 | "state" : { 17 | "revision" : "d7ae15c49d1157e6f3c235e47e6075041ba6c51e", 18 | "version" : "1.0.0" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /SVVSSample/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 | -------------------------------------------------------------------------------- /SVVSSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SVVSSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SVVSSample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SVVSSample 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | NavigationView { 13 | UserView(id: "A") 14 | } 15 | .navigationViewStyle(.stack) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SVVSSample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SVVSSample/SVVSSampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SVVSSampleApp.swift 3 | // SVVSSample 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SVVSSampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SVVSSample/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // SVVSSample 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import SwiftID 9 | 10 | struct User: Sendable, Identifiable, Hashable { 11 | let id: ID 12 | var name: String 13 | var friendIDs: [User.ID] 14 | var isBookmarked: Bool 15 | 16 | struct ID: StringIDProtocol { 17 | var rawValue: String 18 | init(rawValue: String) { 19 | self.rawValue = rawValue 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SVVSSample/UserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepository.swift 3 | // SVVSSample 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | enum UserRepository { 9 | static func fetchValue(for id: User.ID) async throws -> User? { 10 | try await Task.sleep(nanoseconds: 500_000_000) 11 | return try await Backend.shared.value(for: id) 12 | } 13 | 14 | static func fetchValues(for ids: [User.ID]) async throws -> [User] { 15 | try await Task.sleep(nanoseconds: 500_000_000) 16 | return try await ids.compactMap { id in try await Backend.shared.value(for: id) } 17 | } 18 | 19 | static func updateValue(_ value: User) async throws { 20 | try await Task.sleep(nanoseconds: 500_000_000) 21 | try await Backend.shared.setValue(value) 22 | } 23 | 24 | static func updateBookmarked(_ isBookmarked: Bool, for id: User.ID) async throws { 25 | try await Task.sleep(nanoseconds: 500_000_000) 26 | try await Backend.shared.updateBookmarked(isBookmarked, for: id) 27 | } 28 | 29 | // Simulated backend 30 | private actor Backend { 31 | static let shared: Backend = .init() 32 | 33 | var values: [User.ID: User] = [ 34 | "A": User(id: "A", name: "UserA", friendIDs: ["B", "C", "D"], isBookmarked: false), 35 | "B": User(id: "B", name: "UserB", friendIDs: ["A", "C"], isBookmarked: false), 36 | "C": User(id: "C", name: "UserC", friendIDs: ["A", "B"], isBookmarked: false), 37 | "D": User(id: "D", name: "UserD", friendIDs: ["A"], isBookmarked: false), 38 | ] 39 | 40 | func value(for id: User.ID) async throws -> User? { 41 | return values[id] 42 | } 43 | 44 | func setValue(_ value: User) async throws { 45 | values[value.id] = value 46 | } 47 | 48 | func updateBookmarked(_ isBookmarked: Bool, for id: User.ID) async throws { 49 | values[id]?.isBookmarked = isBookmarked 50 | } 51 | } 52 | } 53 | 54 | private extension Sequence { 55 | func compactMap(_ transform: (Element) async throws -> T?) async rethrows -> [T] { 56 | var result: [T] = [] 57 | for element in self { 58 | guard let transformed = try await transform(element) else { continue } 59 | result.append(transformed) 60 | } 61 | return result 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SVVSSample/UserStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserStore.swift 3 | // SVVSSample 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import Combine 9 | 10 | @MainActor 11 | final class UserStore { 12 | static let shared: UserStore = .init() 13 | 14 | @Published private(set) var values: [User.ID: User] = [:] 15 | 16 | func loadValue(for id: User.ID) async throws { 17 | if let value = try await UserRepository.fetchValue(for: id) { 18 | values[value.id] = value 19 | } else { 20 | values.removeValue(forKey: id) 21 | } 22 | } 23 | 24 | func loadValues(for ids: [User.ID]) async throws { 25 | var values = self.values 26 | let fetchedValues = try await UserRepository.fetchValues(for: ids) 27 | var idsToBeRemoved: Set = .init(ids) 28 | for value in fetchedValues { 29 | values[value.id] = value 30 | idsToBeRemoved.remove(value.id) 31 | } 32 | for id in idsToBeRemoved { 33 | values.removeValue(forKey: id) 34 | } 35 | self.values = values 36 | } 37 | 38 | func updateValue(_ value: User) async throws { 39 | try await UserRepository.updateValue(value) 40 | values[value.id] = value 41 | } 42 | 43 | func updateBookmarked(_ isBookmarked: Bool, for id: User.ID) async throws { 44 | try await UserRepository.updateBookmarked(isBookmarked, for: id) 45 | values[id]?.isBookmarked = isBookmarked 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SVVSSample/UserView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserView.swift 3 | // SVVSSample 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UserView: View { 11 | @StateObject private var state: UserViewState 12 | 13 | @State private var navigatesToFriendView: Set = [] 14 | 15 | init(id: User.ID) { 16 | self._state = .init(wrappedValue: .init(id: id)) 17 | } 18 | 19 | var body: some View { 20 | ScrollView(showsIndicators: true) { 21 | VStack(spacing: 24) { 22 | HStack(spacing: 16) { 23 | Image(systemName: "person.circle.fill") 24 | .resizable() 25 | .scaledToFit() 26 | .foregroundColor(.gray) 27 | .frame(width: 60) 28 | 29 | Text(state.user?.name ?? "User Name") 30 | .font(.title) 31 | .redacted(reason: state.user == nil ? .placeholder : []) 32 | } 33 | VStack { 34 | Text("Friends") 35 | .font(.headline) 36 | Toggle("Only bookmarked", isOn: $state.showsOnlyBookmarkedFriends) 37 | .padding(.horizontal) 38 | 39 | VStack(spacing: 0) { 40 | Divider() 41 | ForEach(state.filteredFriends.values) { friend in 42 | NavigationLink { 43 | UserView(id: friend.id) 44 | } label: { 45 | HStack(spacing: 16) { 46 | Image(systemName: "person.circle.fill") 47 | .resizable() 48 | .scaledToFit() 49 | .foregroundColor(.gray) 50 | .frame(width: 40, height: 40) 51 | Text(friend.name) 52 | .font(.title2) 53 | Spacer() 54 | Button { 55 | Task { 56 | await state.toggleFriendBookmark(for: friend.id) 57 | } 58 | } label: { 59 | Image(systemName: friend.isBookmarked ? "bookmark.fill" : "bookmark") 60 | } 61 | Image(systemName: "chevron.forward") 62 | } 63 | .padding() 64 | // Hack to change a cell color when selected 65 | .background( 66 | Color(uiColor: .systemBackground) 67 | .background(Color.gray) 68 | ) 69 | } 70 | .buttonStyle(.plain) 71 | 72 | Divider() 73 | } 74 | } 75 | } 76 | } 77 | .frame(maxWidth: .infinity) 78 | .padding(.vertical) 79 | } 80 | .task { 81 | await state.onAppear() 82 | } 83 | } 84 | } 85 | 86 | struct UserView_Previews: PreviewProvider { 87 | static var previews: some View { 88 | UserView(id: "A") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /SVVSSample/UserViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserViewState.swift 3 | // SVVSSample 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import Combine 9 | import OrderedCollections 10 | 11 | @MainActor 12 | final class UserViewState: ObservableObject { 13 | let id: User.ID 14 | 15 | @Published private(set) var user: User? 16 | @Published private(set) var filteredFriends: OrderedDictionary = [:] 17 | 18 | @Published var showsOnlyBookmarkedFriends: Bool = false 19 | 20 | private var cancellables: Set = [] 21 | 22 | init(id: User.ID) { 23 | self.id = id 24 | 25 | UserStore.shared.$values.map { $0[id] }.removeDuplicates().assign(to: &$user) 26 | 27 | $user 28 | .combineLatest(UserStore.shared.$values, $showsOnlyBookmarkedFriends) 29 | .map { user, users, showsOnlyBookmarkedFriends in 30 | guard let user else { return [:] } 31 | return OrderedDictionary( 32 | uniqueKeysWithValues: user.friendIDs.lazy 33 | .compactMap { friendID in users[friendID] } 34 | .filter { user in !showsOnlyBookmarkedFriends || user.isBookmarked } 35 | .map { user in (user.id, user) } 36 | ) 37 | } 38 | .removeDuplicates() 39 | .assign(to: &$filteredFriends) 40 | 41 | $user.sink { user in 42 | guard let user else { return } 43 | Task { 44 | do { 45 | try await UserStore.shared.loadValues(for: user.friendIDs) 46 | } catch { 47 | // TODO: Error Handling 48 | print(error) 49 | } 50 | } 51 | } 52 | .store(in: &cancellables) 53 | } 54 | 55 | func onAppear() async { 56 | do { 57 | try await UserStore.shared.loadValue(for: id) 58 | } catch { 59 | // TODO: Error Handling 60 | print(error) 61 | } 62 | } 63 | 64 | func toggleFriendBookmark(for id: User.ID) async { 65 | guard var friend = filteredFriends[id] else { return } 66 | friend.isBookmarked.toggle() 67 | filteredFriends[id] = friend // to apply changes to views immediately 68 | do { 69 | try await UserStore.shared.updateBookmarked(friend.isBookmarked, for: id) 70 | } catch { 71 | filteredFriends[id] = UserStore.shared.values[id] // resets changes 72 | // TODO: Error Handling 73 | print(error) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SVVSSampleTests/SVVSSampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SVVSSampleTests.swift 3 | // SVVSSampleTests 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import XCTest 9 | @testable import SVVSSample 10 | 11 | final class SVVSSampleTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /SVVSSampleUITests/SVVSSampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SVVSSampleUITests.swift 3 | // SVVSSampleUITests 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SVVSSampleUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SVVSSampleUITests/SVVSSampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SVVSSampleUITestsLaunchTests.swift 3 | // SVVSSampleUITests 4 | // 5 | // Created by Yuta Koshizawa on 2023/08/15. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SVVSSampleUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | --------------------------------------------------------------------------------