Framework
アプリを横断して同じコードやViewにViewControllerクラスを使いまわしたい時は時折あると思います。旧来は ソースそのものを使いまわしたり、static library を作って使いまわしていた事と思いますが、Swift時代では、そもそも Swift のコードは static library が作れなかったり、ViewController だけでなく、.xib (.nib) や storyboard を使いまわそうとすると、static library では無理があります。
iOS8 からは Framework が使えるようになりました。Frameworkはコードだけでなく、storyboard や画像などのリソースを含める事が出来。そして Header ファイルを含める事ができるので、使い回す側のクライアントサイドへ class 名や API を引き渡す事が出来ます。 OS X ではこれまでも Framework は使えるようになっていましたが、これがやっと iOS にもやってきましたので、試してみる事にしました。
環境
今回の記事は以下の環境で行っています。
- Xcode 7.3.1
- Apple Swift version 2.2 (swiftlang-703.0.18.8 clang-703.0.31)
- OS X El Capitan 10.11.5
また、話をシンプルにするために、以下の点は考慮しないものとします。
- iOS と OS X の両面戦略は考慮しない
構成
今回、Framework を試してみるにあたり、次のような構成を考えてみました。一つは「HelloKit」今後モジュール化したい部品を含めます。これには「HelloView」「HelloViewController」そしてその二つを含む storyboard。もう一つは「HelloApp」、先の「HelloKit」を利用する側のアプリ本体になります。
HelloKit Framework
では、HelloKit を作成してみましょう。Xcodeで、以下のメニューをたどり、Cocoa Touch Framework を選びます。
Xcode > File > New > Project...
そして、共有化したい HelloView
HelloViewController
そしてその storyboard HelloView
を用意します。
@IBDesignable public class HelloView: UIView {
override public func drawRect(rect: CGRect) {
UIColor.yellowColor().set()
UIBezierPath(ovalInRect: self.bounds).fill()
}
}
HelloView は view に黄色い楕円を描くという仕様とします。
class HelloViewController: UIViewController {
@IBOutlet var helloView: HelloView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
さらには、View や ViewController だけではなく、UIを持たない class も定義して見る事にします。
public class NSHello: NSObject {
public func hello() {
print("Hello")
}
}
public class SwiftyHello {
public func hello() {
print("Hello")
}
}
ビルド
では、ビルドしてみましょう。ビルド後は、「Products」内の「HelloKit」を右クリック「Show in Finder」でその内容を確認してみます。中には「Headers」や storyboard などが含まれている事が確認出来ます。
「HelloKit」は実行可能コードで、file
コマンドなどで確認する事が出来ます。iphonesimulator
と iphoneos
では当然アーキテクチャの違うコードになっています。
$ cd (略)/Debug-iphonesimulator/HelloKit.framework/
$ file HelloKit
HelloKit: Mach-O dynamically linked shared library i386
$ cd (略)/Debug-iphoneos/HelloKit.framework/
$ file HelloKit
HelloKit: Mach-O universal binary with 2 architectures
HelloKit (for architecture armv7): Mach-O dynamically linked shared library arm
Debug-iphoneos/HelloKit.framework/HelloKit (for architecture arm64): Mach-O 64-bit dynamically linked shared library
ヘッダーファイルを確認してみます。先ほどのクラスが並んでいますが、一つ気になるところがあります。NSObject をベースとした、NSHello
class はあるのに、Swift ネイティブ の SwiftyHello
class はヘッダーに入っていません。
(略)
SWIFT_CLASS("_TtC8HelloKit9HelloView")
@interface HelloView : UIView
- (void)drawRect:(CGRect)rect;
- (nonnull instancetype)initWithFrame:(CGRect)frame OBJC_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
@end
@class NSBundle;
SWIFT_CLASS("_TtC8HelloKit19HelloViewController")
@interface HelloViewController : UIViewController
@property (nonatomic, weak) IBOutlet HelloView * _Null_unspecified helloView;
- (void)viewDidLoad;
- (void)didReceiveMemoryWarning;
- (nonnull instancetype)initWithNibName:(NSString * _Nullable)nibNameOrNil bundle:(NSBundle * _Nullable)nibBundleOrNil OBJC_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
@end
SWIFT_CLASS("_TtC8HelloKit7NSHello")
@interface NSHello : NSObject
- (void)hello;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
HelloApp
今度は、Frameworkを使う側の HelloApp を作成してみます。プロジェクトのターゲットから、iOS の Application のターゲットを追加します。今回は Single View Application をベースに初めて見たいと思います。
Main.storyboard
を編集して、Navigation Controller ベースにしてみたいと思います。そして ViewController
の Hello
ボタンを押すと、HelloKit
の HelloViewController
を表示してみたいと思います。
そして、先ほどビルドした「HelloKit.framework」を「General」タグから「HelloApp」ターゲットに含めます。
Embedded Binaries に framework を追加
Project のターゲットから「HelloApp」を選び、「General」を選択。そこに 「Embedded Binaries」のセクションの「+」から HelloKit のフレームワークを選びます。ちなみに「Embedded Binaries」にフレームワークを追加するまでは、「Build Phases」タブを見ても「Embedded Binaries」のセクションはないのですが、「General」タブから一度「Embedded Binaries」にフレームワークを追加すると、なぜか「Build Phases」にも「Embedded Binaries」が現れます。
「Build Phases」の中はこんな感じになります。
HelloKit の呼び出し
では、ViewController
クラスに「Hello」ボタンを押された時の処理を書きます。import HelloKit
の一文を忘れないでください。
import UIKit
import HelloKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
@IBAction func helloAction(sender: AnyObject) {
let bundle = NSBundle(forClass: HelloViewController.self)
let storyboard = UIStoryboard(name: "HelloView", bundle: bundle)
let viewController = storyboard.instantiateInitialViewController() as! HelloViewController
self.navigationController?.pushViewController(viewController, animated: true)
}
}
HelloKit のバンドルより HelloView
の storyboard を取り出し、initial view controller をインスタンス化しています。HelloKit側では ViewController を HelloViewController
class に設定しているので、HelloViewController
にキャストしています。そして、navigation controller に push しています。
では、実行してみましょう。
課題
この記事ではスムーズな手順で進めているように見えますが、本来例えばSDKを提供する場合などは、HelloKit と HelloApp が同じワークスペースでビルドできる事は稀であると言えます。この場合、Debug/Release または Device/Simulator の区分は選択中のビルドターゲットが変われば Xcode が自動的に切り替えてくれます。ところが、これを別ワークスペース例えば、それぞれプロジェクトを分けてビルドすると、最初の一回目はちゃんとビルドしてくれるようなのですが、一度 Device/Simulator などの設定を入れ替えるとビルドエラーを起こすようになり、検索しても、なかなか的を得た回答が得られず、止むを得ず上記のような記事になっています。
'HelloViewController' is unavailable: cannot find Swift declaration for this class
実際には、様々はトライ&エラーを繰り返しているので、原因と結果の因果関係がはっきりできない状態ですので、上記で説明したビルドエラーの説明も的を得ているのかどうかなんとも言えず歯切れの悪い記事となってしまいました。
Github
上記のプロジェクトは github より取得可能です。ライセンスは MIT になっていますので、ご自由におためしくださいませ。
参考資料
Building Modern Frameworks WWDC2014
Universal Cocoa Touch Frameworks for iOS 8
[Xcode6] Universal Frameworkを作る
質問ばかりで答えのないStackoverflow
MyClass is unavailable: cannot find Swift declaration for this class - Release Build Only
MyClass is unavailable: cannot find Swift declaration for this class
Bridging Header issue - MyClass is unavailable: cannot find Swift declaration for this class
Class is unavailable: Cannot find swift declaration for this Class