iOS Dev

[Tuist] Tuist를 활용하여 SwiftUI 클린 아키텍쳐를 적용한 모듈로 나눈 후 Github에 업로드 하기 2탄!

Mosu(정종인) 2023. 5. 7. 01:04
반응형

1탄 : https://sunrinnote.tistory.com/172

 

[Tuist] Tuist를 활용하여 SwiftUI 클린 아키텍쳐를 적용한 모듈로 나눈 후 Github에 업로드 하기

Tuist라는 툴이 있다. 모듈화, 패키지 의존 관리, 타겟 관리 등등을 지원해준다고 한다. Tuist 자체를 소개해주는 블로그는 많이 있으니 건너뛴다. https://nsios.tistory.com/183 https://nsios.tistory.com/195 목표

sunrinnote.tistory.com

자동완성이 안되는 이유를 알았다. 그것은.. public을 안줘서..ㅠ

tuist edit으로 edit 페이지에 들어간 다음 Tuist/ProjectDescriptionHelpers/Project+Templates 코드를 다음과 같이 수정한다.

import ProjectDescription

/// Project helpers are functions that simplify the way you define your project.
/// Share code to create targets, settings, dependencies,
/// Create your own conventions, e.g: a func that makes sure all shared targets are "static frameworks"
/// See https://docs.tuist.io/guides/helpers/

public enum ModuleType: String, CaseIterable {
    case DoForestApp
    case DoForestCommon
    case DoForestCommonUI
    case DoForestData
    case DoForestDomain
    case DoForestEntity
    case DoForestMain
    case DoForestRoomDetail
    case DoForestSetting
    
    var dependencies: [TargetDependency] {
        switch self {
        case .DoForestApp:
            return [
                .with(.DoForestMain),
                .with(.DoForestRoomDetail),
                .with(.DoForestSetting),
                .with(.DoForestData)
            ]
        case .DoForestCommon:
            return []
        case .DoForestCommonUI:
            return []
        case .DoForestData:
            return [
                .with(.DoForestDomain)
            ]
        case .DoForestDomain:
            return [
                .with(.DoForestEntity)
            ]
        case .DoForestEntity:
            return []
        case .DoForestMain:
            return [
                .with(.DoForestDomain),
                .with(.DoForestCommon),
                .with(.DoForestCommonUI)
            ]
        case .DoForestRoomDetail:
            return [
                .with(.DoForestDomain),
                .with(.DoForestCommon),
                .with(.DoForestCommonUI)
            ]
        case .DoForestSetting:
            return [
                .with(.DoForestDomain),
                .with(.DoForestCommon),
                .with(.DoForestCommonUI)
            ]
        }
    }
}

private let infoPlist: [String: InfoPlist.Value] = [
    "CFBundleShortVersionString": "1.0",
    "CFBundleVersion": "1",
    "UIMainStoryboardFile": "",
    "UILaunchStoryboardName": "LaunchScreen"
]

public extension Project {
    /// Helper function to create the Project for this ExampleApp
    static func app(name: String, platform: Platform, additionalTargets: [String]) -> Project {
        var targets = makeAppTargets(name: name,
                                     platform: platform,
                                     dependencies: additionalTargets.map { TargetDependency.target(name: $0) })
        targets += additionalTargets.flatMap({ makeFrameworkTargets(name: $0, platform: platform) })
        return Project(name: name,
                       organizationName: "yourssu.com",
                       targets: targets)
    }
    
    static func app(_ type: ModuleType) -> Project {
        let name = type.rawValue
        let dependencies = type.dependencies
        return Project(name: name,
                       organizationName: "yourssu.com",
                       targets: [
                        Target(name: name,
                               platform: .iOS,
                               product: .app,
                               bundleId: "com.yourssu.\(name)",
                               infoPlist: .extendingDefault(with: infoPlist),
                               sources: ["Sources/**"],
                               resources: ["Resources/**"],
                               dependencies: dependencies
                              )
                       ])
    }
    
    static func framework(_ type: ModuleType) -> Project {
        let name = type.rawValue
        let dependencies = type.dependencies
        return Project(name: name,
                       organizationName: "yourssu.com",
                       targets: [
                        Target(name: name,
                               platform: .iOS,
                               product: .framework,
                               bundleId: "com.yourssu.\(name)",
                               infoPlist: .extendingDefault(with: infoPlist),
                               sources: ["Sources/**"],
                               resources: ["Resources/**"],
                               dependencies: dependencies
                              )
                       ])
    }
    
    // MARK: - Private
    
