Xcodeのカラーパレットを作るコマンドをSwiftで書いた

f:id:naoty_k:20160317020138g:plain

clrというコマンドラインツールSwiftで書いた。上のスクリーンキャストにあるようにXcodeで使うカラーパレットをターミナルから作成できる。

近年ではStoryboard(かつてはXib)がどんどん進化しているので、見た目に関する設定はコードじゃなくてStoryboardに任せたいなという気持ちがある。特に色については、コードでやろうと思うとUIColorのextensionをひとつひとつ書いていく感じになると思うけど、カラーパレットを自作する手もあるなということに最近気づいた。カラーパレットを使うと2、3回クリックするだけで色を指定できるから簡単だと思う。

技術的な解説をすると、自作したカラーパレットは実は$HOME/Library/Colors/以下に*.clrという拡張子で保存されていて、このファイルを共有すれば他の開発者とカラーパレットを共有できる。ただ、これをXcodeでポチポチ自作するのはけっこう大変なので、これをコマンドラインから作れるようにした。カラーパレットはNSColorListというOSXAppKitフレームワークにあるクラスで表されていて、-writeToFile(_:)というメソッドでファイルに出力できる。なので、NSColorListを操作するコマンドラインツールSwiftで実装した。

参考

ブログタイトルを変えた

以前はnaoty.to_sというRuby-ishなタイトルだったけど、Swiftをやっていきたいという気持ちにマッチしなくなってきたのでSwiftyなタイトルに変えた。

AnyTypeはもちろんSwiftのそれのようにいろんなTypeの記事を書いていきたいという気持ちを表しているし、一方でタイピングのTypeにもかけている。一時期はかなり更新頻度が落ちていたけど、どんどんTypeしてブログを更新していきたい。

今後ともこのブログをどうぞよろしくおねがいします。

型消去を用いたSwiftによるリポジトリパターンの実装

リポジトリパターンとは

リポジトリはオブジェクトの参照を取得するのに必要なロジックをすべてカプセル化するためのパターンです。

Domain Driven Design Quickly 日本語訳

iOSアプリ開発の文脈では、オブジェクトをWeb APIから取得するのかRealmから取得するのかといった関心ごとがある。リポジトリを実装することで次のようなメリットがあると思う。

  • どこからどのように取得するのかなどの関心ごとからドメインモデルを切り離せるため、ドメインモデルをクリアに保つことができる。(DDDの観点)
  • テスト時にWeb APIやRealmにアクセスするリポジトリをメモリにアクセスするリポジトリに差し替えること(Dependency Injection)が可能になるため、テストデータを簡単に用意できたりテストのパフォーマンスを向上できるなど、テストしやすくなる。(テスタビリティの観点)

型消去とは

先日行われたtry! Swiftで紹介されたテクニックで、トークの内容については以下の書き起こし記事が詳しいと思う。

型消去とは何か、端的に説明するのはかなり難しい。ただ、リポジトリパターンをSwiftで実装するにあたって非常に強力なテクニックであることが分かったので、型消去を用いない場合と用いた場合とを比べて型消去について説明してみたいと思う。

型消去を用いない場合

例のごとくPokemonオブジェクトを取得するリポジトリを考える。PokemonはWeb APIから取得するかもしれないし、Realmから取得するかもしれない。とりあえず以下のようなprotocolを定義して、Pokemonを取得するインターフェイスを用意する。

protocol PokemonRepository {
    func find(ID: UInt) -> Pokemon?
    func findAll() -> [Pokemon]
}

そして、実際にRealmからPokemonを取得するリポジトリはこのprotocolを実装して以下のように書けると思う。

struct RealmPokemonRepository: PokemonRepository {
    func find(ID: UInt) -> Pokemon? {
        let realm = try! Realm()
        return realm.objects(Pokemon).filter("ID == %d", ID).first
    }

    func findAll() -> [Pokemon] {
        let realm = try! Realm()
        return realm.objects(Pokemon)
    }
}

同様にメモリ内の[Pokemon]からPokemonを取得するリポジトリは以下のように書けると思う。

