※注意
本記事は Xcode 11.0 GM Seed の環境で動作確認したものです。正式版では動作が変わる可能性もあります。~
(うまく動作しない場合は、シュミレータやXcodeを再起動したりするとうまくいきます。)
前書き
過去に以下のような記事を上げていましたが、そのCombine版になります。
そのまんま置き換えというのは、厳しい部分がありましたがなんとか同じような書き方に集約しております。
Combineについては【iOS】Combineフレームワークまとめを見ていただくのが早いかと思います。
導入
1.環境
- MacOS Catalina 10.15 Beta 8
- Xcode 11.0 GM Seed (Swift 5)
- Alamofire (手軽に試すならPodがおすすめ)
をインストールしてください。
余談(Alamofireをインストールしていることに関して)
URLSession
使うのにAlamofire
を入れるんかい?という疑問が来そうなので、記載しておきます。
①当初はAlamofireをラップしたものでやろうとしていたが、URLSessionに用意されているDataTaskPublisherを用いたほうが綺麗だと気づいた
②基底クラスの作成において、Alamofireで用意されているtypealias、メソッド、エンコード部分が自前で作るより優れていたので採用したかった
(そのうちAlamofire本家が対応してくれるのではと思っている笑)
2.ネットワークプロトコルの作成
こちらに関しては、上記でもあげた以下
に全く同じ記載があるので省略します。
3.リクエスト・レスポンスの作成
こちらも先の記事内に同様の記載あるので、必要ない方は読み飛ばして大丈夫です。レスポンスモデルが違うので一応記述しています。
①レスポンスの作成
例として、APIを叩くと以下のレスポンス(Json)が返ってくるとする。
{
"data": [
{
"name": "Bob",
"id": 0
},
{
"name": "Sam",
"id": 1
}
]
}
上記のJsonからレスポンスを作成。
// MARK: - Response
struct UserResponse: Codable {
var data: [UserModel]
}
struct UserModel: Codable {
let name: String
let id: Int
}
②リクエストの作成
[2]で作成したプロトコルに適応するリクエストになるよう作成。
import Alamofire
// MARK: - Request
struct UserRequest: BaseRequestProtocol {
typealias ResponseType = UserResponse
var method: HTTPMethod {
return .get
}
var path: String {
return "/xxx/yyy/zzz" // ※必要に応じて各自でセットしてください
}
var parameters: Parameters? {
return nil
}
}
余談
こちらのMyjsonというとサービスを使うと、指定したjson
をhttps
で返してくれるURLを発行してくれるので、サクッと試したい方はお使いください。
サービスが終了しました。json-server などで代用しましょう。
4.ネットワーククライアントを作成
①DataTaskPublisherに関して
URLSession
に用意されたDataTaskPublisher
を用いることで比較的きれいに書くことができます。
URLSession.shared.dataTaskPublisher(for: /* URLRequest or URL */)
以下の記事を見ていただけるとわかるかと思いますが、
自前で作るとなると
- Subscriber
- Subscription
とかを意識する必要があり、結構めんどくさいです。
DataTaskPublisher
でその辺をまるっと考えなくて済むのはありがたい感じです。
(細かいところ処理わけしたいとかある場合は、結局自前でやることになるかと思います)
②コード
エラーの種類を先に定義しておきます。各自の状況に応じて拡張してください。
// MARK: - Error
enum NetworkError: Error {
case networkError(code: Int, description: String)
case decodeError(reason: String)
case irregularError(info: String)
}
実際に作成したコードは以下になります。
import Alamofire
import Combine
// MARK: - NetworkPublisher
struct NetworkPublisher {
// MARK: Variables
private static let successRange = 200..<300
private static let retryCount: Int = 1
private static let decorder: JSONDecoder = {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
return jsonDecoder
}()
// MARK: Methods
static func publish<T, V>(_ request: T) -> AnyPublisher<V, NetworkError>
where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
return URLSession.shared
.dataTaskPublisher(for: try! request.asURLRequest()) // 絶対に落ちない保証があるので`try!`
.timeout(.seconds(20), scheduler: DispatchQueue.main)
.retry(retryCount)
.validate(statusCode: successRange) // ※[5.オプション]で後述
.decode(type: V.self, decoder: decorder)
.mapDecodeError() // ※[5.オプション]で後述
.eraseToAnyPublisher()
}
}
備考
メソッドの説明を記載しておきます。
-
.timeout(Scheduler, scheduler: Queue)
: タイムアウトの設定 -
.retry()
: 名前の通り失敗したときのリトライ回数を指定 -
.validate(statusCode: Range)
: ステータスコードの判定。独自関数につき後述 -
.decode(type: Class, decoder: JSONDecoder)
: 指定した型にデコード -
.mapDecodeError()
: デコード失敗時のエラー変換。独自関数につき後述 -
.eraseToAnyPublisher()
: 型消し(Combineではお作法みたいなもの)
③使用例
実際に使用した際の記述例になります。
var cancellables: Set<AnyCancellable> = []
func publish() {
let request = UserRequest()
NetworkPublisher.publish(request)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { result in
switch result {
case .finished: print("finished")
case .failure(let e): print("failure", e) // failure
}
}, receiveValue: { value in
print("receiveValue", value) // success
}).store(in: &cancellables)
}
deinit {
cancellables.forEach { $0.cancel() }
}
実行結果(成功時)
receiveValue [UserModel(name: "Bob", id: 0), UserModel(name: "Sam", id: 1)]
finished
備考
① sinkに関して
成功時でもreceiveValueで値を受け取った後に、receiveCompletionのfinishedに入ってくる点に注意。
流れが逆になってる感じが若干違和感ですが、プログレスを消す処理などは成功・失敗共にここで一元管理できそう。
② RxSwiftで考えた時
-
.receive(on: )
→ .observeOn() -
.sink()
→ .subscribe() -
.store(in: )
→ .disposed(by: ) -
Set<AnyCancellable>
→ Disposebag
だと思ってみると上記のコードもスーッと入ってこないでしょうか?
やっていることは同じです。(内部的には違うでしょうが笑)
④拡張(RxSwiftぽく使うために)
上記でも記載したが、値を受け取った後に、receiveCompletionのfinishedに入ってくるのが気持ち悪いので、
RxSwiftっぽく使えるようにもう一段階ラップしたものも用意してみた
static func sink<T, V>(_ request: T, _ scheduler: DispatchQueue = DispatchQueue.main,
success: @escaping (V) -> Void,
failure: @escaping (Error) -> Void,
completion: @escaping () -> Void = {}) -> AnyCancellable
where T : BaseRequestProtocol, V == T.ResponseType, T.ResponseType : Codable {
publish(request)
.receive(on: scheduler)
.sink(receiveCompletion: { result in
switch result {
case .finished: break
case .failure(let e): failure(e)
}
completion()
}, receiveValue: success)
}
使用例
~省略~
let request = UserRequest()
NetworkPublisher.sink(request, success: { value in
print("success", value)
}, failure: { error in
print("failure", error)
}).store(in: &cancellables)
~省略~
こちらのほうがより直感的で、かつ必要に応じてcompletionのクロージャをセットすれば良い仕組みになっている。
(あくまでも個人的な好み)
5.オプション
[4]のネットワーククライアント内で説明していなかったものになります。
実際はなくても動作しますが、あったほうが良いと思い作成。
①DataTaskPublisherを拡張
StatusCodeのバリデーションに対応
Alamofireに用意されているValidateのようなことがしたかったので作成。
extension Publisher {
func validate<S>(statusCode range: S) -> Publishers.TryMap<Self, Data>
where S:Sequence, S.Iterator.Element == Int {
tryMap {
guard let output = $0 as? (Data, HTTPURLResponse) else {
throw NetworkError.irregularError(info: "irregular error")
}
guard range.contains(output.1.statusCode) else {
throw NetworkError.networkError(code: output.1.statusCode, description: "out of statusCode range")
}
return output.0
}
}
}
また、特定のエラーコードの場合は特定の処理をしたい
ex
- 強制アプデのアラートが出る
- 再ログインのために、ルート画面をスプラッシュやログイン画面に入れ替える
みたいなことは、こちらで一元管理することもできます。
####Decodeのエラーに対応
- デコードに失敗した場合
- エラーだけど成功として違うレスポンスが返ってくる場合
などのハンドリングに対応できます。
extension Publisher {
func mapDecodeError() -> Publishers.MapError<Self, NetworkError> {
mapError {
switch $0 as? DecodingError {
case .keyNotFound(_, let context):
return .decodeError(reason: context.debugDescription)
default:
return .decodeError(reason: $0.localizedDescription)
}
}
}
}
DecodingError
はいくつか種類があるので、分岐を細かく設定する場合は各自で設定してください。
後書き
RxSwift導入した頃は、なんやこれ、、、と思っていたが、、、
いざCombineに移行しようとしたときに、その便利さにあらために気づかされたりしました笑
とはいえ、公式でサポートしてくれるのはありがたいことであり、
RxSwiftはライブラリとしても地味に大きかったりするので(セットアップとかアプデ時間かかるよね問題)
その辺は移行して解消できたらなぁとは思いつつ。
コードの指摘等あればお願いしますmm
追記1
リポジトリではAPIのURL(エンドポイント)が古くなっているので、自前で返ってくるものを設定してください。
(それにあわせてレスポンスの型も修正してください)
追記2
続編を書きました