自前CocoaPodsライブラリで公開するヘッダファイルを限定する方法

CocoaPodsは、自動作成するumbrellaヘッダにデフォルトで全ヘッダファイルを含めます。

Objective-Cだけで書いてある場合、この振る舞いでも特に問題にはなりません。・・・ですが、ライブラリにC++コードなどSwiftで対応しない言語のファイルが含まれているとなれば、話は別です。

CocoaPodsでc++ソースを含む自前ライブラリを何も考えずに公開しようとしても、umbrellaヘッダにc++向けヘッダファイルまでも全て含まれてしまい、Swift環境でコンパイルできなくなってしまうのです。C++で作ったライブラリをSwiftで使う場合、Objective-C++で作成したファイルをブリッジとして、Swift側からはc++のコードが見えないようにしなければいけないのに、です。

umbrellaヘッダに含めるヘッダファイルを限定したい場合、.podspecファイルでs.public_header_filesを使います。

# umbrellaヘッダでのinclude対象になるファイルを指定するオプション
s.public_header_files = ["Core/*.h", "Tree/**.h"]

s.source_filesは、コンパイルに必要なファイルを指定するのに対して、今回紹介するs.public_header_filesはumbrellaヘッダに含めるファイルを指定するのに使います。複数ある場合は配列で指定します。

以下はサンプルです。末尾にs.public_header_filesがあります。

Pod::Spec.new do |s|
    s.name         = "MyLibrary"
    s.version      = "1.0.0"

    s.description  = <<-DESC
    MyLibrary is an awesome library!
    DESC

    s.homepage     = "https://github.com/foobar/MyLibrary"
    s.source       = { :git => "https://github.com/foobar/MyLibrary.git", :tag => "v#{s.version}" }

    s.license      = "New BSD"
    s.author       = "Foo Bar"

    s.platform     = :ios
    s.ios.deployment_target = "8.0"

    s.requires_arc = true
    s.frameworks   = 'Foundation'
    s.module_name  = "MyLibrary"
    s.source_files = "MyLibrary/**/*.{h,cpp,mm,swift}" # コンパイルに必要な全ソースファイル
    s.public_header_files = ["MyLibrary/MyLibrary.h", "MyLibrary/PublicHeader.h"]  # umbrellaヘッダでのinclude対象
end

こっそり公開しているライブラリを自分でpod installしようとしたらエラーになってしまい、慌てて直したわけで…とほほ。

参考サイト

blog.cocoapods.org

RxSwiftでUIKitを扱うにはRxCocoaが必要

内容はタイトル通りです。引き続きRxSwiftについて。

github.com

「RxSwiftでUIKitを試してみよう。このAPIページを見ると色々とあるな…あれ?何も使えない?」と。

よくよく確認してみると、RxSwiftの下には別のpodspecファイルとして、RxCocoa.podspecがありました。RxSwiftだけでなくRxCocoaも必要なようです。RxCocoaという名前から、てっきりObjective-CのRxライブラリだとばかり思っていましたが、UIKitやAppKit向けのextensionがswiftで集められたものでした。

というわけで、Podfileに

platform :ios, '8.0'
use_frameworks!

target 'MyApp' do
pod 'RxSwift',    '~> 2.0'
pod 'RxCocoa'   # ここを追加
end

としてpod install。無事にiOS向けのRxCocoaがPods下に追加されました。

RxSwiftのMainSchedulerは何物か

遅まきながら、RxSwiftに手を付け始めました。

github.com

リアクティブプログラミングはまったく経験がないので、公式の「Getting Started」を読みながら、概念や基本を調べることにします。

ですが、まだ分からないことばかりで詳細には全然立入れていません。

オブザーバとジェネレータ

リアクティブプログラミングには「オブザーバ」と「ジェネレータ」の2つのキーワードがあるようです。このEquivalenceを理解することがRxホニャララを理解する上でもっとも重要なのだそう。

このページの記述を引用すると、それぞれは次のようにプッシュ型かプル型かで使い分けるとのこと。

  • Push interface - Observer (observed elements over time make a sequence)
  • Pull interface - Iterator / Enumerator / Generator

まだよく分からず。

スケジューラ

