解決したい問題
アプリ内にSwiftとObjective-Cのコードが混在しているときに、Swiftのクラスに @objc
をつけることでObjective-Cからでもそのクラスを使うことができます。
import Foundation
@objc
class MySwiftClass: NSObject {
@objc(addX:andY:)
func add(x: Int, y: Int) -> Int {
return x + y
}
}
#import "MyObjcClass.h"
#import "MyApp-Swift.h"
@implementation MyObjcClass
- (NSInteger)compute {
MySwiftClass *mySwift = [[MySwiftClass alloc] init];
return [mySwift addX:10 andY:20];
}
@end
ところが、Swift Package Manager(以下 SwiftPM)のパッケージとして公開しているライブラリで、同じように @objc
を付けても、そのままではライブラリを利用するアプリのObjective-Cコードからは呼び出すことができません。
import Foundation
// "OurLibrary" という名前のSwift Packageの中にあるクラス
@objc
public class OurSwiftClass: NSObject {
@objc(addX:andY:)
public func add(x: Int, y: Int) -> Int {
return x + y
}
}
#import "MyObjcClass.h"
// パッケージを利用するアプリの中にあるクラス
@implementation MyObjcClass
- (NSInteger)compute {
OurSwiftClass *ourSwift = [[OurSwiftClass alloc] init];
// error: Use of undeclared identifier 'OurSwiftClass'
return [ourSwift addX:10 andY:20];
}
@end
これをなんとか解決してみたいと思います。
注意: 最初に書いておきますが、あまりスマートには解決できませんでした…
なぜ呼び出すことができないのか
呼び出すことができない理由は、ズバリ、 インポートができないから です。
そういえば、Swiftのコードからこのクラスを使うときでも import
を行いますね。
import Foundation
import OurLibrary // ← これ
func compute() -> Int {
let ourSwift = OurSwiftClass()
return ourSwift.add(x: 10, y: 20)
}
では、Objective-Cの方にもインポートを追加してみましょう。
……えっと、何を追加したらいいんでしたっけ?
#import "OurLibrary"
// error: 'OurLibrary' file not found
#import "OurLibrary.h"
// error: 'OurLibrary.h' file not found
@import OurLibrary;
// error: Module 'OurLibrary' not found
どれもエラーになってしまいました
気を取り直して、アプリ内のSwiftのクラスを使うときはどうしてましたっけ?
そうです、アプリ内のSwiftのクラスを使うときは「(プロジェクト名)-Swift.h」をインポートしていました。
#import "MyApp-Swift.h" // ← これ
そうか!「(パッケージ名)-Swift.h」をインポートすれば!
#import "OurLibrary-Swift.h"
// error: 'OurLibrary-Swift.h' file not found
まあ、ダメですよね
「(プロジェクト名)-Swift.h」とはなんなのか
デタラメにトライ&エラーを繰り返してもうまくいかないので、もう少しきちんと考えてみます。
アプリ内で使える「(プロジェクト名)-Swift.h」って何でしょう?
Xcodeで #import "MyApp-Swift.h"
の部分をCommandキーを押しながらクリックして、[Jump to Definition]を実行すると、こんなヘッダファイルに飛びます。
// Generated by Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7)
...
SWIFT_CLASS("_TtC5MyApp12MySwiftClass")
@interface MySwiftClass : NSObject
- (NSInteger)addX:(NSInteger)x andY:(NSInteger)y SWIFT_WARN_UNUSED_RESULT;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
...
1行目のコメントにもあるように、SwiftのクラスからObjective-C用のヘッダファイルが生成されていることがわかります。
このファイルはいつ生成されているのでしょうか? 答えはビルドログの中にあります。
Xcodeの左側にあるNavigatorのうち、右端のReport Navigatorを表示します(ショートカットキーは ⌘9
)
適当に最新のBuildを選択して、アプリターゲットのログの「Compile Swift source files」の部分を開いてみます。

文字がいっぱいでわかりづらいですが、一部だけ抜き出したものが以下です。
CompileSwiftSources normal x86_64 com.apple.xcode.tools.swift.compiler (in target 'MyApp' from project 'MyApp')
...
/Applications/Xcode.app/.../bin/swiftc ...
-emit-objc-header -emit-objc-header-path .../MyApp-Swift.h
...
Swiftコンパイラの swiftc
に -emit-objc-header
と -emit-objc-header-path
オプションが渡され、それにより「(プロジェクト名)-Swift.h」が生成されていることがわかります。
$ swiftc --help
OVERVIEW: Swift compiler
...
OPTIONS:
...
-emit-objc-header-path <path>
Emit an Objective-C header file to <path>
-emit-objc-header Emit an Objective-C header file
...
ということは、Swift Packageのライブラリでも、ソースコードからこのヘッダファイルを生成し、それが利用側でインポートできるようになれば、今回の問題が解決できそうです
SwiftPMの仕組みを考えてみる
SwiftPMは、大雑把に言えば、パッケージのソースコードをgitリポジトリから取得してきたあと、それをローカルでコンパイルして一緒にリンクするようにしてくれるものです。
先ほどのアプリのビルドログにも、取ってきたパッケージのビルドフェーズが出力されています。

