OpenCV を XCFramework にして Swift Package Manager 経由で iOS で使ってみた

この記事は OPTiM TECH BLOG Advent Calendar 2020 12/17 の記事であり、Swift その2 Advent Calendar 2020 の 17 日目の記事です。

こんにちは。R&D チームの久保です。今月でオープンソース化されてから 5 周年を迎えた Swift ですが、今回の記事はそんな Swift から OpenCV (C++) を今風のやり方で呼び出してみるまでの方法についてです。成功した方法だけ見ると大したことはなさそうですが、一歩足を踏み外すと多くの罠が潜んでいる状態だったので、今後同様のことをしたい人が同じ罠にはまらないようにまとめてみました。

f:id:optim-tech:20201214043140p:plain

XCFramework とは

XCFramework は 2019 年に Xcode 11 で対応された Apple プラットフォーム (iOS, iPadOS, macOS, tvOS, watchOS) 向けのバイナリフレームワークの形式で、様々なターゲット向けのライブラリやフレームワークをひとまとめにして可搬性を高めることができます。例えばあるライブラリを iOS 実機向けにビルドしたバイナリとシミュレータ向けにビルドしたバイナリや、Mac Catalyst 向けにビルドしたバイナリを 1 つ XCFramework にまとめて配布することができます。

XCFramework 登場以前では同様のことをしたい場合に lipo コマンドを使って複数のターゲット向けのバイナリを一つの *.framework ファイルにまとめることで不可能ではなかったようですが、App Store Connect にアップロードする場合にシミュレータ向けのバイナリを除外する必要があったりと色々と手間がかかっていたようです。実際に使用したことはないため、ここの解説は偉大な先人達にお任せします。

XCFramework ではさらに静的ライブラリとその Public API が記述されたヘッダファイルを XCFramework としてまとめて配布することもできます。ライブラリ自体を記述する言語としては Swift, C/C++, Objective-C が想定されています。

大部分が Swift もしくは Objective-C で書かれているライブラリだったり、C/C++ であっても C API を持ち CMake などの外部ツールを必要とせずに単純にソースとヘッダからビルドできるようなライブラリであれば、後で説明する Swift Package Manager のパッケージとして配布した方がさらに扱いやすいことが多いですが、そうではないようなライブラリで Apple プラットフォーム向けのものでは XCFramework での配布が今後のベストプラクティスになると思われます。

Swift Package Manager によるサポート

2020 年に入ると SE-0272 で Swift Package Manager (以下 SwiftPM) が XCFramework を直接扱うことに対応しました。この実装が入った Swift 5.3 は Xcode 12 と同じタイミングでリリースされました。

SwiftPM を説明しておくと、依存する Swift パッケージ (ライブラリなど) を Package.swift に記述しておくことで、依存関係の解決や各パッケージのフェッチ、コンパイル、リンク等を行ってくれるパッケージマネージャで、Swift のツールチェーンに標準で付属しています。Node.js でいう NPM や Rust でいう Cargo、Ruby でいう Bundler、Go Modules 対応以降の go コマンドなどのようなものだと思っていただけるとわかりやすいかと思います。

つまりは SwiftPM の依存関係に XCFramework を記述することで、勝手に XCFramework をダウンロードしてきてリンクしてくれるようになりました。

また、Xcode では 11 から SwiftPM がインテグレートされているので、例えば iOS アプリ開発時に外部のライブラリを使いたい場合、それが Swift パッケージとして公開されていれば Xcode 上で簡単に依存関係に追加して使用することができました。したがって、iOS プロジェクトで XCFramework を使用したい場合、それをリポジトリに含めたりダウンロードするスクリプトを書いたりせずとも、Swift パッケージとして別リポジトリを作っておけば、そのパッケージを依存関係に追加するだけで XCFramework のダウンロードからリンクまで Xcode が行ってくれるようになりました。

これを活用している例としては以下のようなものがすでにあります。

  • PSPDFKit
    • PDF の表示から編集から様々な機能を提供しているマルチプラットフォームな SDK
    • Binary Frameworks as Swift Packages というブログ記事にて PSPDFKit の XCFramework を SwiftPM 経由で利用する方法を紹介
  • ios_system
    • iOS で使用できないシステムコールをダミーの関数で差し替えて CLI プログラムを iOS 上で実行するための Objective-C や C で書かれたライブラリ
    • XCFramework をリリースノートの Assets に添付し、それを使用する Package.swift を置くことで Swift パッケージとして提供されている
    • a-Shell という同じ作者が作った iOS 向けのターミナルエミュレータで SwiftPM 経由で使用されている (一部以外はリンクせずに Embed Framework としてアプリに埋め込んで実行時に dlopen する目的で使われているので少し特殊)

