XcodeでStaticライブラリを別のStaticライブラリに依存させる方法


Staticライブラリを別のStaticライブラリに依存させようとしてつまずきました。ツイッターでつぶやいたところ、アドバイスをいただくことができて無事解決したので、その解決方法を共有します。

※説明の都合上、Twitter上のやりとりそのままではありません。

背景(やりたいこと)

話をわかりやすくするために単純な例で説明します。

登場人物は次の3つです。これらをそれぞれ別プロジェクトとして作成します。

  • App(アプリ)
  • Logger(Staticライブラリ)
  • Repository(Staticライブラリ)

ここで、AppはLoggerの機能も使うし、Repositoryの機能も使います。また、Repositoryはその実装の中でLoggerの機能を使います。

具体的なソースコードはこんな感じ(内容に意味がないのには目を瞑ってください)。

ViewController.swift
import UIKit
import Logger
import Repository

class ViewController: UIViewController {
    let logger = Logger()
    let repository = Repository()

    @IBAction func fetch(_ sender: Any) {
        logger.log("Fetching start")
        _ = repository.fetch()        
        logger.log("Fetching end")
    }

    // ...略
}
Repository.swift
import Foundation
import Logger

public struct Repository {
    public init() {}

    public func fetch() -> String {
        Logger().log("fetching...")
        return "result"
    }
}
Logger.swift
import Foundation

public struct Logger {
    public init() {}

    public func log(_ message: String) {
        print(message)
    }
}

それではつまずいてみましょう

ワークスペースを作成

ワークスペースを作成し、App、Logger、Repositoryの3つのプロジェクトをワークスペース内に追加します。

AppからLogger、Repositoryを参照

Appのプロジェクトで、LoggerやRepositoryを使いたいので、Appのプロジェクト設定にてこれらのライブラリを追加します。

[General]タブの[Frameworks, Libraries, and Embedded Content]の部分で+を押して、libLogger.aとlibRepository.aを追加します。

これで、AppからLogger、Repositoryをimportできるようになりました。

RepositoryからLoggerを参照

同じようにRepositoryでもLoggerをリンクするようにします。
Repositoryはライブラリなのでちょっと位置が違います。[Build Phases]タブの[Link Binary With Libraries]のところで+ボタンを押して、libLogger.aを追加します。

Appをビルドしてみる

Appをビルドすると…すべてうまくいきました。なんの問題ありません!あれ?つまずかなかった!?

-ObjC の登場

ここでAppのプロジェクト設定を変更します。[Build Settings]タブで[Other Linker Flags]のところに -ObjC を追加します。

そして、もう一度Appをビルドすると…リンカエラーが!

ld: 5 duplicate symbols for architecture x86_64

無事、つまずくことができました!

-ObjC とは何か

先ほどの -ObjC というリンカフラグは何でしょうか。サードパーティ製のライブラリを組み込む際に、これを付けるように指示されているものがあります。例えば、 Firebase SDKもそのひとつです

Objective-Cのカテゴリメソッドが正しくリンクされるようにするフラグ

このフラグについては、以下に解説があります。

自分なりに読み解いてみました。

ソースコードをコンパイルする際に、利用しているメソッド(正確にはシンボル)がソースコード中に足りないと、それが足りないという情報が出力されます。
リンク時に足りないものがあれば、ライブラリからそのシンボルを探してきて、できあがるアプリに追加します。

次の例は、funcAが使っているfuncXが足りなかったので、ライブラリから追加された様子です。

ところが、Objective-Cのメソッドは動的に解決されるため、コンパイル時にはこのメソッドが足りないという情報が出力されません。問題になるのはカテゴリメソッドです。カテゴリメソッドは別のクラスを拡張するメソッドのため、この情報が出力されないとそのカテゴリ自体がライブラリから追加されません。

その結果、実行時に動的に解決した時点でコードが見つからず、実行時エラーになってしまいます。

そこで、 -ObjC フラグを使います。
このフラグをつけると、足りないものだけでなく、ライブラリに存在するものは全部追加せよいう指示になります。