Rxは非同期(asynchronous)で動いているので、処理をどのスケジューラ(スレッド)で動かすかを指定しなければいけません。この点はGCDと同じです。 Getting Startedの中では、「Dispose」の節にてはじめてソースコードが登場し、Observable<SignedIntegerType>interval()メソッドが持つschedulerなる引数で、スケジューラを指定しています。

let subscription = Observable<Int>.interval(0.3, scheduler: scheduler)
    .subscribe { event in
        print(event)
    }

NSThread.sleepForTimeInterval(2)

subscription.dispose()

これは0.3秒ごとに2秒になるまで(つまり合計6回)eventオブジェクトをコンソールに出力し続ける処理ですが、schedulerとして突然出てきている変数があります。これが処理実行のスレッドとして使われるようです。ただ、このコードだけを記述してもschedulerが未定義になってエラーになるので、何かきちんとしたインスタンスを指定する必要があります。メソッドの型宣言ではSchedulerType型のインスタンスが使えるとのこと。

何を指定するのかなとGetting Startedをもう少し先に進んでみたところ、同様の処理で「MainScheduler.instance」と書いてある箇所がありました。試しにこれを使ってみたところ、エラーなく実行できるようになりました。何者なのか、MainSchedulerの中身を見てみます。

public final class MainScheduler : SerialDispatchQueueScheduler {

    private let _mainQueue: dispatch_queue_t

    var numberEnqueued: AtomicInt = 0

    private init() {
        _mainQueue = dispatch_get_main_queue()
        super.init(serialQueue: _mainQueue)
    }

    /**
    Singleton instance of `MainScheduler`
    */
    public static let instance = MainScheduler()
    :

まだ処理の詳細は追えないものの、dispatch_get_main_queue()を使っていることから、メインスレッドが使われると考えて良さそうです。

時間切れになったので、今回はここまで。

今回のまとめ

  • MainScheduler.instanceは、MainSchedulerオブジェクトをインスタンス化して返している。このスケジューラはメインスレッドを使用している
    • てことはUI変更も処理の中で行えるのですね
  • Rx、用語やAPIを多く覚えないと使いこなせなさそう

コマンドラインからApp Storeのアプリをインストール

下のブログで、コマンドラインからApp Storeのアプリをインストールするコマンドラインツールmas」が公開されていることを知りました。

rcmdnk.github.io

確かにわざわざAppStoreから1つずつインストールするのは面倒なので、自分の環境にも入れてみることにします。 brewコマンドを使ってインストールします。

% brew install argon/mas/mas
==> Tapping argon/mas
Cloning into '/usr/local/Library/Taps/argon/homebrew-mas'...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 2 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
Checking connectivity... done.
Tapped 1 formula (25 files, 16.7K)
==> Installing mas from argon/mas
==> Downloading https://github.com/argon/mas/releases/download/v1.0.2/mas-cli.zip
==> Downloading from https://github-cloud.s3.amazonaws.com/releases/40092232/db54c896-af61-11e5-81e6-82cf43672901.zip?X-Amz-Algorithm
######################################################################## 100.0%
🍺  /usr/local/Cellar/mas/1.0.2: 2 files, 4.0M, built in 10 seconds

% mas
Available commands:

