baegteun - iOS

Tuist 사용법 - 1. 이론 및 샘플 예제 본문

Tuist

Tuist 사용법 - 1. 이론 및 샘플 예제

baegteun 2022. 8. 15. 23:43

Tuist?

Xcode 프로젝트를 관리하는 툴입니다. Project.swift 파일을 통해서 프로젝트 설정을 관리할 수 있습니다.

Xcode 프로젝트 관리 툴?

.xcodeproj를 깃허브에 올려놓고 협업을 한다면 .pbxproj파일에서 충돌이 일어날 수 있습니다. 이를 예방하기 위해 코드 적으로 프로젝트를 관리 할 수 있게 해주는 것이 프로젝트 관리 툴입니다. 또한 모듈화를 할 때 모듈 구조를 관리하는 데에 편리합니다.

대표적으로 XcodeGen, Tuist 등이 있습니다.

Why Tuist?

위에서 나온 두 프로젝트 관리 툴의 가장 큰 차이점은 XcodeGen은 yml 또는 json 파일을 통해 프로젝트를 관리하지만 Tuist는 swift파일로 프로젝트를 관리할 수 있다는 것입니다.

여기에서 전체 문서를 확인할 수 있습니다

설치

curl -Ls https://install.tuist.io | bash

터미널에서 위 명령어를 입력하면 최신버전 Tuist가 설치됩니다.
이 글은 Tuist 버전 3.9.0 기준으로 진행되었습니다.

Manifests

Project.swift

Project.swift는 Tuist에서 .xcodeproj, 프로젝트를 어떻게 만들지 정의하는 파일입니다.

Project는 아래와 같은 initializer를 가집니다.

public init(
    name: String,
    organizationName: String? = nil,
    options: ProjectDescription.Project.Options = .options(),
    packages: [ProjectDescription.Package] = [],
    settings: ProjectDescription.Settings? = nil,
    targets: [ProjectDescription.Target] = [],
    schemes: [ProjectDescription.Scheme] = [],
    fileHeaderTemplate: ProjectDescription.FileHeaderTemplate? = nil,
    additionalFiles: [ProjectDescription.FileElement] = [],
    resourceSynthesizers: [ProjectDescription.ResourceSynthesizer] = .default
)

가장 핵심적인 부분은 name, organizationName, packages, targets, schemes입니다.

name

이름 그대로 프로젝트의 이름입니다. "asdf"를 넣었다면

이렇게 xcodeproj가 만들어집니다.

organizationName

이 친구 또한 말 그대로 organization의 이름을 의미합니다.

프로젝트 파일의 inspector를 봤을때 있는 Organization에 들어가는 그 organization이름입니다.

options

Tuist가 xcodeproj 파일을 만들때의 옵션을 설정해줄 수 있습니다.
Options에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

packages

Swift Package Manager의 package를 의미합니다. Xcode의 SPM을 사용할 경우 먼저 Github에서 패키지를 가져오고 추가해야 하는데 이때 쓰이기도 하고 여러분이 SPM으로 패키지를 직접 만들고 이를 가져올 때 쓰이기도 합니다. 라이브러리 가져와서 쓰는 건 나중에 추가로 다루도록 하겠습니다.
Package에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

settings

프로젝트 파일에 있는

Build Settings의 정보들을 설정해줍니다. Dictionary로 값을 줄 수 있는데 key 값은 이곳에서 찾을 수 있습니다.
Settings에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

targets

프로젝트의 타겟을 의미합니다. 프로젝트 파일을 보면 있는

이 TARGETS입니다. Target을 만들어서 Array로 넣어주면 그대로 만들어줍니다.
Target에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

schemes

프로젝트의 scheme을 의미합니다. Xcode로 프로젝트를 하나 열어서 중앙 최상단에서 약간 왼쪽을 보면 있는

이 Scheme들을 의미합니다.
Scheme에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

fileHeaderTemplate

내장 Xcode 템플릿에 Custom으로 파일 헤더를 만들 수 있어요. 예를 들어서

fileHeaderTemplate: "  ___COPYRIGHT___"

이렇게 만들고 프로젝트를 생성한 다음 swift파일을 만들면

