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 }
Related
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 am new to PromiseKit and am having trouble getting the value of the Promise. The end goal is to have the async call resolve to an array of JSON. Currently the compiler is complaining that:
Cannot convert return expression of type 'Promise<Void>' to return type 'Promise<JSON>'
I am pretty confused, and haven't been able to find a solution that looks like what I have going. Can anyone see where I am going wrong?
Here is the get method that makes the call to my backend for Json data.
public func get(_ url: URL) -> Promise<JSON> {
return Promise<JSON> { resolver -> Void in
Alamofire.request(url)
.validate()
.responseJSON { response in
switch response.result {
case .success(let json):
let json = JSON()
if let data = response.data {
guard let json = try? JSON(data: data) else {
resolver.reject("Error" as! Error)
return
}
resolver.fulfill(json)
}
case .failure(let error):
resolver.reject(error)
}
}
}
}
Here is the method that uses the above method:
public func fetchAllocations() -> Promise<JSON> {
let url: URL = createAllocationsUrl()
var allocations = JsonArray()
let responsePromise = httpClient.get(url).done { (fetchedJSON) in
let fetchedAlloc: JsonArray = JSON(fetchedJSON).array ?? []
if fetchedAlloc.count > 0 {
let eid = "9b0e33b869"
let previousAlloc = self.store.get(eid)
if let previousAllocations = previousAlloc {
if previousAllocations.count > 0 {
allocations = Allocations.reconcileAllocations(previousAllocations, allocations)
}
}
self.store.set(eid, val: allocations)
self.allocationStatus = AllocationStatus.RETRIEVED
if self.confirmationSandbagged {
self.eventEmitter.confirm(allocations)
}
}
}
return responsePromise
}
And in the button of my view controller, I am executing that function as so:
// This should be a Promise<JSON> that I can resolve and fulfill
let promise = alloc.fetchAllocations().done { (json) in
self.allocations = [json]
}
let eid = "9b0e33b869"
let cached = store.get(eid)
print("YOUR FETCHED ALLOCATION: \(String(describing: self.allocations))")
print("YOUR CACHED ALLOCATION: \(String(describing: cached))")
When I get to my print statements, I have nil each time.
public func fetchAllocations() -> Promise<[JSON]> {
return Promise { resolve in
let url = self.createAllocationsUrl()
let strPromise = HttpClient.get(url: url).done { (stringJSON) in
var allocations = JSON.init(parseJSON: stringJSON).arrayValue
let previous = self.store.get(self.participant.getUserId())
if let prevAlloc = previous {
if Allocator.allocationsNotEmpty(allocations: prevAlloc) {
allocations = Allocations.reconcileAllocations(previousAllocations: prevAlloc, currentAllocations: allocations)
}
}
self.store.set(self.participant.getUserId(), val: allocations)
self.allocationStatus = AllocationStatus.RETRIEVED
if (self.confirmationSandbagged) {
self.eventEmitter.confirm(allocations: allocations)
}
if self.contaminationSandbagged {
self.eventEmitter.contaminate(allocations: allocations)
}
resolve.fulfill(allocations)
do {
try self.execution.executeWithAllocation(rawAllocations: allocations)
} catch let err {
let message = "There was an error executing with allocations. \(err.localizedDescription)"
Log.logger.log(.error, message: message)
}
}
}
}
I have an application which uses SwiftyJSON and works. How ever, I now want to expand the project and refactor the codes but I am having a bit of issue as I am now switching to Codable and I need to be able to mapJSON from any path and not a hard coded path. Currently my jsonResponse looks like this
/// handle the network response and map to JSON
/// - returns: Observable<JSON>
func handleResponseMapJSON() -> Observable<Result<JSON, ORMError>> {
return self.map { representor in
guard let response = representor as? Moya.Response else {
return .failure(ORMError.ORMNoRepresentor)
}
guard ((200...299) ~= response.statusCode) else {
return .failure(ORMError.ORMNotSuccessfulHTTP)
}
guard let json = JSON.init(rawValue: response.data),
json != JSON.null,
let code = json["code"].int else {
return .failure(ORMError.ORMParseJSONError)
}
guard code == BizStatus.BizSuccess.rawValue else {
let message: String = {
let json = JSON.init(data: response.data)
guard let msg = json["status"].string else { return "" }
return msg
}()
log(message, .error)
return .failure(ORMError.ORMBizError(resultCode: "\(code)", resultMsg: message))
}
return .success(json["result"])
}
}
how do I eliminate the passage of hardcoded json[""] value. Any help is appreciated
I suggest you try something like this:
protocol ResponseType: Codable {
associatedtype ResultType
var status: String { get }
var code: Int { get }
var result: ResultType { get }
}
func handleResponseMap<T, U>(for type: U.Type) -> (Any) -> Result<T, ORMError> where U: ResponseType, T == U.ResultType {
return { representor in
guard let response = representor as? Moya.Response else {
return .failure(.ORMNoRepresentor)
}
guard ((200...299) ~= response.statusCode) else {
return .failure(.ORMNotSuccessfulHTTP)
}
return Result {
try JSONDecoder().decode(U.self, from: response.data)
}
.mapError { _ in ORMError.ORMParseJSONError }
.flatMap { (response) -> Result<T, ORMError> in
guard response.code == BizStatus.BizSuccess.rawValue else {
log(response.status, .error)
return Result.failure(ORMError.ORMBizError(resultCode: "\(response.code)", resultMsg: response.status))
}
return Result.success(response.result)
}
}
}
Then you can map directly to your Codable type:
let result = self.map(handleResponseMap(for: MyResponse.self))
In the above, result will end up being an Observable<Result<ResultType, ORMError>>
I would like to do extension over PrimitiveSequenceType to treat it as Single
import Foundation
import RxSwift
import Moya
public extension PrimitiveSequenceType where TraitType == SingleTrait, ElementType == Response {
func map<T>(_ type: T.Type, using decoder: JSONDecoder? = nil) -> PrimitiveSequence<TraitType, T> where T: Decodable {
return self.map { data -> T in
let decoder = decoder ?? JSONDecoder()
return try decoder.decode(type, from: data.data)
}
}
}
and you can simply use it like:
return PokeAPIEndPoints.shared.provider.rx
.request(.specieDetails(id: specieId))
.filterSuccessfulStatusCodes()
.map(SpecieDetails.self)
I successfully fetched and decoded data from an API and now have access to all the data I need to be used in the algorithm I want to write in my App.
The issue is that I don't know how to access this data after I decoded it, I can print it immediately after it's decoded but I have no idea how to use it in another function or place in my app.
Here is my Playground:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError : Error {
case FoundNil(String)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
print(marketData.data[0].open)
print(marketData.data[1].open)
print("Average=", (marketData.data[0].open + marketData.data[1].open) / 2)
//completion(marketData, nil)
throw MyError.FoundNil("data")
}
} catch {
print(error)
}
}
task.resume()
}
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
How can I use .data[0], .data[1], ..., somewhere else?
You data will be available in your fecthData() call. Probably what you want is your items variable, where you're printing it. But make sure to call the completion in your fetchData implementation.
WARNING: Untested code.
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError: Error {
case FoundNil(String)
case DecodingData(Data)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
completion(nil, MyError.FoundNil("data"))
}
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
completion(marketData, nil)
} else {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
} catch {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
}
task.resume()
}
fetchData() { items, error in
if let error == error {
switch(error) {
case .foundNil(let whatsNil):
print("Something is nil: \(whatsNil)")
case .decodingData(let data):
print("Error decoding: \(data)")
}
} else {
if let items = items {
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
print(items)
} else {
print("No items to show!")
}
}
}
I don't understand what is your real issue, because you have written everything you need here, but as far I understand , to pass data
just uncomment this line completion(marketData, nil)
and in
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
items is an object of your struct Response. You can pass this anywhere in your other class , by just creating an another variable like:
var items : Response!
for example :
class SomeOtherClass : NSObject{
var items : Response!
func printSomeData()
{
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
}
}
and in fetchData method write this:
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
let otherObject = SomeOtherClass()
otherObject.items = items
otherObject.printSomeData()
}
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