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 need to cast the below response from my server as [UserResult] but I cannot get it to work??
What am I doing wrong?
func userSearch(keyword: String, completion: #escaping (Result<[UserResult], ResponseError>) -> Void ) {
socket.emit("userSearch", keyword)
socket.on("userFound") { ( data, ack) in
print(data) // prints below NSArray
if !data.isEmpty {
if let response = data as? [UserResult] {
print("USERS \(response)") // WILL NOT WORK?
completion(.success(response))
}
} else {
completion(.failure(.badRequest("No users found")))
}
}
}
Data from server
[<__NSArrayM 0x60000040e5b0>(
{
profileUrl = "www.address1.com";
username = chrissmith;
},
{
profileUrl = "www.address2.com";
username = johnsmith;
},
{
profileUrl = "www.address3.com";
username = alicesmith;
}
)
]
UserResult Model
struct UserResult: Decodable {
let username: String
let profileUrl: String
}
Well you are using Socket.IO library and specifically method
socket.on(clientEvent: .connect) {data, ack in
...
}
defined as
#discardableResult
open func on(clientEvent event: SocketClientEvent, callback: #escaping NormalCallback) -> UUID
using typealias:
public typealias NormalCallback = ([Any], SocketAckEmitter) -> ()
So basically at the and you are being returned data of type [Any] according to documentation.
Since you do not know what is inside your data it is better for you to unwrap objects in your array one by one (instead casting it directly to [UserResult]) and try to find out what Type there are by comparing to some set of known types as some of answers from this question suggest.
I would start with verifying the data structure with example code below , and only move on with casting to various type afterwards:
Lets assume example data1 is your data:
let dict1 = ["profileUrl":"www.address1.com","username":"chrissmith"]
let data1: NSArray = [dict1]
//printed data1:
// (
// {
// profileUrl = "www.address1.com";
// username = chrissmith;
// }
// )
if data1[0] as? [String:String] != nil {
print("We found out that first object is dictionary of [String:String]!")
}
else if data1[0] as? Dictionary<NSObject, AnyObject> != nil {
print("We found out that first object is dictionary of mixed values!")
} else {
print("We found out that first object has different data structure")
}
Hopefully this answer was at least a little bit helpfull, even though not providing direct easy solution for your problem.
I'm actually trying to some code logic with swift for training purpose at the moment, I was wondering what is the proper way to throw my error from my init ?
So the flow is Controller ask for account creation when initializing Model is asking my sql manager to create the account and this method return the result from a closure.
But something feels wrong, should I just use a return from the sql manager who contained both my Int? and Error? ?
init(_ username: String, _ password: String) throws {
self.id = 0
self.username = username
self.password = password
var toThrow: Error? = nil
// Register in database
userManager.create(self) { (id: Int?, err: Error?) in
Thread.sleep(forTimeInterval: 10)
if let error = err {
// Register in database goes wrong
debugPrint("Handle error from user creation...")
toThrow = error
} else {
// There is no id and no error ?
guard let _ = id else { return }
self.id = id!
}
}
if let error = toThrow {
throw error
}
}
If you are on Swift 5 you could look into using Result and define your closure like
(id: Int) -> Result<Int, Error>
and change your code to
userManager.create(self) { (id: Int?) -> Result<Int, Error> in
Thread.sleep(forTimeInterval: 10)
if let error = err {
// Register in database goes wrong
debugPrint("Handle error from user creation...")
return .failure(error)
} else {
// There is no id and no error ?
guard let _ = id else { return }
return .success(id)
}
}
If you have your own error enum for the Db class like
enum DbError {
case create
case update
//...
}
Then you can use that type in the closure declaration
(id: Int?) -> Result<Int, DbError>
and return a specific error for this action
return .failure(.create)
Note that I haven't compiled this so consider it an example
Here is the solution if people want to see:
#IBAction func didPressRegister() {
guard let username = usernameField.text else { return }
guard let password = passwordField.text else { return }
let user = UserModel(username, password)
userManager.create(user) { result in
switch(result) {
case .failure(let error):
// TODO: UIAlert
debugPrint(error)
case .success(let int):
// TODO: Generate user token and redirect main
debugPrint(int)
}
}
}
// TODO
public func create(_ user: UserModel, _ complete: #escaping (Result<Int, Error>) -> ()) {
debugPrint("Requested to create the user... \(user)")
complete(.failure(toThrow.ACCOUNT_ERROR))
}
I'm working with Alamofire and SwiftyJSON. I want to build general request and parse model for common situations. Firstly, I make a protocol called JSONConvertible.
protocol JSONConvertible {
init?(json: JSON)
}
Secondly, I extend Request class in Alamofire.
extension Request {
func getResult(format: [String: AnyClass]) {
self.responseJSON { (response) in
guard let statusCode = response.response?.statusCode else {
return
}
switch statusCode {
case 200:
var result = [String: AnyObject]()
let json = JSON(rawValue: response.result.value!)!
for (key, className) in format {
if className.self is JSONConvertible {
let value = className.self(json: json[key]) // get error in this line
}
}
case 201..<400:
break
case 400...Int.max:
break
default:
break
}
}
}
}
But I get an error from the compiler. Because AnyObject is only protocol and it doesn't have this initializer. I don't want get a dictionary or array only. I want to get instances with concrete class. Please help me. Many thanks!
That's because inside the if the type for className.self is still AnyObject. You need to cast it to JSONConvertible and then can use the initializer.
if let concreteClass = className.self as? JSONConvertible.Type
{
let value = concreteClass.init(json: json[key])
}
If you are familiar with kotlin, swift doesn't do casting automatically when testing for type in an if clause.
I found another way to solve this. Define a new protocol.
protocol JSONConvertibleObject: AnyObject, JSONConvertible {
}
And use this instead.
extension Request {
func getResult(format: [String: JSONConvertibleObject]) {
self.responseJSON { (response) in
guard let statusCode = response.response?.statusCode else {
return
}
switch statusCode {
case 200:
var result = [String: AnyObject]()
let json = JSON(rawValue: response.result.value!)!
for (key, className) in format {
let value = className.self.dynamicType.init(json: json[key])
}
case 201..<400:
break
case 400...Int.max:
break
default:
break
}
}
}
}
What I am trying to achieve is perform a URLSession request in swift 3. I am performing this action in a separate function (so as not to write the code separately for GET and POST) and returning the URLSessionDataTask and handling the success and failure in closures. Sort of like this-
let task = URLSession.shared.dataTask(with: request) { (data, uRLResponse, responseError) in
DispatchQueue.main.async {
var httpResponse = uRLResponse as! HTTPURLResponse
if responseError != nil && httpResponse.statusCode == 200{
successHandler(data!)
}else{
if(responseError == nil){
//Trying to achieve something like below 2 lines
//Following line throws an error soo its not possible
//var errorTemp = Error(domain:"", code:httpResponse.statusCode, userInfo:nil)
//failureHandler(errorTemp)
}else{
failureHandler(responseError!)
}
}
}
}
I do not wish to handle the error condition in this function and wish to generate an error using the response code and return this Error to handle it wherever this function is called from.
Can anybody tell me how to go about this? Or is this not the "Swift" way to go about handling such situations?
In your case, the error is that you're trying to generate an Error instance. Error in Swift 3 is a protocol that can be used to define a custom error. This feature is especially for pure Swift applications to run on different OS.
In iOS development the NSError class is still available and it conforms to Error protocol.
So, if your purpose is only to propagate this error code, you can easily replace
var errorTemp = Error(domain:"", code:httpResponse.statusCode, userInfo:nil)
with
var errorTemp = NSError(domain:"", code:httpResponse.statusCode, userInfo:nil)
Otherwise check the Sandeep Bhandari's answer regarding how to create a custom error type
You can create a protocol, conforming to the Swift LocalizedError protocol, with these values:
protocol OurErrorProtocol: LocalizedError {
var title: String? { get }
var code: Int { get }
}
This then enables us to create concrete errors like so:
struct CustomError: OurErrorProtocol {
var title: String?
var code: Int
var errorDescription: String? { return _description }
var failureReason: String? { return _description }
private var _description: String
init(title: String?, description: String, code: Int) {
self.title = title ?? "Error"
self._description = description
self.code = code
}
}
You should use NSError object.
let error = NSError(domain: "", code: 401, userInfo: [ NSLocalizedDescriptionKey: "Invalid access token"])
Then cast NSError to Error object.
You can create enums to deal with errors :)
enum RikhError: Error {
case unknownError
case connectionError
case invalidCredentials
case invalidRequest
case notFound
case invalidResponse
case serverError
case serverUnavailable
case timeOut
case unsuppotedURL
}
and then create a method inside enum to receive the http response code and return the corresponding error in return :)
static func checkErrorCode(_ errorCode: Int) -> RikhError {
switch errorCode {
case 400:
return .invalidRequest
case 401:
return .invalidCredentials
case 404:
return .notFound
//bla bla bla
default:
return .unknownError
}
}
Finally update your failure block to accept single parameter of type RikhError :)
I have a detailed tutorial on how to restructure traditional Objective - C based Object Oriented network model to modern Protocol Oriented model using Swift3 here https://learnwithmehere.blogspot.in Have a look :)
Hope it helps :)
Details
Xcode Version 10.2.1 (10E1001)
Swift 5
Solution of organizing errors in an app
import Foundation
enum AppError {
case network(type: Enums.NetworkError)
case file(type: Enums.FileError)
case custom(errorDescription: String?)
class Enums { }
}
extension AppError: LocalizedError {
var errorDescription: String? {
switch self {
case .network(let type): return type.localizedDescription
case .file(let type): return type.localizedDescription
case .custom(let errorDescription): return errorDescription
}
}
}
// MARK: - Network Errors
extension AppError.Enums {
enum NetworkError {
case parsing
case notFound
case custom(errorCode: Int?, errorDescription: String?)
}
}
extension AppError.Enums.NetworkError: LocalizedError {
var errorDescription: String? {
switch self {
case .parsing: return "Parsing error"
case .notFound: return "URL Not Found"
case .custom(_, let errorDescription): return errorDescription
}
}
var errorCode: Int? {
switch self {
case .parsing: return nil
case .notFound: return 404
case .custom(let errorCode, _): return errorCode
}
}
}
// MARK: - FIle Errors
extension AppError.Enums {
enum FileError {
case read(path: String)
case write(path: String, value: Any)
case custom(errorDescription: String?)
}
}
extension AppError.Enums.FileError: LocalizedError {
var errorDescription: String? {
switch self {
case .read(let path): return "Could not read file from \"\(path)\""
case .write(let path, let value): return "Could not write value \"\(value)\" file from \"\(path)\""
case .custom(let errorDescription): return errorDescription
}
}
}
Usage
//let err: Error = NSError(domain:"", code: 401, userInfo: [NSLocalizedDescriptionKey: "Invaild UserName or Password"])
let err: Error = AppError.network(type: .custom(errorCode: 400, errorDescription: "Bad request"))
switch err {
case is AppError:
switch err as! AppError {
case .network(let type): print("Network ERROR: code \(type.errorCode), description: \(type.localizedDescription)")
case .file(let type):
switch type {
case .read: print("FILE Reading ERROR")
case .write: print("FILE Writing ERROR")
case .custom: print("FILE ERROR")
}
case .custom: print("Custom ERROR")
}
default: print(err)
}
Implement LocalizedError:
struct StringError : LocalizedError
{
var errorDescription: String? { return mMsg }
var failureReason: String? { return mMsg }
var recoverySuggestion: String? { return "" }
var helpAnchor: String? { return "" }
private var mMsg : String
init(_ description: String)
{
mMsg = description
}
}
Note that simply implementing Error, for instance, as described in one of the answers, will fail (at least in Swift 3), and calling localizedDescription will result in the string "The operation could not be completed. (.StringError error 1.)"
I still think that Harry's answer is the simplest and completed but if you need something even simpler, then use:
struct AppError {
let message: String
init(message: String) {
self.message = message
}
}
extension AppError: LocalizedError {
var errorDescription: String? { return message }
// var failureReason: String? { get }
// var recoverySuggestion: String? { get }
// var helpAnchor: String? { get }
}
And use or test it like this:
printError(error: AppError(message: "My App Error!!!"))
func print(error: Error) {
print("We have an ERROR: ", error.localizedDescription)
}
let error = NSError(domain:"", code:401, userInfo:[ NSLocalizedDescriptionKey: "Invaild UserName or Password"]) as Error
self.showLoginError(error)
create an NSError object and typecast it to Error ,show it anywhere
private func showLoginError(_ error: Error?) {
if let errorObj = error {
UIAlertController.alert("Login Error", message: errorObj.localizedDescription).action("OK").presentOn(self)
}
}
protocol CustomError : Error {
var localizedTitle: String
var localizedDescription: String
}
enum RequestError : Int, CustomError {
case badRequest = 400
case loginFailed = 401
case userDisabled = 403
case notFound = 404
case methodNotAllowed = 405
case serverError = 500
case noConnection = -1009
case timeOutError = -1001
}
func anything(errorCode: Int) -> CustomError? {
return RequestError(rawValue: errorCode)
}
I know you have already satisfied with an answer but if you are interested to know the right approach, then this might be helpful for you.
I would prefer not to mix http-response error code with the error code in the error object (confused? please continue reading a bit...).
The http response codes are standard error codes about a http response defining generic situations when response is received and varies from 1xx to 5xx ( e.g 200 OK, 408 Request timed out,504 Gateway timeout etc - http://www.restapitutorial.com/httpstatuscodes.html )
The error code in a NSError object provides very specific identification to the kind of error the object describes for a particular domain of application/product/software. For example your application may use 1000 for "Sorry, You can't update this record more than once in a day" or say 1001 for "You need manager role to access this resource"... which are specific to your domain/application logic.
For a very small application, sometimes these two concepts are merged. But they are completely different as you can see and very important & helpful to design and work with large software.
So, there can be two techniques to handle the code in better way:
1. The completion callback will perform all the checks
completionHandler(data, httpResponse, responseError)
2. Your method decides success and error situation and then invokes corresponding callback
if nil == responseError {
successCallback(data)
} else {
failureCallback(data, responseError) // failure can have data also for standard REST request/response APIs
}
Happy coding :)