見出し画像

社内フレームワークをSwift Package Manager化:導入と運用改善

この記事は、NAVITIME JAPAN Advent Calendar 2024の20日目の記事です🎄


はじめに

こんにちは、Matsuとジャコポです。

Matsu
研究開発部門で、ナビゲーションフレームワークエンジニアを担当

ジャコポ
カーナビ系iOSアプリ開発を担当

今回の記事はこの2人で執筆します。

本日は当社で使用しているiOSの社内フレームワークをSwiftPackageManagerで配布できるように対応した内容をご紹介します。

Swift Package Managerへの対応背景

ナビタイムジャパンのiOSアプリの多くは社内フレームワークをCarthageで取り込んでいます。
しかし、アプリのCI/CD環境にXcodeCloudを利用するようになったことで、社内フレームワーク以外のライブラリはキャッシュが利用できるSwift Package Manager(以下、SwiftPM)で導入するケースが増えています。その結果、社内フレームワークにもSwiftPM対応の需要が高まり、XCFramework対応を進めると同時に、SwiftPMを利用して社内フレームワークを配布できるよう対応を行いました。

対応することによるメリットは以下です。(ナビタイムジャパンの環境でのメリット、XCFrameworkのメリットも含む)

  • Appleが提供しているツールのためサポート面でのリスクが少ない。

  • XcodeCloudのキャッシュが利用できる。

  • SwiftPMへ1本化でき、環境構築がシンプルになる。

  • 環境構築の時間が短縮される。

社内フレームワークの多くはC++, Objective-Cで書かれているため、それらをXCFrameworkにしてSwiftPMで配布します。

XCFrameworkの対応はこちらの記事で説明しています。


XCFrameworkをSwiftPMで配布する方法

SwiftPMではPackage.swiftというファイルを使って構成を定義します。Package.swiftにはファイル構成やTargetの設定を記載できるため、iOSアプリでいうXcodeプロジェクトのようなものと思ってください。

Package.swiftは作成したいディレクトリで `swift package init` を実行することで新規生成できます。

Package.swift内の関係性が分かりづらく感じたため、順を追って説明します。

Package.swiftのソースコード

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "SamplePackage",
            targets: ["SamplePackage"]),
    ],
    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: "SamplePackage"),
        .testTarget(
            name: "SamplePackageTests",
            dependencies: ["SamplePackage"]),
    ]
)

コメント等削除

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
    ],
    targets: [
    ]
)

まずはXCFrameworkを扱えるようにします。
今回はLFSでリポジトリに含める方法で説明します。binaryTargetとしてそのフレームワークを定義します。

binaryTargetを追加したソースコード

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
    ],
    targets: [
        .binaryTarget(
            name: "SampleXCFramework",
            path: "Sources/SampleXCFramework.zip"
        )
    ]
)

アプリからこのPackageを利用するにはproductsに定義が必要です。
libraryとして先程のターゲットを追加します。SampleXCFramework.zipをGit LFSを利用してBitbucket上で管理しています。

libraryを追加したソースコード

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
        .library(name: "SamplePackage", targets: ["SampleXCFramework"])
    ],
    targets: [
        .binaryTarget(
            name: "SampleXCFramework",
            path: "Sources/SampleXCFramework.zip"
        )
    ]
)

何にも依存していないフレームワークであれば、このPackage.swiftをリポジトリのrootに置くことで、アプリから取り込むことができます。

依存関係を持つ場合はもう少し手を加える必要があります。
依存関係を持つtargetをlibraryとして定義する必要がありますが、binaryTargetは依存関係を定義できないため、別のtargetを定義します。

その際、targetの依存関係として、先程のbinaryTargetを含めるようにします。

依存関係用targetを追加したソースコード

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
        .library(name: "SamplePackage", targets: ["SamplePackage"])
    ],
    targets: [
        .target(
            name: "SamplePackage",
            dependencies: [.target(name: "SampleXCFramework")] // 先程のbinaryTargetに依存
        ),
        .binaryTarget(
            name: "SampleXCFramework",
            path: "Sources/SampleXCFramework.zip"
        )
    ]
)

