So I'm trying to make sense of how to use Realm, Moya, and ObjectMapper.
I use Moya to make requests to my API. I use Realm to keep the returned data in a local database. And I use ObjectMapper to map the JSON objects to correct Realm variable.
However, I've come to an issue now where I'm not sure how to decode the JSON response in order to put it through the mapper.
Here is my Moya code:
provider.request(.signIn(email: email, password: password)) { result in
switch result {
case let .success(response):
do {
// Get the response data
let data = try JSONDecoder().decode(MyResponse.self, from: response.data)
// Get the response status code
let statusCode = response.statusCode
// Check the status code
if (statusCode == 200) {
// Do stuff
}
} catch {
print(error)
}
case let .failure(error):
print(error)
break
}
}
The error happens on this line:
In argument type 'MyResponse.Type', 'MyResponse' does not conform to expected type 'Decodable'
The MyResponse class looks like this:
class MyResponse: Object, Mappable {
#objc dynamic var success = false
#objc dynamic var data: MyResponseData? = nil
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
}
}
I understand why I'm getting that error, I just don't know the correct way to solving it. Am I missing something in the documentation of one of the above frameworks? Am I doing this totally wrong? How should I fix my line of code?
I've tried #Kamran's solution, but I got the error:
Argument labels '(JSON:)' do not match any available overloads
On the line:
let myResponse = MyResponse(JSON: json)
You are getting that error because you are decoding using Swift JSONDecoder which requires you to implement Codable which wraps Encodable and Decodable (JSON <-> YourObject).
If you're using Swift 4 you can use Codable instead of relying on 3rd party libraries.
MyResponse would become:
class MyResponse: Codable {
let success: Bool
let data: MyResponseData?
}
And MyResponseData should implement Codable too.
After this you should be able to do:
do {
let data = try JSONDecoder().decode(MyResponse.self, from: response.data)
} catch let error {
// handle error
}
Related
Language:Swift
Hello, I'd like some help in resolving an error being thrown when I try to retrieve data from an Apollo GraphQL request that I'm making. The API in use is the AniList API utilizing GraphQL.
Here's what I've tried:
In my model I'm making the Apollo GraphQL query inside of a search() function. I want to then use the Codable protocol to fill an array of anime objects. Currently it's setup to return just for 1 anime object. I was planning on using this anime list as a data set for TableView later. I wanted to take small steps so my current goal is to at least get the Codable protocol to work and return the response data to an anime Struct object.
The documentation for Apollo shows how to get individual fields but when I try to get the corresponding fields from my response , I don't even have the option.
func search(){
Network.shared.apollo.fetch(query: AnisearchQuery()){ result in
guard let data = try? result.get().data else { return }
var topData:APIResponse?
do{
topData = JSONDecoder().decode(APIResponse.self, from: data.self)
}catch{
}
}
}
Here are the data structures that I've set up as a representation of the JSON data I expect to receive with respect to the hierarchy it is laid out in the response.
struct APIResponse:Codable{
let data:data
}
struct data:Codable{
let Page:page
let media:media
}
struct media:Codable{
let animeResults:anime
}
struct anime:Codable{
var romaji:String
var english: String
var native:String
var episodes:Int
var duration:Int
var medium:String
}
Here is the error in question.
"Cannot convert value of type 'AnisearchQuery.Data' to expected argument type 'Data'". This is generated by this line of code
topData = JSONDecoder().decode(APIResponse.self, from: data.self)
For further context , AnisearchQuery.Data is generated in response to the query I created for the codgen.
Here's what the data would look like in JSON format
This is the setup of the query:
query anisearch($page:Int, $perPage:Int, $search:String){
Page (page:$page, perPage:$perPage){
pageInfo {
total
currentPage
lastPage
hasNextPage
perPage
}
media(search:$search){
title{
romaji
english
native
}
episodes
duration
coverImage{
medium
}
}
}
}
Here's the Data object in the API.swift file:
public struct Data: GraphQLSelectionSet {
public static let possibleTypes: [String] = ["Query"]
public static var selections: [GraphQLSelection] {
return [
GraphQLField("Page", arguments: ["page": GraphQLVariable("page"), "perPage": GraphQLVariable("perPage")], type: .object(Page.selections)),
]
}
I'd be open to any alternative methods as to getting this task done or perhaps fixes to the error being thrown.
Many thanks in advance.
Inefficient Workaround
var animeCollection:SearchAnimeQuery.Data?
var media:[SearchAnimeQuery.Data.Page.Medium]?
var filteredData:[SearchAnimeQuery.Data.Page.Medium] = []
func loadData(search:String = "") {
if !search.isEmpty{
Network.shared.apollo.fetch(query: SearchAnimeQuery(search: search)){
[weak self] result in
//Make Sure ViewController Has not been deallocated
guard let self = self else{
return
}
/*defer {
}*/
switch result {
case .success(let graphQLResult):
if let animeData = graphQLResult.data {
DispatchQueue.main.async {
self.animeCollection = animeData
self.media = self.animeCollection?.page?.media as! [SearchAnimeQuery.Data.Page.Medium]
self.filteredData = self.media!
self.tableView.reloadData()
}
}
if let errors = graphQLResult.errors {
let message = errors
.map { $0.localizedDescription }
.joined(separator: "\n")
print(message)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
I'm getting this warning Cast from 'AFError?' to unrelated type 'URLError' always fails when I try to cast the error in the following function
func requestBlock() {
struct ValidationConsumer: ResponseDelegate {
weak var syncRequest: Transfer_PurchaseValidation?
var productIdentifier: String
func didSucceed(_ _: JSON, _ _: AFDataResponse<Any>?) {
DDLogInfo("Purchase payload for productId = \(productIdentifier) was sent")
syncRequest?.didSucceed()
}
func didFail(_ json: JSON, _ code: Int?, _ dataResponse: AFDataResponse<Any>?) {
syncRequest?.didFail(with: .PurchaseValidationError(code,
dataResponse?.error as? URLError))
}
}
guard data.shouldBeTransferred else {
return
}
guard isUnderTest == nil else {
executeTestsequence()
return
}
guard let receiptDataString = data.receiptDataString,
let productIdentifier = data.productIdentifier else {
didFail(with: .InvalidData); return
}
let validationConsumer = ValidationConsumer(syncRequest: self,
productIdentifier: productIdentifier)
self.validatePurchase(receiptDataString, productIdentifier,
validationDelegate: validationConsumer)
}
at this part syncRequest?.didFail(with: .PurchaseValidationError(code, dataResponse?.error as? URLError))
I tried to use NSError or Error classes but no success.
Can anyone let me know how I can get rid of this warning?
Thanks in advance
Alamofire returns AFError instances by default which are not convertible to URLError. You can examine the AFError documentation or source code for the full details, but underlying errors like network failures are exposed through the underlyingError property. So in the case of a network failure, response?.error?.underlyingError as? URLError should give you the correct value, but only if the underlying error is actually a URLError.
Ultimately I suggest you handle the AFError directly, as it's the only full representation of all of the errors Alamofire can return. Either that, or your own error type which does the handling for you. Casting is not the correct way to capture these errors, as any cast will lose some error information.
I had a problem similar to yours.
I made a model as follows:
struct MyErorr: Error {
public var errorCode: Int
public var erorrMessage: String
}
and create object from myError this error part like this:
// error instance from AFError
if error.responseCode == 401 {
let myError: MyErorr = .init(errorCode: 401, erorrMessage: "Unauthorized")
callback(myError)
} else { ... }
I hope it is useful for you
The solution was to add another cast.
So, instead of
syncRequest?.didFail(with: .PurchaseValidationError(code,
dataResponse?.error as? URLError))
I did the case as following
syncRequest?.didFail(with: .PurchaseValidationError(code,
dataResponse?.error as NSError? as? URLError))
I am new in using Alamofire and iOS development in general. I am fetching data from server using the code below
func getDataFromServer(completion: #escaping ( _ dataString: String) -> Void) {
let url = "http://app10.com/index.php/Data"
let parameters : [String:Any] = ["someData": "xxx"]
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers:nil).responseJSON { (response) in
switch response.result {
case .failure(let errorResponse) :
let errorMessage = "error message here: \(errorResponse.localizedDescription)"
print(errorMessage)
completion("")
case .success(let value) :
let json = JSON(value)
let dataString = json["data"].stringValue
completion(dataString)
}
}
}
as you can see from the code above, I am trying to get dataString from server. but what I want to achieve is something like this
if meetSomeCondition {
// get 'dataString' from response cache from alamofire
} else {
// fetch 'dataString' from server, using the code above
}
can I do something like that using only alamofire?
I am sorry, I am newbie, but I can do something like that if I use Firebase Firestore, so maybe alamofire has feature something like that
I have tried to search but I can't find it in Stackoverflow
The underlying URLSession utilises a URLCache. Ensure your URLSession uses a URLCache and your server uses an appropriate response to instruct the client to cache the response.
In order to get the cached response, if any, the method according HTTP would be to set a cache control request header:
Cache-Control: only-if-cached (see RFC 5.2.1.7 only-if-cached)
That is, you would need to set this additional header when you create the request. Then execute the request as usual and you would either get the cached response or a status code 504 if there is none. Exactly what you would need.
Now, when you try this you realise, it unfortunately won't work as expected (bummer!). The reasons for this is manifold and it would be futile to go into detail.
The approach you may try next is to set the cache control policy of the URLRequest as follows:
urlRequest.cachePolicy = .returnCacheDataDontLoad
You can look up the meaning of it here: returnCacheDataDontLoad
Please read the documentation and the source documentation as well carefully, since it's not implemented in every iOS version!
That seems to be a working solution, however when using Alamofire you need to access the underlying URLRequest in order to set the cache policy (you may search on SO how to accomplish this) - and you need to be sure Alamofire does not alter your cache policy under the hood.
Alamofire does not provide cache directly.
It is easy to achieve it yourself.
use NSCache to keep our memory footprint low,
use time stamp, to invalidate stale data
here is an example:
// by writing a thin wrapper around NSCache, we can create a much more flexible Swift caching API
// — that enables us to store structs and other value types, and let us use any Hashable key type
final class Cache<Key: Hashable, Value> {
private let wrapped = NSCache<WrappedKey, Entry>()
private let dateProvider: () -> Date
private let entryLifetime: TimeInterval
init(dateProvider: #escaping () -> Date = Date.init,
entryLifetime: TimeInterval = 12 * 60 * 60) {
self.dateProvider = dateProvider
self.entryLifetime = entryLifetime
}
func insert(_ value: Value, forKey key: Key) {
let date = dateProvider().addingTimeInterval(entryLifetime)
let entry = Entry(value: value, expirationDate: date)
wrapped.setObject(entry, forKey: WrappedKey(key))
}
func value(forKey key: Key) -> Value? {
guard let entry = wrapped.object(forKey: WrappedKey(key)) else {
return nil
}
guard dateProvider() < entry.expirationDate else {
// Discard values that have expired
removeValue(forKey: key)
return nil
}
return entry.value
}
func removeValue(forKey key: Key) {
wrapped.removeObject(forKey: WrappedKey(key))
}
}
private extension Cache {
final class WrappedKey: NSObject {
let key: Key
init(_ key: Key) { self.key = key }
override var hash: Int { return key.hashValue }
override func isEqual(_ object: Any?) -> Bool {
guard let value = object as? WrappedKey else {
return false
}
return value.key == key
}
}
}
private extension Cache {
final class Entry {
let value: Value
let expirationDate: Date
init(value: Value, expirationDate: Date) {
self.value = value
self.expirationDate = expirationDate
}
}
}
extension Cache {
subscript(key: Key) -> Value? {
get { return value(forKey: key) }
set {
guard let value = newValue else {
// If nil was assigned using our subscript,
// then we remove any value for that key:
removeValue(forKey: key)
return
}
insert(value, forKey: key)
}
}
}
call like this:
// global singleton
let cache = Cache<Int, Int>()
// ...
override func viewDidLoad() {
super.viewDidLoad()
cache.insert(5, forKey: 1)
print(cache[1])
}
Persistent caching is also easy.
use Codable to do serialize the data.
use FileManager to persist the data file to disk
I have a struct like this
struct ApiResponse<T: Codable>: Codable {
let result: T?
let statusCode: String?
}
Somewhere in my code I need statusCode. I am not interested in result but Swift is not allowing me to use following:
let apiResponse = value as? ApiResponse
it shows following error:
Generic parameter 'T' could not be inferred in cast to 'ApiResponse'
which is quite obvious since struct definition ask some struct conforming to Codable but at same time i can not use one type as it will fail for other types.
e.g.
let apiResponse = value as? ApiResponse<ApiResult>
would be true for one type of response but if i have ApiResponse<ApiOtherResult> it will fail.
NetworkLayer.requestObject(router: router) { (result: NetworkResult<T>) in
switch result {
case .success(let value):
if let apiResponse = value as? ApiResponse {
}
case .failure: break
}
}
I'd suggest adding a new protocol
protocol StatusCodeProvider {
var statusCode: String? { get }
}
Add it as a requirement in your function making sure that T in NetworkResult<T> conforms to StatusCodeProvider and add conformance for every T you want to request.
What is the recommended approach in RxSwift to implement RAC tryMap-like functionality?
The following code is how I do mapping of json objects to an internal response wrapper class. If the response fails to comply with certain conditions, nil will be returned, which turns into an Error Event(tryMap implementation).
extension RACSignal{
func mapToAPIResponse() -> RACSignal{
return tryMap({ (object) -> AnyObject! in
if let data = object as? [String:AnyObject]{
//Some Logic
return data["key"]
}
return nil
})
}
}
How should this be implemented in RxSwift?
Updated-Possible Solution
I came up with following solution for Rx-Swift. Open for better solutions.
extension Observable{
func mapToAPIResponse() -> Observable<APIResponse>{
return map({ (object) in
guard let dictionary = object as? [String:AnyObject] else{
//APIResponseError.InvalidResponseFormat is defined in other class.
throw APIResponseError.InvalidResponseFormat
}
let response = APIResponse()
//Complete API Response
return response
})
}
My conclusion is to use throw inside a map to handle errors.
Your solution is correct, that's why map operator in RxSwift is annotated with throws. Release notes of RxSwift 2 explicitly state this:
Adds support for Swift 2.0 error handling try/do/catch.
You can now just write
API.fetchData(URL)
.map { rawData in
if invalidData(rawData) {
throw myParsingError
}
...
return parsedData
}
Even in `RxCocoa
There is a great way to implement network layer with these set of PODs
RxSwift + Moya/RxSwift + Moya-ObjectMapper/RxSwift
Finally your code for model will looks like
import ObjectMapper
final class Product: Mappable {
var id: String?
var categoryId: String?
var name: String?
func mapping(map: Map) {
id <- map["id"]
categoryId <- map["category_id"]
name <- map["name"]
}
}
And for service
final class ProductService {
class func productWithId(id: String, categoryId: String) -> Observable < Product > {
return networkStubbedProvider
.request(.Product(id, categoryId))
.filterSuccessfulStatusAndRedirectCodes()
.mapObject(Product)
}
}