I'm having a lot trouble converting my old Alamofire 2.0 to 3.0 in ReactiveCocoa. I keep getting an unknown identifier error on my sendNext and sendCompleted calls.
public final class Network: Networking {
private let queue = dispatch_queue_create( "Beet.BeetModel.Network.Queue", DISPATCH_QUEUE_SERIAL)
public init() { }
public func requestJSON(url: String, parameters: [String : AnyObject]?)
-> SignalProducer<AnyObject, NetworkError>
{
return SignalProducer { observer, disposable in
let serializer = Alamofire.Request.JSONResponseSerializer()
Alamofire.request(.GET, url, parameters: parameters)
.response(queue: self.queue, responseSerializer: serializer) {
_, _, result in
switch result {
case .Success(let value):
sendNext(observer, value)
sendCompleted(observer)
case .Failure(_, let error):
sendError(observer, NetworkError(error: error))
}
}
}
}
}
This syntax changed in 4.0 alpha 2. Observer is now its own type so the old functions sendNext, sendError, etc are no longer free functions:
switch result {
case .Success(let value):
observer.sendNext(value)
observer.sendCompleted()
case .Failure(_, let error):
observer.sendError(NetworkError(error: error))
}
One thing I would add to your solution is to provide a disposable so that requests can be cancelled if needed, to save resources:
return SignalProducer { observer, disposable in
let serializer = Alamofire.Request.JSONResponseSerializer()
let request = Alamofire.request(.GET, url, parameters: parameters)
request.response(queue: self.queue, responseSerializer: serializer) { _, _, result in
switch result {
case .Success(let value):
observer.sendNext(value)
observer.sendCompleted()
case .Failure(_, let error):
observer.sendError(NetworkError(error: error))
}
}
disposable.addDisposable(request.cancel)
}
Try observer.sendNext(value) and ditto for sendCompleted and sendError
Related
I am new to Combine, so I wanted to create class RestManager for networking with generic
fetchData function. Function is returning AnyPublisher<Result<T, ErrorType>, Never> where ErrorType is enum with .noInternetConnection, .empty and .general cases.
I tried to use URLSession with dataTaskPublisher and flatMap
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<Result<T, ErrorType>, Never> {
URLSession
.shared
.dataTaskPublisher(for: url)
.flatMap { (data, response) -> AnyPublisher<Result<T, ErrorType>, Never> in
switch response.result {
case .success(let data):
if let data = try? JSONDecoder().decode(T.self, from: data){
return Just(data).eraseToAnyPublisher()
}
case .failure(let error):
if let error = error as? URLError {
switch error.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return Fail(ErrorType.noInternetConnection).eraseToAnyPublisher()
case .cannotDecodeRawData, .cannotDecodeContentData:
return Fail(ErrorType.empty).eraseToAnyPublisher()
default:
return Fail(ErrorType.general).eraseToAnyPublisher()
}
}
}
}
.eraseToAnyPublisher()
}
But I am getting
Cannot convert return expression of type 'AnyPublisher<AnyPublisher<Result<T, ErrorType>, Never>.Output, URLSession.DataTaskPublisher.Failure>' (aka 'AnyPublisher<AnyPublisher<Result<T, ErrorType>, Never>.Output, URLError>') to return type 'AnyPublisher<Result<T, ErrorType>, Never>' error.
There are several major flows in your implementation.
Firstly, you shouldn't be using Result as the Output type of the Publisher and Never as its Failure type. You should be using T as the Output and ErrorType as Failure.
Second, you need tryMap and mapError, not flatMap.
Lastly, you are handling the result of dataTaskPublisher completely wrong. When dataTaskPublisher fails, it emits an error, so you need to handle that in mapError. When it succeeds, it emits its result as data, so you need to be decoding that, not response.
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { data, _ in
return try JSONDecoder().decode(T.self, from: data)
}
.mapError { error -> ErrorType in
switch error {
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
default:
return .general
}
}
.eraseToAnyPublisher()
}
I'm trying to migrate my project from Alamofire 4.9 to 5.3 and I'm having a hard time with error handling. I would like to use Decodable as much as possible, but my API endpoints return one JSON structure when everything goes well, and a different JSON structure when there is an error, the same for all errors across all endpoints. The corresponding Codable in my code is ApiError.
I would like to create a custom response serializer that can give me a Result<T, ApiError> instead of the default Result<T, AFError>. I found this article that seems to explain the general process but the code in there does not compile.
How can I create such a custom ResponseSerializer?
I ended up making it work with the following ResponseSerializer:
struct APIError: Error, Decodable {
let message: String
let code: String
let args: [String]
}
final class TwoDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
private lazy var successSerializer = DecodableResponseSerializer<T>(decoder: decoder)
private lazy var errorSerializer = DecodableResponseSerializer<APIError>(decoder: decoder)
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Result<T, APIError> {
guard error == nil else { return .failure(APIError(message: "Unknown error", code: "unknown", args: [])) }
guard let response = response else { return .failure(APIError(message: "Empty response", code: "empty_response", args: [])) }
do {
if response.statusCode < 200 || response.statusCode >= 300 {
let result = try errorSerializer.serialize(request: request, response: response, data: data, error: nil)
return .failure(result)
} else {
let result = try successSerializer.serialize(request: request, response: response, data: data, error: nil)
return .success(result)
}
} catch(let err) {
return .failure(APIError(message: "Could not serialize body", code: "unserializable_body", args: [String(data: data!, encoding: .utf8)!, err.localizedDescription]))
}
}
}
extension DataRequest {
#discardableResult func responseTwoDecodable<T: Decodable>(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), of t: T.Type, completionHandler: #escaping (Result<T, APIError>) -> Void) -> Self {
return response(queue: .main, responseSerializer: TwoDecodableResponseSerializer<T>()) { response in
switch response.result {
case .success(let result):
completionHandler(result)
case .failure(let error):
completionHandler(.failure(APIError(message: "Other error", code: "other", args: [error.localizedDescription])))
}
}
}
}
And with that, I can call my API like so:
AF.request(request).validate().responseTwoDecodable(of: [Item].self) { response in
switch response {
case .success(let items):
completion(.success(items))
case .failure(let error): //error is an APIError
log.error("Error while loading items: \(String(describing: error))")
completion(.failure(.couldNotLoad(underlyingError: error)))
}
}
I simply consider that any status code outside of the 200-299 range corresponds to an error.
ResponseSerializers have a single requirement. Largely you can just copy the existing serializers. For example, if you wanted to parse a CSV (with no response checking):
struct CommaDelimitedSerializer: ResponseSerializer {
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> [String] {
// Call the existing StringResponseSerializer to get many behaviors automatically.
let string = try StringResponseSerializer().serialize(request: request,
response: response,
data: data,
error: error)
return Array(string.split(separator: ","))
}
}
You can read more in Alamofire's documentation.
For my networking module, I have this protocol that I adopt for accessing different parts of the API:
protocol Router: URLRequestConvertible {
var baseUrl: URL { get }
var route: Route { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var encoding: ParameterEncoding? { get }
var responseResultType: Decodable.Type? { get }
}
I'm adopting this with enums that look like this:
enum TestRouter: Router {
case getTestData(byId: Int)
case updateTestData(byId: Int)
var route: Route {
switch self {
case .getTestData(let id): return Route(path: "/testData/\(id)")
case .updateTestData(let id): return Route(path: "/testDataOtherPath/\(id)")
}
}
var method: HTTPMethod {
switch self {
case .getTestData: return .get
case .updateTestData: return .put
}
}
var headers: [String : String]? {
return [:]
}
var encoding: ParameterEncoding? {
return URLEncoding.default
}
var responseResultType: Decodable.Type? {
switch self {
case .getTestData: return TestData.self
case .updateTestData: return ValidationResponse.self
}
}
}
I want to use Codable for decoding nested Api responses. Every response consists of a token and a result which content is depending on the request route.
For making the request I want to use the type specified in the responseResultType property in the enum above.
struct ApiResponse<Result: Decodable>: Decodable {
let token: String
let result: Result
}
extension Router {
func asURLRequest() throws -> URLRequest {
// Construct URL
var completeUrl = baseUrl.appendingPathComponent(route.path, isDirectory: false)
completeUrl = URL(string: completeUrl.absoluteString.removingPercentEncoding ?? "")!
// Create URL Request...
var urlRequest = URLRequest(url: completeUrl)
// ... with Method
urlRequest.httpMethod = method.rawValue
// Add headers
headers?.forEach { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) }
// Encode URL Request with the parameters
if encoding != nil {
return try encoding!.encode(urlRequest, with: route.parameters)
} else {
return urlRequest
}
}
func requestAndDecode(completion: #escaping (Result?) -> Void) {
NetworkAdapter.sessionManager.request(urlRequest).validate().responseData { response in
let responseObject = try? JSONDecoder().decode(ApiResponse<self.responseResultType!>, from: response.data!)
completion(responseObject.result)
}
}
}
But in my requestAndDecode method It throws an compiler error (Cannot invoke 'decode' with an argument list of type '(Any.Type, from: Data)'). I can't use ApiResponse<self.responseResultType!> like that.
I could make this function generic and call it like this:
TestRouter.getTestData(byId: 123).requestAndDecode(TestData.self, completion:)
but then I'd have to pass the response type everytime I want to use this endpoint.
What I want to achieve is that the extension function requestAndDecode takes it response type information from itself, the responseResultType property.
Is this possible?
Ignoring the actual error report you have a fundamental problem with requestAndDecode: it is a generic function whose type parameters are determined at the call site which is declared to return a value of type Result yet it attempts to return a value of type self.responseResultType whose value is an unknown type.
If Swift's type system supported this it would require runtime type checking, potential failure, and your code would have to handle that. E.g. you could pass TestData to requestAndDecode while responseResultType might be ValidationResponse...
Change the JSON call to:
JSONDecoder().decode(ApiResponse<Result>.self ...
and the types statically match (even though the actual type that Result is is unknown).
You need to rethink your design. HTH
Create a Generic function with Combine and AlomFire. You can use it for all method(get, post, put, delete)
func fatchData<T: Codable>(requestType: String, url: String, params: [String : Any]?, myType: T.Type, completion: #escaping (Result<T, Error>) -> Void) {
var method = HTTPMethod.get
switch requestType {
case "Get":
method = HTTPMethod.get
case "Post":
method = HTTPMethod.post
print("requestType \(requestType) \(method) ")
case "Put":
method = HTTPMethod.put
default:
method = HTTPMethod.delete
}
print("url \(url) \(method) \(AppConstant.headers) ")
task = AF.request(url, method: method, parameters: params, encoding: JSONEncoding.default, headers: AppConstant.headers)
.publishDecodable(type: myType.self)
.sink(receiveCompletion: { (completion) in
switch completion{
case .finished:
()
case .failure(let error):
// completion(.failure(error))
print("error \(error)")
}
}, receiveValue: {
[weak self ](response) in
print("response \(response)")
switch response.result{
case .success(let model):
completion(.success(model))
print("error success")
case .failure(let error):
completion(.failure(error))
print("error failure \(error.localizedDescription)")
}
}
)
}
I'm trying to use a while loop with Promisekit with Alamofire to chain four GET requests, return a value, and rerun the four requests with a new parameter. This is the current code that I'm using:
var index = 0
var count = classDictionary["class"]!.count-1
while index <= count {
firstly {
parseBooksXML(index)
}.then { abrevCode in
self.parseBooksXML2(index, key: abrevCode)
}.then { courseNumber in
self.parseBooksXML3(index, key: courseNumber)
}.then { instructorCode in
self.parseBooksXML4(instructorCode)
}
index += 1
}
Each of the first three functions returns a promised string value which is then passed onto the next function until the fourth and final function calls another function to parse the downloaded HTML using Kanna.
Ideally I would like all four functions to be called and completed after which the index will increment and run the loop again using the new index number. As a note, the index in passed onto the functions as a way to identify which index in an array a value should be compared to.
For clarity, I have included the code for the parseBooksXML functions below:
func parseBooksXML(index: Int) -> Promise<String> {
return Promise {fulfill, reject in
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
let xml = SWXMLHash.parse(response.data!)
do {
let range = self.classDictionary["class"]![index].rangeOfString("[a-zA-Z]{2,4}", options: .RegularExpressionSearch)
let result = self.classDictionary["class"]![index].substringWithRange(range!)
try self.abrevCode = (xml["departments"]["department"].withAttr("abrev", result).element!.attribute(by: "id")!.text)
}
catch {
print("Error: \(error)")
}
fulfill(self.abrevCode)
case .Failure(let error):
print(error)
}
}
}
}
func parseBooksXML2(index: Int, key: String) -> Promise<String> {
return Promise {fulfill, reject in
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
let xml = SWXMLHash.parse(response.data!)
do {
let range = self.classDictionary["class"]![index].rangeOfString("\\d\\d\\d", options: .RegularExpressionSearch)
let result = self.classDictionary["class"]![index].substringWithRange(range!)
try self.courseNumber = (xml["courses"]["course"].withAttr("name", result).element?.attribute(by: "id")?.text)!
}
catch {
print("Error: \(error)")
}
fulfill(self.courseNumber)
case .Failure(let error):
print(error)
}
}
}
}
func parseBooksXML3(index: Int, key: String) -> Promise<String> {
return Promise {fulfill, reject in
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
let xml = SWXMLHash.parse(response.data!)
do {
let range = self.classDictionary["class"]![index].rangeOfString("[a-zA-Z]{1,3}?\\d?\\d?\\d?$", options: .RegularExpressionSearch)
let result = self.classDictionary["class"]![index].substringWithRange(range!)
try self.instructorCode = (xml["sections"]["section"].withAttr("instructor", self.classTeacher[index]).element?.attribute(by: "id")?.text)!
}
catch {
print("Error: \(error)")
}
fulfill(self.instructorCode)
case .Failure(let error):
print(error)
}
}
}
}
func parseBooksXML4(key: String) -> Void {
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
self.parseISBN(String(data: response.data!, encoding: NSUTF8StringEncoding)!)
case .Failure(let error):
print(error)
}
}
}
Any help would be appreciated!
You need to use when:
let count = classDictionary["class"]!.count-1
let promises = (0..<count).map { index -> Promise<ReplaceMe> in
return firstly {
parseBooksXML(index)
}.then { abrevCode in
self.parseBooksXML2(index, key: abrevCode)
}.then { courseNumber in
self.parseBooksXML3(index, key: courseNumber)
}.then { instructorCode in
self.parseBooksXML4(instructorCode)
}
}
when(fulfilled: promises).then {
//…
}
Since parseBooksXML4 call is async, you should wrap parseBooksXML4() call to return promise and wait for that to finish before increment index.
firstly {
parseBooksXML(index)
}.then { abrevCode in
self.parseBooksXML2(index, key: abrevCode)
}.then { courseNumber in
self.parseBooksXML3(index, key: courseNumber)
}.then { instructorCode in
self.parseBooksXML4(instructorCode)
}.then { _ in
index += 1
}
}
I'm developing an IOS app using swift with Alamofire library. Two files of my network model are described below, the first one is the ApiClient Class with the Alamofire manager session configuration and 2 instances of the services,in my question I'm only describing one, the Service1 struct, in this struct I'm using the Alamofire Router to request to the REST API, and I would like to know, How can I set a default baseURLString ("https://api.com/v1") using manager session configuration? instead of declare it inside each struct Service.
ApiClient.swift
class ApiClient {
var OAuthToken = "ZMpMDJhB0egjFIzFhapuWNFSBhX2conQ1e+3vlv0XrJQVcw7fRg=="
var service1: Service1
var service2: Service2
init() {
let manager = Manager.sharedInstance
manager.session.configuration.HTTPAdditionalHeaders = [
"Authorization": "Bearer \(OAuthToken)"
]
service1 = Service1()
service2 = Service2()
}
}
Service1.swift
struct Service1 {
enum Router: URLRequestConvertible {
static let baseURLString = "https://api.com/v1"
case GetList([String: AnyObject])
case GetById(String)
case Add([String: AnyObject])
case Update(String, [String: AnyObject])
case Delete(String)
case GetListFromPoint([String: AnyObject])
var method: Alamofire.Method {
switch self {
case .GetList:
return .GET
case .GetById:
return .GET
case .Add:
return .POST
case .Update:
return .PUT
case .Delete:
return .DELETE
case .GetListFromPoint:
return .GET
}
}
var path: String {
switch self {
case .GetList:
return "/endPoint"
case .GetById(let id):
return "/endPoint/\(id)"
case .Add:
return "/endPoint"
case .Update(let id, _):
return "/endPoint/\(id)"
case .Delete(let id):
return "/endPoint/\(id)"
case .GetListFromPoint:
return "/near/endPoint"
}
}
var URLRequest: NSURLRequest {
let URL = NSURL(string: Router.baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
switch self {
case .GetList(let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
case .Add(let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .Update(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .GetListFromPoint(let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
}
func getList(completionHandler: ([Object1]?, NSError?) -> ()) {
Alamofire.request(Router.GetList(["skip": 0, "limit": 100])).responseJSON { (request, response, json, error) in
//Converting json to object
}
func getById(completionHandler: ([Object1]?, NSError?) -> ()) {
Alamofire.request(Router.GetById(id)).responseJSON { (request, response, json, error) in
//Converting json to object
}
}
I think there are MANY ways you could do this. Here are a few ideas...
Store the baseURLString in the APIClient and pass the value into each of the Service initializers
Create a BaseService class where you store all common types of information between the services such as baseURLString, additional headers, etc.
Expose the baseURLString in the APIClient and pass a weak reference of the APIClient into each service.
Create a separate object such as an AppEnvironment that stores properties such as OAuth info, baseURLString, etc. that can be accessed through either a global singleton or class properties.
And there are many more ways. Hopefully one of those ideas triggers your imagination.