   account    Prints the primary account Apple ID
   help       Display general or command-specific help
   install    Install from the Mac App Store
   list       Lists apps from the Mac App Store which are currently installed
   outdated   Lists pending updates from the Mac App Store
   upgrade    Performs all pending updates from the Mac App Store
   version    Print version number

あっさり完了。ためしに、インストールされているアプリの一覧を出力するmas listを使ってみます。

% mas list
404647179 OmniOutliner Pro
442947586 Invisor
406056744 Evernote
409183694 Keynote
417375580 BetterSnapTool
409789998 Twitter
425424353 The Unarchiver
682658836 GarageBand
890031187 Marked 2
443987910 1Password
992076693 MindNode
409203825 Numbers
409201541 Pages
408981434 iMovie
417568953 Jedit X Plus
477163052 Linguan
803453959 Slack
445189367 PopClip
568494494 Pocket
449589707 Dash

おお。ちなみにこのIDは、アプリのリンクURLに書かれているIDと同じで、StoreServices.frameworkプライベートフレームワークにあるSSPurchaseクラスから得られる情報でした。「Keynote」や「Dash」のようなアプリ名を使うのでは他のアプリと被ってしまうことも多いので、妥当ですね。

インストールするときは、リストの最初にある数値を使う必要があるとのことです。Linguan(ID:477163052)を一度アンインストールして、インストールしてみます。

% mas install 477163052
==> Downloading Linguan
==> Installed Linguan

初回はAppStoreアプリが開いてパスワードの入力を求められましたが、あとはターミナル上にプログレスが表示され、やがてインストールが終わりました。むむ、これは便利です。

ソースコードを見てみたところ、多くの箇所でSwiftが使われていました。OS Xでも使われるようになったのですね。

spacemacsのトラックパッドスクロール量を調節する

spacemacsが思いのほか使いやすかったので、これまで育ててきた.emacsをバッサリ捨てて、乗り換えることにしました。思っていた以上にspacemacsは至れり尽くせりで動いてくれたので、

spacemacsはユーザー様の設定を~/.spacemacsの末尾、dotspacemacs/user-configで行いますので(user-initもあるけれども、最後の最後に実行されるのがuser-configの方)、幾つかの設定を選んで持ってくることにしました。

(defun dotspacemacs/user-config ()
  "Configuration function for user code.
This function is called at the very end of Spacemacs initialization after
layers configuration. You are free to put any user code."
  ;; ⌘-↑/↓でファイルの先頭・末尾へ移動
  (global-set-key [s-up] 'beginning-of-buffer)
  (global-set-key [s-down] 'end-of-buffer)

  ;; ⌘-C-←/→でバッファ履歴を行き来する
  (global-set-key [C-s-left] 'previous-buffer)
  (global-set-key [C-s-right] 'next-buffer)

  ;; ⌘-C-↑/↓でヘッダファイルとソースファイルを切り替える
  (global-set-key [C-s-up] 'ff-find-other-file)
  (global-set-key [C-s-down] 'ff-find-other-file)

  ;; ⌘-rでファイルの再読み込み
  (global-set-key (kbd "s-r") 'revert-buffer)

  ;; トラックパッド用のスクロール設定
  (defun scroll-down-with-lines ()
    "" (interactive) (scroll-down 3))
  (defun scroll-up-with-lines ()
    "" (interactive) (scroll-up 3))
  (global-set-key [wheel-up] 'scroll-down-with-lines)
  (global-set-key [wheel-down] 'scroll-up-with-lines)
  (global-set-key [double-wheel-up] 'scroll-down-with-lines)
  (global-set-key [double-wheel-down] 'scroll-up-with-lines)
  (global-set-key [triple-wheel-up] 'scroll-down-with-lines)
  (global-set-key [triple-wheel-down] 'scroll-up-with-lines)
  )

XcodeSafariと同じように⌘-←/→⌘-↑/↓でバッファを移動できるようにすること、トラックバッドの反応がデフォルトで良すぎるため、スクロール量を3行ずつに抑え込んで(人間が追えるスピードで)なめらかにしたことがポイントです。

std::basic_stringの「&foo[0]」は何を返すか

とあるコードを読んでいたら、

std::string foo = "abc";
printf("[%s]\n", &foo[0]); // [abc]

みたいな書き方をしていて「ん?stringなのに??」と思ったので、きちんと調べてみました。結論を書いてしまうと、

というルール2つによって、正しくC文字列のアドレスが返せているとわかりました。 …でも、こういう場合はstd::basic_string::c_str()を使うべきですよね、見にくいったらありません。

(ようやく)SwiftのflatMap()を追いかける

前回(といっても大分前になってしまいましたが)、flatMap==flatten(map())であることから、flatMap()の準備としてflatten()を先に読みました。

swiftlife.hatenablog.jp

Swiftflatten()は平坦化を直ちに行うのではなく、要素が必要になった時にはじめて処理が実行されるよう、遅延評価の仕組みが取り入れられたのでした。この「元配列を別視点から見られるようにする」という仕組みによって、実行コストを抑えたまま平坦化を実現する面白いメソッドになっていることが分かりました。

そんなflattenの振る舞いを踏まえて、今回flatMap()に進みます。SwiftflatMap()は、はたしてどのような実装になっているのでしょうか。

…とその前に、ここまでにmap,flattenを見てきたところとして残っている疑問がありますので、先に書き出しておきます。

  • flatten()の返す型はflatMap()のそれとは異なるが、いつ、どうやって変換しているか
  • flatten()underestimateCount()は常に0を返すけれど大丈夫?

どちらもflatMapを読んで解決できるといいのですが…。それでは読んでみましょう。

flatMap()を提供している型

flatMap()は、Optional, ImplicitlyUnwrappedOptionalSequenceTypeの3つの型で利用できます。ソースコードの定義箇所は、それぞれ以下のとおりです。

  • Optional : stdlib/public/core/Optional.swift
  • ImplicitlyUnwrappedOptional : stdlib/public/core/ImplicitlyUnwrappedOptional.swift
  • SequenceType : stdlib/public/core/SequenceAlgorithms.swift.gyb

さらには、遅延評価型のLazySequenceTypeLazyCollectionTypeでも定義されているので、合計5箇所です。

  • LazySequenceType : stdlib/public/core/flatMap.swift
  • LazyCollectionType : stdlib/public/core/flatMap.swift

OptionalとImplicitlyUnwrappedOptionalのflatMap()

OptionalImplicitlyUnwrappedOptionalの2つは、以前の記事で読んでいるので省略します。

swiftlife.hatenablog.jp

やっていることはとても単純で、アンラップしたオブジェクトに関数を適用して返すことをしていたのでした。

SequenceTypeのflatMap()

SequenceTypeのflatMap()は、SequenceAlgorithms.swift.gybの中で、SequenceTypeのextensionとして一般化された内容で記されています。2つあり、それぞれ短い内容です。

// 1つ目
@warn_unused_result
public func flatMap<S : SequenceType>(
  transform: (${GElement}) throws -> S
) rethrows -> [S.${GElement}] {
  var result: [S.${GElement}] = []
  for element in self {
    result.appendContentsOf(try transform(element))
  }
  return result
}
// 2つ目
@warn_unused_result
public func flatMap<T>(
  @noescape transform: (${GElement}) throws -> T?
) rethrows -> [T] {
  var result: [T] = []
  for element in self {
    if let newElement = try transform(element) {
      result.append(newElement)
    }
  }
  return result
}

ほとんど同じ内容ですが、1つ目がtransform引数の戻り型が「S」であることからnilを返さないこと、2つ目は逆に「T?」となっていて、Optionalで返すことができます。

ここでの${GElement}は、同gybファイルの先頭でGenerator.Elementの言い換えであると定義されているものです。

# We know we will eventually get a SequenceType.Element type.  Define
# a shorthand that we can use today.
GElement = "Generator.Element"

要素をtransform()に渡しつつ、その結果をresultにappend()あるいはappendContentsOf()で追加しています。言うなればflatMap()というよりconcatMap()と表現した方がしっくりくる感じです。

LazySequenceTypeとLazyCollectionTypeのflatMap()

flatMap.swiftという、そのものの名前のファイルでは、LazySequenceTypeLazyCollectionTypeのflatMap()が定義されています。これらは関数宣言が複雑に見えますが、やっていることはどれも同じで次の1行が記述されているのみです。

return self.map(transform).flatten()

これが1番シンプルですね。flatMap()の由来である「flatten(map(x))」そのものになっています。

まとめ

これまで長々とflatMap()に至る実装を追いかけてきましたが、ようやく一区切りつけることができました。

flatten(map(x))が由来のflatMap()ですが、SwiftではLazySequenceTypeとLazyCollectionType以外はflatten()を使っておらず、それぞれの型で異なる実装になっていることがわかりました。実装を区別しているのは、その方がパフォーマンスが良いのかもしれませんし、あるいは疑問の一つだったflatten()の特性「underestimateCount()が0を返す」から、期待する結果をベースに実装を考えているのかもしれません。

細かな不明点・疑問点は残っていますが、それはおいおい調べていこうと思います。

おまけ

RxJS, RxSwiftなどReactive Programmingライブラリのドキュメントで、flatMapを表すとても分かりやすい図があったので引用します。

http://reactivex.io/documentation/operators/images/flatMap.c.png ReactiveX - FlatMap operator

配列(ストリーム)の要素に対して順に処理を実施し、処理では任意型の配列を返します。そして各結果をflattenして、最終的な別のストリームを返します。