なぜ OpenCV を XCFramework にして SwiftPM 経由で使おうと思ったか

ちょうど iOS アプリで OpenCV を使用したいことがあったので、OpenCV を XCFramework にして SwiftPM 経由で Swift から呼び出せるようにしてみます。

そもそも OpenCV は iOS 向けには *.framework 形式のビルド済みフレームワークを配布していて、以下のサイトをはじめいろいろなところで解説しているように Objective-C++ でラッパーを書いて Bridging Header を用意すれば Swift から呼び出すことができていました。

しかしそうなると *.framework を直接プロジェクトに追加する必要があり、git リポジトリで開発している場合 *.framework ファイルをリポジトリに追加するか或いはビルド時にダウンロードしてくるようなスクリプトを書く必要があります。また、Objective-C++ のラッパーのコードを別リポジトリに分離することもしづらくなります。そもそも OpenCV は様々なモジュールから構成される巨大なライブラリなので、不要なモジュールまで含まれているとアプリのサイズも大きくなってしまい、できれば自前でビルドして必要なモジュールのみに軽量化するのがベストです。

これらの理由もあり、配布されている *.frameowrk を直接利用するのではなく、XCFramework にした上で Objective-C++ ラッパーも含めて Swift パッケージにし、それを SwiftPM 経由で利用するという形を取りました。

必要環境

まず初めに話しておくと、OpenCV のビルドには CMake が使われ、CMake を呼び出して iOS 向けに *.framework をビルドするようなビルドスクリプトも存在します。

しかし、XCFramework では (Swift で書かれていなくとも) *.swiftinterface ファイルが必要で、それが出力されるには Xcode 12 かつビルド設定で BUILD_LIBRARY_FOR_DISTRIBUTIONYES になっている必要がるのですが、OpenCV のビルドスクリプトでは 4.5.0 リリース時点ではそれが考慮されておらず、修正されたのが opencv/opencv#18637 のプルリクエストです。この変更は 4.5.1 でシップされるようなので、それがリリースされるまでは master ブランチでビルドするしかありません。

また、先程のプルリクエストでの変更にはつい最近リリースされたばかりの CMake 3.19 で追加された Xcode の新しいビルドシステムのサポートが必須なため、CMake 3.19 以降である必要があります。

さらに、先程のプルリクエストをマージしても Xcode 12 以降でビルドしなければ *.swiftinterface ファイルが出力されないため、Xcode 12 以降が必要なのですが、さらに後述するコード署名のバグが修正された Xcode 12.2 以降でなければ iOS アプリの手元での実行はできても最終的に App Store に提出することができないため、Xcode 12.2 以降である必要があります。

つまり、今回やろうとしていることに必要な各ツールのバージョンは以下のようになります。

  1. ビルド対象の OpenCV のバージョンは 4.5.1 以降
  2. CMake 3.19 以降
  3. Xcode 12.2 以降 (ただし OpenCV のビルドだけには Xcode 12.1 推奨 (詳細は第二の罠「OpenCV (CMake) と Xcode 12.2 の相性問題」を参照))

余談ですが、この取り組みをしていた時点では OpenCV 4.5.1 がリリースされていないのはもちろんのこと、CMake 3.19 すらリリースされておらず、Xcode 12.2 もちょうど Release Candidate が出るかどうかというギリギリのタイミングでした。CMake 3.19 RC と Xcode 12.1 で OpenCV の master ブランチをビルドして、iOS アプリは Xcode 12.2 Release Candidate でビルドしていましたが、あと一日でも早かったら Xcode 12.2 Beta では App Store に提出することができないので諦めていたことでしょう。

XCFramework の作り方

XCFramework を作成するには xcodebuild コマンドを使用します。例えばライブラリ (*.a or *.dylib) と Public API のヘッダファイルを指定して作成する場合は、

xcodebuild -create-xcframework -library <ライブラリのパス> -headers <ヘッダファイルのディレクトリのパス> -output <出力する *.xcframework のパス>

