ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Tuist활용하여 멀티 모듈 SwiftUI 프로젝트 생성하기
    개발/Swift 2024. 10. 15. 22:26

    https://inf.run/V3b51

     

    iOS Clean Architecture & MVVM: RxSwift 완전 정복 강의 | 덤벨로퍼 - 인프런

    덤벨로퍼 | , iOS Clean Architecture & MVVM: RxSwift 완전 정복현업에서 Clean Architecture와 MVVM 패턴은 이미 널리 사용되고 있으며, 많은 채용 공고에서도 필수 역량으로 요구되고 있습니다. 이 강의는 Clean Ar

    www.inflearn.com

     

     

     tuist init --platform ios --template swiftui
    

    tuist init --platform ios --template swiftui 명령어를 사용하여 SwiftUI 용으로 프로젝트를 초기화합니다. 이때, 명령어는 반드시 빈 폴더에서 실행해야 합니다.

     

    tuist edit 명령어를 통해 프로젝트 설정을 할 수 있습니다.

    멀티 모듈 형태로 구성할 것이기 때문에 Project 대신 Workspace를 사용합니다. 기존의 Project 파일은 지워버립니다.

     

    Workspace.swift 파일을 생성하고 다음과 같이 내용을 작성합니다.

    let workspace = Workspace(
        name: "MyWeather",
        projects: [
            "Projects/**"
        ]
    )
    
    

    이렇게 그냥 간단지게 만들어버림

     

    프로젝트 폴더 구조 및 Project.swift 파일 생성

    다음으로 Project 폴더를 생성하고 그 안에 App (메인 애플리케이션) 폴더와 Module (생성할 모듈) 폴더를 생성합니다.

    Module 폴더 안에는 예시로 Network 모듈Entity 모듈을 생성했습니다.

    이제 각 폴더 (App, Network, Entity 등) 내부에 Project.swift 파일을 새로 생성하여 각 모듈의 설정을 정의합니다.

     

     

     

     

    App에서 Project 객체를 생성

    App 폴더의 Project.swift 파일에서는 메인 애플리케이션의 Project 객체를 생성합니다.
    import Foundation
    import ProjectDescription
    
    let infoPlist: [String: InfoPlist.Value] = [:]
    
    let project = Project(
        name: "MyWeather", // 프로젝트 이름 지정
        organizationName: "MyName",
        targets: [
            Target(
                name: "MyWeather",
                platform: .iOS,
                product: .app, // 애플리케이션 타입 지정
                bundleId: "simon.kang.myweather", // 번들 아이디 지정
                infoPlist: .extendingDefault(with: infoPlist),
                sources: ["Sources/**"], // 소스 파일 경로 지정
                resources: ["Resources/**"], // 리소스 파일 경로 지정
                dependencies: [
                    .project(target: "Network", path: "../Network"), // Network 모듈 종속성 추가
                    .project(target: "Entity", path: "../Entity"),   // Entity 모듈 종속성 추가
                    // 에러 발생 시 해당 .project 라인을 잠시 지웠다가 나중에 다시 추가해 볼 수 있습니다. [2, 3]
                ]
            )
        ]
    )

    위 코드에서 번들 아이디, 이름, 소스 경로, 리소스 경로 등을 지정했습니다. 이에 맞게 App 폴더 내부에 Sources 폴더와 Resources 폴더를 생성해야 합니다.

     

    모듈 (Framework)의 Project 객체 생성

    모듈 폴더 (예: Entity, Network) 내부에서도 App과 마찬가지로 Project.swift 파일을 생성합니다.

    모듈의 Project.swift는 App과 유사하지만 다음과 같은 차이점이 있습니다.
    name: 모듈 이름으로 변경합니다. (예: "Entity", "Network")
    product: .framework 로 변경하여 프레임워크 형태로 빌드되도록 합니다.
    dependencies: 빈 배열 [] 로 설정합니다 (기본적으로 다른 모듈에 의존하지 않는다고 가정).

     

    import Foundation
    import ProjectDescription
    
    let infoPlist: [String: InfoPlist.Value] = [:]
    let project = Project(
        name: "Entity", // 모듈 이름 지정
        organizationName: "MyName",
        targets: [
            Target(
                name: "Entity",
                platform: .iOS,
                product: .framework, // 프레임워크 타입 지정
                bundleId: "simon.kang.myweather", // App과 동일한 번들 아이디 사용
                infoPlist: .extendingDefault(with: infoPlist),
                sources: ["Sources/**"], // 소스 파일 경로 지정
                dependencies: [ // 빈 배열
                ]
            )
        ]
    )

     

    infoPlist 변수명같다고 compile 에러났지만 무시하니 사라졌음

    이 모듈 폴더들 내부에도 App과 마찬가지로 Sources 폴더를 만들어줘야 합니다.

    이러고 tuist generate

     

    tuist generate

    이 명령어를 실행하면 설정한 대로 **세 개의 모듈 (App, Network, Entity)**이 생성됩니다.

    이제 생성된 Xcode Workspace를 열고, 각 Project 내부에 Source 폴더를 추가해주고, 각 모듈의 Targets 설정에 맞게 파일 하나씩 아무거나 추가해줘야 Xcode가 제대로 인식합니다.

    SwiftUI 프로젝트이므로 App 모듈의 Sources 폴더에 SwiftUI Main View 파일도 하나 만들어줘야 합니다.

     

    import 까지 성공

    외부 라이브러리사용

    외부 라이브러리를 사용하기 위해서는 tuist edit 명령어를 실행하여 Tuist 폴더 내부에 Dependencies.swift 파일을 생성합니다.

    Dependencies.swift 파일에는 사용할 외부 라이브러리 정보를 Swift Package Manager (SPM) 형태로 정의합니다.

    import ProjectDescription
    
    let spm = SwiftPackageManagerDependencies( [
        .remote(url: "<https://github.com/Alamofire/Alamofire.git>", requirement: .upToNextMajor(from: "5.10.0"))
    ])
    
    let dependencies = Dependencies(
        swiftPackageManager: spm,
        platforms: [.iOS]
    )
    
    

     

    위 코드는 Alamofire 라이브러리를 SPM 종속성으로 추가하는 예시입니다.

    이제 이 라이브러리가 필요한 모듈의 Project.swift 파일 (예: Network 모듈)의 dependencies 배열에 추가해 줍니다.

     

    그리고 이 라이브러리가 필요한 Network모듈 project에 dependencies 에 추가

    let project = Project(name: "Network",
                          organizationName: "MyName",
                          targets: [
                            Target(name: "Network", platform: .iOS, product: .framework, bundleId: "simon.kang.myweather",
                                   infoPlist: .extendingDefault(with: infoPlist),
                                   sources: ["Sources/**"],
                                   dependencies: [
                                    .external(name: "Alamofire")
                                   ]
                                  )
                          ])
    
    

    이러고 tuist fetch

    Resolving and fetching plugins.
    Plugins resolved and fetched successfully.
    Resolving and fetching dependencies.
    Installing Swift Package Manager dependencies.
    Fetching <https://github.com/Alamofire/Alamofire.git> from cache
    Fetched <https://github.com/Alamofire/Alamofire.git> from cache (1.83s)
    Computing version for <https://github.com/Alamofire/Alamofire.git>
    Computed <https://github.com/Alamofire/Alamofire.git> at 5.10.0 (0.46s)
    Creating working copy for <https://github.com/Alamofire/Alamofire.git>
    Working copy of <https://github.com/Alamofire/Alamofire.git> resolved at 5.10.0
    Swift Package Manager dependencies installed successfully.
    Dependencies resolved and fetched successfully.
    
    

    성공

     

     

     

    문제 해결 (Troubleshooting)

    1. SwfitUI의 view가 위 아래 잘린 형태로 노출이됨

     

    원인: 커뮤니티를 통해 LaunchScreen 세팅이 제대로 되어있지 않아서 발생할 수 있다는 정보를 얻었습니다. Info.plist에 원래 LaunchScreen 정보가 비어있어야 하는데, 제 경우 infoPlist 변수에 빈 Dictionary [:] 를 넣어주어 이 설정이 누락된 것이 원인이었습니다.

     

    해결: App 모듈의 Project.swift 파일에 있는 infoPlist 변수에 LaunchScreen 관련 설정을 추가해 주었습니다.

     

    let infoPlist: [String: InfoPlist.Value] = [
        "UILaunchScreen": [
               "UILaunchBackgroundColor": "#FFFFFF"
           ]
    ]
    
    

    위 코드는 배경색만 넣어준 것이지만, LaunchScreen 설정을 아예 비워두고 싶다면 [:] 대신 해당 키를 포함하지 않는 방식으로 처리하거나, 위처럼 빈 설정을 넣어주는 것도 무방해 보입니다. 이 설정을 통해 문제가 해결되었습니다.

     

     

    2. 모듈에서 번들 관리하기

    문제: Network 모듈 내부에 Secret.plist 파일을 두고 Bundle에 접근하려 했으나 불러오지 못했습니다.

    extension Bundle {
        var apiKey: String? {
            guard let file = self.path(forResource: "Secret", ofType: "plist"),
                  let resource = NSDictionary(contentsOfFile: file),
                  let key = resource["APIKEY"] as? String else {
                print("API KEY를 가져오는데 실패하였습니다.")
                return nil
            }
        
            return key
        }
        
    }
    
    //사용 예시
    let apikey = Bundle.main.apiKey
    
    원인: 일반적으로 Bundle.main에 접근하면 모듈이 아닌 메인 앱의 번들에 접근하게 됩니다. 따라서 Secret.plist 파일이 모듈 내부에 있을 경우 Bundle.main으로는 찾을 수 없습니다.
     

    해결을 위해서 모듈에서는 접근 방식을 달리 해야함

     

    우선 모듈 폴더 내부에 Resources 폴더를 생성하고 그 안에 Secret.plist 파일을 추가합니다. 이때 파일의 타겟 설정이 해당 모듈로 되어 있는지 확인해야 합니다.

     

    해당 모듈의 Project.swift 파일에서 Target 설정 부분에 resources 경로를 지정해 줍니다.

    설정 후 tuist generate 를 다시 실행합니다.

     

      resources: ["Resources/**"],
    

     

     

    Bundle에 접근할 때는 모듈 내부의 특정 Class (아무 파일이나 상관없음)를 사용하여 해당 모듈의 번들을 생성해야 합니다.

    class BundleManager {
        static var apiKey: String? {
            let bundle = Bundle(for: BundleManager.self)
            guard let file = bundle.path(forResource: "Secret", ofType: "plist"),
                  let resource = NSDictionary(contentsOfFile: file),
                  let key = resource["APIKEY"] as? String else {
                print("API KEY를 가져오는데 실패하였습니다.")
                return nil
            }
            
            return key
        }
    }
    
    

    이렇게 접근하면 성공적으로 모듈 내부의 리소스 파일에 접근할 수 있습니다.

     

    댓글

Designed by Tistory.