Balto開発で得たiOSのSDK開発の知見

昨日は @inoinojp 大先生の[自称JavaScript中級者が知らなさそうな、10個の仕様](http://qiita.com/inoinojp/items/e368589c7787823dce4c)でした！

# はじめに

ということでこんにちは、Goodpatch で [Balto](https://www.balto.io/) の iOS/iOS SDK/サーバサイド(ベータまではWebフロントも)を開発しています HirokiTerashima です。
一応 iOS エンジニアです。

[Balto](https://www.balto.io/)  はまだベータ版ですが間もなく正式リリースをしますので、ぜひともよろしくお願いします！m(_ _)m

2016年はこの [Balto](https://www.balto.io/) の開発に心血を注いだ一年でした……
初めてのSDK開発や比較的新しい Elixir という言語でサーバサイドを開発したりだとか、新しいことへのチャレンジで溢れておりました。

そんな[Balto](https://www.balto.io/)の SDK 開発で得てきた、あんまり使われないであろう知見を共有していこうかと思います。

# iOSのSDK開発における注意

よく見る `xxxxx.framework` となっているやつですね。
基本事項はここでは省きます。

## CocoaPods や Carthage は使えないと思う

いきなり辛いですね。
iOS Developer の皆さんはよくご存知、ライブラリ管理のツールです。

これが何故使えないのかというと、ライブラリのクラスが Public クラスになっていたり、グローバル関数が存在している可能性があるからです。

自分で作った SDK を組み込んだアプリ側で、SDKに入れたライブラリが使えてしまうことになってしまいます。 Xcode の変換候補に入れてないはずの Alamofire のメソッドが出てきたぞ？？？と思われてしまいますね……

なのでもし使うなら手動で入れて Public クラスを全部消して……という途方もない作業が待っています。
それをするくらいなら自分で実装するのも良いと思います、勉強にもなるし。

## SDK側に画面を持つ場合は注意しよう

裏側で勝手に何かやってくれるタイプの SDK ならあまり意識する必要はないのですが、もし SDK 側で画面を作って表示するようなことがあれば、UI の実装に注意して下さい。

当たり前っちゃ当たり前なんですが、もし組み込んだ側のアプリケーションで UINavigationBar.appearance などが設定されていた場合、引きずられます。
なので SDK の実装では正しくデザインを設定していたとしても組み込んだらおかしなデザインになった！となりかねません。

逆も然りですね。
SDK 側で appearance を設定すると……言わずもがな。

また UIImage をコードから生成するときも注意です。
普段なら

```swift
let hogeImageView.image = UIImage(named: "hoge")
```

とサラッと書くと思いますが、SDK 側が SDK にある Asset から画像を引っ張るには bundle の指定が必要です。

```swift
let hogeImageView.image = UIImage(named: "hoge", in: Bundle(for: type(of: self)), compatibleWith: nil)
```

こんな感じですね。
画像だけではなく Plist や Storyboard などのリソースを読み込むときは注意しましょう。

## Bitcode 対応

Bitcode についての説明は省きますが、これが曲者でした。
有名な話ではあるんですが、作成した SDK を入れた側で 'Rebuild from bitcode' にチェックを入れて Archive すると、 
`xxx does not contain bitcode.` のエラーが出ます。
SDK の BuildSettings では `Enable Bitcode` は `YES` になっているのに……

こちら 'Rebuild from bitcode' のチェックを外すと Archive 出来るんですが、SDK を提供する側としてどちらにも対応させないと、使い勝手の悪い SDK になってしまいます。

これの解決方法として `BuildSettings` に `BITCODE_GENERATION_MODE` を `bitcode` で追加します。逆に必要ないときは `marker` を入れます。 `BITCODE_GENERATION_MODE` はデフォルトでは存在しないので `Add User-Defined Setting` から自分で追加して下さい。

<img width="420" alt="スクリーンショット 2016-12-13 22.03.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/98880/95015adb-19d2-7460-7f7e-f2128ed5745a.png">

余談ですが Xcode 7.x (細かい数字は忘れました) のときにこれを入れたらシンタックスハイライトが全く効かなくなって辛かったです……

## 不要なアーキテクチャの削除

Xcode 7 までは Store に SDK を入れたアプリを申請するときだけ必要だったようですが、Xcode 8 になってからは普通に Archive するときもこの作業が必要となったようです(自分の環境では7から8に上がったタイミングで出ました)。

こちらは Archive 時に `Failed to verify bitcode in xxx. error: Cannot extract bundle from ...` というエラーが出ます。

SDK を作るときはシミュレータにも対応させることが多いので Universal Framework を作ることがほとんだと思いますが Embedded Framework にシミュレータのアーキテクチャである `i386` `x86_64` が含まれているとエラーが出るようです。

対応方法としてはアーカイブのときに BuildPhaseScript などで `i386` `x86_64` のアーキテクチャを削除してあげます。

この方法のスクリプトは色々なところにありますが、そのうち自分が使ったものです。

```bash
APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"

# ここの *.framework は必要なものだけに絞る場合は指定して下さい。
find "$APP_PATH" -name '*.framework' -type d | while read -r FRAMEWORK
do
    FRAMEWORK_EXECUTABLE_NAME=$(defaults read "$FRAMEWORK/Info.plist" CFBundleExecutable)
    FRAMEWORK_EXECUTABLE_PATH="$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME"

    EXTRACTED_ARCHS=()

    for ARCH in $ARCHS
    do
        lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
        EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
    done

    lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
    rm "${EXTRACTED_ARCHS[@]}"

    rm "$FRAMEWORK_EXECUTABLE_PATH"
    mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"
done
```

`lipo` コマンドは複数アーキテクチャ対応のユニバーサルファイルを生成するためのコマンドですね。

もし RunScript でやる場合、 BuildPhase の順番にも気をつけて下さい。
**`Embedded Frameworks` より上に書いていた場合は同じようにエラーになります**。
`Embedded Frameworks` はデフォルトからあるのでそれより下に来ることはあまりないかもしれませんが、自分はそれでハマりました……

Bitcode 対応と併せて、iOS の SDK 開発は、**組み込んだ側でArchive/Storeにアップロード出来るまで**です。
早目にそこを確認したほうが得策かもしれません。

## CocoaPods や Carthage で配布

今やこれに対応していないと、せっかく作ったものがなかなか使用されないかもですね(ベータ版 Balto は対応していませんでした……すみません……)

通常のオープンソースであるライブラリなどであれば良いのですが、ソースを公開したくない SDK などの `.framework` 形式になっているものを配布する場合、若干通常と違うのでご紹介します。

### Carthage の場合

こちらはシンプルです。

```bash
carthage build --no-skip-current
carthage archive xxx
```

これで `xxx.framework.zip` というファイルが出来るので、GitHub のリポジトリの Release にファイルを上げましょう。

### CococaPods の場合

当然ながら Podspec を書きます。

```ruby
Pod::Spec.new do |s|
  s.name         = "xxxxx"
  s.version      = "3.0.0"
  s.summary      = ""
  s.description  = <<-DESC
                   僕の考えた最強の SDK
                   DESC

  s.homepage     = "https://github.com/xxxxx/xxxxx"
  s.license      = { :type => "Apache 2.0", :file => "LICENSE" }
  s.author       = { "Hiroki Terashima" => "hoge@hoge.com" }
  s.platform     = :ios, "9.0"
  s.requires_arc = true
  
  # ここは何処かに上げたファイルのURLでも良い
  # s.source = { :http => "https://github.com/xxxxx/xxxxx/releases/download/#{s.version}/Cocoapods.zip" }
  s.source = { :git => "https://github.com/xxxxx/xxxxx.git", :tag => "#{s.version}" }

  s.preserve_paths      = 'xxxxx.framework'
  s.source_files        = 'xxxxx.framework/Headers/xxxxx-Swift.h'
  s.public_header_files = 'xxxxx.framework/Headers/xxxxx-Swift.h'
  s.vendored_frameworks = 'xxxxx.framework'
end
```

生成した `xxxxx.framework` にある Header ファイルのパスなどを記載してあげるのが、他と違う感じがありますね。

こんな感じで書けたら

```bash
pod trunk push
```

で CocoaPods に登録しましょう。

## まとめ

もっと他にも色々あるんですが (IPA ファイルの中の話とか、IPA を Elixir で解析した話しとか) 一旦はこの辺で終わります。

まとめとしてiOS の SDK 開発は、**組み込んだ側でArchive/Storeにアップロード出来るまで**です。最後の最後で ▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂ うわあああああぁぁぁぁぁぁぁ とならないように気をつけて下さい！

明日はヘッドホンがトレードマーク(だと勝手に思っている) @kubosho_ さんのProttのdark themeのお話です！乞うご期待！