Hello, RxSwift #iOSDC
2016/08/20に開催されたiOSDCに参加してきた。浜松市からの移動なので前日はソニックガーデンの自由が丘ワークプレイスに宿泊しての参加。自由が丘→練馬は乗り換え無しで行ける奇跡の立地。楽しく参加できた。スタッフも大勢いた。開催ありがとうございました。
色々感想があるのでそれは別途書くかもしれない。今日は色々聞いた中でもishkawaさんのRxSwiftの発表を見てRxSwiftに興味を持ったことと、会場で質問もさせてもらったのでその部分を試しがてらちょっと検証してみようと思ったので半年ぶりぐらいに投稿する。
今回は発表スライドにもあった例を使ってみることにする。複数のテキストフィールドとボタンがあって、ボタンを押したら2つのフィールドの値を結合してラベルに表示するというもの。
http://blog.ishkawa.org/talks/2016-08-20-iosdc/#/19
まずはこの例をそのまま実装することにした。なお、今回はRxSwift 2.6.0を使用した。
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { @IBOutlet weak var textField1: UITextField! @IBOutlet weak var textField2: UITextField! @IBOutlet weak var label: UILabel! @IBOutlet weak var button: UIButton! let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() Observable .combineLatest(textField1.rx_text, textField2.rx_text) { "\($0) \($1)" } .sample(button.rx_tap) .bindTo(label.rx_text) .addDisposableTo(disposeBag) } }
動かしてみるともちろん想定した通りに動く。
このようなコードのときに、「combineLatestに渡すclosureはUITextFiledのテキストが変わる度に毎に呼ばれてしまうのでパフォーマンス的によろしくないこともありますよね?」という点を質問した。それに対するishkawaさんの回答はざっくり要約すると「多分毎回呼ばれるのでそういうこともあるでしょう、でもRxSwiftで制限する仕組みもあるはずです」という感じだった。
ということで2点を試してみようと思う。
まずはクロージャはテキスト変更の度に毎回呼ばれるか、という点。適当にprintして試したところ、次のようになった。
やはりクロージャは都度呼ばれているようだ。なのでこの部分で少々重たい処理をしてしまうと操作する人にはもたつくような印象を与えてしまうことになる。
なので次にここを改善する方法を考えてみる。combineLatestしたストリームをsampleすることで最新の1つを使うことにしているところの順序を逆にすることで、テキスト変更の度にcombineLatestのクロージャが呼ばれることがないので、もう少しマシなのかもしれない。つまり、テキストフィールドの更新をsampleして、それらをcombineLatestするといった感じか。
コードにすると、こう。
let field1 = textField1.rx_text.sample(button.rx_tap) let field2 = textField2.rx_text.sample(button.rx_tap) Observable .combineLatest(field1, field2) { print("called") return "\($0) \($1)" } .bindTo(label.rx_text) .addDisposableTo(disposeBag)
これならprint("called")のところはボタンをタップしたときにしか呼ばれない。あまり意味のあるサンプルコードではないので適切かどうかもよくわからないが、一応呼ばれる回数は減らせた。
他にもthrottleやdebounce(throttleの別名)など一定時間内のものを無視するようなオペレーターも存在するけど、今回の例はボタンをタップするその直前に届いているイベントが欲しいので時間で制御するのは適さない。他の要件なら時間制御も良さそうだ。
こういったオペレーターには色々あって、↓のドキュメントに一覧があるので困ったときは読んでみたいと思っている。
というわけで、RxSwiftの世界へ一歩足を踏み入れた話でした。
SwiftでMicrosoft Azure StorageのAPIクライアントを書きました(書いてます)

