※注意
本記事は Xcode 13.1 beta の環境で動作確認したものです。正式版では動作が変わる可能性もあります。
前書き
過去に以下のような記事を上げていましたが、その async/await 版になります。
- [Swift] ObjectMapper+RxSwiftを実装した備忘録
- [Swift] Alamofire+RxSwift+CodableでAPIクライアントを作る
- [Swift] URLSession+Combine+CodableでAPIクライアントを作る
そのまんま置き換えというのは、厳しい部分がありましたがなんとか同じような書き方に集約しております。
導入
1.環境
- MacOS Big Sur
- Xcode 13 Beta1
エンコードの実装を考えたくないので、Alamofire
の機能を使用します。(個々に入れてください)
また、入れずにエンコードを自前で実装しても構いません。
Carthageのエラー
Xcode13 beta1 でCarthageエラーが起こる場合は、回避するスクリプト(下記carthage.sh
)で対応できます。
2.ネットワークプロトコルの作成
API ごとのリクエストを作成、雛形 Protocol
を作成します。
こちらに関しては、上記でもあげた以下
にほぼ同じ記載があるので、解説を読みたい方はこちらを読んでください。
ここではコードのみ置いておきます。
- 基底のプロトコル
protocol BaseAPIProtocol {
associatedtype ResponseType
var method: HTTPMethod { get }
var baseURL: URL { get }
var path: String { get }
var headers: [String : String]? { get }
}
extension BaseAPIProtocol {
var baseURL: URL {
return try! "http://localhost:3000".asURL() // エンドポイント例
}
var headers: [String : String]? { // ヘッダー例
return [
"Content-Type": "application/json; charset=utf-8",
"User-Agent": ""
]
}
}
- リクエストのプロトコル
protocol BaseRequestProtocol: BaseAPIProtocol, URLRequestConvertible {
var parameters: Parameters? { get }
var encoding: URLEncoding { get }
}
extension BaseRequestProtocol {
var encoding: URLEncoding {
return URLEncoding.default
}
func asURLRequest() throws -> URLRequest {
var urlRequest = URLRequest(url: baseURL.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue
urlRequest.allHTTPHeaderFields = headers
urlRequest.timeoutInterval = TimeInterval(30)
if let params = parameters {
urlRequest = try encoding.encode(urlRequest, with: params)
}
return urlRequest
}
}
3.リクエスト・レスポンスの作成
API ごとのリクエスト・レスポンスを作成します。
こちらに関しては、上記でもあげた以下
に全く同じ記載があるので、解説を読みたい方はこちらを読んでください。
4.ネットワーククライアントの作成
実際に作成したコードになります。
(以下、@available(iOS 15, *)
は全て省略しています。)
struct APICliant {
// MARK: Variables
private static let successRange = 200..<300
private static let decoder: JSONDecoder = {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
return jsonDecoder
}()
// MARK: Method
// APIの呼び出し
static func call<T, V>(_ request: T) async throws -> V
where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
let result = try await URLSession.shared.data(for: request.asURLRequest())
let data = try validate(data: result.0, response: result.1)
return try decoder.decode(V.self, from: data)
}
// ネットワークの精査
static func validate(data: Data, response: URLResponse) throws -> Data {
guard let code = (response as? HTTPURLResponse)?.statusCode else {
throw NSError(domain: String(data: data, encoding: .utf8) ?? "Network Error", code: 0)
}
guard successRange.contains(code) else {
throw NSError(domain: "out of statusCode range", code: code)
}
return data
}
}
ステータスコードをより詳細にハンドリングしたいのであれば、分岐してあげると良いでしょう。(例として)
static func validateCode(data: Data, response: URLResponse) throws -> Data {
switch (response as? HTTPURLResponse)?.statusCode {
case .some(let code) where code == 401:
throw NSError(domain: "Unauthorized", code: code)
case .some(let code) where code == 404:
throw NSError(domain: "Not Found", code: code)
case .none:
throw NSError(domain: "Irregular Error", code: 0)
case .some:
return data
}
}
5. 使用例
とてもスッキリしています。
Task {
let request = UserRequest()
let response = try await APICliant.call(request)
debugPrint(response)
}
await
のメソッドを呼び出すためには、async
で明示的に囲う必要があります。
他にも asyncDetached
、detach
などがあります。
await
のメソッドを呼び出すためには、Task
で明示的に囲う必要があります。
今回は説明しませんが、この辺を読んでおくと理解が深まると思います。
実用例
具体的に2つ出してみます。
1. 実装
① SwiftUI の実装例
SwiftUI
での簡単な呼び出し例を紹介します。
- UseCase
まずは UseCase
を作ります。
final class APIUseCase {
@Published var users = [UserModel]()
func fetch() async {
do {
let request = UserRequest()
let response = try await APICliant.call(request)
users = response.data
} catch let error {
debugPrint(error.localizedDescription)
}
}
}
メソッドの後ろに async
を付けたものでラップしています。
結果を @Published
なプロパティでバインディングします。
(UseCase
という名前ですが、ViewModel
と同じだと思っていただいて構いません。)
- View
struct ContentView: View {
private var useCase = APIUseCase()
var body: some View {
EmptyView()
.task {
await useCase.fetch()
}
.onReceive(useCase.$users) { users in
print("users: ", users)
}
}
}
iOS15 から使えるようになった task
を使用することで async
で囲う必要がなくなります。
以下の説明にある通り、画面が表示された際に実行されます。
(引用:Developer - task(_:))
② Protocol で縛った実装例
@Published
を使用する場合、Protocol
で縛れないので Combine
を使っていきます。
- Inputs/Outputs
MVVM
を実装する際に良く見られるパターンですが、入力と出力をProtocol
で縛っていきます。
// 入力と出力の制限
protocol APIUseCasable {
var inputs: APIUseCaseInputs { get }
var outputs: APIUseCaseOutputs { get }
}
// 入力 ( View → UseCase )
protocol APIUseCaseInputs {
func fetch() async
}
// 出力 ( UseCase → View )
protocol APIUseCaseOutputs {
var users: PassthroughSubject<[UserModel], Never> { get set }
}
- UseCase
作ったものを適応した UseCase
を作成します。
final class APIUseCase: APIUseCasable, APIUseCaseInputs, APIUseCaseOutputs {
// MARK: APIUseCasable
var inputs: APIUseCaseInputs { self }
var outputs: APIUseCaseOutputs { self }
// MARK: APIUseCaseOutputs
var users = PassthroughSubject<[UserModel], Never>()
// MARK: APIUseCaseInputs
func fetch() async {
do {
let request = UserRequest()
let response = try await APICliant.call(request)
users.send(response.data)
} catch let error {
debugPrint(error.localizedDescription)
}
}
}
inputs
とoutputs
を通してのみ、メソッドの実行や値のやりとりを行うことが保証されます。
また、Combine
の PassthroughSubject
を使用して、View
側に値を返します。
- View
struct ContentView: View {
private var useCase: APIUseCasable = APIUseCase()
var body: some View {
EmptyView()
.task {
await useCase.inputs.fetch()
}
.onReceive(useCase.outputs.users) { users in
print("users: ", users)
}
}
}
UseCase
プロパティを Protocol
(APIUseCasable) で定義することで縛ります。
実行では inputs
を通し、値を受け取る側では outputs
を通しています。
この方法だと、UseCase
はそのまま UIKit
でも使うことができるメリットもあります。
(ただし、SwiftUI
特有の Binding
が必要な場合は、onReceive
で受け取ってからView
のプロパティに再度バインディングさせるといったひと工夫が必要になります)
あとがき
Combine
が出てきたと思えば、あっという間に新しい技術が出てきてしまいました。
(議論は前からされていたけど...)
まだ、見切れていない部分もあるので、今回記載したものよりもっと良い実装ができるかもしれません。
コードの指摘等あればお願いしますmm
動作するリポジトリを置いておきます