From adced78cb0063d6323ec40f52569e7374411edf6 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:28:00 +0900 Subject: [PATCH 01/16] =?UTF-8?q?chore:=20ExampleApp=20project=EC=99=80=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Examples/ExampleApp/App/ExampleApp.swift | 19 ++ .../ExampleApp.xcodeproj/project.pbxproj | 320 ++++++++++++++++++ .../xcschemes/ExampleApp.xcscheme | 26 ++ 3 files changed, 365 insertions(+) create mode 100644 Examples/ExampleApp/App/ExampleApp.swift create mode 100644 Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj create mode 100644 Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme diff --git a/Examples/ExampleApp/App/ExampleApp.swift b/Examples/ExampleApp/App/ExampleApp.swift new file mode 100644 index 0000000..0d5a615 --- /dev/null +++ b/Examples/ExampleApp/App/ExampleApp.swift @@ -0,0 +1,19 @@ +// +// ExampleApp.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import SwiftUI + +// Cradle 소비 예제의 앱 진입점 +@main +struct ExampleApp: App { + // ExampleApp 장면 구성 + var body: some Scene { + WindowGroup { + EmptyView() + } + } +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..df7ed69 --- /dev/null +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -0,0 +1,320 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + A1000000000000000000001A /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000019 /* ExampleApp.swift */; }; + A1000000000000000000001D /* Cradle in Frameworks */ = {isa = PBXBuildFile; productRef = A1000000000000000000001C /* Cradle */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A10000000000000000000005 /* ExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A10000000000000000000019 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A10000000000000000000007 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000000000000000000001D /* Cradle in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A10000000000000000000002 = { + isa = PBXGroup; + children = ( + A10000000000000000000010 /* App */, + A10000000000000000000011 /* Domain */, + A10000000000000000000012 /* Data */, + A10000000000000000000016 /* Presentation */, + A10000000000000000000017 /* AppComposition */, + A10000000000000000000003 /* Products */, + ); + sourceTree = ""; + }; + A10000000000000000000003 /* Products */ = { + isa = PBXGroup; + children = ( + A10000000000000000000005 /* ExampleApp.app */, + ); + name = Products; + sourceTree = ""; + }; + A10000000000000000000010 /* App */ = { + isa = PBXGroup; + children = ( + A10000000000000000000019 /* ExampleApp.swift */, + ); + path = App; + sourceTree = ""; + }; + A10000000000000000000011 /* Domain */ = { + isa = PBXGroup; + children = ( + ); + path = Domain; + sourceTree = ""; + }; + A10000000000000000000012 /* Data */ = { + isa = PBXGroup; + children = ( + A10000000000000000000013 /* Persistence */, + A10000000000000000000014 /* Infra */, + A10000000000000000000015 /* Repository */, + ); + path = Data; + sourceTree = ""; + }; + A10000000000000000000013 /* Persistence */ = { + isa = PBXGroup; + children = ( + ); + path = Persistence; + sourceTree = ""; + }; + A10000000000000000000014 /* Infra */ = { + isa = PBXGroup; + children = ( + ); + path = Infra; + sourceTree = ""; + }; + A10000000000000000000015 /* Repository */ = { + isa = PBXGroup; + children = ( + ); + path = Repository; + sourceTree = ""; + }; + A10000000000000000000016 /* Presentation */ = { + isa = PBXGroup; + children = ( + ); + path = Presentation; + sourceTree = ""; + }; + A10000000000000000000017 /* AppComposition */ = { + isa = PBXGroup; + children = ( + A10000000000000000000018 /* Previews */, + ); + path = AppComposition; + sourceTree = ""; + }; + A10000000000000000000018 /* Previews */ = { + isa = PBXGroup; + children = ( + ); + path = Previews; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A10000000000000000000004 /* ExampleApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = A10000000000000000000009 /* Build configuration list for PBXNativeTarget "ExampleApp" */; + buildPhases = ( + A10000000000000000000006 /* Sources */, + A10000000000000000000007 /* Frameworks */, + A10000000000000000000008 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ExampleApp; + packageProductDependencies = ( + A1000000000000000000001C /* Cradle */, + ); + productName = ExampleApp; + productReference = A10000000000000000000005 /* ExampleApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A10000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2660; + LastUpgradeCheck = 2660; + TargetAttributes = { + A10000000000000000000004 = { + CreatedOnToolsVersion = 26.0; + }; + }; + }; + buildConfigurationList = A1000000000000000000000A /* Build configuration list for PBXProject "ExampleApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A10000000000000000000002; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + A1000000000000000000001B /* XCLocalSwiftPackageReference "Cradle" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = A10000000000000000000003 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A10000000000000000000004 /* ExampleApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A10000000000000000000008 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A10000000000000000000006 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000000000000000000001A /* ExampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A1000000000000000000000B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_TESTABILITY = YES; + GCC_OPTIMIZATION_LEVEL = 0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A1000000000000000000000C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + A1000000000000000000000D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.opfic.CradleExampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A1000000000000000000000E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.opfic.CradleExampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A10000000000000000000009 /* Build configuration list for PBXNativeTarget "ExampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1000000000000000000000D /* Debug */, + A1000000000000000000000E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A1000000000000000000000A /* Build configuration list for PBXProject "ExampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1000000000000000000000B /* Debug */, + A1000000000000000000000C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + A1000000000000000000001B /* XCLocalSwiftPackageReference "Cradle" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../..; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A1000000000000000000001C /* Cradle */ = { + isa = XCSwiftPackageProductDependency; + package = A1000000000000000000001B /* XCLocalSwiftPackageReference "Cradle" */; + productName = Cradle; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = A10000000000000000000001 /* Project object */; +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme b/Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme new file mode 100644 index 0000000..162b0ad --- /dev/null +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme @@ -0,0 +1,26 @@ + + + + + + + + + + + From 9741e2cd707477afb2e05c72126da609d3385e23 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:29:05 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Domain=20=EA=B3=84=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Domain/LoadProductUseCase.swift | 22 +++++++++++++++++++ Examples/ExampleApp/Domain/Product.swift | 22 +++++++++++++++++++ .../ExampleApp/Domain/ProductRepository.swift | 12 ++++++++++ .../ExampleApp.xcodeproj/project.pbxproj | 12 ++++++++++ 4 files changed, 68 insertions(+) create mode 100644 Examples/ExampleApp/Domain/LoadProductUseCase.swift create mode 100644 Examples/ExampleApp/Domain/Product.swift create mode 100644 Examples/ExampleApp/Domain/ProductRepository.swift diff --git a/Examples/ExampleApp/Domain/LoadProductUseCase.swift b/Examples/ExampleApp/Domain/LoadProductUseCase.swift new file mode 100644 index 0000000..5b1f645 --- /dev/null +++ b/Examples/ExampleApp/Domain/LoadProductUseCase.swift @@ -0,0 +1,22 @@ +// +// LoadProductUseCase.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +// 상품 조회 계약에 실행을 위임하는 Domain use case +struct LoadProductUseCase { + // 상품 조회를 수행할 Domain 계약 + private let repository: any ProductRepository + + // Domain 계약 주입 + init(repository: any ProductRepository) { + self.repository = repository + } + + // 식별자에 해당하는 상품 상세 조회 + func execute(id: ProductID) -> Product { + repository.product(for: id) + } +} diff --git a/Examples/ExampleApp/Domain/Product.swift b/Examples/ExampleApp/Domain/Product.swift new file mode 100644 index 0000000..fd7e50a --- /dev/null +++ b/Examples/ExampleApp/Domain/Product.swift @@ -0,0 +1,22 @@ +// +// Product.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +// 상품을 구분하는 Domain 식별자 +struct ProductID: Hashable, Sendable { + // 외부 입력으로 전달할 원시 식별값 + let rawValue: String +} + +// 상품 상세 화면이 표시할 Domain entity +struct Product: Equatable, Sendable { + // 조회한 상품의 식별자 + let id: ProductID + // 화면에 표시할 상품 이름 + let name: String + // 화면에 표시할 상품 설명 + let summary: String +} diff --git a/Examples/ExampleApp/Domain/ProductRepository.swift b/Examples/ExampleApp/Domain/ProductRepository.swift new file mode 100644 index 0000000..d74d9bc --- /dev/null +++ b/Examples/ExampleApp/Domain/ProductRepository.swift @@ -0,0 +1,12 @@ +// +// ProductRepository.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +// Data 계층이 구현할 상품 조회 계약 +protocol ProductRepository { + // 식별자에 해당하는 상품 조회 + func product(for id: ProductID) -> Product +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index df7ed69..1ecbc4a 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -9,11 +9,17 @@ /* Begin PBXBuildFile section */ A1000000000000000000001A /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000019 /* ExampleApp.swift */; }; A1000000000000000000001D /* Cradle in Frameworks */ = {isa = PBXBuildFile; productRef = A1000000000000000000001C /* Cradle */; }; + A10000000000000000000023 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000020 /* Product.swift */; }; + A10000000000000000000024 /* ProductRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000021 /* ProductRepository.swift */; }; + A10000000000000000000025 /* LoadProductUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000022 /* LoadProductUseCase.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A10000000000000000000005 /* ExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; A10000000000000000000019 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; + A10000000000000000000020 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; }; + A10000000000000000000021 /* ProductRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductRepository.swift; sourceTree = ""; }; + A10000000000000000000022 /* LoadProductUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadProductUseCase.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -59,6 +65,9 @@ A10000000000000000000011 /* Domain */ = { isa = PBXGroup; children = ( + A10000000000000000000020 /* Product.swift */, + A10000000000000000000021 /* ProductRepository.swift */, + A10000000000000000000022 /* LoadProductUseCase.swift */, ); path = Domain; sourceTree = ""; @@ -192,6 +201,9 @@ buildActionMask = 2147483647; files = ( A1000000000000000000001A /* ExampleApp.swift in Sources */, + A10000000000000000000023 /* Product.swift in Sources */, + A10000000000000000000024 /* ProductRepository.swift in Sources */, + A10000000000000000000025 /* LoadProductUseCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 7b8d5ba976e83ec4cb2f4e68fb37dc8c851f8c11 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:30:07 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=20source=20graph=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Persistence/PersistenceGraph.swift | 18 ++++++++++ .../Data/Persistence/ProductStore.swift | 35 +++++++++++++++++++ .../ExampleApp.xcodeproj/project.pbxproj | 8 +++++ 3 files changed, 61 insertions(+) create mode 100644 Examples/ExampleApp/Data/Persistence/PersistenceGraph.swift create mode 100644 Examples/ExampleApp/Data/Persistence/ProductStore.swift diff --git a/Examples/ExampleApp/Data/Persistence/PersistenceGraph.swift b/Examples/ExampleApp/Data/Persistence/PersistenceGraph.swift new file mode 100644 index 0000000..2fe5644 --- /dev/null +++ b/Examples/ExampleApp/Data/Persistence/PersistenceGraph.swift @@ -0,0 +1,18 @@ +// +// PersistenceGraph.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import Cradle + +// 상품 저장소 등록을 소유하는 override 가능 source graph +@DependencyGraph(overrides: true) +final class PersistenceGraph { + // 기본 구성에서 비어 있는 메모리 저장소 생성 + @Provide + private func makeProductStore() -> ProductStore { + ProductStore(storedProduct: nil) + } +} diff --git a/Examples/ExampleApp/Data/Persistence/ProductStore.swift b/Examples/ExampleApp/Data/Persistence/ProductStore.swift new file mode 100644 index 0000000..949cb54 --- /dev/null +++ b/Examples/ExampleApp/Data/Persistence/ProductStore.swift @@ -0,0 +1,35 @@ +// +// ProductStore.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +// Persistence 계층이 보관할 상품 데이터 +struct StoredProduct { + // 저장한 상품 식별자 + let id: ProductID + // 저장한 상품 이름 + let name: String + // 저장한 상품 설명 + let summary: String +} + +// 단일 상품의 메모리 저장 상태를 제공하는 저장소 +struct ProductStore { + // 메모리에 보관한 상품 데이터 + private let storedProduct: StoredProduct? + + // 상품이 있거나 없는 저장 상태 구성 + init(storedProduct: StoredProduct?) { + self.storedProduct = storedProduct + } + + // 식별자가 일치하는 저장 상품 조회 + func product(for id: ProductID) -> StoredProduct? { + guard storedProduct?.id == id else { + return nil + } + return storedProduct + } +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index 1ecbc4a..18ad3b4 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ A10000000000000000000023 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000020 /* Product.swift */; }; A10000000000000000000024 /* ProductRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000021 /* ProductRepository.swift */; }; A10000000000000000000025 /* LoadProductUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000022 /* LoadProductUseCase.swift */; }; + A10000000000000000000028 /* ProductStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000026 /* ProductStore.swift */; }; + A10000000000000000000029 /* PersistenceGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000027 /* PersistenceGraph.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -20,6 +22,8 @@ A10000000000000000000020 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; }; A10000000000000000000021 /* ProductRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductRepository.swift; sourceTree = ""; }; A10000000000000000000022 /* LoadProductUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadProductUseCase.swift; sourceTree = ""; }; + A10000000000000000000026 /* ProductStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductStore.swift; sourceTree = ""; }; + A10000000000000000000027 /* PersistenceGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceGraph.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +89,8 @@ A10000000000000000000013 /* Persistence */ = { isa = PBXGroup; children = ( + A10000000000000000000026 /* ProductStore.swift */, + A10000000000000000000027 /* PersistenceGraph.swift */, ); path = Persistence; sourceTree = ""; @@ -204,6 +210,8 @@ A10000000000000000000023 /* Product.swift in Sources */, A10000000000000000000024 /* ProductRepository.swift in Sources */, A10000000000000000000025 /* LoadProductUseCase.swift in Sources */, + A10000000000000000000028 /* ProductStore.swift in Sources */, + A10000000000000000000029 /* PersistenceGraph.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From edfcc2b8ace24bb394967b9d8c87e162d55870ca Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:30:56 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=20source=20graph=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExampleApp/Data/Infra/InfraGraph.swift | 18 ++++++++++++ .../ExampleApp/Data/Infra/ProductClient.swift | 28 +++++++++++++++++++ .../ExampleApp.xcodeproj/project.pbxproj | 8 ++++++ 3 files changed, 54 insertions(+) create mode 100644 Examples/ExampleApp/Data/Infra/InfraGraph.swift create mode 100644 Examples/ExampleApp/Data/Infra/ProductClient.swift diff --git a/Examples/ExampleApp/Data/Infra/InfraGraph.swift b/Examples/ExampleApp/Data/Infra/InfraGraph.swift new file mode 100644 index 0000000..68ad11f --- /dev/null +++ b/Examples/ExampleApp/Data/Infra/InfraGraph.swift @@ -0,0 +1,18 @@ +// +// InfraGraph.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import Cradle + +// 상품 client 등록을 소유하는 source graph +@DependencyGraph +final class InfraGraph { + // 결정적인 상품 응답을 제공할 client 생성 + @Provide + private func makeProductClient() -> ProductClient { + ProductClient() + } +} diff --git a/Examples/ExampleApp/Data/Infra/ProductClient.swift b/Examples/ExampleApp/Data/Infra/ProductClient.swift new file mode 100644 index 0000000..267a43e --- /dev/null +++ b/Examples/ExampleApp/Data/Infra/ProductClient.swift @@ -0,0 +1,28 @@ +// +// ProductClient.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +// Infra 계층이 제공할 상품 응답 +struct ProductResponse { + // 요청한 상품 식별자 + let id: ProductID + // 결정적인 상품 이름 + let name: String + // 결정적인 상품 설명 + let summary: String +} + +// 실제 통신 없이 결정적인 상품 응답을 제공하는 client +struct ProductClient { + // 식별자에 대응하는 상품 응답 생성 + func product(for id: ProductID) -> ProductResponse { + ProductResponse( + id: id, + name: "기본 상품", + summary: "Infra 계층에서 제공한 상품 \(id.rawValue)" + ) + } +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index 18ad3b4..d9ea324 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ A10000000000000000000025 /* LoadProductUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000022 /* LoadProductUseCase.swift */; }; A10000000000000000000028 /* ProductStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000026 /* ProductStore.swift */; }; A10000000000000000000029 /* PersistenceGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000027 /* PersistenceGraph.swift */; }; + A1000000000000000000002C /* ProductClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002A /* ProductClient.swift */; }; + A1000000000000000000002D /* InfraGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002B /* InfraGraph.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -24,6 +26,8 @@ A10000000000000000000022 /* LoadProductUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadProductUseCase.swift; sourceTree = ""; }; A10000000000000000000026 /* ProductStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductStore.swift; sourceTree = ""; }; A10000000000000000000027 /* PersistenceGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceGraph.swift; sourceTree = ""; }; + A1000000000000000000002A /* ProductClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductClient.swift; sourceTree = ""; }; + A1000000000000000000002B /* InfraGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfraGraph.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -98,6 +102,8 @@ A10000000000000000000014 /* Infra */ = { isa = PBXGroup; children = ( + A1000000000000000000002A /* ProductClient.swift */, + A1000000000000000000002B /* InfraGraph.swift */, ); path = Infra; sourceTree = ""; @@ -212,6 +218,8 @@ A10000000000000000000025 /* LoadProductUseCase.swift in Sources */, A10000000000000000000028 /* ProductStore.swift in Sources */, A10000000000000000000029 /* PersistenceGraph.swift in Sources */, + A1000000000000000000002C /* ProductClient.swift in Sources */, + A1000000000000000000002D /* InfraGraph.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 2dcf3b6a6b6137a5df3e900a661df067a812f7d2 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:32:47 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20Repository?= =?UTF-8?q?=20graph=20=EC=A1=B0=ED=95=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Repository/DataGraph.swift | 50 +++++++++++++++++++ .../ExampleApp.xcodeproj/project.pbxproj | 4 ++ 2 files changed, 54 insertions(+) create mode 100644 Examples/ExampleApp/Data/Repository/DataGraph.swift diff --git a/Examples/ExampleApp/Data/Repository/DataGraph.swift b/Examples/ExampleApp/Data/Repository/DataGraph.swift new file mode 100644 index 0000000..2929796 --- /dev/null +++ b/Examples/ExampleApp/Data/Repository/DataGraph.swift @@ -0,0 +1,50 @@ +// +// DataGraph.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import Cradle + +// Persistence와 Infra를 Domain 상품 계약으로 감추는 Repository +private struct DefaultProductRepository: ProductRepository { + // 저장된 상품을 먼저 조회할 저장소 + let store: ProductStore + // 저장된 상품이 없을 때 사용할 client + let client: ProductClient + + // 저장 상품을 우선 사용하고 없으면 Infra 응답을 Domain entity로 변환 + func product(for id: ProductID) -> Product { + if let product = store.product(for: id) { + return Product( + id: product.id, + name: product.name, + summary: product.summary + ) + } + + let response = client.product(for: id) + return Product( + id: response.id, + name: response.name, + summary: response.summary + ) + } +} + +// Persistence와 Infra source graph를 Domain 계약으로 조합하는 Data graph +@DependencyGraph(sources: [ + PersistenceGraph.self, + InfraGraph.self +]) +final class DataGraph { + // 구체 구현을 숨긴 Domain 상품 조회 계약 생성 + @Provide + private func makeProductRepository() -> any ProductRepository { + DefaultProductRepository( + store: persistenceGraph.productStore, + client: infraGraph.productClient + ) + } +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index d9ea324..d338bc2 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ A10000000000000000000029 /* PersistenceGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000027 /* PersistenceGraph.swift */; }; A1000000000000000000002C /* ProductClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002A /* ProductClient.swift */; }; A1000000000000000000002D /* InfraGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002B /* InfraGraph.swift */; }; + A10000000000000000000031 /* DataGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002F /* DataGraph.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -28,6 +29,7 @@ A10000000000000000000027 /* PersistenceGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceGraph.swift; sourceTree = ""; }; A1000000000000000000002A /* ProductClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductClient.swift; sourceTree = ""; }; A1000000000000000000002B /* InfraGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfraGraph.swift; sourceTree = ""; }; + A1000000000000000000002F /* DataGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGraph.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -111,6 +113,7 @@ A10000000000000000000015 /* Repository */ = { isa = PBXGroup; children = ( + A1000000000000000000002F /* DataGraph.swift */, ); path = Repository; sourceTree = ""; @@ -220,6 +223,7 @@ A10000000000000000000029 /* PersistenceGraph.swift in Sources */, A1000000000000000000002C /* ProductClient.swift in Sources */, A1000000000000000000002D /* InfraGraph.swift in Sources */, + A10000000000000000000031 /* DataGraph.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 9bba1732d493e7cb43037c580be031f65799eb3d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:33:56 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20MVVM=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExampleApp.xcodeproj/project.pbxproj | 8 ++++ .../Presentation/ProductDetailView.swift | 44 +++++++++++++++++++ .../Presentation/ProductDetailViewModel.swift | 31 +++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 Examples/ExampleApp/Presentation/ProductDetailView.swift create mode 100644 Examples/ExampleApp/Presentation/ProductDetailViewModel.swift diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index d338bc2..2c0b049 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ A1000000000000000000002C /* ProductClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002A /* ProductClient.swift */; }; A1000000000000000000002D /* InfraGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002B /* InfraGraph.swift */; }; A10000000000000000000031 /* DataGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002F /* DataGraph.swift */; }; + A10000000000000000000034 /* ProductDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000032 /* ProductDetailViewModel.swift */; }; + A10000000000000000000035 /* ProductDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000033 /* ProductDetailView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -30,6 +32,8 @@ A1000000000000000000002A /* ProductClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductClient.swift; sourceTree = ""; }; A1000000000000000000002B /* InfraGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfraGraph.swift; sourceTree = ""; }; A1000000000000000000002F /* DataGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGraph.swift; sourceTree = ""; }; + A10000000000000000000032 /* ProductDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailViewModel.swift; sourceTree = ""; }; + A10000000000000000000033 /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -121,6 +125,8 @@ A10000000000000000000016 /* Presentation */ = { isa = PBXGroup; children = ( + A10000000000000000000032 /* ProductDetailViewModel.swift */, + A10000000000000000000033 /* ProductDetailView.swift */, ); path = Presentation; sourceTree = ""; @@ -224,6 +230,8 @@ A1000000000000000000002C /* ProductClient.swift in Sources */, A1000000000000000000002D /* InfraGraph.swift in Sources */, A10000000000000000000031 /* DataGraph.swift in Sources */, + A10000000000000000000034 /* ProductDetailViewModel.swift in Sources */, + A10000000000000000000035 /* ProductDetailView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/ExampleApp/Presentation/ProductDetailView.swift b/Examples/ExampleApp/Presentation/ProductDetailView.swift new file mode 100644 index 0000000..186dc68 --- /dev/null +++ b/Examples/ExampleApp/Presentation/ProductDetailView.swift @@ -0,0 +1,44 @@ +// +// ProductDetailView.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import SwiftUI + +// 주입받은 ViewModel의 수명을 소유하는 상품 상세 View +@MainActor +struct ProductDetailView: View { + // 화면 수명 동안 보관할 ViewModel + @StateObject private var viewModel: ProductDetailViewModel + + // Optional이 아닌 ViewModel 주입과 StateObject 초기화 + init(viewModel: ProductDetailViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + // 상품 조회 상태에 따른 화면 구성 + var body: some View { + NavigationStack { + Group { + // ViewModel이 제공한 상품 상세 표시 + if let product = viewModel.product { + VStack(alignment: .leading, spacing: 12) { + Text(product.name) + .font(.title) + Text(product.summary) + .foregroundStyle(.secondary) + } + .padding() + } else { + ProgressView() + } + } + .navigationTitle("상품 상세") + } + .task { + viewModel.load() + } + } +} diff --git a/Examples/ExampleApp/Presentation/ProductDetailViewModel.swift b/Examples/ExampleApp/Presentation/ProductDetailViewModel.swift new file mode 100644 index 0000000..170f0e6 --- /dev/null +++ b/Examples/ExampleApp/Presentation/ProductDetailViewModel.swift @@ -0,0 +1,31 @@ +// +// ProductDetailViewModel.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import Combine + +// 상품 상세 화면의 상태와 Domain 실행을 연결하는 ViewModel +@MainActor +final class ProductDetailViewModel: ObservableObject { + // 화면에 표시할 상품 상태 + @Published private(set) var product: Product? + + // 상품 조회를 수행할 Domain use case + private let useCase: LoadProductUseCase + // 호출 시점에 정해진 상품 식별자 + private let productID: ProductID + + // Domain use case와 외부 상품 식별자 주입 + init(useCase: LoadProductUseCase, productID: ProductID) { + self.useCase = useCase + self.productID = productID + } + + // 상품 상세를 조회해 화면 상태 갱신 + func load() { + product = useCase.execute(id: productID) + } +} From cdc5bd3ace1b32365067af941136393fa68ab6c1 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:35:58 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20Composition=20Root=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExampleApp/AppComposition/AppGraph.swift | 28 ++++++++++++++++++ .../ExampleAppComposition.swift | 29 +++++++++++++++++++ .../ExampleApp.xcodeproj/project.pbxproj | 8 +++++ 3 files changed, 65 insertions(+) create mode 100644 Examples/ExampleApp/AppComposition/AppGraph.swift create mode 100644 Examples/ExampleApp/AppComposition/ExampleAppComposition.swift diff --git a/Examples/ExampleApp/AppComposition/AppGraph.swift b/Examples/ExampleApp/AppComposition/AppGraph.swift new file mode 100644 index 0000000..0d80ec7 --- /dev/null +++ b/Examples/ExampleApp/AppComposition/AppGraph.swift @@ -0,0 +1,28 @@ +// +// AppGraph.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import Cradle + +// Data graph와 Presentation 생성을 연결하는 MainActor graph +@MainActor +@DependencyGraph(sources: [DataGraph.self]) +final class AppGraph { + // Data graph의 Domain 계약으로 상품 조회 use case 생성 + @Provide + private func makeLoadProductUseCase() -> LoadProductUseCase { + LoadProductUseCase(repository: dataGraph.productRepository) + } + + // graph 의존성과 외부 상품 식별자로 ViewModel 생성 + @Provide(.transient) + private func makeProductDetailViewModel( + useCase: LoadProductUseCase, + @External productID: ProductID + ) -> ProductDetailViewModel { + ProductDetailViewModel(useCase: useCase, productID: productID) + } +} diff --git a/Examples/ExampleApp/AppComposition/ExampleAppComposition.swift b/Examples/ExampleApp/AppComposition/ExampleAppComposition.swift new file mode 100644 index 0000000..0b71913 --- /dev/null +++ b/Examples/ExampleApp/AppComposition/ExampleAppComposition.swift @@ -0,0 +1,29 @@ +// +// ExampleAppComposition.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +// leaf source graph부터 앱 graph까지 조립하고 소유하는 Composition Root +@MainActor +final class ExampleAppComposition { + // 조립한 계층 graph를 소유할 앱 graph + private let graph: AppGraph + + // Persistence부터 앱 graph까지 명시적인 생성 순서로 조립 + init() { + let persistenceGraph = PersistenceGraph() + let infraGraph = InfraGraph() + let dataGraph = DataGraph( + infraGraph: infraGraph, + persistenceGraph: persistenceGraph + ) + graph = AppGraph(dataGraph: dataGraph) + } + + // 호출 시점 상품 식별자로 새로운 ViewModel 생성 + func makeProductDetailViewModel(productID: ProductID) -> ProductDetailViewModel { + graph.productDetailViewModel(productID: productID) + } +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index 2c0b049..686a187 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ A10000000000000000000031 /* DataGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000002F /* DataGraph.swift */; }; A10000000000000000000034 /* ProductDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000032 /* ProductDetailViewModel.swift */; }; A10000000000000000000035 /* ProductDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000033 /* ProductDetailView.swift */; }; + A10000000000000000000038 /* AppGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000036 /* AppGraph.swift */; }; + A10000000000000000000039 /* ExampleAppComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000037 /* ExampleAppComposition.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -34,6 +36,8 @@ A1000000000000000000002F /* DataGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGraph.swift; sourceTree = ""; }; A10000000000000000000032 /* ProductDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailViewModel.swift; sourceTree = ""; }; A10000000000000000000033 /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = ""; }; + A10000000000000000000036 /* AppGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGraph.swift; sourceTree = ""; }; + A10000000000000000000037 /* ExampleAppComposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppComposition.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -134,6 +138,8 @@ A10000000000000000000017 /* AppComposition */ = { isa = PBXGroup; children = ( + A10000000000000000000036 /* AppGraph.swift */, + A10000000000000000000037 /* ExampleAppComposition.swift */, A10000000000000000000018 /* Previews */, ); path = AppComposition; @@ -232,6 +238,8 @@ A10000000000000000000031 /* DataGraph.swift in Sources */, A10000000000000000000034 /* ProductDetailViewModel.swift in Sources */, A10000000000000000000035 /* ProductDetailView.swift in Sources */, + A10000000000000000000038 /* AppGraph.swift in Sources */, + A10000000000000000000039 /* ExampleAppComposition.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 9f7bd7f3dd46faa29c6b3dc2e7fc8dd051f3b752 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:37:18 +0900 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20ExampleApp=20=EC=A7=84=EC=9E=85?= =?UTF-8?q?=EC=A0=90=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Examples/ExampleApp/App/ExampleApp.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Examples/ExampleApp/App/ExampleApp.swift b/Examples/ExampleApp/App/ExampleApp.swift index 0d5a615..fdb4ce9 100644 --- a/Examples/ExampleApp/App/ExampleApp.swift +++ b/Examples/ExampleApp/App/ExampleApp.swift @@ -9,11 +9,28 @@ import SwiftUI // Cradle 소비 예제의 앱 진입점 @main +@MainActor struct ExampleApp: App { + // 앱 수명 동안 graph를 보유할 Composition Root + private let composition: ExampleAppComposition + // ViewModel 수명을 맡을 최상위 View + private let rootView: ProductDetailView + + // Composition Root에서 생성한 ViewModel을 최상위 View에 주입 + init() { + let composition = ExampleAppComposition() + self.composition = composition + rootView = ProductDetailView( + viewModel: composition.makeProductDetailViewModel( + productID: ProductID(rawValue: "featured-product") + ) + ) + } + // ExampleApp 장면 구성 var body: some Scene { WindowGroup { - EmptyView() + rootView } } } From 59a78f606bfb695ac8a92683db0def1b54ae8289 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:39:29 +0900 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20=EB=8F=85=EB=A6=BD=20Preview=20gr?= =?UTF-8?q?aph=20=EC=A1=B0=EB=A6=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Previews/ProductDetailPreviews.swift | 71 +++++++++++++++++++ .../ExampleApp.xcodeproj/project.pbxproj | 4 ++ 2 files changed, 75 insertions(+) create mode 100644 Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift diff --git a/Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift b/Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift new file mode 100644 index 0000000..ea59c01 --- /dev/null +++ b/Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift @@ -0,0 +1,71 @@ +// +// ProductDetailPreviews.swift +// ExampleApp +// +// Created by opfic on 9/4/26. +// + +import Cradle +import SwiftUI + +// 저장값이 없을 때 Infra 결과를 표시하는 Preview 구성 +@MainActor +private func infraProductDetailPreview() -> some View { + // 저장값이 없는 기본 Persistence graph + let persistenceGraph = PersistenceGraph() + // 결정적인 상품을 제공할 기본 Infra graph + let infraGraph = InfraGraph() + // Preview가 독립적으로 소유할 Data graph + let dataGraph = DataGraph( + infraGraph: infraGraph, + persistenceGraph: persistenceGraph + ) + // Preview가 독립적으로 소유할 앱 graph + let graph = AppGraph(dataGraph: dataGraph) + // 외부 상품 식별자로 생성한 Preview 전용 ViewModel + let viewModel = graph.productDetailViewModel( + productID: ProductID(rawValue: "remote-product") + ) + + return ProductDetailView(viewModel: viewModel) +} + +// Persistence 등록을 교체한 Preview 구성 +@MainActor +private func persistenceProductDetailPreview() -> some View { + // 등록 소유 graph에 직접 적용한 저장 상품 교체 + let persistenceGraph = PersistenceGraph.override( + productStore: .replace { + ProductStore( + storedProduct: StoredProduct( + id: ProductID(rawValue: "stored-product"), + name: "저장 상품", + summary: "Persistence 계층에서 제공한 상품" + ) + ) + } + ).build() + // 교체를 전파하지 않는 독립 Infra graph + let infraGraph = InfraGraph() + // 교체한 Persistence graph를 직접 받는 Data graph + let dataGraph = DataGraph( + infraGraph: infraGraph, + persistenceGraph: persistenceGraph + ) + // Preview가 독립적으로 소유할 앱 graph + let graph = AppGraph(dataGraph: dataGraph) + // 저장 상품 식별자로 생성한 Preview 전용 ViewModel + let viewModel = graph.productDetailViewModel( + productID: ProductID(rawValue: "stored-product") + ) + + return ProductDetailView(viewModel: viewModel) +} + +#Preview("Infra 대체 조회") { + infraProductDetailPreview() +} + +#Preview("Persistence 교체") { + persistenceProductDetailPreview() +} diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index 686a187..1ec61c8 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ A10000000000000000000035 /* ProductDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000033 /* ProductDetailView.swift */; }; A10000000000000000000038 /* AppGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000036 /* AppGraph.swift */; }; A10000000000000000000039 /* ExampleAppComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000037 /* ExampleAppComposition.swift */; }; + A1000000000000000000003B /* ProductDetailPreviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000003A /* ProductDetailPreviews.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -38,6 +39,7 @@ A10000000000000000000033 /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = ""; }; A10000000000000000000036 /* AppGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGraph.swift; sourceTree = ""; }; A10000000000000000000037 /* ExampleAppComposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppComposition.swift; sourceTree = ""; }; + A1000000000000000000003A /* ProductDetailPreviews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailPreviews.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -148,6 +150,7 @@ A10000000000000000000018 /* Previews */ = { isa = PBXGroup; children = ( + A1000000000000000000003A /* ProductDetailPreviews.swift */, ); path = Previews; sourceTree = ""; @@ -240,6 +243,7 @@ A10000000000000000000035 /* ProductDetailView.swift in Sources */, A10000000000000000000038 /* AppGraph.swift in Sources */, A10000000000000000000039 /* ExampleAppComposition.swift in Sources */, + A1000000000000000000003B /* ProductDetailPreviews.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From c8a68e70d47dfc64abc6a01c9bcea2c2013623c5 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:40:30 +0900 Subject: [PATCH 10/16] =?UTF-8?q?chore:=20ExampleApp=20iOS=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20CI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da8af4d..35f1bd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,43 @@ jobs: path: build.log if-no-files-found: ignore + example_app: + name: ExampleApp Build + runs-on: macos-26 + env: + DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: List ExampleApp Project + run: xcodebuild -list -project Examples/ExampleApp/ExampleApp.xcodeproj + + - name: Build ExampleApp + shell: bash + run: | + set -euo pipefail + set -o pipefail + set +e + xcodebuild \ + -project Examples/ExampleApp/ExampleApp.xcodeproj \ + -scheme ExampleApp \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + CODE_SIGNING_ALLOWED=NO \ + build 2>&1 | tee example-app-build.log + buildStatus=${PIPESTATUS[0]} + set -e + exit $buildStatus + + - name: Upload ExampleApp build log artifact + if: always() + uses: actions/upload-artifact@v7 + with: + name: example-app-build-log + path: example-app-build.log + if-no-files-found: ignore + test: name: Swift Test needs: build From cd2bee03d3d76fc70e2f77a9e219d6508c823897 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:16:34 +0900 Subject: [PATCH 11/16] =?UTF-8?q?fix:=20ExampleApp=20workspace=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExampleApp.xcworkspace/contents.xcworkspacedata | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Examples/ExampleApp/ExampleApp.xcworkspace/contents.xcworkspacedata diff --git a/Examples/ExampleApp/ExampleApp.xcworkspace/contents.xcworkspacedata b/Examples/ExampleApp/ExampleApp.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..c81d0a8 --- /dev/null +++ b/Examples/ExampleApp/ExampleApp.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + + + From 100fda41da9209eb64dd3a20d585a22e1cdaf3eb Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:36:59 +0900 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20ExampleApp=20Simulator=20=EC=84=9C?= =?UTF-8?q?=EB=AA=85=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index 1ec61c8..2596484 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -281,6 +281,7 @@ A1000000000000000000000D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGNING_ALLOWED[sdk=iphonesimulator*]" = NO; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_PREVIEWS = YES; @@ -307,6 +308,7 @@ A1000000000000000000000E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGNING_ALLOWED[sdk=iphonesimulator*]" = NO; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_PREVIEWS = YES; From 9ea0cceaa44eb623334854e64df7d5a2e434f503 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:49:54 +0900 Subject: [PATCH 13/16] =?UTF-8?q?chore:=20ExampleApp=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20scheme=20=EB=8F=99=EC=9E=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xcschemes/ExampleApp.xcscheme | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme b/Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme index 162b0ad..5420c70 100644 --- a/Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme +++ b/Examples/ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme @@ -23,4 +23,37 @@ + + + + + + + + + + From 52226d398abc5b68b2054778bbac5bc237be72a0 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:54:00 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20ExampleApp=20Observation=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Examples/ExampleApp/App/ExampleApp.swift | 1 - Examples/ExampleApp/AppComposition/AppGraph.swift | 3 +-- .../ExampleApp/AppComposition/ExampleAppComposition.swift | 1 - .../AppComposition/Previews/ProductDetailPreviews.swift | 8 ++++---- Examples/ExampleApp/Presentation/ProductDetailView.swift | 7 +++---- .../ExampleApp/Presentation/ProductDetailViewModel.swift | 8 ++++---- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Examples/ExampleApp/App/ExampleApp.swift b/Examples/ExampleApp/App/ExampleApp.swift index fdb4ce9..318ea7b 100644 --- a/Examples/ExampleApp/App/ExampleApp.swift +++ b/Examples/ExampleApp/App/ExampleApp.swift @@ -9,7 +9,6 @@ import SwiftUI // Cradle 소비 예제의 앱 진입점 @main -@MainActor struct ExampleApp: App { // 앱 수명 동안 graph를 보유할 Composition Root private let composition: ExampleAppComposition diff --git a/Examples/ExampleApp/AppComposition/AppGraph.swift b/Examples/ExampleApp/AppComposition/AppGraph.swift index 0d80ec7..3ca534d 100644 --- a/Examples/ExampleApp/AppComposition/AppGraph.swift +++ b/Examples/ExampleApp/AppComposition/AppGraph.swift @@ -7,8 +7,7 @@ import Cradle -// Data graph와 Presentation 생성을 연결하는 MainActor graph -@MainActor +// Data graph와 Presentation 생성을 연결하는 app graph @DependencyGraph(sources: [DataGraph.self]) final class AppGraph { // Data graph의 Domain 계약으로 상품 조회 use case 생성 diff --git a/Examples/ExampleApp/AppComposition/ExampleAppComposition.swift b/Examples/ExampleApp/AppComposition/ExampleAppComposition.swift index 0b71913..9750bd4 100644 --- a/Examples/ExampleApp/AppComposition/ExampleAppComposition.swift +++ b/Examples/ExampleApp/AppComposition/ExampleAppComposition.swift @@ -6,7 +6,6 @@ // // leaf source graph부터 앱 graph까지 조립하고 소유하는 Composition Root -@MainActor final class ExampleAppComposition { // 조립한 계층 graph를 소유할 앱 graph private let graph: AppGraph diff --git a/Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift b/Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift index ea59c01..cd1fa30 100644 --- a/Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift +++ b/Examples/ExampleApp/AppComposition/Previews/ProductDetailPreviews.swift @@ -9,7 +9,7 @@ import Cradle import SwiftUI // 저장값이 없을 때 Infra 결과를 표시하는 Preview 구성 -@MainActor +@MainActor @ViewBuilder private func infraProductDetailPreview() -> some View { // 저장값이 없는 기본 Persistence graph let persistenceGraph = PersistenceGraph() @@ -27,11 +27,11 @@ private func infraProductDetailPreview() -> some View { productID: ProductID(rawValue: "remote-product") ) - return ProductDetailView(viewModel: viewModel) + ProductDetailView(viewModel: viewModel) } // Persistence 등록을 교체한 Preview 구성 -@MainActor +@MainActor @ViewBuilder private func persistenceProductDetailPreview() -> some View { // 등록 소유 graph에 직접 적용한 저장 상품 교체 let persistenceGraph = PersistenceGraph.override( @@ -59,7 +59,7 @@ private func persistenceProductDetailPreview() -> some View { productID: ProductID(rawValue: "stored-product") ) - return ProductDetailView(viewModel: viewModel) + ProductDetailView(viewModel: viewModel) } #Preview("Infra 대체 조회") { diff --git a/Examples/ExampleApp/Presentation/ProductDetailView.swift b/Examples/ExampleApp/Presentation/ProductDetailView.swift index 186dc68..020c7c7 100644 --- a/Examples/ExampleApp/Presentation/ProductDetailView.swift +++ b/Examples/ExampleApp/Presentation/ProductDetailView.swift @@ -8,14 +8,13 @@ import SwiftUI // 주입받은 ViewModel의 수명을 소유하는 상품 상세 View -@MainActor struct ProductDetailView: View { // 화면 수명 동안 보관할 ViewModel - @StateObject private var viewModel: ProductDetailViewModel + @State private var viewModel: ProductDetailViewModel - // Optional이 아닌 ViewModel 주입과 StateObject 초기화 + // Optional이 아닌 ViewModel 주입과 State 초기화 init(viewModel: ProductDetailViewModel) { - _viewModel = StateObject(wrappedValue: viewModel) + _viewModel = State(initialValue: viewModel) } // 상품 조회 상태에 따른 화면 구성 diff --git a/Examples/ExampleApp/Presentation/ProductDetailViewModel.swift b/Examples/ExampleApp/Presentation/ProductDetailViewModel.swift index 170f0e6..660ac5c 100644 --- a/Examples/ExampleApp/Presentation/ProductDetailViewModel.swift +++ b/Examples/ExampleApp/Presentation/ProductDetailViewModel.swift @@ -5,13 +5,13 @@ // Created by opfic on 9/4/26. // -import Combine +import Foundation // 상품 상세 화면의 상태와 Domain 실행을 연결하는 ViewModel -@MainActor -final class ProductDetailViewModel: ObservableObject { +@Observable +final class ProductDetailViewModel { // 화면에 표시할 상품 상태 - @Published private(set) var product: Product? + private(set) var product: Product? // 상품 조회를 수행할 Domain use case private let useCase: LoadProductUseCase From 805c60b9fa78d480b318a4dcc255b33823284026 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:58:32 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20ExampleApp=20SwiftSyntax=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=20=EB=B9=8C=EB=93=9C=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35f1bd1..921a729 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,9 @@ jobs: - name: List ExampleApp Project run: xcodebuild -list -project Examples/ExampleApp/ExampleApp.xcodeproj + - name: Disable SwiftSyntax Prebuilts + run: defaults write com.apple.dt.Xcode IDEPackageEnablePrebuilts NO + - name: Build ExampleApp shell: bash run: | From 65962424595669f5181f26adb7b4217a6ad4accf Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:02:15 +0900 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20ExampleApp=20CI=20workspace=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 921a729..918984c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,8 +51,8 @@ jobs: - name: Checkout uses: actions/checkout@v5 - - name: List ExampleApp Project - run: xcodebuild -list -project Examples/ExampleApp/ExampleApp.xcodeproj + - name: List ExampleApp Workspace + run: xcodebuild -list -workspace Examples/ExampleApp/ExampleApp.xcworkspace - name: Disable SwiftSyntax Prebuilts run: defaults write com.apple.dt.Xcode IDEPackageEnablePrebuilts NO @@ -64,7 +64,7 @@ jobs: set -o pipefail set +e xcodebuild \ - -project Examples/ExampleApp/ExampleApp.xcodeproj \ + -workspace Examples/ExampleApp/ExampleApp.xcworkspace \ -scheme ExampleApp \ -configuration Debug \ -destination 'generic/platform=iOS Simulator' \