はじめに
アプリ内に画像のアップロード機能を実装したことがあるでしょうか?
だいぶ前に書いた「Alamofire+RxSwift+CodableでAPIクライアントを作る」において、
アップロードのクライアントに関しては、触れただけで説明していませんでした。
というわけで、実際に作る必要に迫られたので、せっかくなので延長としてここにまとめておきます。
導入
1. 準備
「Alamofire+RxSwift+CodableでAPIクライアントを作る」の記事内で行なっている
1. ライブラリのインストール
2. ネットワークプロトコルの作成
上記までを必ず行なってください。
(次の手順と続いているため)
2. Upload Request用のProtocolを作成
**「1. 準備」**が完了していれば、BaseRequestProtocol
が作成されているはずです。
このプロトコルを元にアップロード用のプロトコル(BaseUploadProtocol
)を作ります。
今回はアップロード成功時にも特定にレスポンスを受け取れる想定で作っていきます。
(ない場合は空のレスポンスクラスを作れば良いので大丈夫です。)
protocol BaseUploadProtocol: BaseRequestProtocol {
var contentType: String { get }
var boundary: String { get }
func encoded(data: MultipartFormData)
}
Alamofireでは画像を扱う際のメソッドがいくつかサポートされていますが、今回はMultipartFormData
で対応します。
(これが一番面倒な実装なので、これができれば他のはもっと簡単にできるかと思います)
extension BaseUploadProtocol {
// MultipartFormDataにセットするためのもの
var contentType: String {
return "multipart/form-data;"
}
// MultipartFormDataにセットするためのもの
var boundary: String {
return ""
}
// `BaseRequestProtocol`内の`URLRequestConvertible`からこれを追加
func asURLRequest() throws -> URLRequest {
var urlRequest = URLRequest(url: baseURL.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue
urlRequest.allHTTPHeaderFields = headers
urlRequest.timeoutInterval = TimeInterval(20)
return urlRequest
}
}
上記は、サーバーの設定によって少し変わるかと思うので、臨機応変に設定を変えていただきたいです。
3.Request/Responseの作成
先に述べたようにサーバーから何も返ってこない実装が多いかと思いますが、、、
今回は、簡易な例として以下のレスポンスが返ってくることを想定します。
{
"message": "success"
"result": true
}
① Responseの作成
import Alamofire
struct UploadResponse: Codable {
let message: String
let result: Bool
}
②Requestの作成
先ほど作成したBaseUploadProtocol
に準拠させます。
import Alamofire
struct UploadRequest: BaseUploadProtocol {
typealias ResponseType = UploadResponse
// 画像データをData型として渡す
private let imageData: Data
init(imageData: Data) {
self.imageData = imageData
}
var method: HTTPMethod {
return .post
}
var path: String {
return "xxx/xxx"
}
var parameters: Parameters? {
return nil
}
// 必要な形にエンコードする
func encoded(data: MultipartFormData) {
// 以下の設定はあくまでも実装例なので、各自の実装に合わせてください。
data.contentType = contentType
data.boundary = boundary
data.append(
imageData,
withName: "image",
fileName: "image.jpg",
mimeType: "image/jpeg"
)
}
}
画像データはそのまま送れないので、
.jpegData(compressionQuality:)
.pngData()
などを使ってData型に変換しましょう。
また、画像に付与されるパラメータは各自の実装で変わってくるので各自の設定に変更してください。
append
の中身に関しては、以下の解説を参考にすると良いでしょう。
4.Network Cliantの作成
Alamofireを使ったアップロードでは2つの段階を踏むことになります。
① エンコードの成功・失敗
② 通信の成功・失敗
では、1つずつ進めていきます。
先にAPICliant
クラスを作成しておきましょう。
また、結果をsuccess/failureで分岐するために以下を作成しておきます。
enum APIResult {
case success(Codable)
case failure(Error)
}
では、クライアントを作っていきます。
① Alamofire呼び出し部分の作成
Alamofireのアップロードメソッドを呼び出すメソッドを作成します。
// Alamofire内のSessionManagerクラス
typealias EncodingResult = SessionManager.MultipartFormDataEncodingResult
static func requestUpload<T>(
_ request: T,
completion: @escaping (EncodingResult) -> Void
) where T: BaseUploadProtocol {
Alamofire.upload(
multipartFormData: request.encoded,
with: request,
encodingCompletion: completion
)
}
MultipartFormDataの型が長いのでtypealiasで置いていますが、置かなくても大丈夫です。
先ほど作成した、request内のencodedメソッドはこちらで使用されます。
② アップロード部分の作成
通信をバリデーションするコードを最初に作成します。
static func validate(_ request: DataRequest) -> DataRequest {
return request
.validate(statusCode: 200..<400)
.validate(contentType: ["application/json"])
}
こちらは、画像ではない通常のAPIと共通化して使用できるので、切り出しておく方が使いがってがよくおすすめです。
続いて、アップロードのコードです。
static let queue = DispatchQueue(label: "queue.APICliant", attributes: .concurrent)
static func callForUpload<T, V>(
_ request: T,
_ upload: UploadRequest,
completion: @escaping (APIResult) -> Void
) where T: BaseUploadProtocol, T.ResponseType == V {
_ = validate(upload).responseData(queue: queue) { res in
switch res.result.flatMap(request.decode) {
case let .success(data):
completion(.success(data))
case let .failure(error):
completion(.failure(error))
}
}
}
アップロードの通信と、その後の結果をデコードしています。
queueは指定しなくても動作しますが、コントロールしたい場合は設定しておくと良いでしょう。
(しておいた方が、複数の通信が走った場合は順番に流すことができる)
③ エンコード呼び出し部分の作成
static func callForEncode<T, V>(
_ request: T,
completion: @escaping (APIResult) -> Void
) where T: BaseUploadProtocol, T.ResponseType == V {
requestUpload(request) { result in
switch result {
case let .success(rq, _, _): // もし読み込みプログレスを出すならこの辺を修正する必要あり
callForUpload(request, rq, completion: completion)
case let .failure(errer):
completion(.failure(errer))
}
}
}
アップロードの通信前に、画像データのエンコードが問題ないか確認する必要があります。
ここではそれを行なっており、成功した場合のみアップロードの通信を行なっています。
コメントにもありますが、アップロード進行中のプログレスを出す場合は、alamofireから取得可能なので、そこで実装しましょう。
④ Rxで呼び出す部分
static func observeUpload<T, V>(_ request: T) -> Single<V>
where T: BaseUploadProtocol, T.ResponseType == V {
return Single<V>.create { observer in
callForEncode(request) { response in
switch response {
case let .success(result):
observer(.success(result as! V)) // responseがsuccessの段階で型が決まっており問題ないので強制キャストしています。
case let .failure(errer):
observer(.error(errer))
}
}
return Disposables.create()
}
}
Rxになるようcreateしています。
まとめ
上記で1つずつ分割して作成したコードを、1つのクライアントコードとして列挙しました。
struct APICliant {
// MARK: Typealias
typealias EncodingResult = SessionManager.MultipartFormDataEncodingResult
// MARK: Static Variables
private static let successRange = 200..<400
private static let contentType = ["application/json"]
private static let queue = DispatchQueue(label: "queue.APICliant", attributes: .concurrent)
// MARK: Static Methods
static func observeUpload<T, V>(_ request: T) -> Single<V>
where T: BaseUploadProtocol, T.ResponseType == V {
return Single<V>.create { observer in
callForEncode(request) { response in
switch response {
case let .success(result):
observer(.success(result as! V))
case let .failure(errer):
observer(.error(errer))
}
}
return Disposables.create()
}
}
private static func callForEncode<T, V>(
_ request: T,
completion: @escaping (APIResult) -> Void
) where T: BaseUploadProtocol, T.ResponseType == V {
requestUpload(request) { result in
switch result {
case let .success(rq, _, _):
callForUpload(request, rq, completion: completion)
case let .failure(errer):
completion(.failure(errer))
}
}
}
private static func callForUpload<T, V>(
_ request: T,
_ upload: UploadRequest,
completion: @escaping (APIResult) -> Void
) where T: BaseUploadProtocol, T.ResponseType == V {
_ = validate(upload).responseData(queue: queue) { res in
switch res.result.flatMap(request.decode) {
case let .success(data):
completion(.success(data))
case let .failure(error):
completion(.failure(error))
}
}
}
private static func requestUpload<T>(
_ request: T,
completion: @escaping (EncodingResult) -> Void
) where T: BaseUploadProtocol {
Alamofire.upload(
multipartFormData: request.encoded,
with: request,
encodingCompletion: completion
)
}
private static func validate(_ request: DataRequest) -> DataRequest {
return request
.validate(statusCode: successRange)
.validate(contentType: contentType)
}
}
というわけで無事にクライアントコードが完成しました!
使用例
任意の画像データをアップロードする場合に
let disposeBag = DisposeBag()
let imageData: Data = /* 任意の画像データ */
let request = UploadRequest(imageData: imageData)
APICliant.observeUpload(request)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { response in
print("onSuccess")
}, onError: { error in
print("onError")
})
.disposed(by: disposeBag)
という感じでかけます。
普通に書くと長くなってしまいがちなので、通常の書き方よりはだいぶシュッとしたと思います。
先にも同じ記述をしましたが、今回に何かしらレスポンスが返ってくる想定で作成しましたが、ない場合でも空のレスポンスで対応可能です。
(そもそもレスポンスクラスを作らない作りにもできますが、その場合はクライアントコードを自分で修正してください笑)
終わりに
Uploadなので少しコアだったかなとは思いつつ、、笑
一部リネームして表記しているので動かなかったらすいません、、、
誤字脱字あればお願いいたします!