    /// Helper function to create a framework target and an associated unit test target
    private static func makeFrameworkTargets(name: String, platform: Platform) -> [Target] {
        let sources = Target(name: name,
                             platform: platform,
                             product: .framework,
                             bundleId: "com.yourssu.\(name)",
                             infoPlist: .default,
                             sources: ["Targets/\(name)/Sources/**"],
                             resources: [],
                             dependencies: [])
        let tests = Target(name: "\(name)Tests",
                           platform: platform,
                           product: .unitTests,
                           bundleId: "com.yourssu.\(name)Tests",
                           infoPlist: .default,
                           sources: ["Targets/\(name)/Tests/**"],
                           resources: [],
                           dependencies: [.target(name: name)])
        return [sources, tests]
    }
    
    /// Helper function to create the application target and the unit test target.
    private static func makeAppTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] {
        let platform: Platform = platform
        let infoPlist: [String: InfoPlist.Value] = [
            "CFBundleShortVersionString": "1.0",
            "CFBundleVersion": "1",
            "UIMainStoryboardFile": "",
            "UILaunchStoryboardName": "LaunchScreen"
        ]
        
        let mainTarget = Target(
            name: name,
            platform: platform,
            product: .app,
            bundleId: "com.yourssu.\(name)",
            infoPlist: .extendingDefault(with: infoPlist),
            sources: ["Targets/\(name)/Sources/**"],
            resources: ["Targets/\(name)/Resources/**"],
            dependencies: dependencies
        )
        
        let testTarget = Target(
            name: "\(name)Tests",
            platform: platform,
            product: .unitTests,
            bundleId: "com.yourssu.\(name)Tests",
            infoPlist: .default,
            sources: ["Targets/\(name)/Tests/**"],
            dependencies: [
                .target(name: "\(name)")
            ])
        return [mainTarget, testTarget]
    }
}

public extension TargetDependency {
    static func with(_ type: ModuleType) -> Self {
        let name = type.rawValue
        return .project(target: name, path: .relativeToRoot("DoForest/\(name)"))
    }
}

 

이제 각 모듈 안에 있는 Project.swift 파일을 모조리 수정한다.

App모듈인 DoForestApp 폴더 안에는 다음과 같이.

//
//  Project.swift
//  ProjectDescriptionHelpers
//
//  Created by 정종인 on 2023/04/03.
//

import Foundation
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.app(.DoForestApp)

 

Framework 모듈인 DoForestMain에는 다음과 같이.

import Foundation
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.framework(.DoForestMain)

좋다 좋다!

 

tuist graph로 모듈 구조를 한번 보자.

이렇게 생겼는데, 여기서 DoForestCommon을 DoForestDomain에 연결할 것이다.
ModuleType을 다음과 같이 수정해준다. 다른거 바꿀 필요 없이 이것만 바꾸면 적용이 된다!!

public enum ModuleType: String, CaseIterable {
    case DoForestApp
    case DoForestCommon
    case DoForestCommonUI
    case DoForestData
    case DoForestDomain
    case DoForestEntity
    case DoForestMain
    case DoForestRoomDetail
    case DoForestSetting
    
    var dependencies: [TargetDependency] {
        switch self {
        case .DoForestApp:
            return [
                .with(.DoForestMain),
                .with(.DoForestRoomDetail),
                .with(.DoForestSetting),
                .with(.DoForestData)
            ]
        case .DoForestCommon:
            return [
                .with(.DoForestDomain)
            ]
        case .DoForestCommonUI:
            return []
        case .DoForestData:
            return [
                .with(.DoForestDomain)
            ]
        case .DoForestDomain:
            return [
                .with(.DoForestEntity)
            ]
        case .DoForestEntity:
            return []
        case .DoForestMain:
            return [
                .with(.DoForestCommon),
                .with(.DoForestCommonUI)
            ]
        case .DoForestRoomDetail:
            return [
                .with(.DoForestCommon),
                .with(.DoForestCommonUI)
            ]
        case .DoForestSetting:
            return [
                .with(.DoForestCommon),
                .with(.DoForestCommonUI)
            ]
        }
    }
}

 

이런 모듈 구조가 되었다. 굳굳!!

추가로, Workspace.swift도 수정해주자.

import ProjectDescription
import ProjectDescriptionHelpers

private let projects: [Path] = ModuleType.allCases.map { moduleType in
    "DoForest/\(moduleType.rawValue)"
}

let workspace = Workspace(
    name: "DoForest",
    projects: projects
)

전체 코드:

DoForest.zip
0.50MB

반응형