try! Swiftに参加してきた
3/2~3/4の三日間、try! Swiftというカンファレンスに参加してきた。
33セッション * 30分という超濃密な構成で過去参加したカンファレンスの中でも最も充実した内容だった。特に、下のようなトピックが多かったような印象がある。
- Protocol extensionなどのSwiftのパワーを使った、より洗練された実装方法の話
- Swiftを使った関数型プログラミングの話
- デザインやアニメーションなどUIにまつわる話
ちょうどいま直面していた課題に関わるようなトピックもあり、セッション後のQ&Aコーナーでスピーカーに話しかけて、ペアプロまでしてもらった。おかげでその課題はすっきり解決できた。
今回のtry! Swiftで最大の収穫は、英語を本格的に学ぼうと思うきっかけがあったことだった。それは、try! Swift公式アプリで自分のライブラリが使われていたことだった。
公式アプリの開発者の方と直接お話しする機会があった。本当はもっと伝えたいことがあったのだけど、あまりに英語ができなくて、ほとんど伝えられなかった。このライブラリのように海外の開発者に伝えられるコンテンツをもてるようになったものの、肝心の英語ができないばかりに非常にもったいないなーと強く感じた。それがきっかけで先月から英語を勉強しはじめている。
SwiftCSVをフルスクラッチした
おととしSwiftCSVというCSVをSwiftで扱うためのライブラリを作った。
けっこう思いつきで作ったので、あんまりちゃんとパースできないし、思ったよりissueがたくさん来てつらくなってしまったので、放置していた。仕事でもSwiftをまったく書けずにいて、Swiftを触るモチベーションも低かった。
今年に入ってSwiftをガンガン仕事で書くようになってモチベーションが復活したので、ひどい有様だったSwiftCSVをフルスクラッチすることにした。幸い、テストコードはギリギリあったので振る舞いは変えずに内部のコードを綺麗にし、Swift 2.1に対応した。
SwiftCSVを書くにあたって活躍したのはGeneratorType
とSequenceType
というprotocolだった。これらはfor ... in
文に渡すことができる独自のイテレータを定義できる。これらの使い方は以前Qiitaにまとめたので参考になるかもしれない。
これらを使うことで、「イテレートされる要素を作る責務」と「その要素を使う責務」を切り分けて、別々のオブジェクトとして定義できる。実際にSwiftCSVでは以下のように切り分けられている。
// CSV.swift init(string: String, delimiter: NSCharacterSet = comma) { let headerSequence = HeaderSequence(text: string, delimiter: delimiter) for fieldName in headerSequence { header.append(fieldName) columns[fieldName] = [] } // ... }
// HeaderSequence.swift struct HeaderGenerator: GeneratorType { typealias Element = String private var fields: [String] init(text: String, delimiter: NSCharacterSet) { let header = text.lines[0] fields = header.componentsSeparatedByCharactersInSet(delimiter) } mutating func next() -> String? { return fields.isEmpty ? .None : fields.removeAtIndex(0) } } struct HeaderSequence: SequenceType { typealias Generator = HeaderGenerator private let text: String let delimiter: NSCharacterSet init(text: String, delimiter: NSCharacterSet) { self.text = text self.delimiter = delimiter } func generate() -> HeaderGenerator { return HeaderGenerator(text: text, delimiter: delimiter) } }
GeneratorType
とSequenceType
を使うことで設計上はうまく整理できたものの、CSVパーサとしての機能はかなりショボい。ダブルクォーテーションに囲まれた,
や改行を認識できていない。けっこう大変そうで僕だけでは対応ができないので、Pull requestを募集している。
HIGで推奨されているアラートをSwiftで効率的に組み立てる
最近、Swiftにおけるエラーハンドリングについて興味をもっている。エラーハンドリングの中でアラートを組み立てて表示するコードをよく書いたり、目にしている。アラートを実装する際に気をつけているのは、ユーザーが目にしたときになるべく怒らせないようにすることだ。ユーザーフレンドリーなアラートを実装する上で参考にするため、Human Interface Guidelines(以下、HIG)を読んでいる。HIGを読むと、アラートの実装にあたって問題点が見えてきた。
問題点
UIAlertController
でアラートを組み立てるとき、テンプレのようなコードを長々書かないといけない。UIAlertController
を使ってHIGで推奨されるアラートを組み立てるには、HIGの理解と注意深い実装が必要になる。
解決策
Swiftの表現力を駆使して、テンプレのようなコードをなるべく排除し、HIGの中で望ましいとされるUIを効率的に組み立てられるような設計を考えた。HIGでは、アラートは1つまたは2つのボタンを持つべきで、ボタンが3つ以上の場合はアクションシートを検討すべきだと書かれている。アラートを1つのボタンを持つConfirmation
と2つのボタンを持つSuggestion
という2つのタイプに分類して、以下のようなenumで表現することを考えてみた。
enum Alert { case Confirmation case Suggestion }
このAlert
という型の値からUIAlertController
を生成する必要がある。アラートに表示する情報はエラーオブジェクトから取得できると、エラーごとに表示すべき情報が統一されて効率的だと思う。そこで、以下のようにNSError
を各caseに関連付け(前回記事を読むとNSError
ではなくFriendlyErrorType
を使うべき場面だと分かる)、viewController
というプロパティを定義した。
enum Alert { case Confirmation(NSError) case Suggestion(NSError) var viewController: UIAlertController { switch self { case .Confirmation(let error): let alertController = buildAlertControllerWithError(error) let cancel = UIAlertAction(title: "OK", style: .Cancel, handler: nil) alertController.addAction(cancel) return alertController case .Suggestion(let error): // 省略 } } }
ここでのbuildAlertControllerWithError(_:)
はNSError
のもつ各情報を使ってUIAlertController
を初期化するようなイメージだ。
Suggestion
の場合、エラーから復帰するためのアクションをユーザーに提案することになるため、その「復帰するためのアクション」をRecovery
として以下のように表現してみる。
struct Recovery { let name: String let style: RecoveryStyle let recover: UIAlertAction -> Void enum RecoveryStyle { case Nondestructive case Destructive } }
RecoveryStyle
は復帰するためのアクションが破壊的(=アクション前に戻せない)か、非破壊的(=アクション前に戻せる)かを表している。なぜこれらを区別するかというと、HIGでは破壊的なアクションは赤字のタイトルにし、アラートの左側にボタンを置くべきとされているからだ。逆に非破壊的なアクションのためのボタンは右側に置くべきとされている。
Recovery
を踏まえると、Alert
の実装は以下のようになる。
enum Alert { case Confirmation(NSError) case Suggestion(NSError, Recovery) var viewController: UIAlertController { switch self { case .Confirmation(let error): // 省略 case .Suggestion(let error, let recovery): let alertController = buildAlertControllerWithError(error) let cancel = UIAlertAction(title: "Cancel", style: .Default, handler: nil) switch recovery.style { case .Nondestructive: let recover = UIAlertAction(title: recovery.name, style: .Default, handler: recovery.recover) alertController.addAction(cancel) alertController.addAction(recover) case .Destructive: let recover = UIAlertAction(title: recovery.name, style: .Destructive, handler: recovery.recover) alertController.addAction(recover) alertController.addAction(cancel) } return alertController } } }
RecoveryStyle
によってaddAction
の順番を変えている。これによってHIGで推奨されているボタンの配置になる。
利用例
let alert = Alert.Confirmation(error) presentViewController(alert.viewController, animated: true, completion: nil)
let recovery = Alert.Recovery(name: "Recover", style: .Nondestructive) { action in print("Recover!!") } let alert = Alert.Suggestion(error, recovery) presentViewController(alert.viewController, animated: true, completion: nil)
let recovery = Alert.Recovery(name: "Recover", style: .Destructive) { action in print("Recover!!") } let alert = Alert.Suggestion(error, recovery) presentViewController(alert.viewController, animated: true, completion: nil)
まとめ
- HIGに沿って実装するとユーザーフレンドリーなアラートになる(はず)。
- HIGに沿って実装するのは、HIGの理解と注意深い実装が必要になる。
- 上記のようなSwiftの表現力を駆使した設計によって、効率的にHIGに沿ったユーザーフレンドリーな実装を可能にできる。
関連記事
FriendlyErrorType
新しいエラーハンドリング
Swift 2でthrow
を使ったエラーハンドリングが新たに導入された。従来のNSError
を使ったエラーハンドリングの問題点は、メソッドにNSError
ポインタの代わりにnil
を渡すことで無視できてしまうことだった。新たに導入されたエラーハンドリングでは、throws
キーワードが宣言されたメソッドを呼び出す際にdo-catch
文で囲うことを強制される。throw
で投げられるエラーはNSError
ではなくErrorType
というprotocolを実装した値だ。Cocoaフレームワーク内のNSError
を使っていたメソッドはthrows
を使うように置き換えられており、今後は独自のエラーを定義する場合はNSError
ではなくErrorType
を使うのが望ましいと考えられる。しかし、ErrorType
にも問題点はあり現実的な設計方針を検討する必要がある。
アプリ独自エラーの実装
NSError
の代わりにErrorType
を使っていく流れがあるものの、ErrorType
にはNSError
が持っていたlocalizedDescription
やuserInfo
といったエラー情報がないという問題点がある。そこで、ErrorType
を継承した新たなprotocolを定義するという方針を考えてみた。
protocol FriendlyErrorType: ErrorType { var summary: String { get } var reason: String? { get } var suggestion: String? { get } }
このFriendlyErrorType
を使って以下のように独自エラーを定義できる。
enum ApplicationError: FriendlyErrorType { case SomethingWrong case DecodeFailed([String]) var summary: String { switch self { case .SomethingWrong: return "Something wrong" case .DecodeFailed(_): return "Decode failed" } } var reason: String? { switch self { case .SomethingWrong: return .None case .DecodeFailed(let fields): let failedFields = fields.joinWithSeparator(", ") return "Failed to decode following fields: \(failedFields)" } } var suggestion: String? { switch self { case .SomethingWrong: return .None case .DecodeFailed: return .None } } }
また、CocoaフレームワークのメソッドはErrorType
を投げるようになったものの、Alamofire等のライブラリを使う際にはNSError
を使うことになるため、FriendlyErrorType
を実装するようにNSError
を拡張する。
extension NSError: FriendlyErrorType { var summary: String { return localizedDescription } var reason: String? { return userInfo[NSLocalizedFailureReasonErrorKey] as? String } var suggestion: String? { return userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String } }
なぜprotocol extensionではなく継承なのか
protocol extensionだとErrorType
にデフォルトの実装を与えることになる。その場合、ErrorType
として渡されたエラーに対してメソッドを呼ぶと、すべてそのデフォルトの実装の結果が返るようになる。一方、FriendlyErrorType
はただのprotocolなので、メソッドの結果はメソッドを実装する各クラスの結果を反映する。
extension ErrorType { var summary: String { return "" } } extension NSError { var summary: String { return localizedDescription } } let error: ErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"]) print(error.summary) //=> "\n"
protocol FriendlyErrorType: ErrorType { var summary: String { get } } extension NSError: FriendlyErrorType { var summary: String { return localizedDescription } } let error: FriendlyErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"]) print(error.summary) //=> "Something wrong\n"
エラーの利用例
FriendlyErrorType
を実装したエラー型を実際に利用してみる。Alamofire、SwiftTask、Himotokiを使ってQiita APIを呼び出している。
return Task<Void, [Item], FriendlyErrorType> { progress, fulfill, reject, configure in Alamofire.request(.GET, "https://qiita.com/api/v2/items").responseJSON { response in switch response.status { case .Success(let value): if let objects = value as? [AnyObject] { var items: [Item] = [] for object in objects { do { let item = try decode(object) as Item items.append(item) } catch DecodeError.MissingKeyPath(let keyPath) { reject(ApplicationError.DecodeFailed(keyPath.components)) } catch { reject(ApplicationError.SomethingWrong) } } fulfill(items) } else { reject(ApplicationError.DecodeFailed(["root"])) } case .Failure(let error): reject(error) } } }
NSError
を拡張しているため、ApplicationError
とNSError
をFriendlyErrorType
として並べて扱うことができている。
FriendlyErrorType
を使ってアラートを表示する実装は以下のようなイメージだ。
let title = error.summary var message = "" if let reason = error.reason { message += reason message += "\n" } if let suggestion = error.suggestion { message += suggestion } let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) presentViewController(alertController, animated: true, completion: nil)
以上のような方針に基づいたサンプルアプリケーションを用意した。
関連記事
Timepieceを0.4.0にアップデートした
Timepieceを0.4.0
にアップデートした。ぶっちゃけブログの記事にするくらいならちゃんとCHANGELOGにしろという話なんだけど、技術的な詳細も少し話したいのでブログの記事にした。軽微なバグの修正と以下の2点が今回の変更点だ。
タイムゾーンのサポート
これがメインの変更になる。「サーバー側から受け取る時間のタイムゾーンとiOSアプリケーションのタイムゾーンが異なっており、それらを比較したい」みたいなissueをもらったので対応した。「こういう感じのインターフェイスはどう?」みたいなのを聞いてたら「Sweet!」だのと褒められたので、その気になって実装してしまった。けっこう大変だった。
iOSでは、NSDate
オブジェクトそのものにタイムゾーンは存在しない。システムで設定されるタイムゾーンをNSCalendar
経由で取得することになる。
NSCalendar.currentCalendar().timeZone
なので、今回のようなケースだと、NSDate
オブジェクトそれぞれにタイムゾーンが存在するように見せる必要がありそうだった。なお、オフセットを調整することも考えられるが、時間を足し引きしてしまった段階でそれは別の時間となってしまう。同じ時間で別のタイムゾーンを持つNSDate
オブジェクトが必要だった。実装方針としては、
ということを念頭に置いた。
ところで、TimepieceはNSDate
を始めとするいくつかの既存のオブジェクトのextensionとして実装されている。extensionでNSDate
オブジェクトにtimeZone
のようなプロパティを追加することは普通はできない。しかし、Objective-CのランタイムAPIを使って動的にプロパティを追加することで、これをなんとか実現させることができる。Swiftの場合でも、ランタイムAPIを使うことは可能だ。
import ObjectiveC public extension NSDate { var timeZone { return objc_getAssociatedObject(self, &AssociatedKeys.TimeZone) as? NSTimeZone ?? calendar.timeZone } func change(#timeZone: NSTimeZone) -> NSDate! { // ... objc_setAssociatedObject(newDate, &AssociatedKeys.TimeZone, timeZone, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) // ... } }
objc_
から始まる関数がランタイムAPIだ。これらの関数によって動的にオブジェクトにプロパティを読み書きしている。change(timeZone:)
で生成されたNSDate
オブジェクトのみ動的に追加されたプロパティを持つ。タイムゾーンが異なるNSDate
同士の計算も問題なく行われている。これは上述の通りオフセットの調整による実装ではないためだと思う。
playgroundの追加
機能というわけじゃないけど、リポジトリにplaygroundを追加した。それに伴ってプロジェクトからワークスペースに変更した。実際にTimepieceを試してもらうには、サンプルのアプリケーションを作ってPodfileを書いて…みたいなことをする必要がありとても面倒だったので、Timepieceが使えるplaygroundを用意した。
Pull request大歓迎
タイムゾーン周りの実装は正直あんまり自信はないんだけど、テストはちゃんと通ってるしまぁいいかくらいの気持ちでリリースした。観点が漏れている可能性は大いにありうるので、ぜひPull requestしてもらいたい。
「すごいHaskell たのしく学ぼう!」を読んだ
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
本書は一度は8章あたりで挫折したが、今回13章あたりまで読みファンクタ―、アプリカティブファンクタ―、モノイド、モナドといった概念がなんなのか理解とまでは言えないけど知ることができた。
一度は挫折したが今回またリベンジしようと思った理由は、今後モバイルアプリを開発していくにあたって関数型プログラミングの概念を理解して採り入れていくことが必要になってくると思ったからだ。Swiftはlet
による不変型の宣言やOptional
型などの文脈付きの型など関数型プログラミング言語としての側面をもっていると思う。また、データバインディング(SwiftBond/Bondなど)やJSONのパース(thoughtbot/Argoなど)といった場面で関数型プログラミングの概念が登場してきている。Swiftのポテンシャルを最大限に発揮して、堅牢で生産性の高いコードを書くには関数型プログラミングの知識が必要になってきていると最近感じている。
本書を読んだ結果として、データの構造について新しい視点を得ることができた。Maybe
やEither
といった概念を"文脈"と呼んでいるのが自分の中にはなかった発想だった。例えば、Maybe
とMaybe Int
を区別して考えるのはとても抽象的だけど強力な考え方と思った。Maybe
は「あるかもしれないし、ないかもしれない」という文脈を表し、Maybe Int
は「Int
型かもしれないし、何もないかもしれない」型を表している。これらを分けることで、文脈を保ったまま計算するという発想が出てくるのだと思う。文脈を保ったまま計算する段階として、本書ではFunctor
やApplicative
、そしてMonad
が登場してきた。
Swiftでは、Haskellにおける型コンストラクタにあたる概念がない。Genericsを使うことでMaybe
のような型を表現することはできるが、ある型が型引数をとるのかとらないのか、とるとしたらいくつとるのかを知る術はない(はず)。Haskellではそれらは種類という概念で説明されている。Maybe
の種類はMaybe :: * -> *
だし、Either
の種類はEither: * -> * -> *
となっているので、それぞれ型引数を1つと2つとることがわかる。HaskellのFunctor
は種類が* -> *
の型コンストラクタしかインスタンスにできないのだけど、こういう概念をSwiftで表現できない。
というわけで、Swiftで関数型プログラミングをするにはHaskellほどうまくはできないことがなんとなくわかった。Genericsなどで擬似的に表現するしかない。Functor
のfmap
を以下のように実装してみた。
extension Optional { func fmap<U>(f: T -> U) -> U? { switch self { case .Some(let value): return f(value) case .None: return .None } } } let maybeOne: Int? = 1 let maybeTen = maybeOne.fmap({ x in x * 10 })
SwiftのOptional<T>
型はつまりT?
型のことなのだけど、Optional
型を拡張してfmap
を追加している。return f(value)
のところは暗黙的にU?
型にラップしている。このように実装することで、Optional
型のもつ「あるかもしれないし、ないかもしれない」という文脈を保ちつつ、中身の1
というInt
を計算している。
ここではFunctor
だけを簡単に実装してみたが、これに加えてApplicative
とMonad
を実装するとより抽象的な計算が可能になってくる。JSONのパースなどを実装する際にはApplicative
の操作が必要になってきそうな感じがする。自分はまだ関数型プログラミングの実装を実際にしたわけではないので、理解したとは到底いえない。パーサーの実装をしてみたり、上で紹介したライブラリのコードを読んでみることで関数型プログラミングを実践的に理解していきたい。
#potatotips でTimepieceについて発表した
potatotips
資料
最近のTimepiece
- GW前あたりから急激にバズってきた。一時GitHubのトレンドで1位になった。それまでは☆70くらいだったけど、もうそろそろ☆500になりそうな勢いだ。
- それに伴っていくつかの要望をPull requestでいただいた。それらはほぼすべてmergeした。機能追加やバグ修正まで自分では見落としていた部分を指摘していただいて、多くの方に使われていそうだという実感がある。
イベントの感想
- 最近はiOSではなくAndroidアプリ開発をしているので、iOS/Android両方楽しめて非常に良かった。
- Timepieceを検討したけど採用を見送った方の意見を聞けたのが非常に良かった。そういう方の意見を聞ける機会は多くないからだ。いただいた要望について今実装方針を考えていて、ちゃんと形にしていきたい。
- 最近気になっているResultについての議論はとても勉強になった。naoty/SwiftCSVでエラー情報を扱う際にResultが使えそうだと思っていた。ただ、議論を聞いてオレオレResultが乱立しそうな流れがありそうだというのを知った。そうなると、ライブラリ提供者が実装するよりも利用者側でResultを定義する方が利便性を損ねないのでは、という意見に変わった。
- ドキュメントだけではよく理解できなかったDagger 2については、あまりよくわかってなかった
@Provide
について理解が深まった。Androidのテストについて意見交換をさせていただいて、自分の意見は間違ってなさそうだという確信を得られたのもよかった。 - その他、Androidの
@Nullable
,@NonNull
はすぐに使おうと思ったし、Lastlaneやdeliverといったワークフローを自動化するツールも実践的な内容で勉強になった。