pod installしたらgit cloneしてくれるヤツ書いた
ghqを使ったローカルリポジトリの統一的・効率的な管理について - delirious thoughtsを拝見して良さそうだったので、iOS開発にも持ち込むためCocoaPodsのプラグインを書いた。20行くらいしか書いてないし、ghqとの連携もまだ実装できてないけど、取り急ぎ。
使い方
$ gem install cocoapods-src
cocoapodsは入っている前提で、cocoapods-srcをインストールする。
$ pod install
インストールすると、あとはpod install
すれば勝手にpodsをgit clone
してくれる。今のところ~/.cocoapods/src/
に以下のような感じでダウンロードされる。
$ tree ~/.cocoapods/src -I .git -L 2 .cocoapods/src ├── .DS_Store ├── AFNetworking │ ├── .cocoadocs.yml │ ├── .gitignore │ ├── .travis.yml │ ├── AFNetworking │ ├── AFNetworking.podspec │ ├── AFNetworking.xcworkspace │ ├── CHANGES │ ├── CONTRIBUTING.md │ ├── Example │ ├── LICENSE │ ├── README.md │ ├── Rakefile │ ├── Tests │ └── UIKit+AFNetworking
今後
追記(10/22)
0.2.0にアップデートして、ghqと連携できるようになった。
~/.podrc
、~/.cocoapods/.podrc
、./.podrc
のいずれかに以下のような設定を書くとgit clone
の代わりにghqを使ってダウンロードする。
cocoapods-src_use_ghq: true
「集合知プログラミング」を読んでる
「集合知プログラミング」という本を先週から読み始めた。この本は機械学習をテーマとしていて、現実にありそうな問題(例えば、映画の評点から似ているユーザーを推薦するとか、数ある旅行プランの組み合わせから最適なプランを選択するとか)を題材にさまざまなアルゴリズムをチュートリアル形式で学んでいける。登場するサンプルコードはすべてPythonで書かれているため、まずこの本を読む前に軽くPythonについて勉強した。機械学習の本というと、むずかしい数式がたくさんでてきて近寄りがたいイメージがあるのだけど、この本についてはほとんど数式は出てこないので、カジュアルに読み進められる。
まだ半分も進んでないけど、その中で一番おもしろかったのが最適化アルゴリズムの話だった。ある最適な値を求めたいとき、「となりあう値と比較して良い方を選択する」というのを繰り返していくとどこか最適な値で落ち着くはずというアルゴリズム(ヒルクライム)があるのだけど、これだと局所最適に陥ってしまうということを最近勉強した。つまり、全体を見渡すともっと最適な値があるのだけど、近くの値とだけ比較しているとそれを見逃してしまうということだ。また、別のアルゴリズム(模擬アニーリング)は、試行回数が少ないうちは悪い結果を受け入れ、回数を経るにつれてその悪い結果を受け入れ難くしていくことで局所最適を回避する。
これはいろんなところで当てはまりそうな考え方だなと思った。見える範囲、理解できる範囲だけで最適な選択をとろうとするとより適切な解を見落としてしまう。若いうちは結果が悪かろうともそれを受け止めることで局所最適を回避し全体最適に近づくことができるのかもしれない。
この本を読もうと思った理由としては、いろんな領域と機械学習を組み合わせるとなんか面白いものが作れそうな気がしたから。これまで自分が作ってきたソフトウェアの中で自分自身気に入っているものの多くは別の領域のアイデアを持ち込むところから生まれている。だから、組み合わせの可能性が大きい領域を何か新しく学びたいと思ったときに機械学習というものが浮かんでてきてこの本から取り組んでみることにした。今は「iOS x 機械学習」みたいな掛け合わせで何か面白いものが作れないか考えている。
- 作者: Toby Segaran,當山仁健,鴨澤眞夫
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/07/25
- メディア: 大型本
- 購入: 91人 クリック: 2,220回
- この商品を含むブログ (277件) を見る
ストリームを利用したローパスフィルタの実装
このスクリーンショットに映された2つの線は共にiPhoneの加速度センサーの値を表しており、下の緑が加工していない生データ、上の青い線がローパスフィルタという仕組みで揺れを除去したデータだ。
以前の記事でSwiftを使ったストリームの実装をしてみたのだけど、その使いどころを考えてみたところセンサーデータの加工にストリームという概念が適しているのではないかと思いついた。センサーから送られてくるデータは連続的で、その加工には複雑な計算を要するためだ。
そこで、加速度センサーをグラフに表示する簡単なアプリを作ってみて、生データとストリームを使って加工したデータを視覚的に表現してみることにした。その結果が上のスクリーンショットとなる。今回はローパスフィルタと呼ばれる手法を用いて生データを加工した。そちらの方面にはまるっきり分からないのだけど、以下のようなとてもシンプルなアルゴリズムでデータを加工できるとのことだったので利用した。
今回の加工したデータ = 前回の加工したデータ * 0.9 + 今回の生データ * 0.1
このローパスフィルタを以前開発したストリームライブラリで実装してみる。
var x: [CGFloat] = [] var filteredX: [CGFloat] = [] let xStream = Stream<CGFloat>()
まず、生データと加工したデータをグラフに描画するための配列と生データを扱うストリームを用意する。加速度センサーから値を取得する度にこのストリームに値を出力していく。
override func viewDidLoad() { // ... motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue(), withHandler: accelerometerHandler) } private func accelerometerHandler(data: CMAccelerometerData!, error: NSError!) { xStream.publish(CGFloat(data.acceleration.x)) }
ストリームに渡された生データをグラフに描画するための配列に入れるため、値が出力されたときに実行される関数を登録しておく。これで生データが出力されたときはいつでもこの関数が実行される。
override func viewDidLoad() { // ... xStream.subscribe { [unowned self] message in self.x.append(message) } }
続いて、上のストリームに出力された生データを加工して出力する別のストリームを作成する。これはscan
関数を利用することで簡単に実現できる。scan
関数は「前回出力された値と今回出力された値を使って、新たな値を出力するストリーム」を簡単に作成できる。なので、上で示したローパスフィルタのアルゴリズムを以下のように実装することができる。
override func viewDidLoad() { // ... xStream.subscribe { [unowned self] message in self.x.append(message) } let filteredStream: Stream<CGFloat> = xStream.scan(0) { previousMessage, message in return previousMessage * 0.9 + message * 0.1 } }
最後に、加工した値の出力を見張ってグラフ描画用の配列に追加するための関数を登録しておく。
override func viewDidLoad() { // ... xStream.subscribe { [unowned self] message in self.x.append(message) } let filteredStream: Stream<CGFloat> = xStream.scan(0) { previousMessage, message in return previousMessage * 0.9 + message * 0.1 }.subscribe { [unowned self] message in self.filteredX.append(message) // ... } }
このように、ストリームの性質やストリームを扱う様々な関数を利用すると、簡単にセンサーデータを扱うプログラムを実装することができた。アプリのソースコードはgithubにアップしてあるので、参考にしてほしい。
マシなiOSアプリのフォームを実装・デザインする
普段iOSのフロント寄りの実装やデザインについて手が着けられていなかったけど、Xcode6の新機能のおかげでそっちも興味がでてきたので、ログインフォームを想定してiOSアプリのフォームの設計について本気出して考えてみた。
最もシンプルなフォーム
- メールアドレス用の
UITextField
(以下emailField
)、パスワード用のUITextField
(以下passwordField
)、そしてログインボタン用のUIButton
(以下loginButton
)の3つをStoryboardで配置した。 emailField
はKeyboard TypeをE-mail Addressに、Return KeyをNextに設定した。passwordField
はSecure Text EntryのチェックをオンにしReturn KeyをGoに設定した。
問題点
emailField
でReturn Keyを押してもpasswordField
が選択されないし、passwordField
でReturn Keyを押してもsubmitされない。- コントロール部品以外をタップしたとき、キーボードが閉じない。端末サイズが小さい場合、キーボードによって他のコントロールや表示すべきViewが隠れたままになる可能性がある。
- 追加した3つのViewが指の大きさに対して小さい。ユーザーは正確にタップするために注意を向ける必要があり、間違ったViewをタップしてしまう可能性がある。
改善1: Return Keyで適切なアクションを起こす
// ViewController.swift @IBOutlet var emailField: UITextField? @IBOutlet var passwordField: UITextField? @IBOutlet var loginButton: UIButton? @IBAction func login() { println("Login") } // MARK: - UITextFieldDelegate func textFieldShouldReturn(textField: UITextField) -> Bool { if (textField == emailField) { passwordField?.becomeFirstResponder() } else { login() } return true }
login()
はloginButton
が押された場合、またはpasswordField
でReturn Keyが押された場合に実行される。今後、このメソッドにログイン処理を実装していく予定。emailField
とpasswordField
のdelegate
をこのViewControllerに設定しtextFieldShouldReturn(textField:)
を実装することで、2つのUITextFieldでReturn Keyが押されたときの処理を実装できる。becomeFirstResponder()
はレシーバーのViewを最初に応答するオブジェクトとして設定する。キーボードはこのFirst Responderに合わせてキーボードタイプや入力先を替える。
改善2: キーボードを閉じる
@IBAction func login() { resignFirstResponderAtControls() println("Login") } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { resignFirstResponderAtControls() } private func resignFirstResponderAtControls() { emailField?.resignFirstResponder() passwordField?.resignFirstResponder() }
- 非公開メソッドとして
resignFirstResponderAtControls()
を定義した。これによって2つのUITextFieldの選択状態を外しキーボードを閉じることができる。resignFirstResponder()
メソッドはレシーバーのViewをFirst Responderでなくす。これによってキーボードが閉じる。 - これを
login()
とtouchesBegan(touches:withEvent:)
で呼び出す。 UIViewController
はUIResponder
を継承しておりself.view
のイベントハンドリングを扱うことができる。そのため、touchesBegan(touches:withEvent:)
でresignFirstResponderAtControls()
を呼ぶことで、追加した3つのView以外を選択されたときにキーボードを閉じることができる。
改善3: タップしやすくする
「ヒューマンユーザーインターフェイスガイドライン」(以下HIG)にはこのような指針が載っている。
アプリケーション内のタップ可能な要素には、約44x44ポイントのターゲット領域を割り当てる。
これに従って「44x44ポイント以上」にサイズを変更する。
まず、UITextField
は高さが30ポイントに固定されているため、高さ44ポイントのViewの上にUITextField
を乗せてボーダーを非表示にし、その親ViewがタップされたらUITextField
がFirst Responderになるようにする。実装としては、UITextField
を含む高さ44ポイントのUIView
のサブクラスを用意する。
// TextFieldContainer.swift @IBDesignable class TextFieldContainer: UIView { @IBInspectable var borderWidth: CGFloat = 0 { didSet { layer.borderWidth = borderWidth } } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { subviews.first?.becomeFirstResponder() } }
Xcode 6からの新機能であるLive Viewsを利用し、カスタムViewも可能な限りStoryboard上でそのプロパティを変更できるようにする。
@IBDesignable
によってカスタムViewをStoryboard上でレンダリングして、その見た目をStoryboardからも確認できるようになる。@IBInspectable
によって下のスクリーンショットのようにカスタムViewのborderWidth
というプロパティをStoryboardから変更できるようになる。
次に、UIButton
もHIGの方針に従って修正する。UIButton
はサイズを自由に変更できるので、とりあえず44x44ポイントに変更した。ボタンの大きさは変更したものの、ボタンの「Login」というテキストはまだ小さいため、ユーザーの目からはサイズが大きくなったようには見えていない。そこで、ボタンにもボーダーをつけてみる。
// BorderedButton.swift @IBDesignable class BorderedButton: UIButton { @IBInspectable var borderWidth: CGFloat = 0 { didSet { layer.borderWidth = borderWidth } } }
TextFieldContainer
と同じようにstoryboardから枠線の幅を変えられるようにした。
マシなフォーム
以上の変更を行った結果このようになった。
間違いなくタップはしやすくなった。
問題点
- フラットデザインに則っていない。標準のアプリや人気の高いアプリはiOS 7から導入されたフラットデザインに沿ってデザインされており、同様なインターフェイスをもたなければユーザーは慣れ親しんだ動作で直感的にアプリを操作できなくなってしまう。
改善4: フラットデザインに従う
HIGではフラットデザインの基本的な設計方針として以下の3つを挙げている。
- 控えめであること
- 明瞭であること
- 奥行きを与えること
具体的な作業として
- 画面全体を使う
- 枠線を使わない
- 余白を十分にとる
を意識してStoryboardを編集した。Auto Layoutで各Viewの余白を固定したり、枠線の太さを0ポイントにした。その結果、以下のようになった。
フラットデザインに対するよくある批判として「ボタンがどこにあるのか認識しにくい」というものがある。"Email"や"Login"といった文字がある部分にしかViewがないように見えてしまうため、Viewの領域を表す枠線や背景色を控えめに加えた方がもっとよくなると考えた。そこで、2つのUITextField
の領域を控えめに表すため、領域の下辺だけ枠線を表示してみる。
// TextFieldContainer.swift @IBDesignable class TextFieldContainer: UIView { private var width: CGFloat { return CGRectGetWidth(frame) } private var height: CGFloat { return CGRectGetHeight(frame) } private let borderBottom: CALayer = CALayer() @IBInspectable var borderColor: UIColor = UIColor.blackColor() { didSet { setupBorderBottom() } } @IBInspectable var borderBottomWidth: CGFloat = 0 { didSet { setupBorderBottom() } } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { subviews.first?.becomeFirstResponder() } private func setupBorderBottom() { borderBottom.removeFromSuperlayer() borderBottom.frame = CGRectMake(0, height - borderBottomWidth, width, borderBottomWidth) borderBottom.backgroundColor = borderColor.CGColor layer.addSublayer(borderBottom) } }
- 以前の実装にあった
borderWidth
を削除し、下辺の枠線の太さを表すborderBottomWidth
と枠線の色を表すborderColor
を追加した。これらのプロパティがXcodeから変更されるたびにsetupBorderBottom()
が呼び出されてborderが追加される。 - 下辺だけの枠線は枠線の幅を高さとする
CALayer
として実装した。 - ショートカットのため
width
とheight
というcomputed propertyを用意した。
そして、Storyboardから枠線の色と幅を設定し余白を調整すると以下のようになった。
まとめ
最初と最後を比べると少しはマシなフォームになったと思う。改善したポイントをまとめると以下のようになる。
- Return Keyで適切なアクションを起こす
- キーボードを閉じる
- タップしやすくする
- フラットデザインに従う
現実の開発では、アプリごとのテーマに合わせた色やタイポグラフィを使うことになるだろうし、フォームのエラーメッセージの扱いについても触れられていない。残された課題については、経験を積む中で考えていくことにしたい。
最後に上で載せたコードを含んだプロジェクトをGitHubに公開したので参考にしてほしい。
Swiftでストリームを扱うライブラリを書いた
FRPの記事をいくつか見てあまり理解できなかったので、Swiftでストリームを扱うライブラリを書いてみた。結論から言うと、まだストリームについて深く理解できていない感じがするので「FRPとは何か」「ストリームとは何か」といった話はしない。そういう話は他のエントリーを読んでほしいと思う。
demo
let stream = Stream<String>() let counterStream: Stream<Int> = stream.map({ message in return countElements(message) }).scan(0, { previousMessage, message in return previousMessage + message }).subscribe({ message in println(message) }) stream.publish("Hello, ") //=> 7 stream.publish("wor") //=> 10 stream.publish("ld!") //=> 13
機能
- ストリームに
subscribe
で関数を渡すと、publish
されたときにその関数がpublish
の引数が渡されて実行される。subscribe
は失敗時と完了時に実行する関数を指定することもできる。ストリームに失敗を通知するのはpublish
の代わりにfail
、完了を通知するのはcomplete
。 - その他FRPのライブラリで実装されている次のような基本的なストリーム操作を実装した:
map
,filter
,scan
,flatMap
,throttle
,debounce
,buffer
,merge
結果
SwiftでNSDateを簡単に扱うライブラリを書いた
Swiftの実験的なプロジェクトとしてActiveSupportの拡張っぽく直感的に時間を扱うライブラリ"Timepiece"というものを書いた。
demo
let today = NSDate.today() let tomorrow = NSDate.tomorrow() let dayAfterTomorrow = tomorrow + 1.day let dayBeforeYesterday = 2.days.ago let birthday = NSDate.date(year: 1987, month: 6, day: 2)
機能
1.day.ago
(1日前)、4.years.later
(4年後)というようにInt
型を拡張し、数.単位.前/後
という書き方でNSDate
オブジェクトを初期化できる。単位は単数形、複数形どちらも使える。NSDate() + 1.minute
(1分後)、NSDate() - 3.hours
(3時間前)というように、NSDate
オブジェクトから数.単位
を加算・減算できる。NSDate.date(year:month:day:hour:minute:second)
,NSDate.today()
,NSDate.yesterday()
,NSDate.tomorrow()
というように、NSDate
オブジェクトをより直感的に初期化できる。
今後
- 既存の
NSDate
オブジェクトの時間だけ変更したいとか、その日の0:00を取得したいという場合に対応したい。ActiveSupportとかmoment.jsのbeginning_of_day
,endOf("day")
のようなやつ。 - 範囲オブジェクトのようなものを生成できるようにしたい。例えば、今日から1週間分の
NSDate
オブジェクトの範囲オブジェクトを作って繰り返し処理させられたら便利そう。
悩み
こういうライブラリでよくあるのがパース処理とフォーマット処理なんだけど、このライブラリでサポートすべきかどうか悩む。というのも、この手の処理はNSDateFormatter
オブジェクトを使うと思うのだけど、便利なAPIのせいで無意識にムダなNSDateFormatter
オブジェクトを何度も生成してしまう可能性がありそうだなと懸念している。
プルリクチャンス
- こういう機能がほしいという希望があればissuesでどんどん要望を伝えてほしいです。
- もちろんPull requestも歓迎です。コード量は多くないので、ちょっと読めば何をしているか思います。テストはちゃんと書いてます。
Alamofire/**/*Tests.swiftを読んだ
前回Alamofireの実装を読んだので、ついでにテストコードも読んでみた。
$ tree -P "*Tests.swift" -I .git . ├── Source └── Tests ├── DownloadTests.swift ├── ParameterEncodingTests.swift ├── RequestTests.swift ├── ResponseTests.swift └── UploadTests.swift
とりあえずRequestTests.swift
から読んでみる。
RequestTests.swift
L:26
extension Alamofire { struct RequestTests { class RequestInitializationTestCase: XCTestCase { // ... } class RequestResponseTestCase: XCTestCase { // ... } } }
- このプロジェクトはビルドターゲットをもたないため、ターゲットごとに名前空間が作られるわけではない。そのため、
Alamofire
という構造体の中にテストクラスをネストさせることで実装コードを参照できるようにしている。 - テストケース、この場合は
init
とかresponse
といったメソッドの単位でテストケースのクラスを作っているようだ。
L:49
class RequestResponseTestCase: XCTestCase { func testRequestResponse() { let URL = "http://httpbin.org/get" let serializer = Alamofire.Request.stringResponseSerializer(encoding: NSUTF8StringEncoding) let expectation = expectationWithDescription("\(URL)") Alamofire.request(.GET, URL, parameters: ["foo": "bar"]) .response(serializer: serializer){ (request, response, string, error) in expectation.fulfill() XCTAssertNotNil(request, "request should not be nil") XCTAssertNotNil(response, "response should not be nil") XCTAssertNotNil(string, "string should not be nil") XCTAssertNil(error, "error should be nil") } waitForExpectationWithTimeout(10){ error in XCTAssertNil(error, "\(error)") } } }
expectationWithDescription
とwaitForExpectationWithTimeout
はXcode 6からXCTestに追加された非同期テスト用のAPI。- まず、
expectationWithDescription
メソッドでXCTestExpectation
オブジェクトを生成する。 waitForExpectationWithTimeout
は指定された秒数、上で生成されたXCTestExpectation
オブジェクトのfulfill
メソッドが呼ばれるのを待つ。呼ばれれば成功、呼ばれずに指定された秒数が経過すると失敗となり引数に渡されたクロージャを実行する。
他のテストコードも読んでみたけど、上と同じようなコードがあり特に読む必要はなさそうだった。