spacelyのブログ

Spacely Engineer's Blog

社内ライブラリを Swift Package Manager に対応させた

はじめに

株式会社スペースリー iOSエンジニアの樋川です。

普段は弊社サービスの、物件の撮影を補助/管理をするアプリのiOSの開発をしています。

今回は、研究開発チームが作成した、画像を台形補正する社内向けのライブラリをiOSプロジェクトに入れた時の内容をまとめます。

現在、iOSプロジェクトでは、Swift Package Manager(以下”SPM”と略)でライブラリ管理をしており、今回も他のライブラリと同様にSPMで対応することにしました。

今回は通常のSPMへの対応に加えて、下記のケースに対応する必要がありました。

  • ライブラリがC++で書かれている
  • ライブラリがOpenCV, Eigenを利用している

前準備 (C++で書かれたライブラリをSwiftで使えるように)

SPMへの対応に入る前に、全てC++で書かれているライブラリをSwiftで使えるようにする対応をしました。 方法としては、Objective-C++のWrapperを用意してそれを経由して呼び出すようにしました。

Swiftから直接C++を呼び出す方法も、実験的に導入されているみたいですが今回は試していないです。

SPMへの対応

完成したPackage.swiftの中身

手順の前に完成したPackage.swiftをお見せします。

import PackageDescription

let package = Package(
    name: "upright_images",
    products: [
        .library(
            name: "upright_images",
            targets: ["UprightImages"])
    ],
    dependencies: [],
    targets: [
        .binaryTarget(
            name: "opencv2",
            path: "iOS/libs/opencv2.xcframework.zip"
        ),
        .target(
            name: "UprightImagesWrapper",
            dependencies: ["opencv2"],
            path: "iOS",
            sources: ["rectification", "Wrapper"],
            publicHeadersPath: "Wrapper/include",
            cxxSettings: [.headerSearchPath("libs/Eigen")]
        ),
        .target(
            name: "UprightImages",
            dependencies: ["UprightImagesWrapper"],
            path: "iOS/Swift"
        )
    ],
    cxxLanguageStandard: CXXLanguageStandard.cxx14
)

対応手順

1. OpenCVをXCFrameworkにしてプロジェクトに追加

ライブラリが利用しているOpenCVをライブラリに含める必要があります。 OpenCVは、XCFramework形式で追加しました。XCframeworkを使用すると、iOS実機、iOSシミュレーターなどの複数のプラットフォーム用のフレームワークを1つにまとめることができます。

方法はOpenCVのドキュメントに記載されている通りです。

--withoutオプションで、使用しないmoduleを指定してサイズを小さくするのがポイントです。

2. OpenCVをTargetに追加

追加したOpenCVを利用できるようにTargetに追加します。 まず、Package.swiftファイルを作成し、Target.binaryTarget(name:path:)を用いてTargetに追加して、依存関係に追加できるようにします。

targets: [
    .binaryTarget(
        name: "opencv2",
        path: "iOS/libs/opencv2.xcframework.zip"
    ),
]

3. Eigenを追加

submoduleを利用してプロジェクトに追加します。

4. 各Targetを追加

ライブラリ本体をTargetとして追加するのですが、C系のファイル(C, C++, Objective-C, Objective-C++)と Swiftファイルを同じTargetに混在させてはいけないという仕様があります。

Targets can contain Swift, Objective-C/C++, or C/C++ code, but an individual target can’t mix Swift with C-family languages. For example, a Swift package can have two targets, one that contains Objective-C, Objective-C++, and C code, and a second one that contains Swift code.

developer.apple.com

そのため、

  • C++のコードとObjective-C++のWrapperのTarget
  • SwiftのコードのTarget

を分けて作成します。

4-1. C++のコードとObjective-C++のWrapperのTarget
.target(
    name: "UprightImagesWrapper",
    dependencies: ["opencv2"],
    path: "iOS",
    sources: ["rectification", "Wrapper"],
    publicHeadersPath: "Wrapper/include",
    cxxSettings: [.headerSearchPath("libs/Eigen")]
)

ここで、

  • OpenCVに依存するので、dependenciesに先ほど作成したopencv2のTargetを追加します。
  • C系のTargetは、他のTargetに公開するためにTarget.publicHeadersPathで公開するファイルのパスを指定する必要があります。今回利用したいのはObjective-C++のWrapperファイルのみなので、それが含まれたパスWrapper/includeを指定します。
  • Eigenを利用するために、CXXSetting.headerSearchPath(_:_:)でEigenのsubmoduleが含まれるパスlibs/Eigenを指定します。
4-2. SwiftのコードのTarget
.target(
    name: "UprightImages",
    dependencies: ["UprightImagesWrapper"],
    path: "iOS/Swift"
)

こちらは、先ほど作成したC系のTargetのUprightImagesWrapperを依存に追加するだけです。

5. library productとして公開

最後にライブラリとして利用するために、Product.library(name:type:targets:)を使って公開します。

.library(
    name: "upright_images",
    targets: ["UprightImages"]
)

以上でSPMとして公開できました。あとはiOSプロジェクトの方で通常通りSPMを追加するだけで利用できるようになります。

その他つまずいた箇所

おまけでつまずいたところを2箇所紹介します。

sourcesで指定し忘れていた

Undefined symbols for architecture arm64:
  "_OBJC_CLASS_$_UprightImagesWrapper", referenced from:
      objc-class-ref in UprightImages.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

ずっとこのエラーが消えないと悩んで時間を溶かしていたのですが、C系のTarget(UprightImagesWrapper)の方でsourcesにObjective-C++のWrapperのパスを含め忘れていただけでした。

Git LFSを使用するとレポジトリのチェックアウトができなかった

iOSプロジェクトで、SPMでライブラリを追加しようとした時に下記エラーで追加できませんでした。

Couldn’t check out revision 'XXX'

対象のrepositoryがGit LFSを利用していると出るエラーのようです。

forums.swift.org

結局、今回は、OpenCVのmoduleを必要なものだけに削ってzip化した際に、Git LFSを使わなくなったので、どう解決するかは調べていません。

参考文献

さいごに

spacelyでは一緒に働いてくださる方を大募集中です。

iOSエンジニア / 株式会社スペースリー

Androidエンジニア / 株式会社スペースリー