yashiganiの英傑になるまで死ねない日記

週末はマスターバイクでハイラルを走り回ります

CocoaPods0.36をExtensionで使うときに出る警告を抑制する

App Extensionを実装する際,本体のアプリとコードを共有したいことがあります. コード自体をExtensionのターゲットに追加することもできますが,メンテナンス性や複数のアプリで使い回すことを考慮すると,Embedded frameworkとして実装し,各々で取り込むのが理想的です.

しかし,ここで注意する点があります. Extensionでは通常のアプリと違い,いくつかのAPIへのアクセスが禁止されています. 例えば,ExtensionからUIApplicationなどのAPIにアクセスすることができません.

不意にframeworkのコードでこれらのAPIを仕様すると想定の通り動きませんので注意が必要です. そこで,安全にframeworkを共有するためにAllow app extension API onlyというオプションが存在します. (TARGETS > Deployment Info > App Extensionsから指定します.) この設定を有効にすると,framework内のコードで違反をしているときにビルドエラーを発生させることができます. 詳細は以下のドキュメントを参照してください.

CocoaPodsを使うと警告が発生

frameworkに切り出すと独立性が高くなるため,本体のプロジェクトとリポジトリを分けることが可能になります. すると,その取り込みには当然CocoaPodsのようなツールを使いたくなります. 幸いCocoaPodsには0.36よりframeworkのbeta supportが追加されました. podspecを追加し,pod installを実行すると,linking against dylib not safe for use in application extensionsという警告が発生します. この警告は前述した,Allow app extension API onlyオプションが有効でないframeworkをExtensionに取り込んだ場合に見ることができます.

Allow app extension API onlyを有効化したframeworkを取り込んだ場合でもこの警告は発生します. これはCocoaPodsのアーキテクチャに起因する問題です. CocoaPodsはPodを読み込む際,必要なファイルを取り込み,新たにプロジェクトやターゲットを生成して親プロジェクトに埋め込みます. その際,元のプロジェクト設定を引き継いでくれませんので,生成されたframeworkのAllow app extension API onlyはデフォルトでNOになってしまいます. CocoaPodsのframeworkサポートは追加されたばかりなのもあり,この設定をいじるためのAPIは設定は存在しません.

post_installAllow app extension API onlyを有効にする

警告が出たままでは困るのでpost_installで設定を書き換えます. Podfileに以下のようなスクリプトを追加します.

post_install do |installer_representation|
  installer_representation.project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'YES'
    end
  end
end

残念ながらpost_installで変更することは本質的な解決とはいえません. 本来この設定自体はframework側に設定されるものなので,依存する側. しかし,最近出てきたCarthageであればframeworkそのものを利用する設計なので特に困ることがないと思います.. 後発だけあってframework時代に合った仕組みですね. CocoaPodsよりイケテル感があります.

雑談コーナー

config.build_settings['APPLICATION_EXTENSION_API_ONLY']にBooleanを代入していてプロジェクトがぶっこわれました.

参考

追記

laisoさんに対応してるパッチがあることを教えていただきました.

やったね!

Optinalのmapがちょっと使いにくいから拡張してやった #swift

SwiftOptionalにはmapがある.

func map<U>(f: T -> U) -> U?

引数のfT(Optionalで包まれいる値)を受け取ってUを返す関数を受け取って,fの返り値であるUOptionalで包んだU?を返す関数です. 要は,if-letを使って明示的にunwrapしなくてもOptionalの構造を保ったまま中の値に関数を適用するための関数ということですね.

このmapですが,Optionalな値を返す関数と使ってみると案外使いにくいことがわかります.

let URL: NSURL? = NSURL(string: "http://yashigani.hatenablog.com")
if let URLString = URL.map({ $0.absoluteString }) {  // URLStringはString型を期待
    println(URLString)  // => Optional("http://yashigani.hatenablog.com")
}

Optionalを返す関数を適用する場合,mapの返り値は二重にOptionalに包まれて返ってきてしまうのでif-letでunwrapしてもOptionalが残ってしまいます. つまり,実装は以下のようになっていることが予想できます.

func map<U>(f: T -> U) -> U? {
    switch self {
    case .None: return nil
    case .Some(let x): return .Some(f(x))
    }
}

このままでは使いにくいです. ところで,普通の値をとって何かに包まれた値を返す関数を,同じ何かに包まれた値に適用するといえばなにか思い出さないでしょうか? そう,モナドですね. Optionalmapモナドにおけるバインドに似た動作をします. ということはモナドっぽく拡張すればOptionalを返す関数に対しても使いやすくできるのではないでしょうか. 以下のように拡張してみます.

extension Optional {
    func bind<U>(f: T -> U?) -> U? {
        switch self {
        case .None: return nil
        case .Some(let x): return f(x)
        }
    }
}

実行結果は以下のようになります.

if let URLString = URL.bind({ $0.absoluteString }) {
    println(URLString)  // => http://yashigani.hatenablog.com
}

