Goals
Accessing the Dictionaries within an array
Some how get all 1957 Dictionaries decoded without hand coding each ticker name.
The below image is data from https://rapidapi.com/Glavier/api/binance43/ to replicate the below image get Symbol Price Ticker needs to be selected.
With the help of another question which was answered here I have included code below which I am trying to change to accomplish the above goals.
CallApi.swift - this file calls the API and models it to PriceApiModel
import UIKit
class ViewController: UIViewController {
let headers = [
"X-RapidAPI-Key": "Sorry I cannot include this",
"X-RapidAPI-Host": "binance43.p.rapidapi.com"
]
let request = NSMutableURLRequest(url: NSURL(string: "https://binance43.p.rapidapi.com/ticker/price")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
func getData() {
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print("error")
} else {
let httpResponse = response as? HTTPURLResponse
do {
//let dictionary = try JSONSerialization.jsonObject(with: data!, options: [])
let model = try JSONDecoder().decode(PriceApiModel.self, from: data!)
//print(String(model.symbol) + "name") // please see output below
//print(dictionary)
} catch {
print("NOT WORKING ")
}
}
})
dataTask.resume()
}
}
PriceApiModel.swift - I am trying to find a way for this file to be a model for decoding the data
struct PriceApiModel: Hashable, Codable {
//changed the String type to Decimal
var price: String
// every property you are interested to decode needs a CodingKey.
// You can omit values you are not interested in
enum CodingKeys: CodingKey{
case askPrice
}
// here you decode your data into the struct
init(from decoder: Decoder) throws {
// get the container
let container = try decoder.container(keyedBy: CodingKeys.self)
// decode the askPrice into a String and cast it into a Decimal
let askPrice = String(try container.decode(String.self, forKey: .askPrice))
// check if casting was succesfull else throw
guard let askPrice = askPrice else{
throw CustomError.decodingError
}
// assign it
self.askPrice = askPrice
}
}
So I just tried out what you want to achieve here. First of all, you declared a service class (fetching data) as ViewController, by inheritance a UIViewController. It seems to me a bit odd just having this in a class because the UIViewController is not used. Secondly, I would recommend you to watch or read something about Codable for example Hackingforswift. It helped at least me :)
However, here is a Code that shows you a way how it could work:
OptionalObject is needed because of the data structure, holding everything within an array.
struct OptionalObject<Base: Decodable>: Decodable {
public let value: Base?
public init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
self.value = try container.decode(Base.self)
} catch {
self.value = nil
}
}
}
struct PriceApiModel: Codable {
let price: String
let symbol: String
}
enum ServiceError: Error {
case failureAtDecoding
}
// MVVM Pattern https://www.hackingwithswift.com/books/ios-swiftui/introducing-mvvm-into-your-swiftui-project
class ServiceViewModel: ObservableObject {
// Publisher you can subscribe to it.
// Every time the Publisher changes view will re-render.
#Published var priceModel: [PriceApiModel] = []
let headers = [
"X-RapidAPI-Key": "",
"X-RapidAPI-Host": "binance43.p.rapidapi.com"
]
var request = URLRequest(
url: URL(string: "https://binance43.p.rapidapi.com/ticker/price")!,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 5.0
)
init() {
self.getData { priceModel in
// As DocC says:
/// A value that represents either a success or a failure, including an
// So you have to "unwrap" it to handle success or failure
switch priceModel {
case let .success(result):
DispatchQueue.main.async {
self.priceModel = result
}
case let .failure(failure):
print(failure)
}
}
}
func getData(priceModel: #escaping (Result<[PriceApiModel], Error>) -> Void) {
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { (data, response, error) -> Void in
if let error = error {
priceModel(.failure(error))
} else if let data = data {
let model = try? JSONDecoder().decode([OptionalObject<PriceApiModel>].self, from: data)
let editModel = model?.compactMap {
PriceApiModel(price: $0.value?.price ?? "nil", symbol: $0.value?.symbol ?? "nil")
}
if let editModel = editModel {
priceModel(.success(editModel))
} else {
priceModel(.failure(ServiceError.failureAtDecoding))
}
}
}
dataTask.resume()
}
}
struct ContentView: View {
// Initialize the ServiceViewModel as StateObject
#StateObject var viewModel: ServiceViewModel = .init()
var body: some View {
NavigationView {
List {
ForEach(viewModel.priceModel, id: \.symbol) { model in
HStack {
Text(model.symbol)
Spacer()
Text(model.price)
}
}
}
}
}
}
Hope I could help.
Related
I have created a utility class in my Swift project that handles all the REST requests and responses. I have built a simple REST API so I can test my code. I have created a class method that needs to return an NSArray but because the API call is async I need to return from the method inside the async call. The problem is the async returns void.
If I were doing this in Node I would use JS promises but I can't figure out a solution that works in Swift.
import Foundation
class Bookshop {
class func getGenres() -> NSArray {
println("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
println(urlPath)
let url: NSURL = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
var resultsArray:NSArray!
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if(error) {
println(error.localizedDescription)
}
var err: NSError?
var options:NSJSONReadingOptions = NSJSONReadingOptions.MutableContainers
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &err) as NSDictionary
if(err != nil) {
println("JSON Error \(err!.localizedDescription)")
}
//NSLog("jsonResults %#", jsonResult)
let results: NSArray = jsonResult["genres"] as NSArray
NSLog("jsonResults %#", results)
resultsArray = results
return resultsArray // error [anyObject] is not a subType of 'Void'
})
task.resume()
//return "Hello World!"
// I want to return the NSArray...
}
}
You can pass callback, and call callback inside async call
something like:
class func getGenres(completionHandler: (genres: NSArray) -> ()) {
...
let task = session.dataTaskWithURL(url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
and then call this method:
override func viewDidLoad() {
Bookshop.getGenres {
genres in
println("View Controller: \(genres)")
}
}
Introduced in Swift 5.5 (iOS 15, macOS 12), we would now use the async-await pattern:
func fetchGenres() async throws -> [Genre] {
…
let (data, _) = try await URLSession.shared.dataTask(for: request)
return try JSONDecoder().decode([Genre].self, from: data)
}
And we would call it like:
let genres = try await fetchGenres()
The async-await syntax is far more concise and natural than the traditional completion handler pattern outlined in my original answer, below.
For more information, see Meet async/await in Swift.
The historic pattern is to use completion handlers closure.
For example, we would often use Result:
func fetchGenres(completion: #escaping (Result<[Genre], Error>) -> Void) {
...
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
// parse response here
let results = ...
DispatchQueue.main.async {
completion(.success(results))
}
}.resume()
}
And you’d call it like so:
fetchGenres { results in
switch results {
case .failure(let error):
print(error.localizedDescription)
case .success(let genres):
// use `genres` here, e.g. update model and UI
}
}
// but don’t try to use `genres` here, as the above runs asynchronously
Note, above I’m dispatching the completion handler back to the main queue to simplify model and UI updates. Some developers take exception to this practice and either use whatever queue URLSession used or use their own queue (requiring the caller to manually synchronize the results themselves).
But that’s not material here. The key issue is the use of completion handler to specify the block of code to be run when the asynchronous request is done.
Note, above I retired the use of NSArray (we don’t use those bridged Objective-C types any more). I assume that we had a Genre type and we presumably used JSONDecoder, rather than JSONSerialization, to decode it. But this question didn’t have enough information about the underlying JSON to get into the details here, so I omitted that to avoid clouding the core issue, the use of closures as completion handlers.
Swiftz already offers Future, which is the basic building block of a Promise. A Future is a Promise that cannot fail (all terms here are based on the Scala interpretation, where a Promise is a Monad).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Hopefully will expand to a full Scala-style Promise eventually (I may write it myself at some point; I'm sure other PRs would be welcome; it's not that difficult with Future already in place).
In your particular case, I would probably create a Result<[Book]> (based on Alexandros Salazar's version of Result). Then your method signature would be:
class func fetchGenres() -> Future<Result<[Book]>> {
Notes
I do not recommend prefixing functions with get in Swift. It will break certain kinds of interoperability with ObjC.
I recommend parsing all the way down to a Book object before returning your results as a Future. There are several ways this system can fail, and it's much more convenient if you check for all of those things before wrapping them up into a Future. Getting to [Book] is much better for the rest of your Swift code than handing around an NSArray.
Swift 4.0
For async Request-Response you can use completion handler. See below I have modified the solution with completion handle paradigm.
func getGenres(_ completion: #escaping (NSArray) -> ()) {
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
let results = jsonResult["genres"] as! NSArray
print(results)
completion(results)
}
} catch {
//Catch Error here...
}
}
task.resume()
}
You can call this function as below:
getGenres { (array) in
// Do operation with array
}
Swift 3 version of #Alexey Globchastyy's answer:
class func getGenres(completionHandler: #escaping (genres: NSArray) -> ()) {
...
let task = session.dataTask(with:url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
Swift 5.5, async/wait-based solution
The original test URL provided by the original poster is no longer functional, so I had to change things a bit. This solution is based on a jokes API I found. That API returns a single joke, but I return it as an array of String ([String]), to keep it as consistent as possible with the original post.
class Bookshop {
class func getGenres() async -> [String] {
print("Hello inside getGenres")
let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
typealias Continuation = CheckedContinuation<[String], Never>
let genres = await withCheckedContinuation { (continuation: Continuation) in
let task = session.dataTask(with: url) { data, response, error in
print("Task completed")
var result: [String] = []
defer {
continuation.resume(returning: result)
}
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
print("jsonResult is \(jsonResult)")
if let joke = (jsonResult as? [String: String])?["joke"] {
result = [joke]
}
} catch {
print("JSON Error \(error.localizedDescription)")
print("data was \(String(describing: String(data: data, encoding: .utf8)))")
return
}
}
task.resume()
}
return genres
}
}
async {
let final = await Bookshop.getGenres()
print("Final is \(final)")
}
The withCheckedContinuation is how you made the Swift async function actually run in a separate task/thread.
I hope you're not still stuck on this, but the short answer is that you can't do this in Swift.
An alternative approach would be to return a callback that will provide the data you need as soon as it is ready.
There are 3 ways of creating call back functions namely:
1. Completion handler
2. Notification
3. Delegates
Completion Handler
Inside set of block is executed and returned when source is available, Handler will wait until response comes so that UI can be updated after.
Notification
Bunch of information is triggered over all the app, Listner can retrieve n make use of that info. Async way of getting info through out the project.
Delegates
Set of methods will get triggered when delegate is been called, Source must be provided via methods itself
Swift 5.5:
TL;DR: Swift 5.5 is not yet released(at the time of writing). To use swift 5.5, download swift toolchain development snapshot from here and add compiler flag -Xfrontend -enable-experimental-concurrency. Read more here
This can be achieved easily with async/await feature.
To do so, you should mark your function as async then do the operation inside withUnsafeThrowingContinuation block like following.
class Bookshop {
class func getGenres() async throws -> NSArray {
print("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
return try await withUnsafeThrowingContinuation { continuation in
let task = session.dataTask(with: url, completionHandler: {data, response, error -> Void in
print("Task completed")
if(error != nil) {
print(error!.localizedDescription)
continuation.resume(throwing: error!)
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
let results: NSArray = jsonResult!["genres"] as! NSArray
continuation.resume(returning: results)
} catch {
continuation.resume(throwing: error)
}
})
task.resume()
}
}
}
And you can call this function like
#asyncHandler
func check() {
do {
let genres = try await Bookshop.getGenres()
print("Result: \(genres)")
} catch {
print("Error: \(error)")
}
}
Keep in mind that, when calling Bookshop.getGenres method, the caller method should be either async or marked as #asyncHandler
self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) in
self.endNetworkActivity()
var responseError: Error? = error
// handle http response status
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode > 299 , httpResponse.statusCode != 422 {
responseError = NSError.errorForHTTPStatus(httpResponse.statusCode)
}
}
var apiResponse: Response
if let _ = responseError {
apiResponse = Response(request, response as? HTTPURLResponse, responseError!)
self.logError(apiResponse.error!, request: request)
// Handle if access token is invalid
if let nsError: NSError = responseError as NSError? , nsError.code == 401 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Unautorized access
// User logout
return
}
}
else if let nsError: NSError = responseError as NSError? , nsError.code == 503 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Down time
// Server is currently down due to some maintenance
return
}
}
} else {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
self.logResponse(data!, forRequest: request)
}
self.removeRequestedURL(request.url!)
DispatchQueue.main.async(execute: { () -> Void in
completionHandler(apiResponse)
})
}).resume()
There are mainly 3 ways of achieving callback in swift
Closures/Completion handler
Delegates
Notifications
Observers can also be used to get notified once the async task has been completed.
There are some very generic requirements that would like every good API Manager to satisfy:
will implement a protocol-oriented API Client.
APIClient Initial Interface
protocol APIClient {
func send(_ request: APIRequest,
completion: #escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Now Please check complete api structure
// ******* This is API Call Class *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void
/// Implementation of a generic-based API client
public class APIClient {
private let baseEndpointUrl = URL(string: "irl")!
private let session = URLSession(configuration: .default)
public init() {
}
/// Sends a request to servers, calling the completion method when finished
public func send<T: APIRequest>(_ request: T, completion: #escaping ResultCallback<DataContainer<T.Response>>) {
let endpoint = self.endpoint(for: request)
let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
if let data = data {
do {
// Decode the top level response, and look up the decoded response to see
// if it's a success or a failure
let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)
if let dataContainer = apiResponse.data {
completion(.success(dataContainer))
} else if let message = apiResponse.message {
completion(.failure(APIError.server(message: message)))
} else {
completion(.failure(APIError.decoding))
}
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
task.resume()
}
/// Encodes a URL based on the given request
/// Everything needed for a public request to api servers is encoded directly in this URL
private func endpoint<T: APIRequest>(for request: T) -> URL {
guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
fatalError("Bad resourceName: \(request.resourceName)")
}
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!
// Common query items needed for all api requests
let timestamp = "\(Date().timeIntervalSince1970)"
let hash = "\(timestamp)"
let commonQueryItems = [
URLQueryItem(name: "ts", value: timestamp),
URLQueryItem(name: "hash", value: hash),
URLQueryItem(name: "apikey", value: "")
]
// Custom query items needed for this specific request
let customQueryItems: [URLQueryItem]
do {
customQueryItems = try URLQueryItemEncoder.encode(request)
} catch {
fatalError("Wrong parameters: \(error)")
}
components.queryItems = commonQueryItems + customQueryItems
// Construct the final URL with all the previous data
return components.url!
}
}
// ****** API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
/// Response (will be wrapped with a DataContainer)
associatedtype Response: Decodable
/// Endpoint for this request (the last part of the URL)
var resourceName: String { get }
}
// ****** This Results type Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
public let offset: Int
public let limit: Int
public let total: Int
public let count: Int
public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
case encoding
case decoding
case server(message: String)
}
// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
/// Whether it was ok or not
public let status: String?
/// Message that usually gives more information about some error
public let message: String?
/// Requested data
public let data: DataContainer<Response>?
}
// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
let parametersData = try JSONEncoder().encode(encodable)
let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
return parameters.map { URLQueryItem(name: $0, value: $1.description) }
}
}
// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
case string(String)
case bool(Bool)
case int(Int)
case double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw APIError.decoding
}
}
var description: String {
switch self {
case .string(let string):
return string
case .bool(let bool):
return String(describing: bool)
case .int(let int):
return String(describing: int)
case .double(let double):
return String(describing: double)
}
}
}
/// **** This is your API Request Endpoint Method in Struct *****
public struct GetCharacters: APIRequest {
public typealias Response = [MyCharacter]
public var resourceName: String {
return "characters"
}
// Parameters
public let name: String?
public let nameStartsWith: String?
public let limit: Int?
public let offset: Int?
// Note that nil parameters will not be used
public init(name: String? = nil,
nameStartsWith: String? = nil,
limit: Int? = nil,
offset: Int? = nil) {
self.name = name
self.nameStartsWith = nameStartsWith
self.limit = limit
self.offset = offset
}
}
// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
public let id: Int
public let name: String?
public let description: String?
}
// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
let apiClient = APIClient()
// A simple request with no parameters
apiClient.send(GetCharacters()) { response in
response.map { dataContainer in
print(dataContainer.results)
}
}
}
This is a small use case that might be helpful:-
func testUrlSession(urlStr:String, completionHandler: #escaping ((String) -> Void)) {
let url = URL(string: urlStr)!
let task = URLSession.shared.dataTask(with: url){(data, response, error) in
guard let data = data else { return }
if let strContent = String(data: data, encoding: .utf8) {
completionHandler(strContent)
}
}
task.resume()
}
While calling the function:-
testUrlSession(urlStr: "YOUR-URL") { (value) in
print("Your string value ::- \(value)")
}
I know there are already some articles regarding this issue, but I could not find anything related to my JSON.
This is how my JSON likes like:
{
"message": {
"affenpinscher": [],
"african": [],
"airedale": [],
"akita": [],
"appenzeller": [],
"australian": [
"shepherd"
],
"basenji": []
},
"status: "succes"
}
So, if I understand it correctly it is dictionary because it starts with {, but what are the things inside the "message"?
This is my Dog.swift class where I am re-writing the JSON, but I am not sure if it is correct:
class Dog: Decodable, Identifiable {
var message: Message?
var status: String?
}
struct Message: Decodable {
var affenpinscher: [String:[String]]?
var african: [String]?
var airedale: [String]?
var akita: [String]?
var appenzeller: [String]?
var australian: [String]?
var basenji: [String]?
}
As you can see in the first value I was trying to play with data types, but no success.
I am decoding and parsing JSON here:
class ContentModel: ObservableObject {
#Published var dogs = Message()
init() {
getDogs()
}
func getDogs(){
// Create URL
let urlString = Constants.apiUrl
let url = URL(string: urlString)
if let url = url {
// Create URL request
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 10)
request.httpMethod = "GET"
// Get URLSession
let session = URLSession.shared
// Create Data Task
let dataTask = session.dataTask(with: request) { (data, response, error) in
// Check that there is not an error
if error == nil {
do {
// Parse JSON
let decoder = JSONDecoder()
let result = try decoder.decode(Dog.self, from: data!)
print(result)
// Assign result to the dogs property
DispatchQueue.main.async {
self.dogs = result.message!
}
} catch {
print(error)
}
}
}
// Start the Data Task
dataTask.resume()
}
}
}
And here I would love to iterate through it eventually, which I also have no idea how to do it:
struct ContentView: View {
#EnvironmentObject var model: ContentModel
var body: some View {
NavigationView {
ScrollView {
LazyVStack {
if model.dogs != nil {
// ForEach(Array(model.dogs.keys), id: \.self) { d in
// Text(d)
// }
}
}
.navigationTitle("All Dogs")
}
}
}
}
What can I try next to resolve this?
First of all don't use classes for a JSON model and to conform to Identifiable you have to add an id property and CodingKeys if there is no key id in the JSON.
My suggestion is to map the unhandy [String: [String]] dictionary to an array of an extra struct
I renamed Dog as Response and named the extra struct Dog
struct Dog {
let name : String
let types : [String]
}
struct Response: Decodable, Identifiable {
private enum CodingKeys: String, CodingKey { case message, status }
let id = UUID()
let dogs: [Dog]
let status: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(String.self, forKey: .status)
let message = try container.decode([String:[String]].self, forKey: .message)
dogs = message.map(Dog.init).sorted{$0.name < $1.name}
}
}
In the model declare
#Published var dogs = [Dog]()
and decode
let result = try decoder.decode(Response.self, from: data!)
print(result)
// Assign result to the dogs property
DispatchQueue.main.async {
self.dogs = result.dogs
}
The dogs array can be displayed seamlessly in a List
PS: Actually appenzeller is supposed to be
"appenzeller": ["sennenhund"],
or correctly in English
"appenzell": ["mountain dog"],
😉😉😉
Using the native Swift approach that #vadian answered is a much lighter weight solution, but if you work with JSON often I'd recommend using SwiftyJSON.
You can parse the URL data task response into a Swifty json object like so:
import SwiftyJSON
guard let data = data, let json = try? JSON(data: data) else {
return
}
// Make sure the json fetch was successful
if json["status"].stringValue != "success" {
return
}
Then you can access the message object safely without the verbosity of using Decodable. Here the message is parsed into an array of dog structs:
struct Dog {
let name : String
let types : [String]
}
var dogs: [Dog] = []
/// Load the docs into an array
for (name, typesJson) in json["message"].dictionaryValue {
dogs.append(Dog(name: name, types: typesJson.arrayValue.map { $0.stringValue }))
}
print("dogs", dogs)
Apologies if this is a basic question, I am new using Apis and JSON in Swift. I am attempting to submit a post request but am receiving:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (__SwiftValue)'.
I believe that this is due to a incorrect/unconvertible type but I have tried multiple different permutations of the variables I am passing and it continues to fail.
Here is my ContentView:
struct ContentView: View {
#State var town: Space = Space(title: "test city", description: "is this working")
var body: some View {
Button(action: {
Api.postRequest(param: ["space" : town], urlString: Api.spacePostUrl) { (update) in
print("\(update)")
}
}) {
Text("Post Request")
}
}
}
The underlying data struct:
struct Space: Codable {
var title: String
var description: String
}
And my attempted API call:
class Api {
static let spacePostUrl = "http://localhost:3001/spaces"
static let spaceGetUrl = "http://localhost:3001/"
static func postRequest(param: [String : Codable], urlString: String, completion: #escaping (Int) -> ()) {
guard let url = URL(string: urlString) else { return }
let body = try? JSONSerialization.data(withJSONObject: param)
var request = URLRequest(url: url)
request.httpBody = body
request.httpMethod = "POST"
URLSession.shared.dataTask(with: request) { (data, request, error) in
guard let update = data else { return }
do {
let update = try JSONDecoder().decode(Int.self, from: update)
DispatchQueue.main.async {
completion(update)
}
}
catch {
print(error)
}
}
}
}
class Api {
static let spacePostUrl = "http://localhost:3001/spaces"
static let spaceGetUrl = "http://localhost:3001/"
static func postRequest(param: [String : Codable], urlString: String, completion: #escaping (Int) -> ()) {
guard let url = URL(string: urlString) else { return }
let body = try? JSONSerialization.data(withJSONObject: param)
var request = URLRequest(url: url)
request.httpBody = body
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request) { (data, request, error) in
guard let update = data else { return }
do {
let update = try JSONDecoder().decode(Space.self, from: update)
DispatchQueue.main.async {
completion(update)
}
}
catch {
print(error)
}
}
task.resume()
}
}
Beginner here, in a bit over my head with this. ;)
I've found examples that have shown me how to get data from a JSON API feed if the feed is structured as an array of objects, but I don't know how to approach getting the data (specifically, url and title) if the data I'm retrieving comes back in a more complex nested structure like this one:
{
"races": {
"videos": [{
"id": 1,
"url": "firsturl",
"title": "1st Video Title"
}, {
"id": 2,
"url": "secondurl",
"title": "2nd Video Title"
}]
}
}
I've succeeded at get data from another API feed that's structured as a simple array of objects--it's like what's above but without the extra two lead-in objects, namely this: { "races": { "videos":
Here's the code I pieced together from a few examples that worked for the simple array:
import SwiftUI
struct Video: Codable, Identifiable {
public var id: Int
public var url: String
public var title: String
}
class Videos: ObservableObject {
#Published var videos = [Video]()
init() {
let url = URL(string: "https://exampledomain.com/jsonapi")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let videoData = data {
let decodedData = try JSONDecoder().decode([Video].self, from: videoData)
DispatchQueue.main.async {
self.videos = decodedData
}
} else {
print("no data found")
}
} catch {
print("an error occurred")
}
}.resume()
}
}
struct VideosView: View {
#ObservedObject var fetch = Videos()
var body: some View {
VStack {
List(fetch.videos) { video in
VStack(alignment: .leading) {
Text(video.title)
Text("\(video.url)")
}
}
}
}
}
I've spent several hours over a few days reading and watching tutorials, but so far nothing is sinking in to help me tackle the more complex JSON API feed. Any tips would be greatly appreciated!
UPDATE:
With the help of a Swift Playground tutorial and the suggested structs mentioned in the comments below, I've succeeded at retrieving the more complex data, but only in Swift Playgrounds, using this:
import SwiftUI
struct Welcome: Codable {
let races: Races
}
struct Races: Codable {
let videos: [Video]
}
struct Video: Codable {
let id: Int
let url, title: String
}
func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
getJSON(urlString: "https://not-the-real-domain.123/api/") { (followers:Welcome?) in
if let followers = followers {
for result in followers.races.videos {
print(result.title )
}
}
}
Now, I'm struggling with how to properly integrate this Playgrounds snippet in to the working SwiftUI file's VideosViews, etc.
UPDATE 2:
import SwiftUI
struct Welcome: Codable {
let races: RaceItem
}
struct RaceItem: Codable {
let videos: [VideoItem]
}
struct VideoItem: Codable {
let id: Int
let url: String
let title: String
}
class Fetcher: ObservableObject {
func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
}
struct JSONRacesView: View {
#ObservedObject var fetch = Fetcher()
getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
if let followers = followers {
for result in followers.races.videos {
print(result.title )
}
}
}
var body: some View {
VStack {
List(fetch.tracks) { track in
VStack(alignment: .leading) {
Text(track.title)
Text("\(track.url)")
}
}
}
}
There's a great site called QuickType (app.quicktype.io) where you can paste in some JSON and get the Swift structs generated for you. Here's what it gives you:
import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let races: Races
}
// MARK: - Races
struct Races: Codable {
let videos: [Video]
}
// MARK: - Video
struct Video: Codable {
let id: Int
let url, title: String
}
They have a bug in their template generator that mangles the demo line (I've submitted a pull request that is merged but isn't live on the site at the time of this writing), but here's what it should look like:
let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
Using do/try so you can catch the errors, you can decode the data and reach the lower levels by doing:
do {
let welcome = try JSONDecoder().decode(Welcome.self, from: jsonData)
let videos = welcome.races.videos //<-- this ends up being your [Video] array
} catch {
//handle any errors
}
Update, based on your comments and updates:
You chose to go a little bit of a different route than my initial suggestion, but that's fine. The only thing that I would suggest is that you might want to deal with handling errors at some point rather than just returning nil in all of the completions (assuming you need to handle errors -- maybe it just not loading is acceptable).
Here's a light refactor of your code:
class Fetcher: ObservableObject {
#Published var tracks : [VideoItem] = []
private func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
func fetchData() {
getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
DispatchQueue.main.async {
self.tracks = followers?.races.videos ?? []
}
}
}
}
struct JSONRacesView: View {
#StateObject var fetch = Fetcher()
var body: some View {
VStack {
List(fetch.tracks, id: \.id) { track in
VStack(alignment: .leading) {
Text(track.title)
Text("\(track.url)")
}
}
}.onAppear {
fetch.fetchData()
}
}
}
You can see that now Fetcher has a #Published property that will store the tracks ([VideoItem]). getJSON is still in fetcher, but now it's private just to show that it isn't meant to be called directly. But, now there's a new function called fetchData() that your view will call. When fetchData gets data back, it sets the #Published property to that data. I used the ?? operator to tell the compiler that if followers is nil, then just use [] instead. This is all in a DispatchQueue.main.async block because the URL call is probably not going to return on the main thread and we need to make sure to always update the UI on the main thread (Xcode will warn you about this at runtime if you update the UI on a different thread).
JSONRacesView calls fetchData in onAppear, which happens exactly when it sounds like it will.
Last thing to note is I used #StateObject instead of #ObservedObject. If you're not on iOS 14 or macOS 11 yet, you could use #ObservedObject instead. There are some differences outside the scope of this answer, but that are easily Google-able.
So I am trying to fetch data from the Pokemon API, and I am getting stuck at the point where I am trying to decode the JSON into a struct. Here is my code:
{
"count":1118,
"next":"https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20",
"previous":null,
"results":
[
{"name":"bulbasaur","url":"https://pokeapi.co/api/v2/pokemon/1/"},
{"name":"ivysaur","url":"https://pokeapi.co/api/v2/pokemon/2/"},
{"name":"venusaur","url":"https://pokeapi.co/api/v2/pokemon/3/"},
{"name":"charmander","url":"https://pokeapi.co/api/v2/pokemon/4/"},
{"name":"charmeleon","url":"https://pokeapi.co/api/v2/pokemon/5/"},
{"name":"charizard","url":"https://pokeapi.co/api/v2/pokemon/6/"},
{"name":"squirtle","url":"https://pokeapi.co/api/v2/pokemon/7/"},
{"name":"wartortle","url":"https://pokeapi.co/api/v2/pokemon/8/"},
{"name":"blastoise","url":"https://pokeapi.co/api/v2/pokemon/9/"},
{"name":"caterpie","url":"https://pokeapi.co/api/v2/pokemon/10/"},
{"name":"metapod","url":"https://pokeapi.co/api/v2/pokemon/11/"},
{"name":"butterfree","url":"https://pokeapi.co/api/v2/pokemon/12/"},
{"name":"weedle","url":"https://pokeapi.co/api/v2/pokemon/13/"},
{"name":"kakuna","url":"https://pokeapi.co/api/v2/pokemon/14/"},
{"name":"beedrill","url":"https://pokeapi.co/api/v2/pokemon/15/"},
{"name":"pidgey","url":"https://pokeapi.co/api/v2/pokemon/16/"},
{"name":"pidgeotto","url":"https://pokeapi.co/api/v2/pokemon/17/"},
{"name":"pidgeot","url":"https://pokeapi.co/api/v2/pokemon/18/"},
{"name":"rattata","url":"https://pokeapi.co/api/v2/pokemon/19/"},
{"name":"raticate","url":"https://pokeapi.co/api/v2/pokemon/20/"}
]
}
func fetchPokemon() {
let defaultSession = URLSession(configuration: .default)
if let url = URL(string: "https://pokeapi.co/api/v2/pokemon/") {
let request = URLRequest(url:url)
let dataTask = defaultSession.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
guard error == nil else {
print ("error: ", error!)
return
}
guard data != nil else {
print("No data object")
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("response is: ", response!)
return
}
guard let mime = response?.mimeType, mime == "application/json" else {
print("Wrong MIME type!")
return
}
DispatchQueue.main.async {
guard let result = try? JSONDecoder().decode(PokemonList.self, from: data!) else {
print("Error Parsing JSON")
return
}
let pokemon = result.pokemon
self.Pokemon = pokemon
print(self.Pokemon)
}
})
dataTask.resume()
}
}
and here is the pokemon struct:
struct Pokemon {
// Various properties of a post that we either need or want to display
let name: String
let url: String
}
extension Pokemon: Decodable {
// properties within a Post returned from the Product Hunt API that we want to extract the info from.
enum PokemonKeys: String, CodingKey {
// first three match our variable names for our Post struct
case name = "name"
case url = "url"
}
init(from decoder: Decoder) throws {
let postsContainer = try decoder.container(keyedBy: PokemonKeys.self)
name = try postsContainer.decode(String.self, forKey: .name)
url = try postsContainer.decode(String.self, forKey: .url)
}
}
struct PokemonList: Decodable {
var pokemon: [Pokemon]
}
It keeps reaching the point when decoding which says "Error Parsing JSON". I'm assuming that there may be an error in how I setup the pokemon struct?
Any ideas?
you are getting a parse error because the data model is not the same. your struct should be:
struct PokemonList: Decodable {
var results: [Pokemon]
var count: Int
var next: String
}
you don't need the extension.