タマネギプログラマーの雑記

たまねぎ剣士的なニュアンス

iOS開発の自動テストについて最近思っていること

この記事はSwift愛好会 Advent Calendar 2017の19日目の記事です。

はじめに

僕は基本的に自動テストは書くべきであると考えている。
最近仕事で触れているコードが中々のレガシーコードであるから、なおさらだ。

レガシーコードに変更を加えるときには、意図せぬ副作用が生じ、プロダクトを壊してしまう可能性が常につきまとう。
そんな時にテストコードがあれば、自分が既存のロジックを意図せずに破壊してしまっていないか知ることができ、作業に対し安心を感じることができる。

残念ながら、僕が最近対峙しているレガシーコードには、充分なテストコードが用意されていなかった。
そのため、否応なしにiOSプロジェクトでのテストについて考えることが多くなってきた。
一方、僕は幸運にして、周りにテストについての知見者が多い。
そういう人たちに相談したりしながら、最近考えていることをとりとめなく書いていこうと思う。

iOSという環境とテストについて

僕は、iOSプロジェクトという環境は、比較的テストを書くモチベーションを下げやすい環境だと考えている。
その理由は大きく2つだ。

  • テストの実行に時間がかかる
  • UIテストなど、テストが書きづらい箇所が存在する

それぞれの理由についてもう少し掘り下げていこう

テストの実行に時間がかかる

XCodeでのテストの実行は、プロジェクトのビルドがはいるため、基本的に遅い。
その間の時間を潰すために、僕はデスクにハンドグリップとハンドスピナーを置いているくらいだ。

『レガシーコード改善ガイド』に、優れた単体テストの条件として次の2つが上げられている。

  • 実行が速い
  • 問題箇所の特定がしやすい

この条件を見ると、こう思うはずだ。 「iOSでの単体テストは、優れたテストになり得ないじゃないか」と。

『レガシーコード改善ガイド』には、またこうも書いてある。

実行に0.1秒もかかる単体テストは、遅い単体テストである。

つまり、XCode上で動く単体テストは、どうあがいても「遅い単体テスト」になってしまい、「優れたテスト」になりえないのである。
この時点で、僕はiOSプロジェクトにおける単体テストは、別環境における単体テストと比べ、相対的にコストパフォーマンスが低くなる宿命にあると考えてしまう。

UIテストなど、テストが書きづらい箇所が存在する

前提として、UIテストを書くことはコストが高い。
高い上に壊れやすい。
また、昨今のアプリケーションはデザインの変更が頻繁に行われるため、メンテナンスコストも非情に高くなる。
結果として、UIテストはコストパフォーマンスが悪い場合が多く、UIテストは書かない、という風になりがちだ

それならばそうで、別の箇所のテストを充分に書ければ良いのだが、iOSのプロジェクトでは概してViewとロジックが密に結合してしまっている。
これは、iOSで多く使われているアーキテクチャMVCであることに起因する(もしくはテストへの知識不足)。
MVCでは、ViewControllerがファットになりやすい傾向にあり、そこにロジックが多く混入してしまいやすいのだ。 その上、ViewControllerはViewからのデリゲートなどが多く、テストが非常に難しい。
結果として、テストで保護できないロジックが多く出てしまう。

ではiOSでテストを書くのは悪手なのか

上までで、iOS環境でのテストを少しdisったわけなのだが、では、iOSでテストを書くのはあまり価値がないことかというとそうではない。
ただ、iOSでのテストは、他環境とは違ったテストとの向き合いかたをしなければならないという風に僕は考えている。

まず、テストの実行が遅い点について考える。
テストの実行は、Carthageの使用など、ビルド時間を短くする工夫で多少ましになるが、それでも0.1秒以下は望めないだろう。
この時点で、iOSのテストは、実行コストが高いテストであることは間違いない しかし、今まで何気なく使っていた、「コスパ」という言葉に目を向ける必要がある。

