본문 바로가기

iOS

[Tuist 3.12.0] Tuist 및 Local Swift Package를 이용해 Platform 간 Source code 공유하기

Tuist 및 Local Swift Package를 이용해 Platform 간 Source code 공유하기

문제

Tuist Modular Architecture로 구현된 프로젝트에서 Core와 Domain의 요소들을 app과 watchApp 사이에 공유하고자 했습니다.

Core와 Domain 모듈은 dynamic framework로 구현되어 있는 상황인데, tuist의 manifest를 통해 framework를 생성하면 platform이 specific하게 고정되어 버립니다.

 

시도1 : Swift Package는 소스코드의 형태로 export 할 수 있기에, 공유하고자 하는 소스와 리소스를 Swift Package의 형태로 만들어 사용하고자 했습니다.

- xcframework의 형태로도 unversal한 plaform을 타겟으로 dynamic / static하게 사용할 수 있다고 하지만, 수정 및 재생성 과정이 번거로울 것으로 생각되어 swift package를 사용했습니다. 또한 tuist graph를 통해 xcframework 내부의 의존성을 확인할 방법이 없다는 이유도 추가적으로 작용했습니다.

 

시행착오 : Library의 이름을 Network로 했더니 아래와 같은 오류가 발생했습니다. System Framework와 이름이 겹쳐 생긴 이슈였습니다. 링크

dyld: Symbol not found: _OBJC_CLASS_$_NWPathEvaluator

Referenced from: /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/Frameworks/DocumentManager.framework/DocumentManager

Expected in: /Users/david/Library/Developer/Xcode/DerivedData/SafetyMap-dszwcspxrstcjoekouoqfuzfniix/Build/Products/Debug-iphonesimulator/Network.framework/Network

 

문제점 1 : Tuist에서 Xcode Native Swift Package Manager를 이용하도록 설정하면 package의 productType이 static으로 한정된다.

아래와 같이 Project에 사용할 package를 명시하고, 의존성을 부여하고자 하는 Target의 Dependency에 package를 추가해주면 package 내의 코드를 사용할 수 있습니다.

return Project(name: name,
               organizationName: "tuist.io",
               packages: [
                .local(path: "Core/Core")
               ],
               targets: targets
               ]
)

let sources = Target(name: name,
                     platform: .iOS,
                     product: .framework,
                     bundleId: "io.tuist.\(name)",
                     infoPlist: .default,
                     sources: ["Targets/\(name)/Sources/**"],
                     resources: [],
                     dependencies: [.package(product: "LocalCore")],
                     settings: .settings(base: ["OTHER_LDFLAGS": "$(inherited) -all_load"])
)

이대로 tuist generate하면 모듈은 정상적으로 생성되지만, 아래와 같은 경고가 등장하며 static library의 형태로 LocalCore가 import되고 있다는 것을 확인할 수 있습니다.

 

LocalCore에 아래와 같이 productType을 .dynamic으로 명시해줘도 마찬가지로 문제가 발생했습니다.

// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Core",
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "LocalCore",
            type: .dynamic,
            targets: ["LocalCore"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(path: "../../Shared/FoundationUtil")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "LocalCore",
            dependencies: [
                .product(name: "FoundationUtil", package: "FoundationUtil"),
                .product(name: "EnumUtil", package: "FoundationUtil"),
            ]
        )
    ]
)

그리고 Tuist 레포지토리의 Discussion에서 이에 관한 논의가 진행된 것을 확인했습니다.

https://github.com/tuist/tuist/issues/1820

 

스레드 끄트머리에 Dependencies.swift를 사용하여 package 의존성을 관리하는 것을 추천하며 이슈가 종료되었습니다. 아직까지는 native spm을 이용한 dynamic package import는 불가능한 것 같습니다.

 

추가로, tuist 공식 문서에도 Dependencies.swift 사용을 권장하고 있었기에 해당 방법을 시도해보았습니다.

https://docs.tuist.io/guides/third-party-dependencies/

 

시도 2 : Dependencies.swift로 package 의존성 관리하기

아래와 같이 Dependencies.swift를 작성하고 tuist fetch -> tuist generate 했습니다.

각 타겟에서 의존성이 요구될 시 .external로 package를 가져왔습니다.

import ProjectDescription
import ProjectDescriptionHelpers

let spm = SwiftPackageManagerDependencies(
    [
    .local(path: "Core/Core"),
    .local(path: "Shared/FoundationUtil"),
    ]
)

let dependencies = Dependencies(
    carthage: [],
    swiftPackageManager: spm,
    platforms: [.iOS, .watchOS]
)

 

이번에는 iOS와 watchOS로 두 개의 framework Target이 생성되었습니다. 두 개의 target이 생성되는 것은 이슈를 해결하기 위함인 것 같습니다. 추가적으로 native SPM을 사용할 때와 다른 점은 package가 의존하는 package도 모두 dynamic framework로 처리되었다는 점입니다.

 

이러한 결과는 local Package에 의존하는 dynamic framework로 wrapping하는 것과 동일한 결과일 것이라 생각됩니다. 개인적으로 의존하는 package(LocalCore) 내부의 의존성도 그래프로 보고 싶었기에 이러한 방식을 최종적으로 선택했습니다.

 

p.s. multiplatform에서 소스를 share하는 방법이 있거나, 잘못된 정보가 있으면 피드백주시면 감사하겠습니다^^

 


(23.02.20 추가) package를 공유하는 방식으로 하면 tuist로 scheme을 추가하는 방법이 없는 것 같아 일단 동일한 source로 프레임워크를 만드는 방식으로 전환했습니다.