のように作成し、複数の *.framework から作成する場合は

xcodebuild -create-xcframework -framework <*.framework のパス> -framework <*.framework のパス> -output <出力する *.xcframework のパス>

のように作成します。

xcodebuild -create-xcframework の入力に指定するライブラリやフレームワークは単一のプラットフォーム向けのものしか受け付けてもらえないので、arm64e と x86_64 のような複数 CPU アーキテクチャをサポートする Universal バイナリだったり、すでに複数のターゲット向けのバイナリをまとめた *.framework を指定すると

error: binaries with multiple platforms are not supported 'foobar'

のようなエラーを吐かれるのでご注意ください。

OpenCV の XCFramework を作る

OpenCV には iOS 向けのビルドスクリプトとして platforms/ios/build_framework.py が用意されています。ただ、これをそのまま実行すると iOS 実機向けのバイナリとシミュレータ向けのバイナリの両方を持つ *.framework が出力されてしまうため先程のエラーに当たります。

それを回避するため、OpenCV のフォーラムで解説されているようにスクリプトを一時的に書き換えて iOS 実機向けの *.framework とシミュレータ向けの *.framework を別々にビルドする必要があります。(リンク先では Mac Catalyst 向けにビルドしているので多少異なることに注意)

まずは OpenCV をクローンして最新リリースのタグをチェックアウトします。

git clone https://github.com/opencv/opencv

その後、ビルドスクリプトを

diff --git a/platforms/ios/build_framework.py b/platforms/ios/build_framework.py
index e89cf3c666..9437022e93 100755
--- a/platforms/ios/build_framework.py
+++ b/platforms/ios/build_framework.py
@@ -455,7 +455,7 @@ if __name__ == "__main__":
         ] if os.environ.get('BUILD_PRECOMMIT', None) else
         [
             (iphoneos_archs, "iPhoneOS"),
-            (iphonesimulator_archs, "iPhoneSimulator"),
+            # (iphonesimulator_archs, "iPhoneSimulator"),
         ], args.debug, args.debug_info, args.framework_name, args.run_tests, args.build_docs)

     b.build(args.out)

のように書き換えてシミュレータ向けの出力を指定する部分をコメントアウトし、

/usr/bin/python platforms/ios/build_framework.py --iphoneos_archs arm64 --iphonesimulator_archs x86_64 --iphoneos_deployment_target 14.0 --dynamic ios

のようにビルドします。これで iOS 実機向けのフレームワークは完成です。実際には成果物のサイズ削減のためにここで --without オプションを使って不要なモジュールを除外するような指定をすることになります。

今度はシミュレータ向けにビルドするために先程のコメントアウトを戻して

diff --git a/platforms/ios/build_framework.py b/platforms/ios/build_framework.py
index e89cf3c666..9437022e93 100755
--- a/platforms/ios/build_framework.py
+++ b/platforms/ios/build_framework.py
@@ -455,7 +455,7 @@ if __name__ == "__main__":
         ] if os.environ.get('BUILD_PRECOMMIT', None) else
         [
-            (iphoneos_archs, "iPhoneOS"),
+            # (iphoneos_archs, "iPhoneOS"),
             (iphonesimulator_archs, "iPhoneSimulator"),
         ], args.debug, args.debug_info, args.framework_name, args.run_tests, args.build_docs)

     b.build(args.out)

のように書き換え、

/usr/bin/python platforms/ios/build_framework.py --iphoneos_archs arm64 --iphonesimulator_archs x86_64 --iphoneos_deployment_target 14.0 --dynamic ios_simulator

のように実行します。

これでようやく

xcodebuild -create-framework -framework ios/opencv2.framework -framework ios_simulator/opencv2.framework -output opencv2.xcframework

opencv2.xcframework という名前で XCFramework を作成することができます。

ただし、最終的にこれを使用したアプリを App Store Connect にアップロードする場合は少し改変が必要な部分があります。詳細は後述の第三の罠「OpenCV の Info.plist」をご覧ください。

OpenCV の Objective-C++ ラッパー作成と Swift パッケージ化

OpenCV の最新版 (4.5.0) では C++ API しか提供されていないため、Swift から直接呼び出すことはできません。一応試験的に Swift から C++ を直接呼び出せる C++ Interoperability も進んではいますが、まだまだ試験的なので、ここはよくやられるように Objective-C++ でラッパーを書くことにしました。