自動テストにおけるコストは何か。
単純に考えるならば、テストの作成、メンテナンスにかかる労力、実行にかかる時間だろう。
ではパフォーマンスとは何か?
これは、人によっても違うだろうが、少なくとも僕の場合は、「作業時の心理的安全性」である。

そもそも、『レガシーコード改善ガイド』で、速いテストが良いとされているのは、コードの編集都度に、気軽にテストの実行ができるためである。
編集の都度テストを実行することができれば、自分の変更が問題ないかどうか確認しながら作業をすすめることができる。
これが、テストによる心理的安全性の確保である。

では、遅いテストでは何が問題になるのか。
高頻度に実行した際の作業オーバーヘッドが大きくなってしまうのである。

f:id:gaoxin-xixxix:20171218001941p:plain

ならば、長くかかる分、テストの実行頻度を減らして作業すれば、作業能率は下がらないはずである。 もっとも、その分自分の編集による破壊に気づくタイミングは平均して遅くなるが、全く破壊に気づかないことに比べれば遥かに旨味があるだろう。

ただ、その頻度をどれくらいに設定するかと言うのは難しい話で、今のところは、「かかる時間へのコスト間と手に入る安心感が釣り合う程度の頻度」という理解にとどまっている。

テストが書きづらい箇所についてはどうだろうか?
これについてはおそらく多くの人が答えを知っているはずで、MVVMやMVPなどのアーキテクチャを少しづつでも導入してくのが一つの回答になる。
テストしづらいViewControllerから、ViewModelやPresenterといった、ロジックを剥がし取る層を用意してやるのである。
MVCからアーキテクチャを変更できない特別な理由があるのなら、古式ゆかしい~Serviceや~Modelといった名前のモデルたちに可能な限りロジックを移していく。
テスト内にてインスタンスを生成できるオブジェクトは、一般的にテストがしやすい。
そのため、上記の方法で比較的簡単にテスト可能なロジックを増やすことができるはずだ。

また、UIテストについても、いわゆるスモークテストの範囲程度を用意してやる分には充分コストに見合うリターンがあると考えられる。
一般に主導線の大規模な変更は少ないし、書く範囲を限定すれば、その分コストも下がるからだ。

まとめ

結局のところ、テストが僕達にもたらしてくれるものは安心感である。
その安心感は何にも替えがたく、また、手戻り率などの面で定量的なリターンももたらしてくれるだろう。
しかしながらやはり、iOSでのテストとの付き合いというのは、まだまだ考えなければならない部分も多く、自分でもまだ納得のいく答えまでたどり着けていないと考えている。
なので、なにかしらご意見があればご教授いただければなあと思ったりしております。

フロー効率とリソース効率について思うこと

最近、フロー効率、リソース効率という言葉をよく聞くようになってきた。
業界でどの程度流行っているのかは知らないが、少なくとも@i2key御大将の近くのコミュニティに属している関係で、僕は日常的によく聞く。
リソース効率に偏りがちなシステム開発の環境において、フロー効率という考え方が広まっていることは単純に良いことだと思う。

一方で、フロー効率がバズワード的になるにつれて、正しく理解されていないのではないかと思える声もしばしば聞くようになった。
曰く、「リソース効率は悪でフロー効率こそが目指すべき姿」であるとか、「フロー効率こそが価値を最大限に発揮できる方法」であるとかそういう声である。
これはリソース効率というコンセプトの理解として正しくない。

もっとも、これは僕の観測範囲での話なので、実際そう勘違いしている人は少ないのかもしれない。
しかし、もしかしたら多数いるそういう人達へ向けて、そうでなくとも一回自分の考えをまとめる意味も込めて、フロー効率とリソース効率について文章を書いてみる。

但し書き

  • 僕は学生時代の知識から、工程管理的なアプローチでフロー効率とリソース効率について書いていく
  • 僕はリーンやXPの専門家ではない。「こういう解釈もある」程度に思うのがおすすめ
  • フロー効率を銀の弾丸だと思っている人をメインの対象にしている。そうでない人には、焼き直しになって退屈かもしれない
  • この記事は、Recruit Engineers Advent Calendar 201710日目の記事である

