Timepiece 1.0.0をリリースした
最初のバージョンはSwiftが出た2014年だった。それ以降、細かな機能追加やバグ修正を繰り返し、0.5.0でSwift 2.3、0.6.0でSwift 3に対応した。どこかのタイミングで多くのStar、issueがつくようになり、多くのユーザーが使ってくれるようになった。
そして昨日、ようやく安定版となる1.0.0をリリースした。1.0.0では破壊的な変更を行った。
メソッド名の変更
// 0.6.0 Date.date(year: 2016, month: 10, day: 31) "2016-10-31T12:00:00+0900".dateFromFormat("yyyy-MM-dd'T'HH:mm:ssZ") // 1.0.0 Date(year: 2016, month: 10, day: 31) "2016-10-31T12:00:00+0900".date(inFormat: "yyyy-MM-dd'T'HH:mm:ssZ")
よりSwiftらしく、またSwift 3の命名規則に沿ったものに変更した。
タイムゾーンのサポートをやめた
個別の日付オブジェクトについてタイムゾーンをサポートするようにしていたが、バグになりかねない部分であり、後述するメンテナンスコストを下げていく方針に合わないため、この機能を削除した。
Durationの内部表現を変更した
これまでのバージョンでは、期間を表す概念としてDuration
というstructを用意していたが、これをやめた。本来、期間を表す概念としてDateComponents
というものがあるため(これまでDateComponents
が期間を表すということをちゃんと認識してなかった)、これを最大限利用した。この変更によって、1.0.0では新たに以下のような計算や処理が可能になった。
// 1.0.0 Date() + (3.hours - 30.minutes) (3.hours - 30.minutes).string(in: .abbreviated) //=> "2h 30m"
固定フォーマットでの出力をやめた
// 0.6.0 1.weeks.later.stringFromFormat("yyyy/MM/dd")
上のようなメソッドをやめた。DateFormatter
で簡単に実装できるが、この機能は暦やロケールを考慮していない実装なのであまりオススメできない。その代わりにDateFormatter.Style
を指定して出力できるようにした。
// 1.0.0 1.weeks.later.dateString(in: .short)
望ましい機能は実装しやすくする。望ましくない機能は実装しにくくする。というのが理想的なAPIのインターフェイスであるように思う。
変更の背景
OSSにかけられる時間が減った。メンテナンスコストを下げたかった。バグが出やすい機能はできるだけ削った。機能が豊富な日付操作ライブラリは他にもある。そんな中、最小限の機能でバグがなく正確で少し読めば何をしているか分かるようなライブラリが理想的だとおもった。
CocoaPodsにコントリビュートした
開発中のiOSアプリでCocoaPodsでインストールしたライブラリのライセンス表示を実装する際に、とある理由でライセンスに表示したくない状況があった。いろいろ調べたところ、CocoaPodsが出力するPods-{ProjectName}-Acknowledgements.plist
にMIT
といったライセンスタイプが含まれていないことがわかった(ライセンスのテキストはあるけど、そこから抽出するのは大変)。podspecにはライセンスタイプを記載する必要があるため、内部表現としてライセンスタイプをもっているはずだと思った。そこで、それをplistファイルに出力するようにするPull requestを送って、そしてmergeされた。
Pull requestしてみた感想としては、RSpecのようなよく知らないテスティングフレームワークを使っており、自力ではどこをテストすればいいのか分からず困惑した。コミッターの方が修正してくれたようなのでよかった。
1.1.0.beta.1
に含まれているので、今後はMIT
などライセンスタイプを基に表示するライブラリをフィルタリングできたりできると思う。よかったら利用してください。
通信周りの処理をミドルウェアで整理する
課題感
APIリクエストの送信前、APIレスポンスの取得後にさまざまな処理をはさみたいことがある。例えば、こんな処理だ。
- ネットワークインジケータの表示・非表示
- リクエストとレスポンスのロギング
- 二重送信の防止
- ログイントークンが有効期限切れだったときに、リフレッシュトークンを使ってログイントークンを更新した後、再送
- HTTPリクエストのスタブ
ただ、こういった処理をAPIクライアントにそのまま実装していくとAPIクライアントが肥大化するし、かと言ってViewControllerに実装するといろんな箇所で似たようなコードを書くことになる。
解決策
APIクライアントをラップして機能を拡張するミドルウェアをつくる。ミドルウェアはAPIクライアントを呼び出して通信処理を実行しつつ、リクエストの送信前とレスポンスの取得後に処理をはさむ。
例えば、APIClient
というオブジェクトで本来の通信処理を実行するとする。ロギングを行うミドルウェアはこんな感じになる。
extension Middleware { struct Logger: RequestSendable { let client: RequestSendable func send<T: RequestType>(request: T) -> Task<Void, T.Response, ErrorType> { print(request) return client.send(request) .success { response -> Task<Void, T.Response, ErrorType> in print(response) return Task(value: response) } .failure { error, _ in print(error) return Task(error: error ?? ApplicationError.Unknown) } } } }
そして、こんな感じで初期化する。
let client: RequestSendable = Middleware.Logger(client: APIClient())
だけど、ミドルウェアが増えると、以下のように初期化が大変になってくる。
let client: RequestSendable = A(client: B(client: C(client: D(client: APIClient()))))
そこで、ミドルウェア群を簡単に組み合わせるための仕組みをつくる。
extension Middleware { struct Stack { let middlewareTypes: [RequestSendable.Type] init(_ middlewareTypes: [RequestSendable.Type]) { self.middlewareTypes = middlewareTypes } func buildClient() -> RequestSendable { let client = APIClient() return middlewareTypes.reverse().reduce(client) { (result: RequestSendable, middlewareType: RequestSendable.Type) in return middlewareType.init(client: result) } } } }
これによって、こんな感じで直感的にAPIクライアントを初期化できる。
let client = Middleware.Stack([A.self, B.self, C.self, D.self]).buildClient()
たいていの場合、利用するミドルウェアは同じなのでデフォルトで利用するミドルウェアスタックを簡単に初期化できるようにする。
extension Middleware { struct Stack { // ... static func defaultStack() -> Stack { var middlewares: [RequestSendable.Type] = [] middlewares.append(A.self) if someCondition { middlewares.append(B.self) } middlewares.append(C.self) reeturn Stack(middlewares) } } }
そして、APIクライアントの初期化はこうなる。
let client = Middleware.Stack.defaultStack().buildClient()
まとめ
通信周りのさまざまな処理をミドルウェアという形で実装することで、疎結合なモジュールに分離することができた。将来的に新たな処理を追加する場合でもミドルウェアを新たに実装してスタックに追加するだけでよく、既存のAPIクライアントやミドルウェアに手を加える必要はない。テスト時のみ不要なミドルウェアを除くといった柔軟な設定も可能になるだろう。
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公式アプリで自分のライブラリが使われていたことだった。
公式アプリの開発者の方と直接お話しする機会があった。本当はもっと伝えたいことがあったのだけど、あまりに英語ができなくて、ほとんど伝えられなかった。このライブラリのように海外の開発者に伝えられるコンテンツをもてるようになったものの、肝心の英語ができないばかりに非常にもったいないなーと強く感じた。それがきっかけで先月から英語を勉強しはじめている。