//  Copyright © 2022 tuist.io. All rights reserved.

import Foundation

만약 ""으로 해놓으면

//

import Foundation

으로 생성됩니다. nil이면 기본 값으로

//
//  File.swift
//  GRIG
//
//  Created by 최형우 on 2022/08/12.
//  Copyright © 2022 baegteun. All rights reserved.
//

이렇게 생성이 됩니다.
FileHeaderTemplate에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

additionalFiles

additionalFiles는 Tuist에서 프로젝트를 만들때 Xcode에 자동으로 연결해주지 않는 파일을 넣으면 프로젝트에 연결시켜줍니다. 예를 들어서 README.md같은 파일은 프로젝트를 만들때 Xcode에는 자동으로 보여지지않는데 여기에 추가해준다면 Xcode에서도 볼 수 있어요. 아니면 통신할 서버가 GraphQL이여서 Apollo-iOS를 쓰신다면 .graphql 파일을 추가할 수도 있습니다.
additionalFiles에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

resourceSynthesizers

이걸 알기 전에 Tuist에서 제공해주는 기능을 하나 알아야합니다. tuist는 프로젝트를 생성할때 Resources/ 안에 파일 확장자에 따라 enum을 제공해준다는 것입니다. 예시로 Assets에 색이랑 이미지를 넣었다면 아래처럼 enum을 자동으로 생성해줍니다.

public enum CoreAsset {
  public enum Colors {
    public static let girgGray = CoreColors(name: "GIRG_Gray")
    public static let grigBackground = CoreColors(name: "GRIG_Background")
    public static let grigBlack = CoreColors(name: "GRIG_Black")
    public static let grigCompetePrimary = CoreColors(name: "GRIG_CompetePrimary")
    public static let grigCompeteSecondary = CoreColors(name: "GRIG_CompeteSecondary")
    public static let grigOnboardMain = CoreColors(name: "GRIG_OnboardMain")
    public static let grigPrimary = CoreColors(name: "GRIG_Primary")
    public static let grigPrimaryTextColor = CoreColors(name: "GRIG_PrimaryTextColor")
    public static let grigSecondaryTextColor = CoreColors(name: "GRIG_SecondaryTextColor")
    public static let grigWhite = CoreColors(name: "GRIG_White")
  }
  public enum Images {
    public static let grigCompeteIcon = CoreImages(name: "GRIG_CompeteIcon")
    public static let grigGithubIcon = CoreImages(name: "GRIG_GithubIcon")
    public static let grigLogo = CoreImages(name: "GRIG_Logo")
    public static let grigOnboard1 = CoreImages(name: "GRIG_Onboard1")
    public static let grigOnboard2 = CoreImages(name: "GRIG_Onboard2")
    public static let grigOnboard3 = CoreImages(name: "GRIG_Onboard3")
    public static let grigOnboard4 = CoreImages(name: "GRIG_Onboard4")
    public static let grigSword = CoreImages(name: "GRIG_Sword")
  }
}

다시 본론으로 와서 resourceSynthesizers란 위에서 제공해주지 않는 확장자들이 Resources/ 에 들어가 있을때 어떻게 자동 생성해줄지에 대해 .stencil파일로 정의해주면 프로젝트를 생성할 때 .stencil내용에 따라 생성해준답니다. 예시로 프로젝트에 Lottie를 쓴다면 Resources/ 에 .json파일이 들어갔을때 어떻게 자동 생성 시킬지에 대해 만들 수 있습니다. .default는 strings, plists, fonts, assets를 자동으로 생성해줍니다.
resourceSynthesizers에 대한 자세한 내용은 여기에서 확인할 수 있습니다.

Workspace.swift

Workspace.swift는 Tuist에서 .xcworkspace, 워크스페이스를 어떻게 만들지 정의하는 파일입니다.

Workspace는 아래와 같은 initializer를 가집니다.

public init(
    name: String,
    projects: [ProjectDescription.Path],
    schemes: [ProjectDescription.Scheme] = [],
    fileHeaderTemplate: ProjectDescription.FileHeaderTemplate? = nil,
    additionalFiles: [ProjectDescription.FileElement] = [],
    generationOptions: ProjectDescription.Workspace.GenerationOptions = .options()
)