Swift パッケージの構成としては、iOS アプリのプロジェクトとは別に

  • opencv2.xcframework のみの Swift パッケージ
  • 上記のパッケージに依存し、Objective-C++ さらには Swift で書いた薄いラッパー関数を持つ Swift パッケージ

という 2 パッケージの構成でいきます。それぞれ独立した git リポジトリを作っています。同じパッケージにまとめてターゲットだけ分けるということもできるのですが、そうするとアプリ側でこのパッケージを依存に追加してビルドした際に、クリーンビルドするとパッケージをフェッチするタイミングとコード署名を行うタイミングがうまく合わず、フェッチより前にコード署名時を行おうとしてファイルが見つかりませんと言われるバグが Xcode 12.2 で確認されたため、それを回避するためにやむなく分けています。

opencv2.xcframework のみの Swift パッケージでは Frameworks ディレクトリ以下に先程ビルドした opencv2.xcframework を入れておき、Package.swift

// swift-tools-version:5.3

import PackageDescription

let package = Package(
    name: "opencv2",
    platforms: [.iOS(.v14)],
    products: [
        .library(
            name: "opencv2",
            targets: ["opencv2"]
        )
    ],
    dependencies: [],
    targets: [
        .binaryTarget(
            name: "opencv2",
            path: "Frameworks/opencv2.xcframework"
        )
    ]
)

としました。ビルド時に含めたモジュールやビルドターゲットの数によっては opencv2.xcframework のファイルサイズがかなり大きくなっていると思うので、Git LFS を使用するのもありでしょうし、Apple の解説記事にあるようにファイルパスではなく URL とハッシュで指定するタイプであればどこか別の場所に ZIP で圧縮して置いておくこともできます。

一方、ラッパーライブラリ側の Swift パッケージでは Package.swift

// swift-tools-version:5.3

import PackageDescription

let package = Package(
    name: "OpenCVWrapper",
    platforms: [.iOS(.v14)],
    products: [
        .library(
            name: "OpenCVWrapper",
            targets: ["OpenCVWrapper"]
        )
    ],
    dependencies: [
        .package(
            url: "先程の opencv2.xcframework の Swift パッケージの Git URL",
            from: "1.0.0"
        )
    ],
    targets: [
        .target(
            name: "OpenCVWrapperObjC",
            dependencies: ["opencv2"],
            sources: ["src"],
            publicHeadersPath: "include",
            cSettings: [.headerSearchPath("include")]
        ),
        .target(
            name: "OpenCVWrapper",
            dependencies: ["CVEstimateAffine3DObjC"]
        ),
        .testTarget(
            name: "OpenCVWrapperTests",
            dependencies: ["CVEstimateAffine3D"]
        ),
    ],
    cxxLanguageStandard: .cxx14
)

のようなイメージで、OpenCVWrapperObjC ターゲット (Objective-C++ ラッパー) では dependencies で先程の opencv2.xcframework のパッケージを依存に持っていて、さらにそのターゲットに依存した OpenCVWrapper ターゲット (こちらが Swift ラッパー) と OpenCVWrapperTests ターゲット (OpenCVWrapper の単体テストコード) から構成されています。

