[SPM] Swift Package Manager / 내 Library 만들기

2024. 2. 1. 12:33🍏/Swift

Swift Package Manager

SPM

Swift 패키지는 개발자가 프로젝트에서 사용할 수 있는 Swift, Objective-C, Objective-C++, C 또는 C++ 코드의 재사용 가능한 구성 요소입니다. 소스 파일, 바이너리 및 리소스를 앱 프로젝트에서 사용하기 쉬운 방식으로 번들로 제공합니다.

Xcode는 Swift 패키지를 생성 및 게시하고 패키지 종속성을 추가, 제거 및 관리할 수 있도록 지원합니다. Swift 패키지에 대한 지원은 오픈 소스 Swift 패키지 관리자 프로젝트를 기반으로 구축되었습니다.

패키지 매니페스트에서 사용하는 API에 대해 자세히 알아보려면 패키지를 참조하세요. Swift 패키지 관리자에 대해 자세히 알아보려면 Swift.org 및 오픈 소스 Swift 패키지 관리자 리포지토리를 참조하세요.

애플은 Swift 언어의 생태계를 성장시키기 위해 Swift Package Manager(SPM)를 개발하였습니다.
Swift는 Cross-Platform 언어로, 다양한 플랫폼에서 일관된 방식으로 코드를 구성하고 실행할 수 있게 하는 도구가 필요했습니다.
여기서 말하는 플랫폼이란 macOS, iOS, watchOS 등을 뜻합니다.

그리하여 Swift로 작성된 라이브러리들이 쉽게 배포되고 사용될 수 있도록 함으로써, Swift 언어의 미래를 지원하고 발전시키기 위해 SPM이 만들어졌습니다. SPM은 자체 빌드 시스템이 포함되어 있고, 소프트웨어의 구성과 테스트, 실행까지 포함하고 있습니다. 그리고 라이브러리를 배포하는 방법에 대한 새로운 표준을 정의하였습니다. 


이전에 Cocoapod으로 라이브러리를 생성해 본적이 있습니다, 하지만 역시 Cocoapods이나 Carthage는 3rd party tool이여서

이번에는 애플이 공식 지원하는 SwiftPM을 사용해보고, 나만의 Library를 만들어 보려고 합니다.


SPM 사용하기

PROJECT에서 Package Dependencies에서 +로 간단하게 package를 등록해줄 수 있습니다.

Autolayout Library인 SnapKit을 예시로 들어보면, 우측 상단에 SnapKit URL을 입력후 Add Package로 등록하여 사용할 수 있습니다.

https://github.com/SnapKit/SnapKit

 

GitHub - SnapKit/SnapKit: A Swift Autolayout DSL for iOS & OS X

A Swift Autolayout DSL for iOS & OS X. Contribute to SnapKit/SnapKit development by creating an account on GitHub.

github.com

SnapKit ReadMe에서 SPM으로는 어떻게 사용할 수 있는지 확인하실 수 있습니다.

 


New Package

 

New Package

위 방법 혹은, 아래 방법 모두 New Package를 만들 수 있습니다.

cli spm init

 

만들어진 Package를 살펴보면, Package.swift는 manifest파일이고, 이 파일로 해당 디렉토리를 Swift Package로 식별하게 됩니다.

// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
// 해당 버전은 패키지를 빌드 하는데 필요한 Swift 컴파일러의 최소 버전입니다.

import PackageDescription
// PackageDescription 라이브러리는 manifest파일이 패키지를 생성하는데 사용할 API를 포함하고 있습니다.

// Package의 이니셜라이저 입니다.
let package = Package(
    name: "MyLittleLibrary", // MyLittleLibrary라는 이름을 가진 패키지를 생성하였습니다.
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        // 제품은 패키지가 생성하는 실행 파일과 라이브러리를 정의하여 다른 패키지에서 볼 수 있도록 합니다.
        .library(
            name: "MyLittleLibrary",
            targets: ["MyLittleLibrary"]),
            // "MyLittleLibrary" 라이브러리로 "MyLittleLibrary" 모듈을 노출시킨 것을 볼 수 있습니다.
            // targets 파라미터에는 타겟을 여러개 선언할 수 있고, 하나의 라이브러리로 여러 탓겟을 노출할 수 있습니다.
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        // 타깃은 모듈 또는 테스트 스위트를 정의하는 패키지의 기본 구성 요소입니다.
        // 타깃은 이 패키지의 다른 타깃과 종속성의 제품에 종속될 수 있습니다.
        .target(
            name: "MyLittleLibrary"), 
            // Package는 Target에 대한 표준 구조를 가집니다.
            // Sources 디렉토리에 타겟의 모든 파일이 위치하고, 각 타겟은 타겟 이름과 동일한 이름으로 하위 디렉토리에 위치합니다.
        .testTarget(
            name: "MyLittleLibraryTests",
            dependencies: ["MyLittleLibrary"]),
            // Test target은 Tests 디렉터리 아래에 위치하고, 일반적으로 다른 타겟을 테스트하기 때문에 dependencies 파라미터를 선언해야 합니다.
        .target(name: "MyTarget",
                dependencies:[
                	"SnapKit"
                ],
                path: "MyTarget"
               )
           // Target path parameter를 사용하여 custom path를 지정할 수 있습니다.
    ]
)

Resource

Xcode12부터 타겟에 리소스가 있는 경우, 그 리소스들을 번들화 해주어 해당 타겟에서 사용할 수 있도록 해줍니다.