가장 핵심적인 부분은 name, projects입니다.

name

네. 워크스페이스의 이름입니다.

projects

Workspace에 등록할 프로젝트들의 경로를 넣어주면 됩니다. struct인 Path를 받지만 그냥 문자열로 넣어주셔도 됩니다. 기본 경로는 프로젝트의 루트 디렉토리를 기준입니다.
projects에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

schemes, fileHeaderTemplate, additionalFiles

위의 Project에서 설명한 내용이랑 똑같습니다.
자세한 정보는 각각 scheme,
fileheadertemplate, additionalfiles, 에서 확인할 수 있습니다.

generationOptions

Tuist가 xcworkspace 파일을 만들때의 옵션을 설정해줄 수 있습니다.
GenerationOptions에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

Config.swift

프로젝트 전역으로 쓰이는 설정을 설정해줄 수 있습니다. 예를 들어서 Swift의 버전이나 Xcode의 버전 같은게 있습니다.
Config.swift는
{프로젝트 루트 디렉토리}/Tuist/Config.swift
에 있을 때만 적용됩니다.

Target

Project.swift에서 언급했던 Target입니다. Target은 사용할 모듈을 정의하는 struct입니다.

Target은 아래와 같은 initializer를 가집니다.

public init(
    name: String,
    platform: ProjectDescription.Platform,
    product: ProjectDescription.Product,
    productName: String? = nil,
    bundleId: String,
    deploymentTarget: ProjectDescription.DeploymentTarget? = nil,
    infoPlist: ProjectDescription.InfoPlist? = .default,
    sources: ProjectDescription.SourceFilesList? = nil,
    resources: ProjectDescription.ResourceFileElements? = nil,
    copyFiles: [ProjectDescription.CopyFilesAction]? = nil,
    headers: ProjectDescription.Headers? = nil,
    entitlements: ProjectDescription.Path? = nil,
    scripts: [ProjectDescription.TargetScript] = [],
    dependencies: [ProjectDescription.TargetDependency] = [],
    settings: ProjectDescription.Settings? = nil,
    coreDataModels: [ProjectDescription.CoreDataModel] = [],
    environment: [String : String] = [:],
    launchArguments: [ProjectDescription.LaunchArgument] = [],
    additionalFiles: [ProjectDescription.FileElement] = []
)

가장 핵심적인 부분은 name, platform, product, deploymentTarget, infoPlist, sources, resources, entitlements, scripts, dependencies입니다.

name

Target의 이름입니다. 아래처럼 생깁니다.

platform

iOS, macOS, tvOS, watchOS 같은 플랫폼입니다.
platform에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

product

app, appClips, staticFramework, framework, unitTest 같은겁니다.
product에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

productName

The built product name. 만들어진 product의 이름입니다.
productName에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

bundleId

프로젝트 파일을 열었을때 보이는 Bundle Identifier입니다.

deploymentTarget

배포타겟을 설정할 수 있습니다. iOS, macOS, tvOS, watchOS가 있고 버전을 입력받습니다. iOS같은 경우 디바이스도 받으면서 ipad, iphone, mac 3종류가 있습니다.
deploymentTarget에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

infoPlist

Info.plist를 정의합니다. 기본으로 제공되는 것을 쓸 수도 있고 key 값에 따라 value를 넣어주면 커스텀으로 추가적으로 값을 넣어줄 수 있습니다. 또는 미리 Info.plist를 넣어놓고 경로를 줄 수도 있습니다.
info.plist에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

sources

소스 코드의 경로를 입력해주면 됩니다. [ ] 안에 문자열로 경로를 입력해도 됩니다.
sources에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

resources

앞서서 resourceSynthesizers에서 Tuist가 Resources/ 의 리소스들을 자동으로 코드화한다고 했는데, 그때 이 리소스들이 어디에 있는지에 대한 경로입니다. sources와 같이 [ ] 안에 문자열로 경로를 입력해도 됩니다.
resources에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

copyFiles

Target에 대한 Build Phase 파일 복사 작업입니다.
copyFiles에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

headers

Target에 대한 headers입니다.
headers에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

entitlements

