I am working on an app that fetches the data from JSON and displays it.
However, I am stuck with an error saying Instance method 'appendInterpolation(_:formatter:)' requires that '[String : Int]' inherit from 'NSObject'
Here is my data structure:
struct Data: Codable {
var message: String
var data: Objects
}
struct Objects: Codable {
var date: String
var day: Int
var resource: String
var stats, increase: [String: Int]
}
Function to fetch the data:
func getData() {
let urlString = "https://russianwarship.rip/api/v1/statistics/latest"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) { data, _, error in
if let data = data {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(Data.self, from: data)
self.data = decodedData
} catch {
print("Hey there's an error: \(error.localizedDescription)")
}
}
}.resume()
}
And a ContentView with the #State property to pass the placeholder data:
struct ContentView: View {
#State var data = Data(message: "", data: Objects(date: "123", day: 123, resource: "", stats: ["123" : 1], increase: ["123" : 1]))
var body: some View {
VStack {
Button("refresh") { getData() }
Text("\(data.data.date)")
Text("\(data.data.day)")
Text(data.message)
Text("\(data.data.stats)") //error is here
Here is an example of JSON response
I wonder if the problem is in data structure, because both
Text("\(data.data.date)")
Text("\(data.data.day)")
are working just fine. If there are any workarounds with this issue – please, I would highly appreciate your help!:)
stats is [String: Int], and so when you want to use it, you need to supply the key to get the value Int, the result is an optional that you must unwrap or supply a default value in Text
So use this:
Text("\(data.data.stats["123"] ?? 0)")
And as mentioned in the comments, do not use Data for your struct name.
EDIT-1: there are two ways you can make the struct fields camelCase; one is using the CodingKeys as shown in ItemModel, or at the decoding stage, as shown in the getData() function. Note, I've also updated your models to make them easier to use.
struct DataModel: Codable {
var message: String
var data: ObjectModel
}
struct ObjectModel: Codable {
var date: String
var day: Int
var resource: String
var stats: ItemModel
var increase: ItemModel
}
struct ItemModel: Codable {
var personnelUnits: Int
var tanks: Int
var armouredFightingVehicles: Int
// ...
// manual CodingKeys
// enum CodingKeys: String, CodingKey {
// case tanks
// case personnelUnits = "personnel_units"
// case armouredFightingVehicles = "armoured_fighting_vehicles"
// }
}
struct ContentView: View {
#State var dataModel = DataModel(message: "", data: ObjectModel(date: "123", day: 123, resource: "", stats: ItemModel(personnelUnits: 123, tanks: 456, armouredFightingVehicles: 789), increase: ItemModel(personnelUnits: 3, tanks: 4, armouredFightingVehicles: 5)))
var body: some View {
VStack {
Button("get data from Server") { getData() }
Text("\(dataModel.data.date)")
Text("\(dataModel.data.day)")
Text(dataModel.message)
Text("\(dataModel.data.stats.armouredFightingVehicles)") // <-- here
}
}
func getData() {
let urlString = "https://russianwarship.rip/api/v1/statistics/latest"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let data = data {
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // <-- here
dataModel = try decoder.decode(DataModel.self, from: data)
} catch {
print("--> error: \(error)")
}
}
}.resume()
}
}
}
Related
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)
Here is my code. I am pulling JSON data from CalorieNinjas API:
struct Result: Codable {
var items: [FoodItem]?
}
struct FoodItem: Codable {
var name: String?
var calories: String?
}
public class API {
func apiRequest(search: String, completion: #escaping (Result) -> ()) {
//URL
var query = search.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let url = URL(string: "https://calorieninjas.p.rapidapi.com/v1/nutrition?query=" + query!)
//URL REQUEST
var request = URLRequest(url: url!, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
//Specify header
let headers = [
"x-rapidapi-key": "3be44a36b7msh4d4738910c1ca4dp1c2825jsn96bcc44c2b19",
"x-rapidapi-host": "calorieninjas.p.rapidapi.com"
]
request.httpMethod="GET"
request.allHTTPHeaderFields = headers
//Get the URLSession
let session = URLSession.shared
//Create data task
let dataTask = session.dataTask(with: request) { (data, response, error) in
let result = try? JSONDecoder().decode(Result.self, from: data!)
print(result)
DispatchQueue.main.async {
completion(result!)
}
}
//Fire off data task
dataTask.resume()
}
}
this is what my view looks like:
struct ContentView: View {
#State var result = Result()
#State private var searchItem: String = ""
var body: some View {
ZStack(alignment: .top) {
Rectangle()
.fill(Color.myPurple)
.ignoresSafeArea(.all)
VStack {
TextField("Enter food", text: $searchItem)
.background(Color.white)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
SearchButton()
.padding(.top)
.onTapGesture {
API().apiRequest(search: searchItem, completion: { (result) in
self.result = result
})
}
}
}
}
}
This is the output to the terminal as a result of my print statement so I know my data is being fetched and stored:
Optional(CalorieCountApp.Result(items: Optional([CalorieCountApp.FoodItem(name: Optional("pizza"), calories: Optional(262.9))])))
what I was trying to do was something like Text(result.items.name/calories) but I am not able to access the variables like that. I am new to swift and making apps as a whole any help is much appreciated
Looks like you have a few Optionals in there, which means you'll probably be using the ? operator to unwrap them.
Given your type, this should work:
let index = 0
let name = result?.items?[index].name // will be `String?`
let calories = result?.items?[index].calories // according to your code you provided, this says `String?` but in your console output it looks like `Double?`
or in your example:
Text(result?.items?[index].name ?? "unknown")
You might want to do some more reading about unwrapping Optionals or dealing with nil in Swift -- there are a few different strategies. For example, you can see I used ?? in the last example there.
Here's a helpful link: https://www.hackingwithswift.com/sixty/10/2/unwrapping-optionals
I am new to Swift and SwiftUI. I am currently teaching myself how to code (loving it!) through hackingwithswift.com I am currently on Day 60 and I am stuck and not sure what to do from here.
The challenge is to decode some information using Codable and populate SwiftUI.
I created a struct to match the JSON, but when I go to run the app, I keep getting my error "Fetch Failed: Unknown Error" and therefore my UI won't update.
Would someone glance at my code and provide any pointers on where I am going wrong and possibly why? Thank you so much for any suggestions and help, it is much appreciated! Code is posted below.
Cody
import SwiftUI
struct Response: Codable {
var results: [User]
}
struct User: Codable, Identifiable {
let id: String
let isActive: Bool
let name: String
let age: Int
let company: String
let email: String
let address: String
let about: String
let registered: String
let tags: [String]
struct FriendRole: Codable {
let id: String
let name: String
}
let friend: [FriendRole]
}
struct ContentView: View {
#State private var results = [User]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.address)
}
}
.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.results = decodedResponse.results
}
return
}
}
print("Fetch Failed: \(error?.localizedDescription ?? "Unkown Error").")
}.resume()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm also new to swiftUI this is how I managed to make it work by using Observable Objects
Here's the structures Since in the json file there is a [ in the very beginning you do not have to create a struct that has a User array you could create a struct and then create a variable of that struct type as an array
here's how I did it
I created separate files for instance for the structures i had a different file for them
here's what I have for my structure file
import Foundation
struct User : Codable, Identifiable {
let id : String
let isActive : Bool
let name : String
let age : Int
let company : String
let email : String
let address : String
let about : String
let registered : String
let tags = [String]()
let friends = [Friends]()
}
struct Friends : Codable {
let id : String
let name : String
}
I created another file for the observable object class
class JsonChannel : ObservableObject {
#Published var retVal = [User]()
func getInfo () {
guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {return}
URLSession.shared.dataTask(with: url) { (data, resp, err) in
if let data = data {
DispatchQueue.main.async {
do {
self.retVal = try JSONDecoder().decode([User].self, from: data)
}
catch {
print(error)
}
}
}
}.resume()
}
}
and here's what i have for my contentView file
import SwiftUI
struct ContentView : View {
#ObservedObject var info = JsonChannel()
var body: some View {
VStack {
Button(action: {
self.info.getInfo()
}) {
Text("click here to get info")
}
List {
ForEach (self.info.retVal) { item in
VStack {
Text("\(item.name)")
Text("\(item.address)")
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have problem with showing JSON Data in SwiftUI, I get the data from Genius API I currently search for song and can confirm that I get the data extracted correctly; example I can print out the title of the result:
This is how I fetch the data
class NetworkManager: ObservableObject {
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
var fetchedSongsResults = [hits]() {
willSet {
objectWillChange.send(self)
}
}
init() {
fetchSongs()
}
func fetchSongs() {
guard let url = URL(string: "https://api.genius.com/search?q=Sia") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.setValue("Bearer TOKEN", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: urlRequest) {data, response, error in
guard let data = data else { return }
//print(String(decoding: data, as: UTF8.self))
let songs = try! JSONDecoder().decode(feed.self, from: data)
DispatchQueue.main.async {
self.fetchedSongsResults = songs.response.hits
}
}.resume()
}
}
So when I get the data I save to the variable fetchedSongsResults and this seems correctly but for what ever reason when I try to print the count for example it says that i empty and also I can't loop through the fetchedSongsResults using a list or ForEach this is how, (which I believe s because I have not made the model identifiable) I tried to print the count of fetchedSongsResults,
This initialized outside the body (just so you know)
#State var networkManager = NetworkManager()
This is inside the body
Text("\(networkManager.fetchedSongsResults.count)")
If your are wondering how my structure looks like when I decode the JSON Data then here it is
struct feed: Codable {
var meta: meta
var response: response
}
struct meta: Codable {
var status: Int
}
struct response: Codable {
var hits: [hits]
}
struct hits: Codable {
var index: String
var type: String
var result: song
}
struct song: Codable, Identifiable {
var id: Int
var header_image_thumbnail_url: String
var url: String
var title: String
var lyrics_state: String
var primary_artist: artist
}
struct artist: Codable {
var name: String
}
Try: #ObservedObject var networkManager = NetworkManager().
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//API Key: 5ca10b2d20a545099a108a3aeceb329c
//url: https://newsapi.org/v2/top-headlines?country=us&apiKey=5ca10b2d20a545099a108a3aeceb329c
// model
struct Source: Decodable {
var id: String
var name: String
}
struct Articles: Decodable {
var source: Source
var author: String
var title: String
var description: String
var url: String
var urlToImage: String
var publishedAt: String
var content: String
}
struct JSONDescription: Decodable {
var status: String
var totalResults: Int
var articles: Articles
}
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&apiKey=5ca10b2d20a545099a108a3aeceb329c") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
//let dataAsString = String(data: data, encoding: .utf8)
// print(dataAsString)
do {
let jsonDescription = try JSONDecoder().decode(JSONDescription.self, from: data)
print(jsonDescription.totalResults)
}
catch let jsonError {
print("Json Error:", jsonError)
}
}.resume()
}
}
What I expected to see was the JSON data returned here: https://newsapi.org/v2/top-headlines?country=us&apiKey=5ca10b2d20a545099a108a3aeceb329c
You can put it into this formatter to make is readable: https://jsonformatter.curiousconcept.com
I thought I did everything correctly. Have I built my model wrong? I'm not sure how to fix this error.
So, with the help of the error returned, and looking at the data, it seems that "articles" is an array.
Here's what I'd try:
Rename your Articles struct to Article
Change JSONDescription's articles property from Articles to [Article]
I didn't notice any other errors in the data mapping, but hopefully this gets you closer.