I'm using alamofire and it's serializing protocols. I have a model and its working great. Now, exactly how do I get an array of that model from these methods?
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [DataObject] {
var daos: [DataObject] = []
if let representation = representation as? [[String: AnyObject]] {
for contentRepresentation in representation {
if let content = DataObject(response: response, representation: contentRepresentation) {
daos.append(content)
}
}
}
return daos
}
class func populateData() {
Alamofire.request(.GET, url)
.responseCollection { (response: Response<[DataObject], NSError>) in
//response.result.value how do i pass this to my viewcontroller?
}
}
Are you getting a JSON response from the request?
If so, you may be able to try something along the lines of this:
Alamofire.request(.GET, url).responseJSON {
response -> () in
switch response.result {
case .Success(let object):
if let urlResponse = response.response {
let dataObjects = collection(response: urlResponse, representation: object)
// Pass the objects back to your view controller here
}
break
case .Failure(let error):
print("Error: ", error)
break
}
}
Related
In my quest to implement Alamofire 5 correctly and handle custom error model responses, I have yet to find an accepted answer that has an example.
To be as thorough as possible, here is my apiclient
class APIClient {
static let sessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
return Session(configuration: configuration, eventMonitors: [APILogger()])
}()
#discardableResult
private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (Result<T, AFError>)->Void) -> DataRequest {
return sessionManager.request(route)
// .validate(statusCode: 200..<300) // This will kill the server side error response...
.responseDecodable (decoder: decoder){ (response: DataResponse<T, AFError>) in
completion(response.result)
}
}
static func login(username: String, password: String, completion:#escaping (Result<User, AFError>)->Void) {
performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
}
}
I am using it like this
APIClient.login(username: "", password: "") { result in
debugPrint(result)
switch result {
case .success(let user):
debugPrint("__________SUCCESS__________")
case .failure(let error):
debugPrint("__________FAILURE__________")
debugPrint(error.localizedDescription)
}
}
I have noticed that if I use .validate() that the calling function will receive a failure however the response data is missing. Looking around it was noted here and here to cast underlyingError but thats nil.
The server responds with a parsable error model that I need at the calling function level. It would be far more pleasant to deserialize the JSON at the apiclient level and return it back to the calling function as a failure.
{
"errorObject": {
"summary": "",
"details": [{
...
}]
}
}
UPDATE
Thanks to #GIJoeCodes comment I implemented this similar solution using the router.
class APIClient {
static let sessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
return Session(configuration: configuration, eventMonitors: [APILogger()])
}()
#discardableResult
private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (_ response: T?, _ error: Error?)->Void) {
sessionManager.request(route)
.validate(statusCode: 200..<300) // This will kill the server side error response...
.validate(contentType: ["application/json"])
.responseJSON { response in
guard let data = response.data else { return }
do {
switch response.result {
case .success:
let object = try decoder.decode(T.self, from: data)
completion(object, nil)
case .failure:
let error = try decoder.decode(ErrorWrapper.self, from: data)
completion(nil, error.error)
}
} catch {
debugPrint(error)
}
}
}
// MARK: - Authentication
static func login(username: String, password: String, completion:#escaping (_ response: User?, _ error: Error?)->Void) {
performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
}
}
Called like this
APIClient.login(username: "", password: "") { (user, error) in
if let error = error {
debugPrint("__________FAILURE__________")
debugPrint(error)
return
}
if let user = user {
debugPrint("__________SUCCESS__________")
debugPrint(user)
}
}
This is how I get the errors and customize my error messages. In the validation, I get the errors outside of the 200..<300 response:
AF.request(
url,
method: .post,
parameters: json,
encoder: JSONParameterEncoder.prettyPrinted,
headers: headers
).validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON { response in
switch response.result {
case .success(let result):
let json = JSON(result)
onSuccess()
case .failure(let error):
guard let data = response.data else { return }
do {
let json = try JSON(data: data)
let message = json["message"]
onError(message.rawValue as! String)
} catch {
print(error)
}
onError(error.localizedDescription)
}
debugPrint(response)
}
First, there's no need to use responseJSON if you already have a Decodable model. You're doing unnecessary work by decoding the response data multiple times. Use responseDecodable and provide your Decodable type, in this case your generic T. responseDecodable(of: T).
Second, wrapping your expected Decodable types in an enum is a typical approach to solving this problem. For instance:
enum APIResponse<T: Decodable> {
case success(T)
case failure(APIError)
}
Then implement APIResponse's Decodable to try to parse either the successful type or APIError (there are a lot of examples of this). You can then parse your response using responseDecodable(of: APIResponse<T>.self).
I have prepared a Generic Service layer in my tutorial, where the responses differ from one API call to another and as i know generic is capable of handling this.
Yet i find it hard to call different APIs that has differnet responses.
For example the GetPosts --> Response[Array of Post Model]
For Example Delete POst --> Response (Post Model)
Knowing that both calls reach the below in the service layer
else if let jsonArray = json as? [Any] {
let object = Mapper<T>().mapArray( JSONObject: jsonArray)
completion(.success(object!))
}
You can check the response of each call on this Page
Here below is the Service LAYER CLASS this works fine when i call the getPosts API since in the saervice layer i defined the completiuon as Array --> [T], an in the getPosts Function i expect response as [PostsModel]
But when i try to call the DeletePost i face some complications since the result should be PostModel and not an array of PostModel.
When i modify the Servicelayer to accept T and not [T] i face some complications..
Hope anyone can help
import Foundation
import ObjectMapper
import Alamofire
class ServiceLayer {
class func request<T: Mappable>(router: Router, completion: #escaping (Result<[T], Error>) -> ()) {
do {
AF.request(try router.asURLRequest()).validate().responseData { (data) in
print(data)
let result = data.result
switch result {
case .success(let data):
do {
//Response as Dictionary
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? Any {
if let jsonDict = json as? [String : Any] {
let object = Mapper<T>().map(JSON: jsonDict)
completion(.success(object as! [T]))
}
//Response as Array
else if let jsonArray = json as? [Any] {
let object = Mapper<T>().mapArray( JSONObject: jsonArray)
completion(.success(object!))
}
//Response as String
else if let _ = String(data: data, encoding: .utf8){
}
}
} catch {
print(error.localizedDescription)
completion(.failure(error))
}
break
case .failure(let error):
print(error.localizedDescription)
completion(.failure(error))
break
}
}
} catch {
print(error.localizedDescription)
}
}
}
Here is the call to the APIs from the ViewModel
func getPosts() {
self.isLoading.value = true
ServiceLayer.request(router: .getPosts(userId: 1)) { (result : Result<[PostsModel], Error>) in
self.isLoading.value = false
switch result {
case .success(let baseModel):
self.array = baseModel
self.fetchPostsSucceded.value = true
case .failure(let error):
print(error.localizedDescription)
}
}
}
func deletePosts(postid: Int) {
self.isLoading.value = true
ServiceLayer.request(router: .deletePosts(id: postid)) { (result : Result<PostsModel, Error>) in
self.isLoading.value = false
switch result {
case .success(let baseModel):
print("")
case .failure(let error):
print(error.localizedDescription)
}
}
}
This is the PostModel Response, Knwoing that when i modify the Service layer completion to T and not [T] i get this
Class method 'request(router:completion:)' requires that '[PostsModel]' conform to 'Mappable'
import Foundation
import ObjectMapper
class PostsModel: Mappable, Codable {
var userId : Int?
var id : Int?
var title : String?
var body : String?
required init?(map: Map) {
}
func mapping(map: Map) {
userId <- map["userId"]
id <- map["id"]
title <- map["title"]
body <- map["body"]
}
}
Hope someone can help me resolve this, The needed result is to keep only one service layer that accepts any call and in the API calls to specify the return value.
if your service request<T: Mappable> get one object (not array) you decode it as array anyway
if let jsonDict = json as? [String : Any] {
let object = Mapper<T>().map(JSON: jsonDict)
completion(.success(object as! [T])) //<-- array anyway
}
but in function deletePosts you espect as Result<PostsModel, Error> not Result<[PostsModel], Error>. so if your service can answering only arrays you should change your
ServiceLayer.request(router: .deletePosts(id: postid)) { (result : Result<PostsModel, Error>) in
to
ServiceLayer.request(router: .deletePosts(id: postid)) { (result : Result<[PostsModel], Error>) in
I have this Networking class that i declared in the Model .
class Networking {
func response (url : String ) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: urlPathCompletionHandler(data:response:error:)).resume()
}
func urlPathCompletionHandler (data : Data? , response: URLResponse? , error: Error? ) {
guard let data = data else {return }
do {
let jsondecoder = JSONDecoder()
}catch {
print("Error \(error)")
}
}
}
In the controller . I have an array of users i declared and i want the controller to call from the Model Networking class instead of doing the networking inside the controller. This is part of my controller:
var users = [Users]()
var networking : Networking()
#IBOutlet weak var tableview : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
}
func getFromModel() {
var vm = networking.response()
}
I want a way of calling the networking class and return an array of users that i can set to the users array above and use it to populate the table view . If i wanted to do that inside the controller it would easy but i am not sure how i can return an array of users from the Model Networking class .
You need to modify your Network class like this:
class Networking {
func response<T: Codable>(url: String, completion: ((T) -> Void)?) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
self.urlPathCompletionHandler(data: data, response: response, error: error, completion: completion)
}).resume()
}
func urlPathCompletionHandler<T: Codable>(data : Data? , response: URLResponse? , error: Error?, completion: ((T) -> Void)?) {
guard let data = data else { return }
do {
let jsondecoder = JSONDecoder()
// Pseudo Code to decode users
completion?(decodedObject)
} catch {
print("Error \(error)")
}
}
}
And call it like this:
func getFromModel() {
networking.response(url: <#T##String#>) { (users: [User]) in
self.users = users
}
}
OK, there are a few thoughts:
Your response method is performing an asynchronous network request, so you need to give it a completion handler parameter. So, I might suggest something like:
class Networking {
enum NetworkingError: Error {
case invalidURL
case failed(Data?, URLResponse?)
}
private let parsingQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".parsing")
// response method to handle network stuff
func responseData(_ string: String, completion: #escaping (Result<Data, Error>) -> Void) {
guard let url = URL(string: string) else {
completion(.failure(NetworkingError.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
completion(.failure(NetworkingError.failed(data, response)))
return
}
completion(.success(responseData))
}
}.resume()
}
// response method to handle the JSON parsing
func response<T: Decodable>(of type: T.Type, from string: String, completion: #escaping (Result<T, Error>) -> Void) {
responseData(string) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
self.parsingQueue.async {
do {
let responseObject = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(responseObject))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
}
}
}
}
This obviously assumes that you have some Codable types. For example, it’s common for an API to have some common structure in its responses:
struct ResponseObject<T: Decodable>: Decodable {
let code: Int
let message: String?
let data: T?
}
And maybe the User is like so:
struct User: Decodable {
let id: String
let name: String
}
Then getFromModel (perhaps better called getFromRepository or something like that) could parse it with:
networking.response(of: ResponseObject<[User]>.self, from: urlString) { result in
switch result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
// do something with users
}
}
For what it’s worth, if you didn’t want to write your own networking code, you could use Alamofire, and then getFromModel would do:
AF.request(urlString).responseDecodable(of: ResponseObject<[User]>.self) { response in
switch response.result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
}
}
Now, clearly the model types are likely to be different in your example, but you didn’t share what your JSON looked like, so I had to guess, but hopefully the above illustrates the general idea. Make a generic-based network API and give it a completion handler for its asynchronous responses.
I want to create a generic Network Operation class to handle the communication to my server. I tested a few things and the error must be the responseArrayfunction. But I don't know where?
class NetworkOperation<T> {
let requestType: NSMutableURLRequest
var arrayItems: Array<T>?
typealias JSONDictionaryCompletion = ([String:AnyObject]?) -> Void
typealias ObjectArrayCompletion = (Response<[T], NSError>) -> Void
init(requestType: NSMutableURLRequest, arrayItems: Array<T>) {
self.requestType = requestType
self.arrayItems = arrayItems
}
func downloadJSONFromURL(completion: JSONDictionaryCompletion) {
}
func downloadObjectsFromURL(completion: ObjectArrayCompletion) {
Alamofire.request(requestType).responseArray { (response) in
if let httpResponse = response, let statusCode = httpResponse.response {
switch(statusCode) {
case 200:
print("OK")
}
}
}
}
}
extension Alamofire.Request {
// responseObject<...>(...) declares a new .responseX handling function on Alamofire.Request. It uses the responseSerializer as a custom serializer.
// The <T> means this is a genertic method: it can work with different types of objects. These types must implement the ResponseJSONObjectSerializable protocol.
// This is needed to guarantee that any type of object that will be passed in will have an init function that takes JSON. The response function takes a single
// argument called completionHandler. This is the method being called when done parsing the JSON and creating the object to be called async.
// The completion handler has a single argument Response<T, NSError>. The response object packs up the Result along iwth all the other info from the transaction.
// The responseObject function returns an Alamofire.Request object
public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler: Response<T, NSError> -> Void) -> Self {
// Create the response serilaizer that will work with the generic T type and an NSError. It will take in the results of the URL Request (request, response, data, error)
// and use the Result type defined by Alamofire to return success with the object or failure with an error. The responseObject just returns the responseserializer that gets created
// and allows passing the completion handler where it needs to go.
let serializer = ResponseSerializer<T, NSError> { (request, response, data, error) in
// Checks that it has valid data using guard. Then it turns the data into JSON and parses it using SwiftyJSON.
// Then it creates a new copy of the type of class.
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Object could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, responseData, error)
switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
// Get Data content of JSON
let jsonData = json["data"]
if let object = T(json: jsonData) {
return .Success(object)
} else {
let failureReason = "Object could not be created from JSON."
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
// Iterate through the elements in the json and create object out of each one and add it to the objects array.
public func responseArray<T: ResponseJSONObjectSerializable>(completionHandler: Response<[T], NSError> -> Void) -> Self {
let serializer = ResponseSerializer<[T], NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Array could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, responseData, error)
switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
// Get Data content of JSON
let jsonData = json["data"]
var objects: [T] = []
for (_, item) in jsonData {
if let object = T(json: item) {
objects.append(object)
}
}
return .Success(objects)
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
}
But when I want to check the status code in the downloadObjectsFromURL function, I get the compiler error message:
Ambigious Reference to member 'request(::parameters:encoding:headers:)'
Where does this error come from. Did I unwrap the optionals in a wrong way?
UPDATE
I tested this:
func createObjectsFromJSON(completion: ObjectArrayCompletion) {
Alamofire.request(requestType).responseArray { (response: Response<[AED], NSError>) -> Void in
print("OK")
}
}
AED is a custom class I created. No error message anymore. When I switch AED to T this error pops up
func createObjectsFromJSON(completion: ObjectArrayCompletion) {
Alamofire.request(requestType).responseArray { (response: Response<[T], NSError>) -> Void in
print("OK")
}
}
expected argument type Response<[_], NSError> -> Void
Morning all,
I've been following along the examples in the excellent iOS Apps With REST APIs book and as a result using Alamofire and SwiftyJSON. One thing I don't see mentioned in the book is how you would sort the incoming json objects into a specific order, eg. date. The code I've pasted below works fine for pulling in the json objects however as far as I can tell, they're automatically ordered by created_by. I'd like to sort by a different order, lets say my class was called Vegetable and had a name attribute so that I could sort by something like:
.sort { $0.name < $1.name }
I'll start with the Vegetable class in Vegetable.swift
class Vegetable: ResponseJSONObjectSerializable {
var id: Int?
var name : String?
var date: NSDate?
}
Inside my JSONSerializer file I have the following, I'm not sure I'd wish to change the order directly in here as I'd prefer some more flexibility with each call.
public func responseArray<T: ResponseJSONObjectSerializable>(completionHandler: Response<[T], NSError> -> Void) -> Self {
let serializer = ResponseSerializer<[T], NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Array could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, responseData, error)
switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
var objects: [T] = []
for (_, item) in json {
if let object = T(json: item) {
objects.append(object)
}
}
return .Success(objects)
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
Then, in my APIManager I have the following function
func getAllVegetables(completionHandler: (Result<[Vegetable], NSError>) -> Void) {
Alamofire.request(VegetableRouter.GetVegetables())
.responseArray { (response:Response<[Vegetable], NSError>) in
completionHandler(response.result)
}
}
Finally, populate my tableview I have:
func loadVegetables() {
self.isLoading = true
VegetablesAPIManager.sharedInstance.getAllVegetables() {
result in
self.isLoading = false
if self.refreshControl.refreshing {
self.refreshControl.endRefreshing()
}
guard result.error == nil else {
print(result.error)
// TODO: Display Error
return
}
if let fetchedVegetables = result.value {
self.vegetables = fetchedVegetables
for vegetable in fetchedVegetables {
// Nothing here at the moment
}
}
self.tableView.reloadData()
}
}
I appreciate any help I can get with this, Thanks!
Since you have a NSDate property, you can sort with the compare method of NSDate.
let sorted = result.value.sort { $0.date.compare($1.date) == .OrderedAscending }