Target에 대한 entitlements의 경로를 입력해주시면 됩니다. macOS 프로젝트를 만들어봤거나 push알람 해보신 분은 본적있으실거에요.
entitlements에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

scripts

Target에 대한 Build Phase 스크립트 작업입니다.
scripts는 나중에 SwiftLint와 함께 다뤄보겠습니다.
scripts에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

dependencies

Target의 의존성에 대한 것입니다. 라이브러리나 다른 모듈을 의존성으로 넣을 때 씁니다.
라이브러리는 나중에 다뤄보겠습니다.
dependencies에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

settings

Target의 세팅을 정의합니다.
settings에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

coreDataModels

CoreData의 모델들의 경로랑 버전을 정의합니다
coreDataModels에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

environment

scheme에서 Edit Scheme... 버튼을 누르시면 나오는 창에서 Environment Variables를 설정할 수 있는데 이때 environment를 설정하면 자동으로 생성합니다.

environment에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

launchArguments

scheme에서 Edit Scheme... 버튼을 누르시면 나오는 창에서 Arguments Passed On Launch를 설정할 수 있는데 이때 launchArguments 설정하면 자동으로 생성합니다.

launchArguments에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

additionalFiles

프로젝트를 생성할때 자동으로 생겨나지 않는 파일을 등록해놓으면 Xcode에서 볼 수 있습니다.

샘플 프로젝트

지금까지는 이론으로만 알아봤습니다. 그러니 한 번 샘플을 하나 같이 만들어보도록 하겠습니다.

모듈화를 하기 이전에 어떤 모듈을 만들지에 대한 기준을 만들어야합니다.
음.. 저는 아주 간단하게 AppDelegate, SceneDelegate가 있을 App 모듈, 서버와 통신하거나 DB에 저장하기 위한 Service 모듈, ViewController, ViewModel, Reactor 등 실제 기능이 들어갈 Feature 모듈, 라이브러리를 가져다 쓸 ThirdPartyLib 모듈로 총 4개의 모듈만 만들어보겠습니다.

먼저 터미널에서 작업할 디렉토리를 만들고, 이동해줍시다.

mkdir Sample && cd sample

Tuist를 생성해줍니다

tuist init --platform ios

처음부터 해보기 위해 Targets/ 와 Plugins/ 을 싹다 삭제해줍니다.

그리고 나서 터미널에

tuist edit

을 입력해줍시다. 그러면 Xcode가 열릴겁니다.

Project.swift 파일을 삭제하고, Project+Templates.swift 파일의 내용물을 전부 지워주세요.

그리고 Config.swift의 내용이

import ProjectDescription

let config = Config(
    plugins: [
        .local(path: .relativeToManifest("../../Plugins/Sample")),
    ]
)

일텐데

import ProjectDescription

let config = Config(

)

으로 만들어주세요. Plugin는 나중에 다뤄보도록 하겠습니다.

Project+Templates.swift로 갑시다. 여기서 Project를 만드는 메서드를 만들겁니다.

모듈 생성 Method

전체코드 :

import ProjectDescription

public extension Project {
    static func makeModule(
        name: String,
        platform: Platform = .iOS,
        product: Product,
        organizationName: String = "baegteun",
        packages: [Package] = [],
        deploymentTarget: DeploymentTarget? = .iOS(targetVersion: "15.0", devices: [.iphone, .ipad]),
        dependencies: [TargetDependency] = [],
        sources: SourceFilesList = ["Sources/**"],
        resources: ResourceFileElements? = nil,
        infoPlist: InfoPlist = .default
    ) -> Project {
        let settings: Settings = .settings(
            base: [:],
            configurations: [
                .debug(name: .debug),
                .release(name: .release)
            ], defaultSettings: .recommended)

        let appTarget = Target(
            name: name,
            platform: platform,
            product: product,
            bundleId: "\(organizationName).\(name)",
            deploymentTarget: deploymentTarget,
            infoPlist: infoPlist,
            sources: sources,
            resources: resources,
            dependencies: dependencies
        )

        let testTarget = Target(
            name: "\(name)Tests",
            platform: platform,
            product: .unitTests,
            bundleId: "\(organizationName).\(name)Tests",
            deploymentTarget: deploymentTarget,
            infoPlist: .default,
            sources: ["Tests/**"],
            dependencies: [.target(name: name)]
        )

        let schemes: [Scheme] = [.makeScheme(target: .debug, name: name)]

        let targets: [Target] = [appTarget, testTarget]

        return Project(
            name: name,
            organizationName: organizationName,
            packages: packages,
            settings: settings,
            targets: targets,
            schemes: schemes
        )
    }
}