最近はSwift版のMicrosoft Azure StorageのAPIクライアントを書いている。
Microsoft Azure Storageを管理するようなMacまたはiOSのアプリを書きたくてSwiftかObjective-Cで書かれたAPIクライアントライブラリを探したのだけれど、現時点ではそのようなものはないということがわかったのでSwiftを勉強しつつ自分で書くことにした(ここにそう書かれている→Microsoft Azure Storage Client Library for C++ v1.0.0 (General Availability) - Microsoft Azure Storage Team Blog - Site Home - MSDN Blogs)
このクライアントを使って書いたMacアプリは現在申請中でレビュー待ち。というか一回リジェクトされてしまった。
そもそもなぜMacアプリを書く必要があるのか、そしてそもそもなぜAWSではなくAzureなのか、ということに答える必要があるかもしれないけど、今回はその点はスルーすることにする。
MS Azure Storage
(ここ、間違っていたらごめんなさい)
MS Azure StorageにはBlob、Queue、Table、Fileという4つのサービスの総称で、AWSと対応させると大体こんな感じになっている。
| Azure Storage | AWS | 用途 |
|---|---|---|
| Blob | S3 | ファイル |
| Queue | SQS | メッセージング |
| Table | DynamoDB | NoSQL |
| File | (ない?) | ファイル共有 |
今回書いているAPIクライアントはこれら4つのサービスのうちBlobとQueueに対応しようと考えていて、実際主要なAPIはある程度実装できたと思っている。
ちなみにAzure StorageのAPIリファレンスはこちら。
ストレージ サービス REST API リファレンス | Windows Azure のテクニカル ドキュメント ライブラリ
設計
Swift力不足のためAPIクライアントをSwiftでいい感じに書くにはどうしたらいいのか全く検討がつかなかった。そこで「Swift API クライアント」などと適当にググって調べらとてもいい記事を発見し、記事やサンプルアプリケーションを見て良い感じだなと思ったのでほぼそのままの設計で実装することにした。この記事に感謝します。
設計方針はこの3つだそう。
目標としたのは以下の3つの条件を満たすことです。
この記事を読んでいただければ僕のライブラリでやっていることも全部わかると思うが、一応簡単に設計について触れておく。
クライアントの主要コード
クライアントの主要メソッドcallの擬似コードを載せてみる。
public class Client { public func call<T: Request>(request: T, handler: (Response<T.Response>) -> Void) { // ① 成功時の処理 let success = { (task: NSURLSessionDataTask!, responseObject: AnyObject!) -> Void in let statusCode = (task.response as? NSHTTPURLResponse)?.statusCode switch (statusCode, request.convertResponseObject(responseObject)) { case (.Some(200..<300), .Some(let response)): handler(Response(response)) default: let userInfo = [NSLocalizedDescriptionKey: "unresolved error occurred."] let error = NSError(domain: "WebAPIErrorDomain", code: 0, userInfo: userInfo) handler(Response(error)) } } // ② 失敗時の処理 let failure = { (task: NSURLSessionDataTask!, error: NSError!) -> Void in handler(Response(error)) } // ③ リクエスト let manager = AFHTTPSessionManager() let url = scheme + "://" + host() + request.path() manager.responseSerializer = AFHTTPResponseSerializer() manager.responseSerializer.acceptableContentTypes = request.responseTypes() manager.GET(url, parameters: nil, success: success, failure: failure) } }
③の部分は単にAFNetworkingの呼び出しなのでここでは特に触れない。通信のライブラリとしてはAFNetworkingを選択した。同じ作者が書いたSwift版のライブラリ、AlamofireのほうがSwiftらしく書けるのかもしれないけれど、一度も使ったことがなかったので今回はパスした。いずれ書き換えてみてもいいかもしれないと思っている。
次は短い②の部分だけど、これは単にエラーオブジェクトをcallに渡ってきたハンドラに返している。
最後の①はレスポンスが正常に返ってきたときの処理で、ステータスコードとレスポンスから正しくモデルオブジェクトに変換できたかどうかで処理を分岐している。
全体を見ると、個々のリクエストに必要な情報(メソッド、パラメータ、HTTPレスポンスから得たいモデルオブジェクトなど)はリクエストオブジェクトから取り出して使うようになっている。そのためクライアントのコードは全てのリクエストに共通の処理だけ書いておけば良い。
レスポンス
クライアントのcallメソッドに渡すハンドラにはResponse<T>が渡ってくるようになっている。このResponseは値付きenum(というのかなんというのか…?)になっていて、成功時はリクエストオブジェクトが持つレスポンスの型に対応したモデルオブジェクトが、失敗時にはNSErrorを持っていることになる。
public class Wrapper<T> { public let value: T init(_ value: T) { self.value = value } } public enum Response<T> { case Success(Wrapper<T>) case Failure(Wrapper<NSError>) init(_ value: T) { self = .Success(Wrapper(value)) } init(_ error: NSError) { self = .Failure(Wrapper(error)) } }
クライアントの呼び出し側はこのようになる。response: Response<T>でSwitchして成功時、失敗時の処理をしてあげる感じになる。
client.call(AzureQueue.ListQueuesRequest(), handler: { response in switch response { case .Success(let wrapper): println(wrapper.value) // AzureQueue.ListQueuesRequest.Response case .Failure(let wrapper): println(wrapper.value) // NSError } })
リクエストオブジェクト
さっき書いたようにリクエストオブジェクトには個々のHTTPリクエストで必要な情報を個別に定義して、クライアントから使えるようにしてある。
参考にした記事では
- パス
- メソッド(GET,POSTなど)
- モデルオブジェクトへの変換
- モデルオブジェクトへの型
などが書かれていたが、Azure Storageのクライアントではさらに、
- リクエストBody
- 追加のHTTPヘッダー(BodyのContent-Lengthなど)
- HTTPレスポンスのContent−Type
あたりを追加している。
public class ListQueuesRequest: Request { public let method = "GET" public typealias Response = Collection<Queue> public init() {} public func path() -> String { return "/?comp=list" } public func body() -> NSData? { return nil } public func additionalHeaders() -> [String : String] { return [:] } public func convertResponseObject(object: AnyObject?) -> Response? { return ResponseUtility.responseItems(object, keyPath: "Queues.Queue") } public func responseTypes() -> Set<String>? { return ["application/xml"] } }
Promise版の呼び出し
ここまで書いたことでAPIクライアントとしての機能は大体果たせるようになった。
ところでJavascriptなんかではよくあるように、非同期処理を待ってから次の非同期処理を書こうとするととても書きにくいという問題が、Objective-CやSwiftでAPIクライアントを書くときにも現れる。これはまあ放っておいてもいいのだけど使うときに便利なほうがいいと思ったので試しに対応してみることにした。
SwiftでもJavascriptのPromise的なアプローチを使えることができると知っていたので調べてみたところ次のようなライブラリが候補に挙がった。
決め手はなんだったかよく覚えていないが、上に挙げた3つのライブラリを全て試してみて最終的にBrightFuturesを採用してみた。
実装の際に参考にしたリンクはこちら。
BrightFutures版のクライアント
public class Client { public func future<T: Request>(request: T) -> Future<T.Response, NSError> { let promise = Promise<T.Response, NSError>() // ① 成功時の処理 let success = { (task: NSURLSessionDataTask!, responseObject: AnyObject!) -> Void in let statusCode = (task.response as? NSHTTPURLResponse)?.statusCode switch (statusCode, request.convertResponseObject(responseObject)) { case (.Some(200..<300), .Some(let response)): promise.success(response) default: let userInfo = [NSLocalizedDescriptionKey: "unresolved error occurred."] let error = NSError(domain: "WebAPIErrorDomain", code: 0, userInfo: userInfo) promise.failure(error) } } // ② 失敗時の処理 let failure = { (task: NSURLSessionDataTask!, error: NSError!) -> Void in promise.failure(error) } // ③ リクエスト let manager = AFHTTPSessionManager() let url = scheme + "://" + host() + request.path() manager.responseSerializer = AFHTTPResponseSerializer() manager.responseSerializer.acceptableContentTypes = request.responseTypes() manager.GET(url, parameters: nil, success: success, failure: failure) // ④ Futureオブジェクトを返す return promise.future } }
通常版と変わったのはcallメソッドにハンドラを渡さなくなり、代わりにFutureというオブジェクトを返すようになったことで、レスポンスが返ってきたときにはハンドラにモデルオブジェクトやエラーを渡す代わりにPromiseオブジェクトのsuccessやfailureメソッドを呼ぶようになっている。
BrightFutures版を使う側のコード
例えば、
- Queueの一覧を取得した後で、
- Queueを新しく生成し、
- 次にそのQueueを削除する
というAPI呼び出し(意味はないが)をしたいとき、通常版ではこのようになる(ひどすぎる…)。
func onError(error: NSError) { println(error) } func normal() { let req1 = AzureQueue.ListQueuesRequest() queueClient.call(req1, handler: { response in switch response { case .Success(let wrapper): let req2 = AzureQueue.CreateQueueRequest(queue: "brandnewqueue") self.queueClient.call(req2, handler: { response in switch response { case .Success(let wrapper): let req3 = AzureQueue.DeleteQueueRequest(queue: "brandnewqueue") self.queueClient.call(req3, handler: { response in switch response { case .Success(let wrapper): println("Success!!") case .Failure(let wrapper): self.onError(wrapper.value) } }) case .Failure(let wrapper): self.onError(wrapper.value) } }) case .Failure(let wrapper): self.onError(wrapper.value) } }) }
BrightFutures版では煩雑さは残るものの幾分かマシに書けるようになる。
func promise() { let req1 = AzureQueue.ListQueuesRequest() queueClient.future(req1).flatMap { response -> Future<AzureQueue.CreateQueueRequest.Response, NSError> in let req = AzureQueue.CreateQueueRequest(queue: "brandnewqueue") return self.queueClient.future(req) }.flatMap { response -> Future<AzureQueue.DeleteQueueRequest.Response, NSError> in let req = AzureQueue.DeleteQueueRequest(queue: "brandnewqueue") return self.queueClient.future(req) }.onSuccess { response in println(response) }.onFailure { error in println(error) } }
煩雑さの原因になっているflatMap { response -> Future<AzureQueue.CreateQueueRequest.Response, NSError> inのようなクロージャの型の部分だが、これを省略してしまうと現状のXcode6.4ではambiguousとか言われてコンパイルできなかった。PromiseKitなどでも同じように見えるエラーに出会ったのでこのあたりはSwiftやXcodeの進化が必要なのかな。
とにかく、これでAPI呼び出しを順番にしたいという要求にも一応答えられるようになった。
あとPromise系のライブラリはそれぞれ進化が速いらしい。すぐに色々変わってしまうかもしれない。
テスト
プロジェクトをCocoaPodsのpod lib createで生成したらデフォルトでQuickというテストライブラリが入ってきたのでそのままこれを使った。Quickでのテストは書いたことがなかったが参考になるコード(後述)が見つかったのであまり苦労することはなかった。
テストはできるだけ書いておきたいと思ったものの、APIクライアントはAPIサーバーあっての話なのでテストはどうしようか迷って色々試した結果、今のように実際のAzure環境を使ったテストに落ち着いた。
Nocillaでスタブ?
最初はNocillaというライブラリを使ってスタブしてテストを書こうと思ったが、マッチポンプ的あまり意味がない気がしたのでこれは却下することにした。
なお、Nocillaを使おうとして色々調べているときに見つけたサンプルコードがQuickでテストを書くときにも訳にたったので紹介しておく。
Azure Storage Emulator
次に試そうとしたのはWindowで動かすことができるAzure Storage Emulatorを使うことだ。ざっくり言うとAPIサーバーのエミュレータをローカルに立てられるというものらしい。ということはこれをMacに入れたVMWareとかで動かしておけばテストに使えるんじゃねーか(少なくともローカルでは)と考え色々準備してみた。
ところがクライアントの接続先にVMWareのIPを指定してもどうにもうまくいかない。3時間近く奮闘したものの何も得られなかったのでこの作戦も却下することに。
実環境でテストする
ここでようやく他の言語のクライアントではどうしてるんだろうということに気がついたので、Railsで使ったことがあるRuby版のクライアントを見てみることにした。
これのテストコードを見ると環境変数にストレージのアカウントやアクセスキーが設定されているときだけテストを動かせるようになっていた。つまり本物のAzure環境でテストを動かすよ、ということのようだった。
そういうわけでSwift版でも同じようにAzure環境でテストを動かすコードを書くことになった。テストコードを書くにあたってBrightFutures版が非常に役に立った。これをしてそれをして、最後にあれをしたらこうなっている、というコードを通常版で書こうとしていたらだいぶ辛いことになっていたと思う。
コード
もう1回貼っておきます。
宣伝
途中に出てきた表は自作のMacアプリで作りました。日本語入力で若干不具合があるけどMarkdownで表を書くときにはWebにある表生成ツール(Markdown Tables generator)よりも便利なのでぜひどうぞ。
Table2Text (Markdown, CSV)
カテゴリ: 開発ツール, ユーティリティ

- 作者: 荻原剛志
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2014/12/10
- メディア: 大型本
- この商品を含むブログ (2件) を見る
CocoaアプリケーションでファイルやURLを開く
覚書でーす。
URL
NSWorkspace.sharedWorkspace().openURL(NSURL(string: "http://google.co.jp")!)
ファイル/フォルダ
NSWorkspace.sharedWorkspace().openFile("/Applications/")
SwiftでN文字ずつに分割した文字列を得る方法
例えば"hogefugapiyo"という文字列があるとして、これを4文字ずつに分割するなら["hoge", "fuga", "piyo"]、5文字ずつに分割するなら["hogef", "ugapi", "yo"]を得たい。
Rubyであればscanメソッドを使って次のように実行できるということがわかった。
str = "hogefugapiyo" # => "hogefugapiyo" str.scan(/.{1,5}/) # => ["hogef", "ugapi", "yo"]
参考
これと同じことをSwiftでやりたくなったが、どうやら全く同じようなことをする関数はなさそうに見えた。なのでSwift力が足りないながらも自前で書いてみたのでそれを晒してみる。
Rubyの用にArrayを返す関数を追加するのでもよかったが、SequenceTypeを使うほうがよさそうな気がしたので↓のように実装した。Stringにsubstr(Int)という関数を生やして、それを呼ぶとforループやmapとかが使えるという感じになった。
extension String { var length: Int { return count(self) } internal class SubstringGenerator: GeneratorType { typealias Element = String let count: Int let string: String var i = 0 init(count: Int, string: String) { if count <= 0 { fatalError("'count' must be bigger than 0.") } self.count = count self.string = string } func next() -> Element? { if i < string.length { var endIndex : String.Index if i + count > string.length { endIndex = string.endIndex } else { endIndex = advance(string.startIndex, i + count) } var range = advance(string.startIndex, i)..<endIndex i += count return string.substringWithRange(range) } else { return nil } } } internal class SubstringSequence : SequenceType { let count: Int let string : String typealias Generator = SubstringGenerator init(count: Int, string: String) { self.count = count self.string = string } func generate() -> Generator { return Generator(count: count, string: string) } } func substr(count: Int) -> SubstringSequence { return SubstringSequence(count: count, string: self) } }
このsubstrを実際に使ってみるとこうなる。
for substr in "hogefugapiyo".substr(5) { println(substr) } // hogef // ugapi // yo var array = map("hogefugapiyo".substr(5)) {$0} println(array) // [hogef, ugapi, yo]
どうですかね、これ。

- 作者: 荻原剛志
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2014/12/10
- メディア: 大型本
- この商品を含むブログ (2件) を見る
AFHTTPSessionManagerでHTTPBodyを設定する
AFNetworkingのAFHTTPSessionManagerを使っていてBodyに素のXMLを入れてPOSTしたかったのだが、そのやり方がなかなか見当たらずソースコードを読んで解決したのでメモしておく。
ざっくり言うとAFHTTPSessionManagerでのPOSTはこんな感じに行う。
let manager = AFHTTPSessionManager() manager.POST(url, parameters: params, success: { (task, response) -> Void in // success }) { (task, error) -> Void in // failure }
POSTするパラメータがあれば、上のメソッドのparametersのところにdictionaryとかで渡せばいいはずだが(確か)、今回やりたいのはBodyにXMLのテキストを入れるということだった。
まず気軽にparametersにXML文字列を直接渡してみようなどとやってみたら失敗した。
let manager = AFHTTPSessionManager() let xmlstr = "<?xml ..." manager.POST(url, parameters: xmlstr, success: { (task, response) -> Void in // success }) { (task, error) -> Void in // failure }
これでできたリクエストを見てみるとBodyにはこんなのが入っていた。
(null)=%3C%3Fxml%20...
どうやら名前が空で値がURLエンコード済文字列のパラメータをBodyとして設定してしまっているらしい。
ソースコードを読むとそれがデフォルトの動作だということがわかった。
// AFURLRequestSerialization.m switch (self.queryStringSerializationStyle) { case AFHTTPRequestQueryStringDefaultStyle: query = AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding); break; }
じゃあデフォルトじゃない動作は、というと上のコードの近くにあった。
// AFURLRequestSerialization.m if (self.queryStringSerialization) { NSError *serializationError; query = self.queryStringSerialization(request, parameters, &serializationError); if (serializationError) { if (error) { *error = serializationError; } return nil; } }
self.queryStringSerializationはBlockで、このブロックを設定してparameterのシリアライズをカスタマイズできるよ、ということのようだった。
つまり文字列をそのままBodyに入れたい場合は、
let manager = AFHTTPSessionManager() let xmlstr = "<?xml ..." manager.requestSerializer.setQueryStringSerializationWithBlock { (request, params, error) -> String! in return params as! String } manager.POST(url, parameters: xmlstr, success: { (task, response) -> Void in // success }) { (task, error) -> Void in // failure }
manager.requestSerializer.setQueryStringSerializationWithBlockでparameterをそのまま文字列と返してあげればよかった。
これで解決できた。良かった。

- 作者: 佐々木達也,瀬川雄介,内藤賢司
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2015/03/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る
NSTextFieldで入力を検出する
iOS開発からCocoaにも手を付け始めた人間によくあることだろうが、UIKitとAppKitでお作法が同じだったり微妙に違ったりすることでちょくちょくつまずくことがある。
今日はテキストフィールドでユーザーが入力を行うたびに入力値のチェックをしてOKボタンの有効/無効を切り替えるという処理を書こうとしてつまづいた。
UITextFieldの場合
UITextFieldの場合はStoryboardからCtrl+ドラッグでActionを作るときにEditing Changedイベントを選んであげればOK。

NSTextFieldの場合
Storyboardを使って同じようなことをやろうとしてもiOSのようにコントロールに発生したイベントに対応したActionを作ることができない。ここまで書いて気がついたけどAppKitはイベント型(?)じゃないということなんだね。
ではどうすればいいかといえば、Delegateが存在するのでそれを実装してあげれば良い。DelegateはiOS開発でも毎回使うだろうからiOS Developerなら大丈夫のはず。
class ViewController: NSViewController, NSTextFieldDelegate { @IBOutlet weak var textField: NSTextField! override func viewDidLoad() { super.viewDidLoad() // Do view setup here. textField.delegate = self } override func controlTextDidChange(obj: NSNotification) { doSomething() } }
できた。
Single WindowなOSX Appで全てのWindowが閉じたときの挙動でリジェクト
MacのアプリケーションはWindowを赤ボタンとか⌘Wで閉じてもアプリケーション自体が終了しないことが多いが、その場合メインウィンドウを再表示できるようにしておかないとMac App Storeに申請した際にリジェクトを食らってしまう(食らった)。
1つめの対応方法として、Dockアイコンをクリックしたときにメインウィンドウを再表示するコードをAppDelegateに書く。
func applicationShouldHandleReopen(sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { if flag == false && sender.windows.count > 0 { sender.windows[0].makeKeyAndOrderFront(self) } return true }
もしくは全てのウィンドウが閉じたときアプリケーションを終了させてしまうというのもOKらしい。これは推測だけど、申請したのがシングルウィンドウなアプリケーションだったからこの対処方法でもいいよと言ってくれたんじゃないかと思う。ドキュメントベースの場合はウィンドウが閉じることって頻繁にあるだろうから多分だめなんじゃないかな。
その場合はAppDelegateにこんなコードを書く。
func applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication) -> Bool { return true }
今回は単機能のアプリだったので後者で対応することにしてみた。

