Xcodeのカラーパレットを作るコマンドをSwiftで書いた
clr
というコマンドラインツールをSwiftで書いた。上のスクリーンキャストにあるようにXcodeで使うカラーパレットをターミナルから作成できる。
近年ではStoryboard(かつてはXib)がどんどん進化しているので、見た目に関する設定はコードじゃなくてStoryboardに任せたいなという気持ちがある。特に色については、コードでやろうと思うとUIColor
のextensionをひとつひとつ書いていく感じになると思うけど、カラーパレットを自作する手もあるなということに最近気づいた。カラーパレットを使うと2、3回クリックするだけで色を指定できるから簡単だと思う。
技術的な解説をすると、自作したカラーパレットは実は$HOME/Library/Colors/
以下に*.clr
という拡張子で保存されていて、このファイルを共有すれば他の開発者とカラーパレットを共有できる。ただ、これをXcodeでポチポチ自作するのはけっこう大変なので、これをコマンドラインから作れるようにした。カラーパレットはNSColorList
というOSXのAppKit
フレームワークにあるクラスで表されていて、-writeToFile(_:)
というメソッドでファイルに出力できる。なので、NSColorList
を操作するコマンドラインツールをSwiftで実装した。
参考
型消去を用いた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
があることで、PokemonRepository
やHumanRepository
のようなドメインモデルごとのprotocolは不要になり、それぞれAnyRepository<Pokemon>
、AnyRepository<Human>
のような型を使うことで対処できる。これでボイラープレートのようなコードを書く必要はなくなった。
型消去とは、この例で言うとPokemonRepository
型であったrepository
がAnyRepository<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つあった。
- 型消去によってリポジトリのための汎用的なインターフェイスを定義する。
- AnyQueryを使ってクエリのインターフェイスを統一する。
- Promiseライクなライブラリを使って非同期処理も考慮したリポジトリを設計する。
以上で説明したことはすべてこちらのサンプルプロジェクトで詳細を見ることができるので、参考にしてほしい。
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)
以上のような方針に基づいたサンプルアプリケーションを用意した。