ディレクトリ構成は

  • Package.swift
  • Sources/OpenCVWrapperObjC/src/*.mm: Objective-C++ コード (OpenCV の C++ API を呼び出すような API を用意し、OpenCVWrapper から呼び出される)
  • Sources/OpenCVWrapperObjC/include/OpenCVWrapper/OpenCVWrapper.h: ヘッダファイル
  • Sources/OpenCVWrapper/*.swift: Swift コード (Objective-C++ の API を呼び出し、Swift から利用しやすいようなラッパーを用意している)
  • Tests/OpenCVWrapperTests/OpenCVWrapperTests.swift: OpenCVWrapper の単体テストコード

となっています。

実は Swift コードを書かずに Objective-C++ コードだけでも問題はないのですが、iOS アプリ側が Swift の場合、Swift の方が型がしっかりしているため Objective-C++ から受け取った変数に対して型のキャストが大量に発生してしまい面倒くさいので、そこまで含めた Swift ラッパーを用意しておくのがお薦めです。

iOS アプリから呼び出し

iOS アプリのプロジェクトを Xcode で開き、メニューバーから File > Swift Packages > Add Package Dependency... をクリックします。

f:id:optim-tech:20201214040836p:plain

すると、以下のような画面が開くので、先程のラッパーライブラリの Swift パッケージの Git URL を貼り付けて Next をクリックします。

f:id:optim-tech:20201214040843p:plain

次の画面ではパッケージのバージョンの指定やブランチもしくはコミットハッシュでの指定を行います。

f:id:optim-tech:20201214040849p:plain

さらに Next をクリックすると依存関係の解決が始まり、終わると以下のようにどのターゲットの依存として追加するかを選択する画面が出てきます。

f:id:optim-tech:20201214040854p:plain

Finish をクリックすると、以下のようにプロジェクトのファイル一覧のすぐ下に、今回追加された Swift パッケージが表示されているのが確認できると思います。さらに、opencv2 パッケージのリストを展開して Referenced Binaries を展開すると opencv2.xcframework がいるのも確認できるかと思います。

f:id:optim-tech:20201214040901p:plain

これで iOS アプリから、ラッパーライブラリで定義した関数を呼び出すことができます。(要 import)

ハマったポイント

第一の罠「XCFramework の作り方」

XCFramework の作り方のセクションで説明したように、XCFramework にはいくつか種類があり、

  • -library-headers の指定でライブラリとヘッダファイルから作る場合
    • 動的ライブラリ
    • 静的ライブラリ
  • -framework の指定で *.framework から作る場合
    • 動的フレームワーク (動的ライブラリを内包する *.framework)
    • 静的フレームワーク (静的ライブラリを内包する *.frameowrk)

今回 OpenCV で使用したのは -framework で動的フレームワークを指定するタイプでした。

ですが、初めは -library で静的ライブラリを指定する方法をとっていました。しかし、-library で作った XCFramework を iOS アプリで利用する場合にはそもそも罠があるのです。

このような場合、iOS アプリのビルド時にライブラリはちゃんとアプリにリンクされるので、実機デバッグやシミュレータではちゃんと動いてくれます。しかし、App Store Connect にアップロードしようとすると、ITMS-90432 エラーで弾かれます。これはなぜかというと、静的ライブラリは iOS アプリ自体に静的リンクされた後、本来は不要になるはずなのですが、なぜか (コード署名の関係?) ビルド過程で *.xcframework/ 直下のファイルを *.app/Frameworks/ 直下にコピーしてしまうため、*.xcframework/*.a がコピーされて *.app/Frameworks/*.a のように正しくない場所に正しくないファイルがある状態になってしまうからです。-framework で作られた XCFramework だと *.xcframework/*.framework となっているので、*.app/Frameworks/*.framework にコピーされても想定通りということになります。

では -framework で作成した XCFramework であれば動的だろうが静的だろうがどちらでも良いのかというと、ここにもまた罠があります。

どういった罠かというと、静的フレームワークから XCFramework を作成した場合、Xcode 12.2 でアプリのビルド時に code object is not signed at al というエラーが出てしまうというものです。これはビルドの過程で codesign コマンドによって *.app/Frameworks/*.framework のコード署名が行われるのですが、その際に *.app/Frameworks/*.framework/* にあるサブコンポーネント (バイナリ) がコード署名されていないからその親である *.framework ファイルのコード署名もできないというエラーです。そのため、手動でそのサブコンポーネントのファイルを codesign コマンドでコード署名してやるとビルドは通るのですが、今度は実機デバッグしようとした際に Unable to install "アプリ名" というエラーが出ます。

そこで調べてみると、

といった数々のコード署名周りの問題報告が上がっており、まとめると、

  • Xcode 12.2 より前のバージョンではそもそも SwiftPM 経由で取得されたフレームワークをコード署名できない既知の問題があった
  • Xcode 12.2 ではそれが修正されたが、静的フレームワークでは依然として解決に向けた動きがない

という状況であるということがわかりました。Swift の公式フォーラムでは静的フレームワークを使用したい人たちの悲痛な叫びだけが残っています...

こうしてこの記事で説明していたような「-framework で動的フレームワークを指定するタイプ」に落ち着いたというわけです。改めてみてみると、すでに XCFramework を SwiftPM 経由で活用しているような PSPDFKit や ios_system ではどちらもこのタイプでした。

第二の罠「OpenCV (CMake) と Xcode 12.2 の相性問題」

私が試した際は master ブランチの OpenCV を master ブランチの CMake でビルドするしかなかったので、ひょっとしたら現在は修正されているかもしれませんが、OpenCV をビルドスクリプトでビルドする際に xcode-select で Xcode 12.2 が指定されていると、シミュレータ向けのビルドの方でのみ以下のようなエラーが出てしまいました。

-- Setting up iPhoneSimulator toolchain for IOS_ARCH='x86_64'
-- iPhoneSimulator toolchain loaded
-- Setting up iPhoneSimulator toolchain for IOS_ARCH='x86_64'
-- iPhoneSimulator toolchain loaded
-- The CXX compiler identification is AppleClang 12.0.0.12000032
-- The C compiler identification is AppleClang 12.0.0.12000032
CMake Error at CMakeLists.txt:109 (enable_language):
  No CMAKE_CXX_COMPILER could be found.



CMake Error at CMakeLists.txt:109 (enable_language):
  No CMAKE_C_COMPILER could be found.

結局原因はわからず、Xcode 12.1 ではビルドが通るので Xcode 12.1, 12.2 を同居させて 12.1 の方を xcode-select で選択してビルドしましたが、気になるところです。

第三の罠「OpenCV の Info.plist」

今回の XCFramework を使ったアプリを App Store Connect にアップロードする際、ITMS-90208 エラーが返ってきました。これはアプリの最低サポート iOS バージョンは 14.0 になっているのに、*.app/Frameworks/*.framework の最低サポート iOS バージョンが 8.0 になっているのでそれを満たすことができないというエラーです。

なぜそのようなことになっているかというと、OpenCV のビルドスクリプトが使用している *.framework に同梱する Info.plist のテンプレートで、動的フレームワーク向けの Info.Dynamic.plist.in では MinimumOSVersion8.0 が固定値で書かれているためです。静的フレームワーク向けの Info.plist.in ではこれはありません。

ではこのキー自体を削除すれば良いのかというと、そうすると今度は ITMS-90530 エラーで「64-bit デバイスのみをサポートするアプリでは MinimumOSVersion に 8.0 かそれ以上を指定する必要がある」と怒られてしまいます。

そのため、私はビルド後の opencv2.xcframework/ios-arm64/opencv2.framework/Info.plistopencv2.xcframework/ios-x86_64-simulator/opencv2.framework/Info.plistMinimumOSVersion をアプリに合わせて 14.0 に書き換えることで解決しました。

第四の罠「iOS シミュレータ向けのアプリのビルド」

x86_64 な macOS 上で iOS シミュレータ向けにアプリをビルドすると、当然 x86_64 でビルドされるわけなので、今回はシミュレータ向けの *.framework を x86_64 アーキテクチャ向けにのみビルドしました。しかし、今回の構成だと ld が OpenCV 周りのシンボルで arm64 アーキテクチャ向けのシンボルが見つからないというエラーを吐いてきます。

これは

によると、どうやら Xcode 12 から iOS シミュレータで arm64 アーキテクチャのバイナリが求められるようになったことが原因のようです。おそらく Apple Silicon Mac への対応のためでしょうか?

ひとまず、*.xcconfig にビルド設定をまとめている場合は

EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64

を記述することで回避できます。今後社用マシンでも Apple Silicon Mac を使用することになった際にはちゃんと対応を考えたいですね。

参考

最後に

まだまだ情報が少ない XCFramework 周りですが今後間違いなく使われていくと思っているので、静的フレームワークを扱う場合の問題など課題は多いですが少しずつ解決していくことを期待しています。個人的にはせっかく最近 Swift の対応プラットフォームが広がってきているので、Xcode でしか使えない XCFramework よりも Rust の Cargo のように SwiftPM がビルドスクリプトを持つようになればダイレクトに CMake を呼び出してビルドするような Swift パッケージが書けるのになぁと思っていたりしますが、そうすると今度は Rust にある bindgen のようなものが Swift にはないのでラッパーを手書きしないといけないのが辛くなってくるところです。

OPTiM では中途・新卒ともにエンジニアを募集しています。

ライセンス

冒頭の画像で使用している Swift ロゴは Apple Inc. の登録商標です。