フロー効率とリソース効率

まず、フロー効率とリソース効率という言葉についておさらいしてみたいと思う。
これは正直、前述の@i2keyさんのこの記事を読んでしまうのが一番はやいと思う。

簡単に一言で表すならば

フロー効率: それぞれのアイテムが最も効率よく工程を進むことにフィーチャーすること
リソース効率: それぞれのリソースが最大限稼働することにフィーチャーすること

と言える。

とどのつまりこの二つは、何の効率に焦点を当てて考えるかというコンセプトの違いを表す言葉である。

フロー効率は正義なのか

上記の@i2keyさんの記事を読めば、「フロー効率はとても良いものだ」という印象を持つ人が多いだろう。
それ自体は問題ないのだが、「良いものだ」と思いすぎてしまう場合これはまずい。
この記事はとてもいい記事なのだが、バイアスがかかりやすい箇所が多少ある。
例えば、こちらの図などは誤解を生みやすい。

https://cdn-ak.f.st-hatena.com/images/fotolife/i/i2key/20170514/20170514231345.jpg 引用: (http://i2key.hateblo.jp/entry/2017/05/15/082655)

この図だけを見た人は必ずこう思うはずである。
「全てのアイテムが同じ期間で完了するのなら、トータルのリードタイムが短くなるフロー効率の方が良いに決まっているじゃないか」と。
しかし、実際には多くの場合で、フロー効率の方が全てのアイテムが完了するタイミングは遅くなる。
フロー効率側は大体においてオーバーヘッドが発生するため、一つ一つのアイテムが完了するのにかかる時間が長くなってしまうからだ。
@i2keyさんがこの記事の内容を口頭で説明するときには補足してくれるのだが、記事内では充分ではない。

フロー効率に集中するということは、一部のリソースを遊ばせてでもあるアイテムのリードタイムを短くするということである。
そのため、リソース効率に比べオーバーヘッドが発生しやすい。 具体的な例を下に示す。

完了に1単位時間かかる行程が3つずつあるアイテムA・Bを、作業者A・Bの二人で完了させる場合を考える。
ただし、アイテムAの工程2は、同アイテムの工程1が完了しない限り取り掛かれないという制約があるものとする。
フロー効率、リソース効率、それぞれのアプローチで工程を組むと以下のようになる。

f:id:gaoxin-xixxix:20171206231634p:plain
フロー効率時

f:id:gaoxin-xixxix:20171206231843p:plain
リソース効率時

これを見ると、フロー効率の場合、アイテムAのリードタイムは短くなるが、全てのアイテムの完了が遅くなることがわかる。

フロー効率では、個々のアイテムのリードタイムは減るが、フローを優先するために、発揮できる総生産量が低くなってしまうのである。
一方リソース効率は、個々のアイテムのリードタイムは増えるが、極力リソースを遊ばせないため、発揮できる総生産量が高くなる。

最も、アイテムAの工程間に制約がなければフロー効率でも3単位時間で全てのアイテムは完了するのだが、実際の開発においては多くの場合こういった制約がついて回るはずだ。

このように、基本的には、組織の総生産量の観点で有利なのはリソース効率の方である。

ではフロー効率はいけないのか

上で、組織の総生産量の文脈ではリソース効率の方が優れているという話をした。
しかし、だからと言ってフロー効率がいけないというわけではない。
この文章の主旨はフロー効率disではなく、それぞれの特性を理解し、正しく使うことが大事だということである。

もう一度それぞれの特性をまとめてみよう。

フロー効率: あるアイテムのリードタイムを短くすることができるが、総生産量の面で不利
リソース効率: 組織の総生産量の最大化に有利であるが、個々のアイテムのリードタイムの面で不利

こうして見ると、総生産量とリードタイムのコンテクストにおいては、きれいにトレードオフになることがわかる。
では、それぞれはどのような場合に適材なのだろうか。

