Section 4/171 menit
4. ProjectDescriptionHelpers Lanjutan
4. ProjectDescriptionHelpers Lanjutan
Layer Hierarchy untuk Tim Besar
Untuk project dengan 50+ module, ProjectDescriptionHelpers perlu diorganisasi secara hierarkis:
swift
// Tuist/ProjectDescriptionHelpers/Settings/Settings+Base.swift
import ProjectDescription
// Level 1: Base settings untuk semua targets
public extension SettingsDictionary {
static var base: SettingsDictionary {
[
"SWIFT_VERSION": "6.0",
"IPHONEOS_DEPLOYMENT_TARGET": "17.0",
"SWIFT_STRICT_CONCURRENCY": "complete",
"CLANG_ENABLE_OBJC_ARC": "YES",
"ENABLE_BITCODE": "NO",
"DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym",
]
}
static var framework: SettingsDictionary {
base.merging([
"DEFINES_MODULE": "YES",
"SKIP_INSTALL": "YES",
"BUILD_LIBRARY_FOR_DISTRIBUTION": "NO",
])
}
static var appExtension: SettingsDictionary {
base.merging([
"APPLICATION_EXTENSION_API_ONLY": "YES",
])
}
}
extension SettingsDictionary {
// Helper untuk merge dengan override
func merging(_ other: SettingsDictionary) -> SettingsDictionary {
merging(other) { _, new in new }
}
}
swift
// Tuist/ProjectDescriptionHelpers/Target/Target+Feature.swift
import ProjectDescription
public enum FeatureLayer: String {
case core // Foundation, networking, storage — tidak boleh depend ke feature lain
case shared // SharedUI, SharedModels — boleh depend ke core
case feature // Feature modules — boleh depend ke core dan shared
case app // Main app — boleh depend ke semua
}
public extension Target {
static func make(
name: String,
layer: FeatureLayer,
product: Product = .framework,
dependencies: [TargetDependency] = [],
resources: ResourceFileElements? = nil,
additionalSources: [SourceFileGlob] = [],
hasTests: Bool = true,
testDependencies: [TargetDependency] = []
) -> [Target] {
var targets: [Target] = []
// Main target
let mainTarget = Target.target(
name: name,
destinations: .iOS,
product: product,
bundleId: "com.company.\(name.lowercased().replacingOccurrences(of: " ", with: "."))",
deploymentTargets: .iOS("17.0"),
sources: SourceFilesList(
globs: [.glob("Sources/**")] + additionalSources
),
resources: resources,
dependencies: dependencies,
settings: .settings(
base: layer == .app ? .base : .framework,
configurations: makeConfigurations(for: layer)
)
)
targets.append(mainTarget)
// Test target (opsional)
if hasTests {
let testTarget = Target.target(
name: "\(name)Tests",
destinations: .iOS,
product: .unitTests,
bundleId: "com.company.\(name.lowercased()).tests",
deploymentTargets: .iOS("17.0"),
sources: "Tests/**",
dependencies: [.target(name: name)] + testDependencies
)
targets.append(testTarget)
}
return targets
}
private static func makeConfigurations(for layer: FeatureLayer) -> [Configuration] {
let debugSettings: SettingsDictionary = ["OTHER_SWIFT_FLAGS": "$(inherited) -DDEBUG"]
let releaseSettings: SettingsDictionary = ["SWIFT_OPTIMIZATION_LEVEL": "-O"]
return [
.debug(name: "Debug", settings: debugSettings),
.release(name: "Staging", settings: releaseSettings),
.release(name: "Release", settings: releaseSettings),
]
}
}
swift
// Tuist/ProjectDescriptionHelpers/Project/Project+Module.swift
import ProjectDescription
public extension Project {
static func module(
name: String,
layer: FeatureLayer,
dependencies: [TargetDependency] = [],
resources: ResourceFileElements? = nil,
hasExampleApp: Bool = false,
testDependencies: [TargetDependency] = []
) -> Project {
var targets = Target.make(
name: name,
layer: layer,
dependencies: dependencies,
resources: resources,
testDependencies: testDependencies
)
// Example app untuk development isolation
if hasExampleApp {
let exampleTarget = Target.target(
name: "\(name)Example",
destinations: .iOS,
product: .app,
bundleId: "com.company.\(name.lowercased()).example",
deploymentTargets: .iOS("17.0"),
infoPlist: .default,
sources: "Example/Sources/**",
resources: "Example/Resources/**",
dependencies: [.target(name: name)]
)
targets.append(exampleTarget)
}
return Project(
name: name,
organizationName: "Company",
options: .options(
automaticSchemesOptions: .enabled(
targetSchemesGrouping: .singleScheme
)
),
targets: targets
)
}
}
Menggunakan Helper
swift
// Projects/Features/FeaturePayment/Project.swift
import ProjectDescription
import ProjectDescriptionHelpers
let project = Project.module(
name: "FeaturePayment",
layer: .feature,
dependencies: [
.project(target: "NetworkKit", path: "../../Core/NetworkKit"),
.project(target: "SharedUI", path: "../../Shared/SharedUI"),
.external(name: "Stripe"),
],
hasExampleApp: true,
testDependencies: [
.external(name: "Quick"),
.external(name: "Nimble"),
]
)
// Cukup 15 baris untuk mendefinisikan module lengkap dengan test target dan example app
Enforcing Architecture Rules via Helpers
swift
// Tuist/ProjectDescriptionHelpers/Architecture/ArchitectureLayer.swift
import ProjectDescription
// Enforce bahwa Core layer tidak boleh depend ke Feature layer
public extension TargetDependency {
// Tipe-safe dependency yang mencegah salah layer
static func core(_ name: String) -> TargetDependency {
.project(
target: name,
path: .relativeToManifest("../../Core/\(name)")
)
}
static func shared(_ name: String) -> TargetDependency {
.project(
target: name,
path: .relativeToManifest("../../Shared/\(name)")
)
}
static func feature(_ name: String) -> TargetDependency {
.project(
target: name,
path: .relativeToManifest("../../Features/\(name)")
)
}
}
// Usage — lebih ekspresif dan path-safe:
// dependencies: [.core("NetworkKit"), .shared("SharedUI")]
// vs sebelumnya:
// dependencies: [.project(target: "NetworkKit", path: "../../Core/NetworkKit")]