if let URLString = URL.bind({ (x: NSURL?) -> String? in return nil }) {
    println(URLString) // URLStringはnilになるので実行されない
}

狙いどおりいいかんじの動作になりました. 同僚のid:cockscombがはまってて,こうやったらいいんじゃない?ってアドバイスしたらおもったより学びがあった. はてなでの生活は毎日が学びです. 圧倒的感謝です.

余談

Optionalの実装を見たらmapのコメント間違ってた.

    /// If `self == nil`, returns `nil`.  Otherwise, returns `f(self!)`.
    func map<U>(f: (T) -> U) -> U?

返ってくるのはf(self!)じゃなくてf(self!)?です.

今日のお得情報

Swiftは関数型パラダイムの影響を色濃く受けているプログラミング言語なので,少しだけでも関数型プログラミングのエッセンスを学んでおくととても役に立ちます. 簡単に学ぶには以下の書籍がオススメです.

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

読んだ人の感想もご紹介します.

SwiftのPropertyでObjective-Cのアレがしたい

overrideする

Objective-CではPropertyはただのアクセサメソッドの自動生成だったので,overrideするのはなにも考えなくてもいいって感じだったけど,Swiftでは微妙に違う. 例えばUIScrollViewcontentInsetをいじって常に上部にマージンを設けたいときはこうする.

class MyScrollView: UIScrollView {
    private let topMargin: CGFloat = 44
    override var contentInset: UIEdgeInsets {
        get {
            return super.contentInset
        }
        set(newInset) {
            var inset = newInset
            inset.top = topMargin
            super.contentInset = inset
        }
    }
}

Propertyを再定義する感じだった. getterではなにもせずにsuper.contentInsetを返して,setterでは新しい値のtopをいじってからsuperclassのsetterを呼んだ. newInset.topを直接いじれたら楽だったけどできなかった. 理由はよくわからない.

Property名と実際の変数名を分ける

Objective-Cでは以下のようにPropertyと実変数を分けるのもよく見かけるパターンだ.

@interface Animal : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Animal {
    NSString *_name;
}
@synthesize name = _name;
@end

Swiftでやるならこう.

class Animal {
    private var _name: String = ""
    internal var name: String {
        get {
            return _name
        }
        set(newName) {
            _name = newName
        }
    }
}

Objective-Cよりは普通.

まとめ

Objective-Cでできたアレはどうやるんかな?って考えながら書くと楽しい.

Share ExtensionでOpen URLするとかなんとか

注意: beta 5時点での情報です

iOS8で追加される待望のApp Extensionですが,あくまでExtensionなのでContaining appを呼び出したくなることがあると思います. App Extension Programming Guideにもあるように,App ExtensionとContaining appとの連携にはShared resourcesのほかにOpen URLを利用します. Open URLというと従来より形式化された手法ですので,とくにはまりどころは無いと思っていたのですが,はまりどころがあったので紹介させていただきます.

App ExtensionにはUIApplicationが無い

Open URLといえばご存知のようにUIApplicationを取得すればお気楽に実装できるのですが,App ExtensionにはUIApplicationopenURL:メソッドが存在しません.

// UIApplication.h より抜粋
- (BOOL)openURL:(NSURL*)url NS_EXTENSION_UNAVAILABLE_IOS("");

見事に隠されてますね. したがって,Open URLを実装したければ,別の方法を取る必要があります.

App ExtensionではNSExtensionContextを使う

ここで登場するのがNSExtensionContextopenURL:completionHander:です. Share Extensionの場合,エンドポイントとなるSLComposeServiceViewControllerNSExtensionContextを持っていますので,以下のように実装することが考えられます.

// `NSExtensionContext`を使ってopenURLする
NSURL *URL = [NSURL URLWithString:@"myapp://login"];
[self.extensionContext openURL:URL
             completionHandler:^(BOOL success) {
                 // successが`YES`なら成功
             }];

これで安心と思いきや,そうは問屋がおろしません. なんだそれは!と思われるかもしれませんが,openURL:completionHandler:はToday Extension以外で動作しません. 動作を確認してみたとき,successNOが得られて,てっきりContaining appを呼び出すCustom URL Schemeを間違っているのかと思いましたが,どうやらそうではないようです.

それでも絶対Open URLしたい!

我々にはUIWebViewがあります. Zero RectのUIWebViewを作成し,開きたいURLをloadRequest:すると当然のように動作します. もちろんロードが完了するまでUIWebViewを保持する必要がありますのでSLComposeServiceViewControllerに貼り付けておきましょう. 余談ですが,SLComposeServiceViewControllerは特別な存在なのかなあと思ったら,わりと普通にUIViewControllerのサブクラスです.

// Share Extensionでの真のOpen URL
UIWebView *webView = UIWebView.new;
[self.view addSubview:webView]; // ロード完了まで保持しておきたいので適当に貼り付けておく
NSURL *URL = [NSURL URLWithString:@"myapp://login"];
[webView loadRequest:[NSURLRequest requestWithURL:URL]];

まとめ

UIWebViewこそ人類に残された最後の希望.

関連