フロー効率は、任意のアイテムのリードタイムを短くできる。
ということは、仮説検証型の開発にもってこいと言える。
一回のリリースサイクルのリードタイムを短くすることで、仮説検証の試行回数を増やすことができるからだ。
イテレーティブな開発では、完了しなければならないアイテムの総数は一般的に決まっていないため、総生産量はある程度犠牲にしても良い。

リソース効率は、フィードバックを入れて方向転換することには弱いが、予め決められたアイテム群をより早く完了することができる。
よって、いわゆるプロジェクト型の開発に向いていると言える。
プロジェクト型の開発では、プロジェクト完了までアイテムが価値を発揮する必要がないため、個々のリードタイムの長さは問題にならない。

じゃあGoogleとかはどうなの?

こういう話をすると、フロー効率擁護のニュアンスで 「それでもGoogleは、週の20%をフリーにすることで生産性を上げた」や、「稼働率100%のCPUはまともに動かない」といった意見を言ってくる人がたまにいるが、これは詭弁ないしは勘違いである。
というのも、上記2例は本質的にフロー効率とリソース効率とは違う文脈の話だからだ。

(僕が考えるに)フロー効率とリソース効率という言葉は、工程設計の戦略を表す言葉である。
工程設計とはある種の制約充足問題であって、個々のリソースの稼働率は一定であるという前提が必要である。
そのため、フロー効率的なアプローチでも、リソースである人間は制約上可能な限り最大限に稼働することが前提となる。
100%の稼働による人間の生産性の低下は、議論の埒外の話なのである。

それはどちらかというと、リソースとしての人間の特性の話であり、これは人間工学などの方が対象分野としては近いだろう。
そういった点は、フロー効率だろうとリソース効率だろうと、一日の工数を低めに設定することで実現を目指すことができる。

まとめ

リソース効率は銀の弾丸ではなく、利点と欠点を持った一つのアプローチにすぎない。
トレードオフを理解し、適材適所で使う事が大事だ。
もちろん、理想的な状況は、リードタイムが短くかつ総生産量も損ねない状況であるため、フロー効率的な工程でリソースが遊ばないような工程設計ができるのが一番良い。
しかし、現実には制約があり、そのような理想的な工程設計は多くの場合できない。
そのため、自分たちが置かれた状況を理解し、より良い手段を選択しなければならないのである。

WKWebViewのログイン保持

iOSでWebページを表示するために使用するWKWebView
今回、WKWebViewでのログイン保持でハマったためワークアラウンドを書いておく

今回想定するアプリ

f:id:gaoxin-xixxix:20171004234507p:plain

一度webviewに行った後、webview内でログインを行う。 その後もう一度viewに戻り、再度webviewを開く。
この時、ログインが保持されていないという現象にあった。

どうしてこうなる

どうもセッションcookieをWKWebViewがストアしておかないことによるものらしい。

nsmutableurlrequest - Losing cookies in WKWebView - Stack Overflow

iOS WKWebView Tips | Professional Programmer

必要なcookieを調べて、WKWebViewから抜いて直接NSHTTPCookieStorageに突っ込めたりしないかと考えたが、そもそもWKWebViewからセッションcookieを抜くことができないっぽい。

解決策

解決策というか、ワークアラウンドなのだが、なんならバッドノウハウかもしれないので、ご意見があったらうかがいたいのだが。 今回は、上記二つ目のリンクでも述べられているWKProcessPoolを活用することにした。

WKProcessPoolとは、複数のwebview間でWeb Content Processを共有するためのものだ。

WKProcessPool - WebKit | Apple Developer Documentation

これを活用すると、cookieが複数のwebviewで(確かめてないから本当かわからないが)リアルタイムに共有されるらしい。
が、リアルタイムというのは今回はあまり重要ではない。
今回重要なのはWeb Content Processが共有されるという点だ。
つまり、同じWKProcessPoolを使うwebview間であれば、その存在が時系列上ずれていたとしてもセッションcookieを共有できるはずと考えた。
なので

extension WKProcessPool {  
    static let shared = WKProcessPool() 
}  
  