これにより、カテゴリもアプリに追加されるようになりました。が、本来必要なかったfuncYやfuncZも追加されるようになるため、アプリサイズは大きくなります。だから、Objective-C由来のライブラリを使っていなければ -ObjC は付けない方が得策です(なので、デフォルトでは付いていません)。

なお、 -ObjC はライブラリにのみ作用します。アプリのプロジェクト本体でObjective-Cのカテゴリメソッドを使っているだけなら特に必要ないそうです。

なぜエラーになったのか

先ほどのAppと2つのライブラリの例で起こったことを想像してみます。

Staticライブラリの場合、Repositoryの[Link Binary With Libraries]にlibLogger.aを加えると、できあがるlibRepository.aの中にlibLogger.aの内容も含まれるのだと思われます。

-ObjC をつける前は、足りないという情報のあるRepositoryとLoggerをそれぞれのライブラリから持ってくることでうまくいったのでしょう。LoggerについてはlibLogger.aとlibRepository.aのどちらから持ってこられたのかはよくわかりませんが。

ところが -ObjC をつけることで、ライブラリに含まれるものはすべてができあがるアプリに追加されるようになりました。ですから、アプリの中に2つのLoggerが含まれてしまいます。これがduplicate symbolsの原因です。

解決編(うまくいくやり方)

RepositoryにlibLogger.aをリンクするのをやめる

libRepository.aにlibLogger.aの内容が入ってしまっているのが問題です。とにかくRepositoryの[Link Binary With Libraries]からlibLogger.aを除きます。

しかし、このままではLoggerの存在がわかりませんから Loggerのビルドが先に済んでいないと(2019/11/19訂正: 後述の追記を参照)、Repository内でLoggerをimportするところでエラーになります。
ではどうすればいいでしょう。いかにもそれっぽいのが[Dependencies]の部分です。

Repositoryの[Dependencies]の+ボタンを押して、libLogger.aを追加したいところですが、残念ながらlibLogger.aを選ぶことができませんでした

2019/11/19追記:
Twitterで @kishikawakatsumi さんから指摘をいただきました
[Dependencies]の役割は、自身をビルドする前にそこにあるものをビルドする、というだけでした。ワークスペースのLoggerのビルドを先に済ませていれば、importの解決はできました。
そのビルドが先に済むようにするのが[Dependencies]なんですね。

解決方法1: 1つのプロジェクトにする

@omochimetaru さんは、もともと1つのプロジェクト内でターゲットを分けるアプローチで検証されていたのですが、その場合は[Dependencies]にlibLogger.aを追加できていました(ツイート中のlibA、libBは、それぞれこの記事のLogger、Repositoryに相当します)。

このやり方で、期待通り、libRepository.aにlibLogger.aの内容を含めることなく、Repositoryをコンパイルできることがわかりました

なお、当然ですが、Logger.swiftはLoggerターゲットに、Repository.swiftはRepositoryターゲットに入れてあります。

解決方法2: サブプロジェクトとして参照させる

自分のプロジェクト以下に含まれているものが[Dependencies]に追加できるということがわかりました。そこで、ワークスペース+3つのプロジェクトに分けている場合でも、RepositoryのサブプロジェクトとしてLoggerを追加しておけば、[Dependencies]に追加できて、同じように解決することができました

Logger.xcodeprojを選択して追加します。

解決方法3: ワークスペースなしでもよい

AppからRepository、Loggerへの依存も同じと考えて、これら2つの依存先プロジェクトをAppのサブプロジェクトとして追加してもOKです。この場合はワークスペースは不要になります。

ちなみに、アプリ全体ではなくRepository.xcodeprojを開いてRepository単体でビルド可能なので、機能単位にしぼって開発を行うこともできそうです

最後に

この記事のスクリーンショットを撮ったりしたサンプルプロジェクトをGitHubに置いてあります。細かいところを確認したいときは、そちらを参照してください。

また、ツイッターでアドバイスをくださった @omochimetaru さん、 @kishikawakatsumi さん、ありがとうございました!おかげで前に進むことができました