extension Scheme {
    static func makeScheme(target: ConfigurationName, name: String) -> Scheme {
        return Scheme(
            name: name,
            shared: true,
            buildAction: .buildAction(targets: ["\(name)"]),
            testAction: .targets(
                ["\(name)Tests"],
                configuration: target,
                options: .options(coverage: true, codeCoverageTargets: ["\(name)"])
            ),
            runAction: .runAction(configuration: target),
            archiveAction: .archiveAction(configuration: target),
            profileAction: .profileAction(configuration: target),
            analyzeAction: .analyzeAction(configuration: target)
        )
    }
}

차례대로 해석해보겠습니다.

let settings: Settings = .settings(
    base: [:],
    configurations: [
        .debug(name: .debug),
        .release(name: .release)
    ], defaultSettings: .recommended)

settings입니다. base가 빈 Dictionary인데 알맞은 key와 value를 넣어주면 Build Settings에 반영됩니다. configurations는 이름 그대로 Project의 configurations를 설정해줄 수있습니다. defaultSettings은 .recommended로 했는데 혹시 xcconfig를 쓴다면 .none으로 해놓는게 편할 수도 있습니다.

let appTarget = Target(
    name: name,
    platform: platform,
    product: product,
    bundleId: "\(organizationName).\(name)",
    deploymentTarget: deploymentTarget,
    infoPlist: infoPlist,
    sources: sources,
    resources: resources,
    dependencies: dependencies
)

let testTarget = Target(
    name: "\(name)Tests",
    platform: platform,
    product: .unitTests,
    bundleId: "\(organizationName).\(name)Tests",
    deploymentTarget: deploymentTarget,
    infoPlist: .default,
    sources: ["Tests/**"],
    dependencies: [.target(name: name)]
)
...
let targets: [Target] = [appTarget, testTarget]

Target, 모듈을 정의하는 부분입니다. Test를 하기 위해서는 별도로 Target을 빼야했기에 TestTarget도 따로 정의하고 appTarget을 dependency로 가집니다.

let schemes: [Scheme] = [.makeScheme(target: .debug, name: name)]
...

extension Scheme {
    static func makeScheme(target: ConfigurationName, name: String) -> Scheme {
        return Scheme(
            name: name,
            shared: true,
            buildAction: .buildAction(targets: ["\(name)"]),
            testAction: .targets(
                ["\(name)Tests"],
                configuration: target,
                options: .options(coverage: true, codeCoverageTargets: ["\(name)"])
            ),
            runAction: .runAction(configuration: target),
            archiveAction: .archiveAction(configuration: target),
            profileAction: .profileAction(configuration: target),
            analyzeAction: .analyzeAction(configuration: target)
        )
    }
}

schemes을 정의하는 부분입니다.
static func makeScheme 으로 따로 scheme을 만드는 method를 만들었습니다.

return Project(
    name: name,
    organizationName: organizationName,
    packages: packages,
    settings: settings,
    targets: targets,
    schemes: schemes
)

name, organizationName, packages 그리고 위에서 만들어놓은 settings, targets, schemes를 넣어서 Project를 만들어서 return해줍니다.

모듈 구조 만들기

모듈을 만드는 method를 만들었으니 실제로 모듈들을 만들어보겠습니다.
지금까지 똑같이 하셨다면

이런 상태일겁니다.
그러면 이제 Manifests Group 안에 Projects Group을 만들고, 그 안에 App, Service, Feature, ThirdPartyLib 총 4개의 Group을 만들어주겠습니다.

App 모듈