왼편의 타입들은 컴파일러가 해당 파일들의 목적이 분명하다고 판단해서, manifest 파일에 따로 명시하지 않아도 번들화를 해주는 반면에 우측의 타입들은 목적이 다양하다고 판단하기 때문에 manifest 파일에 명시적으로 작성해줘야 리소스 번들에 추가됩니다.


Package Manifest API

타겟에 리소스가 있을 경우, 해당 리소스들을 manifest 파일에서 target의 resources 파라미터에 추가합니다. 아래와 같이 xcassets와 storyboard 파일은 manifest 파일에 추가하지 않아도 번들화를 해주고, png 파일이나 디렉터리는 명시적으로 추가되어야 번들화가 되는 것을 볼 수 있습니다.

리소스를 추가할 때, .process는 리소스 번들 루트에 위치되고, .copy는 디렉터리 구조를 유지하면서 추가됩니다.


타겟 내 코드에서 리소스 접근 방법

타겟에서 리소스 번들에 접근할 때 SPM은 Bundle.module이라는 accessor로 접근합니다. 

// Swift
let image = UIImage(named: "Logo", in: Bundle.module)
// Objective-C
UIImage *image = [UIImage imageNamed:@"Logo" inBundle:SWIFTPM_MODULE_BUNDLE];

Package.resolved

해당 파일은 프로젝트의 워크스페이스에 있는 모든 패키지의 버전 정보를 기록합니다. 이는 Cocoapods의 Podfile.lock과 유사한 개념으로, 협업 시 모든 팀원이 동일한 패키지 버전을 사용할 수 있도록 합니다.

파일은 project.xcworkspace > xcshareddata > swiftpm > Package.resolved 경로에 위치해 있습니다. 협업하는 동안 모든 팀원이 일관된 패키지 버전을 사용하기 위해서는 이 파일을 버전 관리 시스템에 커밋해야 합니다.


빌드 정보

Package Build Process


배포

Tag / Realease로 Package Manager에서 버전 선택가능.

 

GitHub - chanhihi/MyLittleLibrary: My Little Swift Package

My Little Swift Package. Contribute to chanhihi/MyLittleLibrary development by creating an account on GitHub.

github.com


Library Package 

타겟이 다른 이름으로 추가된 경우 Library를 추가해줘야 추가된 라이브러리로 가져올 수 있습니다.

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

import PackageDescription

let package = Package(
    name: "MyLittleLibrary",
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "MyLittleLibrary",
            targets: ["MyLittleLibrary"]),
        .library(name: "BoxThen",
            targets: ["BoxThen"])
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "MyLittleLibrary"),
        .testTarget(
            name: "MyLittleLibraryTests",
            dependencies: ["MyLittleLibrary"]),
        .target(name: "BoxThen",
                dependencies:[],
                path: "BoxThen",
                resources: [
                    .process("Logo.gif"),
                    .copy("Data")
                ]
               )
    ]
)


Main Package

import ProjectDescription

// MARK: - Project Factory

protocol ProjectFactory {
    var projectName: String { get }
    var dependencies: [TargetDependency] { get }
    
    func generateTarget() -> [Target]
}

// MARK: - iBox Factory

class iBoxFactory: ProjectFactory {
    let projectName: String = "iBox"
    let bundleId: String = "com.box42.iBox"
    
    let dependencies: [TargetDependency] = [
        .package(product: "SnapKit"),
        .package(product: "MyLittleLibrary"),
        .package(product: "BoxThen")
    ]
    
    let infoPlist: [String: InfoPlist.Value] = [
        "ITSAppUsesNonExemptEncryption": .boolean(false),
        "CFBundleName": .string("iBox"),
        "CFBundleShortVersionString": .string("1.2.1"),
        "CFBundleVersion": .string("1"),
        "UILaunchStoryboardName": .string("LaunchScreen"),
        "UIApplicationSceneManifest": [
            "UIApplicationSupportsMultipleScenes": .boolean(false),
            "UISceneConfigurations": [
                "UIWindowSceneSessionRoleApplication": [
                    [
                        "UISceneConfigurationName": .string("Default Configuration"),
                        "UISceneDelegateClassName": .string("$(PRODUCT_MODULE_NAME).SceneDelegate")
                    ],
                ]
            ]
        ]
    ]
    
    func generateTarget() -> [ProjectDescription.Target] {[
        Target(
            name: projectName,
            platform: .iOS, // platform 인자 추가
            product: .app,
            bundleId: bundleId,
            deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), // deploymentTargets 형식 수정
            infoPlist: .extendingDefault(with: infoPlist),
            sources: ["\(projectName)/Sources/**"],
            resources: ["\(projectName)/Resources/**"],
            dependencies: dependencies
        )
    ]}
}

// MARK: - Project

let factory = iBoxFactory()

let project = Project(
    name: factory.projectName,
    packages: [
        .remote(url: "https://github.com/SnapKit/SnapKit", requirement: .upToNextMajor(from: "5.0.0")),
        .remote(url: "https://github.com/chanhihi/MyLittleLibrary", requirement: .exact("0.0.2"))
    ],
    targets: factory.generateTarget()
)

협업 툴인 Tuist를 사용해서 XCode Workspace를 관리하고 있는데, Package 을 위와 같이 설정해주어야 tuist generate시 에러가 나지 않고 잘 동작하는것을 확인할 수 있었습니다.

 

출처 및 도움