class ViewController: UIViewController {  
    override func viewDidLoad() {  
        let configuration = WKWebViewConfiguration()  
        configuration.processPool = WKProcessPool.shared
        let webview = WKWebView(frame: .zero, configuration: configuration)  
#中略  
    }  
}  

というようにしてやると良い。
複数のViewControllerで使わないのであれば、WKProcessPoolはシングルトンっぽくしなくても、propertyとしてもってやればいいだろう。

ここで注意しなければいけないのは、WKWebViewConfigurationはWKWebViewのinitializerに渡してやらなければならないという点だ。

let webview = WKWebView()
webview.configuration.processPool = WKProcessPool.shared

のようにやった場合はうまくいかなかった。
今度時間がある時にWKWebViewのコードを呼んで理由を探そうと思う。

RxWebKitがいい感じだったよ

RxSwiftの関連ライブラリーを調べていたところRxWebKitなるものを発見。

github.com

要はWebKitのRxSwiftラッパーです。
いい感じにWKWebViewを扱えてとても楽しい! 結構素敵な感じに使えたので、シンプルなサンプルアプリを作ってみます。

導入

まずは普通に

pod 'RxWebKit'

ってな感じにしてpod install。 Carthageでもできますが、例のごとく割愛いたします。

普通にViewを構築

WKWebViewはコードで追加するので、タイトルやロードビュー、進む戻るボタンなどをテキトーに配置します。 f:id:gaoxin-xixxix:20170531215605p:plain

IBOutletでViewControllerに引っ張ってきます。

import UIKit
import WebKit

class ViewController: UIViewController {
    @IBOutlet weak var progressbar: UIProgressView!
    @IBOutlet weak var refreshButton: UIBarButtonItem!
    @IBOutlet weak var forwardButton: UIBarButtonItem!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var backButton: UIBarButtonItem!
    @IBOutlet weak var toolbar: UIToolbar!
    @IBOutlet weak var containerView: UIView!
}

次に、viewDidLoad内で、WKWebViewを追加します。 navigationDelegateはぶっちゃけ今回いりませんが書いちゃってます。

setUpConstraints(for webView: WKWebView )では、WKWebViewをcontainerViewにぴったり貼り付ける制約を加えています。 webview.loadで、githubをロードし始めます。

override func viewDidLoad() {
        super.viewDidLoad()
        
        // set up WKWebView
        let webview = WKWebView()
        webview.navigationDelegate = self
        webview.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(webview)
        setUpConstraints(for: webview)

        // load github.com
        let request = URLRequest(url: URL(string: "https://www.github.com")!)
        webview.load(request)
}

private func setUpConstraints(for webview: WKWebView) {
        webview.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0).isActive = true
        webview.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0).isActive = true
        webview.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0).isActive = true
        webview.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0).isActive = true
}

これでViewパーツの配置は終わりです。

webviewのプロパティをバインディングしていく

ここからRxWebKitを活用する作業に入っていきます。

webview.rx

からWKWebViewに拡張されたRX系のプロパティにアクセスできます。

まずは、RxSwift、RxWebKitをimportします。

import RxWebKit
import RxSwift

そして、webviewのtitleをtitleLabelのtextにbindします。 こうすることで、webviewが遷移する度に表示されているページのタイトルがtitleLabelに自動的に反映されます。 driveを使うか迷ったのですが、どっちが良いんでしょうねこれ。

// bind title to title label
webview.rx.title.bind(to: titleLabel.rx.text).addDisposableTo(disposeBag)

次に、進む戻るボタンのenabledを制御します。
WKWebViewには、canGoForwardとcanGoBackというプロパティがあり、それぞれ、進めるページがあるか、戻れるページがあるかを取得できます。
丁度ブラウザの進む戻るボタンと同じです。
これもRX化されているので、こちらで用意した進む戻るボタンのisEnabledにbindします。(こちらはdriveを使いました)

ただし、UIBarButtonのisEnabledはRxのobserverが用意されていません。
なので、自分で実装することになります。
今回は、RxSwift公式のサンプルに倣い、BindingExtension.swiftというファイルを作って実装しました。

import UIKit
import RxSwift
import RxCocoa