なるほど。ここを見ても、 -emit-objc-header
と -emit-objc-header-path
オプションは渡されていません。
ここにオプションを渡すことはできるでしょうか?
Package.swiftに書くPackageのドキュメント を読むと、Swiftコンパイル設定としては、 define
によるコンパイルマクロの定義と、 unsafeFlags
による、任意のコンパイルオプションの引き渡しの2つがあることが書かれています。
後者の unsafeFlags
を使えば、 -emit-objc-header
と -emit-objc-header-path
オプションを渡せそうですね!
早速、やってみましょう。
// swift-tools-version:5.1
...
let package = Package(
...
targets: [
.target(
name: "OurLibrary",
dependencies: [],
// ↓これを追加
swiftSettings: [
.unsafeFlags(["-emit-objc-header", "-emit-objc-header-path", "OurLibrary-Swift.h"])
]),
...
]
)
ヘッダファイルのパスですが、とりあえずファイル名だけ指定してみました。
Xcodeでこのパッケージを開いてビルドすると、OutLibrary直下(Package.swiftと同じ場所)に「OurLibrary-Swift.h」が出力されました!
ところがこのパッケージを利用しようとすると、アプリ側ではこんなビルドエラーが出ます。
error: The package product 'OurLibrary' cannot be used as a dependency of this target because it uses unsafe build flags. (in target 'MyApp' from project 'MyApp')
先ほどのPackageのドキュメント をよく読むと、あらやだ、 unsafeFlags
を使うと他のパッケージからこのターゲットが利用不可になると書かれてました。
/// As some build flags could be exploited for unsupported or malicious
/// behavior, the use of unsafe flags make the products containing this
/// target ineligible to be used by other packages.
ですが、もしこれが利用可能だとしても、どこに「OurLibrary-Swift.h」を出力したら良いのかわかりません。Package.swiftと同じところに出力されても、利用するアプリからその場所を知る術がないのです。
Cターゲットのパッケージにする
ちょっと行き詰まってしまった感があるので、ここで方針を変えてみます。
SwiftPMは、Swiftターゲットだけでなく、 Cターゲット も作成できます。
Cターゲットのライブラリであれば、Objective-Cから利用できます。
それでは、Cターゲットを作成してみましょう。Cターゲットは慣習的に名前を「C」から始めるようなので「COurLibrary」としました。
また、「COurLibrary」はSwiftターゲットである「OurLibrary」に依存するようにしました(なお、先ほどの unsafeFlags
は削除しました)。
// swift-tools-version:5.1
...
let package = Package(
...
targets: [
.target(
name: "OurLibrary",
dependencies: []),
// ↓これを追加
.target(
name: "COurLibrary",
dependencies: ["OurLibrary"]),
...
]
)
それから、パッケージで公開するライブラリを、Cターゲットの方にしてしまいましょう(ここはSwift向けとC向けの2つをライブラリとして公開しても別にいいと思いますが)。
// swift-tools-version:5.1
...
let package = Package(
...
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "OurLibrary",
targets: ["COurLibrary"]), // ← Cターゲットを公開
],
...
)
そして、COurLibraryターゲットに含めるソースファイルを用意します。
- Sourcesフォルダの下にCOurLibraryフォルダを作成して、
- その中に COurLibrary.m という空のファイルを置きます。
- また、includeというフォルダを作成して、先ほど
unsafeFlags
を用いて生成された OurLibrary-Swift.h をそこに入れます(ついでにCOurLibrary.hにリネームしました)。