依存するフレームワークも同様にPackage.swiftを用意し、このPackageの依存関係として定義します。

package.dependenciesを追加したソースコード

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
        .library(name: "SamplePackage", targets: ["SamplePackage"]),
    ],
    dependencies: [
        .package(url: "依存するリポジトリのURL", from: Version("x.y.z"))
    ],
    targets: [
        .target(
            name: "SamplePackage",
            dependencies: [.target(name: "SampleXCFramework")]
        ),
        .binaryTarget(
            name: "SampleXCFramework",
            path: "Sources/SampleXCFramework.zip"
        )
    ]
)

依存するフレームワークを扱えるようになったので、最後に先程のtargetの依存関係として追加します。

target.dependenciesを追加したソースコード

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
        .library(name: "SamplePackage", targets: ["SamplePackage"]),
    ],
    dependencies: [
        .package(url: "依存するリポジトリのURL", from: Version("x.y.z"))
    ],
    targets: [
        .target(
            name: "SamplePackage",
            dependencies: [
                .target(name: "SampleXCFramework"),
                .product(name: "依存するPackage名", package: "依存するPackageのリポジトリ名")
            ]
        ),
        .binaryTarget(
            name: "SampleXCFramework",
            path: "Sources/SampleXCFramework.zip"
        )
    ]
)

これで依存関係は定義できましたが、SwiftPMは各targetに対して特にコードが不要でも最低1ファイル必要になります。今回はダミーファイルを配置しました。

公開ヘッダーについて

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
    name: "SamplePackage",
    products: [
        .library(name: "SamplePackage", targets: ["SamplePackage"]),
    ],
    dependencies: [
        .package(url: "依存するリポジトリのURL", from: Version("x.y.z"))
    ],
    targets: [
        .target(
            name: "SamplePackage",
            dependencies: [
                .target(name: "SampleXCFramework"),
                .product(name: "依存するPackage名", package: "依存するPackageのリポジトリ名")
            ]
        ),
        .binaryTarget(
            name: "SampleXCFramework",
            path: "Sources/SampleXCFramework.zip"
        )
    ]
)

アプリからインターフェースを呼ぶためにヘッダを公開する必要があります。

SwiftPMを取り込んで

実際にプロジェクトファイルの「PROJECT」を選択して、Package dependenciesで取り込むと、下記の点が見えてきました。

メリット

  • Carthage周りの処理が不要になる。

    • 毎回CI上で実行していたsetupのscript、romeなどの処理が不要になる。

    • 不要になったことからCarthageのエラーが起きない。

  • Apple Siliconで必要なrosettaが不要になる。

    • arm64で動かせるようになり、rosettaなしでSimulatorビルドやSwiftUIのPreviewが使えるになる。

デメリット

  • Cacheがわかりづらい。

    • Carthageはコマンドラインツールとして利用できていたのでCacheを利用するコマンドがあったがSwitPMはXcodeに統合されているため明示的にCacheを利用しないなど自由度が低い。

  • 詳細エラーがわかりづらい。

    • Cacheが絡んでいるのかそうでないのかが判別つかないことがあるので社内ドキュメントでエラー解決内容を記載しました!

  • 不具合がある。

    • SwiftPM Issue

      • 不具合が多いため、処理の問題なのか不具合なのかを判別しづらい。

      • 更新はされ続けているため今後解消されそう。


最後に

今回のSwiftPM対応では、社内限定で利用されている非公開コードのXCFrameworkをSwiftPMで取り込む仕組みを構築しました。しかし、依存関係が複雑であったため、対応には多くの課題がありました。それでも現在、いくつかのアプリで導入が進み、リリースまで至っています。この取り組みにより、環境構築の負担が大幅に軽減されたことを実感しています。

まだSwiftPM対応を取り込めていないアプリも多いため、今後も順次対応を進めていきます。環境構築の負担が減ることで生まれる時間を活用し、より多くの価値をユーザーに提供していきたいと考えています。