struct MemoryPokemonRepository: PokemonRepository {
    let pokemons = [
        Pokemon(ID: 1, name: "フシギダネ"),
        Pokemon(ID: 2, name: "フシギソウ"),
        Pokemon(ID: 3, name: "フシギバナ")
    ]

    func find(ID: UInt) -> Pokemon? {
        return pokemons.filter { $0.ID == ID }.first
    }

    func findAll() -> [Pokemon] {
        return pokemons
    }
}

ViewController等でこのリポジトリを使う場合は以下のように書けると思う。

class PokedexViewController: UITableViewController {
    var pokedex: [Pokemon] = []
    lazy var repository: PokemonRepository = RealmPokemonRepository()

    override func viewDidLoad() {
        super.viewDidLoad()

        pokedex = repository.findAll()
    }
}

テストを書く際は以下のようにリポジトリを差し替えることでRealmへのアクセスを回避できる。

class PokedexViewControllerTests: XCTestCase {
    var viewController: PokedexViewController!

    override func setUp() {
        viewController = PokedexViewController()
        viewController.repository = MemoryPokemonRepository()
    }
}

こうしてPokemonを取得するリポジトリを実装することで、どのようにオブジェクトを取得するのかという関心ごとをカプセル化し、テスタビリティのある設計が可能になった。しかし、この実装には大きな問題がある。

型消去を用いない実装の問題点は、ドメインモデルごとに似たようなprotocolを用意しなくてはならないことだ。例えば、今度はHumanを取得したいという場合に同様にHumanRepositoryを定義しなくてはならないし、その次にTownを取得したいという場合にはTownRepositoryを定義しなくてはならない。これらのprotocolはほとんど中身が同じボイラープレートになってしまうだろう。

それでは、より汎用的なRepositoryというprotocolを以下のように定義してみてはどうだろうかと考えてみる。

protocol Repository {
    typealias Domain

    func find(ID: UInt) -> Domain?
    func findAll() -> [Domain]
}

typealiasを使ってGenericsなprotocolを定義することでより汎用的になった。そして、これを実装するリポジトリは例えばこんな感じになる。

struct RealmPokemonRepository: Repository {
    typealias Domain = Pokemon

    func find(ID: UInt) -> Pokemon? {
        let realm = try! Realm()
        return realm.objects(Pokemon).filter("ID == %d", ID).first
    }

    func findAll() -> [Pokemon] {
        let realm = try! Realm()
        return realm.objects(Pokemon)
    }
}

しかし、これはすぐにうまくいかないことがわかる。

lazy var repository: Repository = RealmPokemonRepository

のようなコードはコンパイルエラーになってしまうのだ。Repositoryのようなtypealiasをもつprotocolはtypealiasに具体的な型をもっていないため抽象型と呼ばれ、そのまま変数の型として宣言することができない。

このままおとなしくドメインモデルごとにボイラープレートのようなprotocolを書かなくてはいけないんだろうか(←try! Swift参加前の筆者)。

型消去を用いた場合

あらゆるリポジトリを汎用的に扱えるようにするため、以下のようなAnyRepositoryを定義する。

struct AnyRepository<DomainType>: Repository {
    typealias Domain = DomainType

    private let _find: UInt -> DomainType?
    private let _findAll: () -> [DomainType]

    init<T: Repository where T.Domain == DomainType>(_ repository: T) {
        _find = repository.find
        _findAll = repository.findAll
    }

    func find(ID: UInt) -> DomainType? {
        return _find(ID)
    }

    func findAll() -> [DomainType] {
        return _findAll()
    }
}

AnyRepository<Pokemon>として使う場合は、typealias Domain = PokemonとなっているRepositoryを実装した型のみAnyRepository()に渡すことができる。例えば、ViewControllerではこんな感じで使うことになる。

class PokedexViewController: UITableViewController {
    var pokedex: [Pokemon] = []
    lazy var repository: AnyRepository<Pokemon> = AnyRepository(RealmPokemonRepository())

    override func viewDidLoad() {
        super.viewDidLoad()

        pokedex = repository.findAll()
    }
}

同様にテストではこんな感じになると思う。

class PokedexViewControllerTests: XCTestCase {
    var viewController: PokedexViewController!

    override func setUp() {
        viewController = PokedexViewController()
        viewController.repository = AnyRepository(MemoryPokemonRepository())
    }
}

