概要
Moya+SwiftTask+SwiftyJSONを使用して通信クラスを作成したので備忘録的メモ。
環境
1年ぐらい前のプロジェクト開発時のものなので現在から考えるとちょっと古いものになります。
バージョン | |
---|---|
Xcode | 7.3 |
Swift | 2.2 |
Moya | 6.5.0 |
SwiftTask | 4.2.1 |
SwiftyJSON | 2.3.3 |
なぜ各ライブラリを採用したかも記述しておきます。
Moya
ローカルスタブを手軽に導入したかった。
エンドポイントの管理がenumで定義できて見やすそうだった。
SwiftTask
成功時、失敗時の処理をコールバックではなくメソッドチェーンで記述したかった。
SwiftyJSON
JSONを扱えるものだったらなんでも良かった。
エンドポイント
早速サンプルコードの方を記述していきます。
今回はユーザー情報を返すAPIにユーザーIDをGETで渡してレスポンスを取得する通信を想定して以下を記述していきます。
まずはMoyaでAPIリクエストの各エンドポイントの処理を定義していきます。
APIターゲットの定義
ターゲットと引数を定義しておきます。
Userエンドポイントがあって、引数にはidの文字列が必要と言うことを定義します。
import Moya
import Alamofire
internal enum HogeAPITarget {
case User(id: String)
}
ターゲットタイプの指定
通信のエンドポイントのベースとなるURL、パス、メソッド、パラメータの受け渡しに関する情報です。
スタブ使用時にはリソース内の"User.json"を読み取って返す処理もここで記述しています。
import Moya
import Alamofire
func stubbedResponse(filename: String) -> NSData! {
@objc class TestClass: NSObject { }
let bundle = NSBundle(forClass: TestClass.self)
if let path = bundle.pathForResource(filename, ofType: "json") {
return NSData(contentsOfFile: path)
}
return NSData()
}
extension HogeAPITarget: TargetType {
internal var baseURL: NSURL {
switch self {
case .User:
return NSURL(string: "hogeドメイン")!
}
}
internal var path: String {
switch self {
case .User:
return "/user/"
}
internal var method: Moya.Method {
switch self {
case .User,
return .GET
}
}
internal var parameters:[String: AnyObject]? {
switch self {
case .User(let id):
return ["id": id]
default:
return nil
}
}
var parameterEncoding: Moya.ParameterEncoding {
return ParameterEncoding.URL
}
internal var sampleData: NSData {
switch self {
case .User:
return stubbedResponse("User")
default:
return NSData()
}
}
}
プロバイダの定義
通信プロバイダーの情報です。
HttpヘッダにContent-Typeの情報を載せたい場合などはここで記述します。
スタブ使用時のディレイ時間などもここで指定できるようにしておきます。
struct HogeAPIProvider {
private struct SharedProvider {
static var instance = HogeAPIProvider.DefaultProvider()
}
static var sharedProvider: MoyaProvider<HogeAPITarget> {
get {
return SharedProvider.instance
}
set (newSharedProvider) {
SharedProvider.instance = newSharedProvider
}
}
static func StubbingProvider(delay: Double) -> MoyaProvider<HogeAPITarget> {
return MoyaProvider<HogeAPITarget>(stubClosure: MoyaProvider.DelayedStub(delay))
}
static func DefaultProvider() -> MoyaProvider<HogeAPITarget> {
let endpointClosure = { (target: HogeAPITarget) -> Endpoint<HogeAPITarget> in
let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
let endpoint: Endpoint<HogeAPITarget> = Endpoint(URL: url, sampleResponseClosure: { .NetworkResponse(200, target.sampleData) }, method: target.method, parameters: target.parameters, parameterEncoding: target.parameterEncoding)
/*
Httpヘッダにゴニョゴニョしたい時はここに記述します。
*/
return endpoint
}
let requestClosure = { (endpoint: Endpoint<HogeAPITarget>, done: (NSURLRequest -> Void)) -> Void in
let request = endpoint.urlRequest.mutableCopy() as! NSMutableURLRequest
request.timeoutInterval = 10
switch endpoint.parameterEncoding {
case .JSON:
do {
if let parameter = endpoint.parameters {
let json = try NSJSONSerialization.dataWithJSONObject(parameter, options: NSJSONWritingOptions.PrettyPrinted)
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.HTTPBody = json
}
} catch {
print("リクエストパラメータのJSON変換に失敗しました")
}
default: break
}
done(request)
}
return MoyaProvider<HogeAPITarget>(endpointClosure: endpointClosure, requestClosure: requestClosure)
}
}
ここまでで一旦moya側の処理は終了です。
次にモデルクラスを用意します。
モデル
JSONレスポンス
以下のようなJSONのレスポンスを想定します。
{
"name": "hogehogename",
"image": "hogehogeimage"
}
モデルプロトコルの定義
すべてのモデルをジェネリックに扱えるようにプロトコルを定義します。
このプロトコルを継承したモデルはJSONオブジェクトからインスタンスを生成するメソッドを持つようにしておきます。
エラータイプを用意しているのはデバッグ時にどこのクラスのマッピングがエラーになったのかすぐわかるようにです。
import SwiftyJSON
/**
モデルマッピングエラー
*/
enum HogeAPIModelError: ErrorType {
/// 必須項目が無い
case Required(className: String, property: String)
}
/**
APIマッピングモデルプロトコル
*/
public protocol HogeAPIModelProtocol {
static func build(json: JSON) throws -> Self
}
モデルクラス作成
buildメソッドでJSONオブジェクトから必要項目を抜き出してインスタンスを作成します。
import SwiftyJSON
final class User: HogeAPIModelProtocol {
/// ユーザー名
dynamic var name = ""
/// 画像URL
dynamic var image = ""
init(name: String, image: String) {
self.name = name
self.image = image
}
static func build(json: JSON) throws -> User {
guard let name = json["name"].string else {
throw HogeAPIModelError.Required(className: "User", property: "name")
}
guard let image = json["image"].string else {
throw HogeAPIModelError.Required(className: "User", property: "image")
}
return User.init(name: name, image: image)
}
}
エラー定義
通信処理で発生したエラーを返せるようにエラータイプを定義しておきます。
こうしておくと呼び出し元ではエラーを受け取った際にprint(error.userMessage)と記述するだけで各パターンのメッセージを表示できるので便利です。
import Moya
enum HogeError: ErrorType {
/// ネットワークエラー
case Provider(Moya.Error)
/// モデルマッピングエラー
case ModelMapping(HogeAPIModelError)
/// その他のエラー
case Unkown(description: String)
var description: String {
switch self {
case .Provider(let error):
guard let response = error.response else {
return "通信エラーが発生しました"
}
return response.description
case .ModelMapping(HogeAPIModelError.Required(let className, let property)):
return "データ\(className):\(property)がサーバーから正しく取得できませんでした"
case .Unkown(let description):
return description
}
}
var userMessage: String {
switch self {
case .Provider, .ModelMapping:
return "通信に失敗しました。インターネットの接続環境をご確認の上、再度お試しください。"
case .Unkown:
return "不明なエラーが発生しました"
}
}
}
通信処理
通信成功時には<T: HogeAPIModelProtocol>
で指定したモデルを返し、失敗時にはHogeError
を返します。
呼び出し元ではSuccess時にマッピングして返して欲しいモデルを<T: HogeAPIModelProtocol>
で指定します。
モデルはJSONからインスタンスを返すメソッドを持つHogeAPIModelProtocol
を継承している必要があります。
import Moya
import SwiftyJSON
import SwiftTask
class HogeAPI {
func request<T: HogeAPIModelProtocol>(target: HogeAPITarget) -> Task<Void, T, HogeError> {
let task = Task<Void, T, HogeError> { progress, fulfill, reject, configure in
let provider = HogeAPIProvider.sharedProvider
provider.request(target, completion: { (result) in
switch result {
case let .Success(response):
do {
let json = try response.mapJSON()
fulfill(try T.build(JSON(json)))
} catch let error as HogeAPIModelError {
// モデルマッピングエラー(JSONからオブジェクトに変換中に必須項目が見つからなかった)
reject(HogeError.ModelMapping(error))
} catch let error as Moya.Error {
// プロバイダーエラー(MOYAのエラー。ネットワーク系に異常が発生した)
reject(HogeError.Provider(error))
} catch let error {
// その他のエラー
reject(HogeError.Unkown(description: "予期せぬ通信エラーが発生しました"))
}
case let .Failure(error):
// プロバイダーエラー(MOYAのエラー。ネットワーク系に異常が発生した)
reject(HogeError.Provider(error))
}
})
}
return task
}
}
実際に通信を使用するときのテストコード
HogeAPI()
.request(HogeAPITarget.User(id: "ユーザーID"))
.success({ (user: User) in
print("name:\(user.name) image:\(user.image)")
})
.failure({ (error, isCancelled) in
print("API Error! \(error.description)")
})
おわりに
せっかくここまで設計して実装したので記事に残したいと思いササっと投稿してみました。
間違いや勘違い等も多々あるかと思います。。
とりあえずMoyaを使ってみてスタブやエンドポイントの管理が簡潔になることよりもSwiftのenumってスゲーって実感しました。