extension Reactive where Base: UIBarButtonItem {
    var isEnabled: UIBindingObserver<Base, Bool> {
        return UIBindingObserver(UIElement: base) { button, isEnable in
            button.isEnabled = isEnable
        }
    }
}

こんな具合に、送られてきたBoolをUIBarButtonItemのisEnableに対応させるようなObserverを作ります。 これに、先程のcanGoForwardとcanGorBackをbindすることで、自動的にUIBarButtonItemが有効化、無効化するようになります。

// handle back and forward buttons availablity
webview.rx.canGoForward.asDriver(onErrorJustReturn: false).drive(forwardButton.rx.isEnabled).addDisposableTo(disposeBag)
webview.rx.canGoBack.asDriver(onErrorJustReturn: false).drive(backButton.rx.isEnabled).addDisposableTo(disposeBag)

実際にこれらがタップされたときの処理と、 リフレッシュボタンがタップされたときの処理は、RxWebKitの機能を用いていないので割愛します。

最後に、プログレスバーの処理を設定します。
RxWebKitではページの読み込みに関係するプロパティとして、loadingプロパティとestimatedProgressが存在します。
前者は現在読み込みを行っているかを表しており、
後者は読み込みがどの程度完了しているか推定値で表しています。

プログレスバーにはUIProgressViewを用い、loadingプロパティを表示非表示に、estimatedProgressを進行度合いにbindしました。 なお、UIProgressViewのisHiddenと進行度合いを表すprogressはRX対応されていないので、またも自作observerを実装します。

先程のBindingExtensionに下記を追加します。 isProgressingは進行中かどうかisHiddenは消えているかを表しているため、 notを入れることでちょうど進行中に表示されるようになります。

extension Reactive where Base: UIProgressView {
    var progress: UIBindingObserver<Base, Float> {
        return UIBindingObserver(UIElement: base) { progressbar, progressRate in
            progressbar.progress = progressRate
        }
    }
    
    var isProgressing: UIBindingObserver<Base, Bool> {
        return UIBindingObserver(UIElement: base) { progressbar, isProgressing in
            progressbar.isHidden = !isProgressing
        }
    }
}

これらに前述のプロパティをbindします。

コメントの下の行では、WKWebViewより前に来るように、bringSubViewで最前線に持ってきています。

// set up progressbar
containerView.bringSubview(toFront: progressbar)
webview.rx.loading.asDriver(onErrorJustReturn: false).drive(progressbar.rx.isProgressing).addDisposableTo(disposeBag)
webview.rx.estimatedProgress.map { progress in
     return Float(progress)
}.asDriver(onErrorJustReturn: 0.0).drive(progressbar.rx.progress).addDisposableTo(disposeBag)

ここで気をつけなければいけないのはestimatedProgressのbindです。 estimatedProgressはDouble型、UIProgressViewのprogressはFloat型なので、一度mapを噛ませるなりして型を揃えてやる必要があります。

以上で必要な処理は全てです。

結果

f:id:gaoxin-xixxix:20170531224656g:plain

すっきりとしたコードでこんな感じのwebviewが実装できちゃいます!素敵!!

今回のサンプルのコードです。

github.com

Moyaでお手軽API通信

ブログ始めてみたんですけど、特に抱負も何もないので、しれっとMoyaで遊んだ事について書きます。

Moyaって?

github.com

APIとか叩く時に作られがちな、"APIManager"とか"NetworkModel"みたいな、 ネットワークを抽象化するレイヤーの役割をしてくれるライブラリ。 内部的にはAlamofireを叩いていて、言うならAlamofireのラッパーとも言えるかもしれない(言えないかもしれない)。

何が便利?

公式が言うには

  • 正しいAPIのエンドポイントにアクセスしてるかコンパイル時にチェックできる
  • enumの連想値使って異なるAPIの使い方をちゃんと定義できる
  • testする時stub的な役割を果たせる

個人的に思ったのは

  • siestaとかよりhttp通信の層をよく隠してくれるので楽
  • 仕組みが割とわかりやすいので使う上でリーズナブル