AnyRepositoryがあることで、PokemonRepositoryHumanRepositoryのようなドメインモデルごとのprotocolは不要になり、それぞれAnyRepository<Pokemon>AnyRepository<Human>のような型を使うことで対処できる。これでボイラープレートのようなコードを書く必要はなくなった。

型消去とは、この例で言うとPokemonRepository型であったrepositoryAnyRepository<Pokemon>という型にしてしまうことを指しているようだ。型消去というのは、より柔軟な設計のための結果として考えることができそう。


以降は型消去とは無関係だけど、リポジトリパターンを実装するにあたって必要となった技術要素を紹介していきたいと思う。

クエリのインターフェイス

上で紹介したRepositoryは意図的に不十分なインターフェイスだった。というのは、findAll()というメソッドはその名の通りすべてのオブジェクトを取得してしまうので、現実的には検索条件やソートなどのパラメータを指定できる必要があると思う。

ここで指定される検索条件は内部的にGETリクエストのパラメータやRealmに渡されるNSPredicateに変換されることになる。また、検索条件といっても単純に一致するためのものだけでなく、不一致や含んでいるかといった検索方法もある。Web APIに問い合わせるのかRealmに問い合わせるのかといったバックエンドに関わらず、これらを統一的に表すクエリの表現が必要となると思った。

そこでAnyQueryという小さなライブラリを開発した。

これを使ってRepositoryはこんな感じに定義できる。

protocol Repository {
    typealias Domain

    func find(ID: UInt) -> Domain?
    func findAll(query query: AnyQuery?, sort: AnySort?) -> [Domain]
}

extension Repository {
    func findAll() -> Domain? {
        return findAll(query: nil, sort: nil)
    }
}

RealmPokemonRepositoryで実際に使う場合はこんな感じになる。

struct RealmPokemonRepository: Repository {
    typealias Domain = Pokemon

    func find(ID: UInt) -> Pokemon? {
        let realm = try! Realm()
        return realm.objects(Pokemon).filter("ID == %d", ID).first
    }

    func findAll(query query: AnyQuery?, sort: AnySort?) -> [Pokemon] {
        let realm = try! Realm()
        var result realm.objects(Pokemon)

        if let predicate = query?.predicate {
            result = result.filter(predicate)
        }

        if let sortDescriptors = sort?.sortDescriptors {
            for sortDescriptor in sortDescriptors {
                guard let key = sortDescriptor.key else {
                    continue
                }
                result = result.sorted(key, ascending: sortDescriptor.ascending)
            }
        }

        return result
    }
}

そして、ViewControllerからはこんな感じでクエリを組み立てることができる。

class PokedexViewController: UITableViewController {
    var pokedex: [Pokemon] = []
    lazy var repository: PokemonRepository = RealmPokemonRepository()

    override func viewDidLoad() {
        super.viewDidLoad()

        let query = AnyQuery.In(key: "ID", values: [1, 2, 3, 4, 5])
        let sort = AnySort.Ascending(key: "name")
        pokedex = repository.findAll(query: query, sort: sort)
    }
}

詳細はREADMEに書いてあるが、例えば、こんな風に複雑な条件も表現できる。

let query = AnyQuery.Between(key: "ID", lhs: 1, rhs: 100) && AnyQuery.NotEqual(key: "type", PokemonType.Fire.rawValue)
let sort = AnySort.Descending(key: "weight") > AnySort.Descending(key: "height")

非同期処理の取り扱い

たいていの取得処理は非同期に行われるため、リポジトリインターフェイスも非同期処理を前提にしなくてはならないと思う。しかし、取得完了時の処理をクロージャとして渡すインターフェイスはコールバック・ヘルにつながるため、Promiseライクなライブラリを使ってオブジェクトの代わりにPromiseオブジェクトを返すような形がいいと思った。

例としてSwiftTaskを使って以下のようにRepositoryを定義してみた。

protocol Repository {
    typealias Domain
    
    func find(ID: UInt) -> Task<Float, Domain, ErrorType>
    func findAll(query query: AnyQuery?, sort: AnySort?) -> Task<Float, [Domain], ErrorType>
}

そして、実装は以下のようになる。

