Swift で Framework を作ってみる使ってみる

# 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...

<img width="480" alt="Screen Shot" src="https://qiita-image-store.s3.amazonaws.com/0/65634/27dd8ec6-830e-d5b9-f39a-295a3a87bad1.png">

そして、共有化したい　`HelloView` `HelloViewController` そしてその storyboard `HelloView` を用意します。

```.swift
@IBDesignable public class HelloView: UIView {

    override public func drawRect(rect: CGRect) {
		UIColor.yellowColor().set()
		UIBezierPath(ovalInRect: self.bounds).fill()
    }

}
```

HelloView は view に黄色い楕円を描くという仕様とします。


```.swift
class HelloViewController: UIViewController {

    @IBOutlet var helloView: HelloView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
}
```

<img width="800" alt="Screen Shot 2016-07-15 at 3.59.47 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/22820ef4-fed0-2169-09d1-6fad3f23d86b.png">

さらには、View や ViewController だけではなく、UIを持たない class も定義して見る事にします。

```.swift
public class NSHello: NSObject {
	public func hello() {
		print("Hello")
	}
}


public class SwiftyHello {
	public func hello() {
		print("Hello")
	}
}
```

## ビルド

では、ビルドしてみましょう。ビルド後は、「Products」内の「HelloKit」を右クリック「Show in Finder」でその内容を確認してみます。中には「Headers」や storyboard などが含まれている事が確認出来ます。

<img width="337" alt="Screen Shot 2016-07-13 at 12.44.52 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/c6a5669f-25e4-381a-9fc2-0691c5f576f0.png">

<img width="807" alt="Screen Shot 2016-07-14 at 10.55.56 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/6a18acae-bb9f-80d2-1ecd-ff8707822df9.png">

「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 はヘッダーに入っていません。

```.h
(略)
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 をベースに初めて見たいと思います。

<img width="726" alt="Screen Shot 2016-07-14 at 12.16.12 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/cf3aa727-2962-bdd6-ae98-fe0fdf3ea99a.png">

`Main.storyboard` を編集して、Navigation Controller ベースにしてみたいと思います。そして `ViewController` の `Hello` ボタンを押すと、`HelloKit` の `HelloViewController` を表示してみたいと思います。

<img width="1659" alt="Screen Shot 2016-07-14 at 12.53.16 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/6710d40c-f1be-2174-1077-4cf0c0643052.png">


そして、先ほどビルドした「HelloKit.framework」を「General」タグから「HelloApp」ターゲットに含めます。

<img width="800" alt="Screen Shot 2016-07-15 at 3.43.08 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/83ff6f5e-9cd7-ae38-e054-74984d16c98c.png">


### Embedded Binaries に framework を追加

Project のターゲットから「HelloApp」を選び、「General」を選択。そこに 「Embedded Binaries」のセクションの「＋」から HelloKit のフレームワークを選びます。ちなみに「Embedded Binaries」にフレームワークを追加するまでは、「Build Phases」タブを見ても「Embedded Binaries」のセクションはないのですが、「General」タブから一度「Embedded Binaries」にフレームワークを追加すると、なぜか「Build Phases」にも「Embedded Binaries」が現れます。

<img width="800" alt="Screen Shot 2016-07-15 at 1.14.11 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/f89724b0-a82a-64ed-6d5a-9c9152714d82.png">

「Build Phases」の中はこんな感じになります。

<img width="800" alt="Screen Shot 2016-07-15 at 4.24.14 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/e4fe8658-9b0f-77c0-3ba2-876eadd1e943.png">



### HelloKit の呼び出し

では、`ViewController` クラスに「Hello」ボタンを押された時の処理を書きます。`import HelloKit` の一文を忘れないでください。

```ViewController.swift
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 しています。

では、実行してみましょう。

<img width="568" alt="Screen Shot 2016-07-15 at 4.31.15 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/086a3f84-b4b6-d1e4-8b57-51a0e22947e7.png">

### 課題

この記事ではスムーズな手順で進めているように見えますが、本来例えばSDKを提供する場合などは、HelloKit と HelloApp が同じワークスペースでビルドできる事は稀であると言えます。この場合、Debug/Release または Device/Simulator の区分は選択中のビルドターゲットが変われば Xcode が自動的に切り替えてくれます。ところが、これを別ワークスペース例えば、それぞれプロジェクトを分けてビルドすると、最初の一回目はちゃんとビルドしてくれるようなのですが、一度 Device/Simulator などの設定を入れ替えるとビルドエラーを起こすようになり、検索しても、なかなか的を得た回答が得られず、止むを得ず上記のような記事になっています。

<img width="1103" alt="Screen Shot 2016-07-15 at 1.28.59 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/65634/7399ec6b-6a37-a6ef-3cc6-ed8ef7008525.png">

```
 'HelloViewController' is unavailable: cannot find Swift declaration for this class
```

実際には、様々はトライ＆エラーを繰り返しているので、原因と結果の因果関係がはっきりできない状態ですので、上記で説明したビルドエラーの説明も的を得ているのかどうかなんとも言えず歯切れの悪い記事となってしまいました。

### Github

上記のプロジェクトは github より取得可能です。ライセンスは MIT になっていますので、ご自由におためしくださいませ。

https://github.com/codelynx/HelloKit


### 参考資料

[Building Modern Frameworks WWDC2014](https://developer.apple.com/videos/play/wwdc2014/416/)
[Universal Cocoa Touch Frameworks for iOS 8](https://kodmunki.wordpress.com/2014/11/07/universal-cocoa-touch-frameworks-for-ios-8/)
[[Xcode6] Universal Frameworkを作る](http://mushikago.com/i/?p=5646)

### 質問ばかりで答えのないStackoverflow

[MyClass is unavailable: cannot find Swift declaration for this class - Release Build Only](http://stackoverflow.com/questions/29851550/myclass-is-unavailable-cannot-find-swift-declaration-for-this-class-release-b)
[MyClass is unavailable: cannot find Swift declaration for this class](http://stackoverflow.com/questions/35030199/myclass-is-unavailable-cannot-find-swift-declaration-for-this-class)
[Bridging Header issue - MyClass is unavailable: cannot find Swift declaration for this class](http://stackoverflow.com/questions/32312489/bridging-header-issue-myclass-is-unavailable-cannot-find-swift-declaration)
[Class is unavailable: Cannot find swift declaration for this Class](https://forums.developer.apple.com/thread/25171)