RxSwiftを勉強していますが、なかなか理解が進まないシリーズです。

RxSwift / RxCocoaのバージョンは2.2を対象にしています。

学習の手始めに「UIButtonがタップされたら、画面上にあるラベルをhiddenにする」というごくごく簡単な処理を書いてみようと思ったのですが・・・。どうにか動くものはできたものの、まだまだ予断を許さない状況です。

  • ボタンがタップされたことを伝えるrx_tapに、ラベルを隠すrx_hiddenを繋ぐだけでいいんじゃない?…と思ったけれど、タップと隠すのは別のオブジェクトだ。どうやってつなぐんだろう。
  • どうやら別のObservableにつなぐ場合はbindTo()を使うと良さそうだ。でも、あれ?下のコードみたく書いてもエラーが出るだけだぞ?
button.rx_tap.bindTo(label.rx_hidden)
// -> error: cannot convert value of type 'AnyObserver<Bool>' to expected argument type 'Variable<Void>' (aka 'Variable<()>')
  • RxCocoaのサンプルを見るとbindTo()する前にmap()を挟んでいるのが多いけれど、これはどういう意味だろう

と紆余曲折の結果、次のコードで動くようになりました。

button.rx_tap.map { true }
    .bindTo(label.rx_hidden)
    .addDisposableTo(bag)

問題点はなんだったかというと、

  • rx_hiddenAnyObserver<Bool>という、Boolを型とするObservableでないといけなかった
  • map()trueを返しているのは、タップというUIControlイベントをtrue/falseに型変換するため

ということでした。深く考えずつないでいけるかと思いきや、型の安全さにともなう制限があったわけですね。

まだまだ先は長そうです。

Emacsで選択した領域を検索対象にする

EmacsC-sで検索するときに領域が選択されていたら、その領域を検索対象として検索するマクロを、自分の.emacsに追加しました。

  ;; 選択した領域を使ってインクリメンタル検索を実行
  (defadvice isearch-mode (around isearch-mode-default-string
                                  (forward &optional regexp op-fun recursive-edit word-p)
                                  activate)
  (if (and transient-mark-mode mark-active (not (eq (mark) (point))))
      (progn
        (isearch-update-ring (buffer-substring-no-properties (mark) (point)))
        (deactivate-mark)
        ad-do-it
        (if (not forward)
            (isearch-repeat-backward)
          (goto-char (mark))
          (isearch-repeat-forward)))
    ad-do-it))

C-s C-wで単語を検索対象にできますが、これだとカーソルよりも前にあった内容は選択されないので不便に感じていました。そもそも検索しようとしてキーワードを選ぶのではなく、キーワードに注目してから検索するケースは多いので、なかなか便利です。

自前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行ずつに抑え込んで(人間が追えるスピードで)なめらかにしたことがポイントです。