COurLibrary.m は空のファイルですが、COurLibraryがCターゲットだとSwiftPMに認識させるために必要です。中に入っているソースファイルの拡張子でターゲットの種類を認識しているのです。
これで、Objective-Cからも利用できるライブラリになりました。パッケージ側の作業はここまでです。
最後に、利用側のアプリのObjective-Cソースで、C向けのターゲットとして作られたCOurLibraryをインポートします。
#import "MyObjcClass.h"
@import COurLibrary; // ← これを追加
// パッケージを利用するアプリの中にあるクラス
@implementation MyObjcClass
- (NSInteger)compute {
OurSwiftClass *ourSwift = [[OurSwiftClass alloc] init];
return [ourSwift addX:10 andY:20];
}
@end
やった!これでビルドが通りました
なお、これ、全部Staticリンクされているから大丈夫なだけのような気もしていますが、もしDynamicならどうなるのかはよくわかりません
また、CターゲットのCOurLibraryをライブラリとして公開しましたが、Swiftソースからも、 import OurLibrary
(Swiftターゲットの方をインポート)をすることで、そのまま使うことができています。COurLibraryがOurLibraryに依存しているから可能なのかなと思っていますが、これもDynamicリンクなら(以下略)
ヘッダを更新する
さて、一見、うまく解決したかのようですが、上に書いたやりかたでは、Cターゲットのincludeフォルダに置いたCOurLibrary.hもソースファイルの一部なので、必要に応じてこれをメンテナンスする必要があります。つまり、Sources/OurLibrary以下のSwiftソースを変更するたびに、COurLibrary.hを更新しないといけないわけです。
ヘッダの更新は、 -emit-objc-header
を付けてSwiftコードをコンパイルすることでできます。しかし、SwiftPMによるSwiftコードのコンパイルに -emit-objc-header
を渡すには unsafeFlags
を使う必要があります。そして、それを付けたターゲットは他から利用できないわけですね。
…そこで思いつきました。
パッケージで公開するターゲットと、公開しないターゲットの2種類を作っておいて、公開しないターゲットの方に unsafeFlags
を使えばいいのでは? 開発中はそちらのターゲットをビルドすれば、ヘッダファイルを更新できます。
やってみましょう!
// swift-tools-version:5.1
...
let package = Package(
...
targets: [
.target(
name: "OurLibrary",
dependencies: []),
// ↓これを追加
.target(
name: "UpdateHeader",
dependencies: [],
path: "Sources/OurLibrary",
swiftSettings: [
.unsafeFlags(["-emit-objc-header", "-emit-objc-header-path", "Sources/COurLibrary/include/COurLibrary.h"])
]),
...
]
)
ターゲットには path
引数があり、指定しないとデフォルトで Sources/(ターゲット名) になっているのですが、それではコンパイルするソースファイルがないので、この引数で敢えてOurLibraryのソースを指定してみました。すると、
target 'UpdateHeader' has sources overlapping sources:
という無慈悲なエラーが。そうか、ダメなのか…
となると、つまるところ、同じターゲットでやるしかないわけで、そうすると、ビルド環境によって挙動を変えるくらいしか手がない。そう、環境変数です。
Package.swiftはSwiftソースなので、 Package
オブジェクトを作るところはSwiftのコードが使えます。そこで、環境変数で挙動を変えるようにしてみます。
// swift-tools-version:5.1
...
import PackageDescription
import Foundation // ←ProcessInfoを使うために追加
// 環境変数を見てヘッダを更新するかどうかを判断
let updateHeader = ProcessInfo.processInfo.environment["UPDATE_OBJC_HEADER"] != nil
let package = Package(
...
targets: [
.target(
name: "OurLibrary",
dependencies: [],
// ↓ヘッダを更新する場合にのみunsafeFlagsを使う
swiftSettings: updateHeader
? [.unsafeFlags(["-emit-objc-header", "-emit-objc-header-path", "Sources/COurLibrary/include/COurLibrary.h"])]
: nil),
...
]
)
これで、UPDATE_OBJC_HEADER
という環境変数が定義されているときだけ、ビルド時にヘッダも更新されるようになりました。
とりあえず、この環境変数を定義して、コマンドラインからビルドを行うという下記のシェルスクリプトを作りましたが、結局、Swiftソースを更新した後に、これを何らかの方法で呼び出す必要があります。CIでフォローするとか、やりようはありそうですが、そこはまだ特には解決していません…
#/bin/sh
export UPDATE_OBJC_HEADER=1
xcodebuild -scheme OurLibrary -destination "name=iPhone 11"
謝辞というか宣伝というか
ここに書いたようなことを試行錯誤していたときに、Discordというチャットのswift-developers-japanサーバーで色々と助けていただきました。ありがとうございました。相談に乗っていただいたみなさまのおかげで、ここまでたどり着きました!
SwiftやiOS/Macアプリの開発に興味がある方は、↓ここからswift-developer-japanへ、ぜひどうぞ!
https://medium.com/swift-column/discord-ios-20d586e373c0