といったところでしょうか。

使い方

導入

Podfileに

pod 'Moya'

を記入し、pod installで簡単にインストールできます。 なお、Moyaを入れるとAlamofireも一緒に入るので別途記述する必要はありません。 ちなみにSwift Package ManagerやCarthageでも導入可能です。

今回は、 github.com を使って基本的な使い方をなぞってみます。 MoyaのレポジトリにあるBasicUsageとほぼ同じ内容です。

enumを作成

Moyaでは、自分が使いたいAPIの情報をenumに持たせます。 まずは、それぞれのAPIターゲットをenumとして列挙します。 また、それぞれのターゲットに対し渡すパラメータがある場合は、連想値でこれを定義します。

import Moya

enum HackersNewsAPI {
    case item(id: Int)
    case user(id: String)
    case maxItem
    case topStories
    case newStories
}

また、このenumTargetTypeプロトコルに準拠する必要があります。 具体的には

  • var baseURL: URL
  • var path: String
  • var method: Moya.Method
  • var parameters: [String: Any]
  • var parameterEncoding: ParameterEncoding
  • var sampleData: Data
  • var task: Task

のプロパティを設定しなければなりません。 今回は以下のように準拠させます。

extension HackersNewsAPI: TargetType {
    // ベースURL
    var baseURL: URL { 
          return URL(string: "https://hacker-news.firebaseio.com/v0")! 
    }
    
    // それぞれのターゲットごとのpath
    var path: String {
        switch self {
        case .item(let id), .user(let id):
            return "/\(id).json"
        case .maxItem:
            return "/maxitem.json"
        case .newStories:
            return "/newstories.json"
        case .topStories:
            return "/topstories.json"
        }
    }
    
    // それぞれのターゲットごとのhttpメソッド
    // 今回はgetしかないが、postなどある場合はswitch selfなどで適切な値を返す
    var method: Moya.Method {
        switch  self {
        case .item, .user, .maxItem, .newStories, .topStories:
            return .get
        }
    }

    // 今回はパラメーターとして渡さないのですべてnil
    var parameters: [String: Any]? {
        return nil
    }
    
    // パラメーターのエンコーディング指定
    // リクエストボデイにjsonとしてセットすることもできる
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    
    // テスト時に使われる(なんでマストなのかわからない)
    // めんどくさかったので今回はずるします
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch  self {
        case .item, .user, .maxItem, .newStories, .topStories:
            return .request
        }
    }
}

以上でenumの準備は完了です。

ここまで書いて、題材にしたAPIがあんま良くなかったことに気づく。 が、面倒くさいので強行します。

MoyaProviderを使ってAPIリクエストする

enumの準備が整ったら、MoyaProviderを生成し、リクエストを送ることができます。

let provider = MoyaProvider<HackersNewsAPI>()
provider.request(.newStories) { result in
    
}

provider.requestの第二引数となっているクロージャが受け取っているresultは、 .success(Moya.Response).failure(MoyaError)を持つenumです。 なので基本的には

provider.request(.newStories) { result in
    switch result {
    case let .success(moyaResponse):
        do {
            try moyaResponse.filterSuccessfulStatusCodes()
            let data = try moyaResponse.mapJSON()
            // do something with the data
        }
        catch {
           // failed to convert to JSON
        }
    case let .failure(error):
           // failed at api access
    }
            
}

のようにしてエラー検知すると良いと思います。

filterSuccessfulStatusCodesは、ステータスが200~299でなければ例外を投げるメソッド。 mapJSONは名前の通りデータをJSONマッピングしてくれるメソッドです。

どちらもMoya.Responseに組み込まれたメソッドです。

まとめ

今回は本当に触りしかやりませんでしたが、Moyaは、APIリクエストの際に挟み込めるクロージャーや、 テストを意識した機能、RX対応など、まだまだ魅力的な機能を持っています。 特にRX対応は割とAPIもイケてると思うので是非触ってみてもらえたらなと思います。

一応今回のソース

github.com