class RealmPokemonRepository: Repository {
    typealias Domain = Pokemon

    func find(ID: UInt) -> Task<Float, Pokemon, ErrorType> {
        return Task<Float, Pokemon, ErrorType> { fulfill, reject in
            let realm = try! Realm()
            if let pokemon = realm.objects(Pokemon).filter("ID == %d", ID).first {
                fulfill(pokemon.pokemon)
            } else {
                reject(RepositoryError.NotFound)
            }
        }
    }

    func findAll(query query: AnyQuery?, sort: AnySort?) -> Task<Float, [Pokemon], ErrorType> {
        return Task<Float, [Pokemon], ErrorType> { fulfill, reject in
            let realm = try! Realm()
            var result = realm.objects(RealmPokemon)
            
            if let predicate = query?.predicate {
                result = result.filter(predicate)
            }
            
            if let sortDescriptors = sort?.sortDescriptors {
                for sortDescriptor in sortDescriptors {
                    guard let key = sortDescriptor.key else {
                        continue
                    }
                    result = result.sorted(key, ascending: sortDescriptor.ascending)
                }
            }
            
            if result.isEmpty {
                reject(RepositoryError.NotFound)
            } else {
                let pokemons = result.map { $0.pokemon }
                fulfill(pokemons)
            }
        }
    }
}

最後に

Swiftリポジトリパターンを実装するにあたってのポイントは3つあった。

以上で説明したことはすべてこちらのサンプルプロジェクトで詳細を見ることができるので、参考にしてほしい。

try! Swiftに参加してきた

http://www.tryswiftconf.com/

3/2~3/4の三日間、try! Swiftというカンファレンスに参加してきた。

33セッション * 30分という超濃密な構成で過去参加したカンファレンスの中でも最も充実した内容だった。特に、下のようなトピックが多かったような印象がある。

  • Protocol extensionなどのSwiftのパワーを使った、より洗練された実装方法の話
  • Swiftを使った関数型プログラミングの話
  • デザインやアニメーションなどUIにまつわる話

ちょうどいま直面していた課題に関わるようなトピックもあり、セッション後のQ&Aコーナーでスピーカーに話しかけて、ペアプロまでしてもらった。おかげでその課題はすっきり解決できた。

今回のtry! Swiftで最大の収穫は、英語を本格的に学ぼうと思うきっかけがあったことだった。それは、try! Swift公式アプリで自分のライブラリが使われていたことだった。

f:id:naoty_k:20160307003947p:plain

公式アプリの開発者の方と直接お話しする機会があった。本当はもっと伝えたいことがあったのだけど、あまりに英語ができなくて、ほとんど伝えられなかった。このライブラリのように海外の開発者に伝えられるコンテンツをもてるようになったものの、肝心の英語ができないばかりに非常にもったいないなーと強く感じた。それがきっかけで先月から英語を勉強しはじめている。

SwiftCSVをフルスクラッチした

おととしSwiftCSVというCSVSwiftで扱うためのライブラリを作った。

けっこう思いつきで作ったので、あんまりちゃんとパースできないし、思ったよりissueがたくさん来てつらくなってしまったので、放置していた。仕事でもSwiftをまったく書けずにいて、Swiftを触るモチベーションも低かった。

今年に入ってSwiftをガンガン仕事で書くようになってモチベーションが復活したので、ひどい有様だったSwiftCSVをフルスクラッチすることにした。幸い、テストコードはギリギリあったので振る舞いは変えずに内部のコードを綺麗にし、Swift 2.1に対応した。

SwiftCSVを書くにあたって活躍したのはGeneratorTypeSequenceTypeという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)
    }
}

GeneratorTypeSequenceTypeを使うことで設計上はうまく整理できたものの、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)

f:id:naoty_k:20160216000628p:plain

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)

f:id:naoty_k:20160216001159p:plain

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)

f:id:naoty_k:20160216000952p:plain

まとめ

  • 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が持っていたlocalizedDescriptionuserInfoといったエラー情報がないという問題点がある。そこで、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を拡張しているため、ApplicationErrorNSErrorFriendlyErrorTypeとして並べて扱うことができている。

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)

以上のような方針に基づいたサンプルアプリケーションを用意した。

関連記事