App부터 내용물을 채워나가 보겠습니다. 먼저 Sources Group을 만들고 그 안에 또 Application Group을 만들겠습니다. 그리고 그 안에 AppDelegate.swift와 SceneDelegate.swift 파일을 만들겠습니다. 그리고 각각 코드를 넣어주겠습니다. 또는 직접 파일을 넣어주셔도 됩니다.

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(
        _ application: UIApplication,
        configurationForConnecting connectingSceneSession: UISceneSession,
        options: UIScene.ConnectionOptions
    ) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(
        _ application: UIApplication,
        didDiscardSceneSessions sceneSessions: Set<UISceneSession>
    ) {}
}
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let scene = (scene as? UIWindowScene) else { return }
    }

    func sceneDidDisconnect(_ scene: UIScene) {}

    func sceneDidBecomeActive(_ scene: UIScene) {}

    func sceneWillResignActive(_ scene: UIScene) {}

    func sceneWillEnterForeground(_ scene: UIScene) {}

    func sceneDidEnterBackground(_ scene: UIScene) {}
}

그리고 돌아가서 App Group아래에 Resources Group을 만들겠습니다. 이 안에는 AppIcon을 위한 Assets과 LaunchScreen.storyboard을 넣겠습니다.

Assets.xcassets.zip
1.5 kB
LaunchScreen.storyboard
1.7 kB

마지막으로 App Group아래에 Tests Group을 만들겠습니다. 여기에는 .gitkeep 파일을 만들겠습니다.

이제 App 모듈의 내용물은 만들었으니 프로젝트를 정의하겠습니. App Group아래에 Project.swift 파일을 만들겠습니다.

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.makeModule(
    name: "Sample",
    platform: .iOS,
    product: .app,
    dependencies: [
        .project(target: "Feature", path: .relativeToRoot("Projects/Feature"))
    ],
    resources: ["Resources/**"],
    infoPlist: .extendingDefault(with: [
        "UIMainStoryboardFile": "",
        "UILaunchStoryboardName": "LaunchScreen",
        "ENABLE_TESTS": .boolean(true),
    ])
)

안에 소스 코드는 이렇게 됩니다.
resources는 경로를 넣어줬는데 sources는 왜 안넣었냐면 makeModule method에 sources에 기본 값으로 ["Sources/**"]가 있습니다. infoPlist에 .extendingDefault로 Info.plsit에 추가 내용을 넣어준 이유는 tuist에서 .default 만들어주는 Info.plist는 앱을 실행할 때 화면이 어딘가 나사가 빠진상태로 실행되기 때문입니다.
dependencies는 Feature 모듈을 의존성으로 추가해줍니다.

import ProjectDescriptionHelpers 이부분에서 에러가 날 수도 있는데 일단은 무시해주세요. 별 문제 안됩니다.

if Info.plsit의 자동완성을 지원하고 싶다면?

이게 무슨 소리냐, 면은 Tuist에서 만들어 주는 Info.plist는 자동완성이 되지 않습니다. 그래서 Source Code에서 직접 key를 찾아서 value를 넣어줘야합니다. 하지만 만약 Xcode로 프로젝트를 만들 때의 Info.plist를 직접 넣어 .extendingDefault로 추가 옵션을 넣어주는게 아닌 경로를 지정해줘서 infoPlist를 지정해준다면 에디터 내에서 Property List로 보면서 자동완성이 사용가능합니다.

Info.plist
1.9 kB

App Group안에 Support Group을 만들어줍니다. 그리고 그 안에 다운받은 Info.plist를 넣어주세요.

그리고 Project.swift 파일의 내용물은

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.makeModule(
    name: "Sample",
    platform: .iOS,
    product: .app,
    dependencies: [
        .project(target: "Feature", path: .relativeToRoot("Projects/Feature"))
    ],
    resources: ["Resources/**"],
    infoPlist: .file(path: "Support/Info.plist")
)

로 변경해주겠습니다. 그러면 이제 Info.plist가 자동완성이 지원됩니다.


App 모듈의 구성은 완료되었습니다. 그러면 아래처럼 보일겁니다. (Info.plist는 다를 수 있습니다)

Feature 모듈

다음으로 Feature 모듈의 내용물을 구성해보겠습니다.

