Swift
ReactiveSwift

ReactiveSwift+Alamofire サンプル

はじめに

ReactiveSwiftでAlamofireを使う場合のAPIクライアントのサンプル実装
JSONのdecodeやエラーの構造体などについては記事には書かない

使用したExtension https://github.com/gkaimakas/AlamofireReactiveExtensions

実装

AlamofireのresponseJSON()で最初からJSONにすることも考えたが、
statusCode404など、エラーを示すJSONが返却されることもあるなと思い、まずは生データで扱うようにした
どっちでもいいかなとは思う

Service.swift
import Foundation
import Alamofire
import Result
import ReactiveSwift
import Argo

internal enum Router: URLRequestConvertible {
    static let baseURL = "https://example.com"

    case sample(sampleId: Int)

    var requestProperties: (method: Alamofire.HTTPMethod, path: String, query: [String: Any]) {
        switch self {
        case let .sample(sampleId):
            let params: [String: Any] = ["sampleId": sampleId]
            return (.get, "/v1", params)
        }
    }

    func asURLRequest() throws -> URLRequest {
        let url = URL(string: Router.baseURL)!
        var urlRequest = URLRequest(url: url.appendingPathComponent(requestProperties.path))
        urlRequest.httpMethod = requestProperties.method.rawValue

        return try Alamofire.URLEncoding.default.encode(urlRequest, with: requestProperties.query)
    }
}

public struct Service {

    /// fetch
    internal func fetchSample(sampleId: Int)
        -> SignalProducer<Sample, ErrorEnvelope> {
            return request(.sample(sampleId: sampleId))
    }

    private func request<M: Argo.Decodable>(_ router: Router)
        -> SignalProducer<M, ErrorEnvelope> where M == M.DecodedType {
            return self.rac_JSONResponse(router)
                .flatMap(.concat, decodeModel) // json decode
    }

    private func request<M: Argo.Decodable>(_ router: Router)
        -> SignalProducer<[M], ErrorEnvelope> where M == M.DecodedType {
            return self.rac_JSONResponse(router)
                .flatMap(.concat, decodeModels) // json decode
    }

    private func rac_JSONResponse(_ router: Router) -> SignalProducer<Any, ErrorEnvelope> {
        return self.rac_dataResponse(router)
            .map(parseJSONData)
            .flatMap(.concat) { json -> SignalProducer<Any, ErrorEnvelope> in
                guard let json = json else {
                    return .init(error: .couldNotParseJSON) // おそらく入ることはありえない たぶん
                }
                return .init(value: json)
        }
    }

    private func rac_dataResponse(_ router: Router) -> SignalProducer<Data, ErrorEnvelope> {
        let dataRequest = Alamofire
            .request(router)
            .validate(statusCode: 200..<300)
            .validate(contentType: ["application/json"])

        return dataRequest.reactive
            .response()
            .flatMap(.concat) { (response: DefaultDataResponse) -> SignalProducer<Data, ErrorEnvelope> in
                guard let data = response.data else {
                    return SignalProducer(error: .couldNotParseErrorEnvelopeJSON)
                }

                if response.error != nil {
                    if let json = self.parseJSONData(data) {
                        switch decode(json) as Decoded<ErrorEnvelope> {
                        case let .success(envelope):
                            return SignalProducer(error: envelope)
                        case let .failure(error):
                            return SignalProducer(error: .couldNotDecodeJSON(error))
                        }
                    } else {
                        return SignalProducer(error: .couldNotParseErrorEnvelopeJSON)
                    }
                }
                return SignalProducer(value: data)
            }
    }

    private func parseJSONData(_ data: Data) -> Any? {
        return (try? JSONSerialization.jsonObject(with: data, options: []))
    }
}