먼저 Feature Group아래에 Sources Group을 만들겠습니다. 그리고 Sources Group아래에는 Feature.swift를 만들어주세요. 안에 코드는 비워둡니다. Feature.swift를 만든 이유는 나중에 프로젝트를 만들때, Group이 비어있다면 Xcode에서 보이지 않게됩니다. 그래서 약간 XcodeKeep(?)같은 느낌으로 세워둡니다. 메인코드를 만들면 나중에 Feature.swift를 제거해줄겁니다.

그러고 이제 Resources Group을 만들겠습니다. 이 안에는 Assets를 넣어줄건데, 그냥 cmd + n을 눌러서 Asset Catalog을 추가해주세요. App에 넣어준 Assets랑 달리 AccentColor, AppIcons를 없앤 비어있는 Assets를 넣어줄겁니다. 또는 Colors.xcassets와 Images.xcassets이런식으로 만들어줄 수도 있습니다.

그리고 Tests Group을 만들어서 이 안에 FeatureTests.swift를 만들고 빈 파일로 만들어주세요. Sources에 만든 Feature.swift와 같은 역할입니다.

그리고 이제 Project.swift 파일을 만들겠습니다. Feature Group아래에 Project.swift 파일을 만들어주세요.
그리고 그 안에는

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.makeModule(
    name: "Feature",
    product: .staticFramework,
    dependencies: [
        .project(target: "Service", path: .relativeToRoot("Projects/Service"))
    ],
    resources: ["Resources/**"]
)

를 넣어주세요.

이러면 Feature 모듈도 작업이 끝났습니다.

Service

다음으로 Service 모듈 내용물을 구성해보겠습니다.
Service Group 아래에 Sources Group을 만들어 주고 그 안에 내용물이 빈 Service.swift 파일을 만들어주세요. 그리고 Tests Group을 만들고 ServiceTests.swift 파일을 만들고 내용물은 똑같이 비워주세요. 이번에는 Resources Group를 만들지 않겠습니다. 아무래도 Service 모듈에서는 이미지나 컬러같은 리소스들을 쓸 일이 없기에 Resources는 스킵해줍니다.

이제 Project.swift 파일을 만들어주겠습니다. 내용물은

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.makeModule(
    name: "Service",
    product: .staticFramework,
    dependencies: [
        .project(target: "ThirdPartyLib", path: .relativeToRoot("Projects/ThirdPartyLib"))
    ]
)

로 채워주겠습니다.

ThirdPartyLib

마지막으로 ThirdPartyLib 모듈을 만들어보겠습니다.

ThirdPartyLib 모듈은 오직 라이브러리를 갖다 쓰기 위한 용도이기 때문에 소스코드나 테스트코드는 필요없을겁니다. 그리고 Sources Group과 Tests Group을 만들고 .gitkeep 파일만 넣어주세요. 바로 Project.swift 파일을 작업해주겠습니다. 이 안에는

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.makeModule(
    name: "ThirdPartyLib",
    product: .framework,
    packages: [],
    dependencies: []
)

이렇게 코드를 넣어주세요.
외부 의존성 관리하는건 다음에 다뤄보겠습니다.

Workspace

이렇게 4개의 모듈을 만들었습니다. 정말 마지막으로 .xcodeproj가 여러개라면 .xcworkspace가 필요하기에 Workspace.swift를 만들겁니다. Manifests Group아래에 Workspace.swift 파일을 만들어주세요. 내용물은 아래처럼 채워주세요.

import ProjectDescription

let workspace = Workspace(
    name: "Sample",
    projects: [
        "Projects/App"
    ]
)

끝났습니다! 이제 터미널에

tuist generate

를 입력해주세요. 그러면 프로젝트가 만들어질겁니다. 자동으로 Xcode가 열리면서 아래와 같은 결과를 볼 수 있습니다.

결론

Tuist에 대해 알아보고, 샘플 프로젝트도 만들어봤습니다.

여기에서 샘플을 볼 수 있습니다.

'Tuist' 카테고리의 다른 글

Tuist 사용법 - 6. 버전 고정  (0) 2022.09.19
Tuist 사용법 - 5. Plugin  (0) 2022.08.28
Tuist 사용법 - 4. Script  (5) 2022.08.20
Tuist 사용법 - 3. extension  (2) 2022.08.19
Tuist 사용법 - 2. 외부 의존성  (0) 2022.08.17
Comments