Swift loading JSON in extension of struct - swift

Given this (partially pasted) struct:
struct InstrumentsSet: Identifiable, Decodable {
static func withJSON(_ fileName: String) -> InstrumentsSet? {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "json", subdirectory: "Sets") else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
return try? JSONDecoder().decode(InstrumentsSet.self, from: data)
}
let name: String
let bpm: Double
let timeSignature: Int
var tracks: [Track]
}
And this extension:
extension InstrumentsSet {
struct Track: Identifiable, Decodable {
static func withJSON(_ fileName: String) -> InstrumentsSet.Track? {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "json", subdirectory: "Sets") else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
//At this point data has the JSON loaded and decode makes it jump to the fatal error located at the call
return try? JSONDecoder().decode(InstrumentsSet.Track.self, from: data)
}
let instrumentType: InstrumentType
let startType: StartType
}
}
I can load successfully an instrument set from JSON with nested Tracks this way:
guard var instrumentSet1 = InstrumentsSet.withJSON("file-with-set") else {
fatalError("Error loading set JSON")
}
Next I'd like to add a track to the existing set by loading a json with just Track data:
guard let masterTrack = InstrumentsSet.Track.withJSON("file-with-track") else {
fatalError("Error loading track JSON")
}
When running this part I can see the JSON data loading successfully, but on this line return try? JSONDecoder().decode(InstrumentsSet.Track.self, from: data) the fatalError is triggered without further explanation. My guess is something is missing in the instrumentSet part since the function "InstrumentsSet.Track.withJSON" is called through the original InstrumentsSet struct so passing down just a track is not possible?
My question, given the struct and its extension, is it possible to load just the extension part (Track) of the struct into the let masterTrack?
Example Set JSON:
{
"name": "Set name",
"bpm": 124.00,
"timeSignature": 4,
"tracks": [
{
"instrumentType": "exsSampler",
"startType": "trigger"
}
]
}
Example Track JSON. (I've tried passing this as an Array, but still no success)
{
"instrumentType": "audioBuffer",
"startType": "global"
}
Thank you!

Related

How to bind data to ViewModel for showing it on UI in MVVM?

In my app I am using MVVM pattern.
Below is my Model.
struct NewsModel: Codable {
let status: String
let totalResults: Int
let articles: [Article]
}
struct Article: Codable {
let source: Source
let author: String?
let title: String
let articleDescription: String?
let url: String
let urlToImage: String?
let publishedAt: Date
let content: String?
enum CodingKeys: String, CodingKey {
case source, author, title
case articleDescription = "description"
case url, urlToImage, publishedAt, content
}
}
struct Source: Codable {
let id: String?
let name: String
}
Below is my ViewModel. Which is used for show the data from API.
struct NewsArticleViewModel {
let article: Article
var title:String {
return self.article.title
}
var publication:String {
return self.article.articleDescription!
}
var imageURL:String {
return self.article.urlToImage!
}
}
Below is my API request class.
class Webservice {
func getTopNews(completion: #escaping (([NewsModel]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let news = try? JSONDecoder().decode([NewsModel].self, from: data)
DispatchQueue.main.async {
completion(news)
}
}.resume()
}
}
After receiving response from my API I want to show it on screen. For this I added below ViewModel.
class NewsListViewModel: ObservableObject {
#Published var news: [NewsArticleViewModel] = [NewsArticleViewModel]()
func load() {
fetchNews()
}
private func fetchNews() {
Webservice().getTopNews {
news in
if let news = news {
//How to bind this data to NewsArticleViewModel and show it on UI?
}
}
}
}
Please let me know. What I have to write there for showing it on UI.
According to the documentation of newsapi.org your request will return one NewsModel object not an array. So change your Webservice class to:
class Webservice {
//Change the completion handler to return an array of Article
func getTopNews(completion: #escaping (([Article]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
// decode to a single NewsModel object instead of an array
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let news = try? decoder.decode(NewsModel.self, from: data)
DispatchQueue.main.async {
// completion with an optional array of Article
completion(news?.articles)
}
}.resume()
}
}
You would need to map those received values to NewsArticleViewModel types. For example:
Webservice().getTopNews { articles in
if let articles = articles {
self.news = articles.map{NewsArticleViewModel(article: $0)}
}
}
And remove let news: NewsModel from the NewsArticleViewModel struct as it is not needed.
Edit:
It seems:
let publishedAt: Date
is throwing an error. Jsondecoder fails to interpret the string to a date. Change your Webservice. I´ve updated it in my answer.
You could remove the legacy MVVM pattern and do it in proper SwiftUI like this:
struct ContentView: View {
#State private var articles = [Article]()
var body: some View {
NavigationView {
List(articles) { article in
Text(article.title)
}
.navigationTitle("Articles")
}
.task {
do {
let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb")!
let (data, _) = try await URLSession.shared.data(from: url)
articles = try JSONDecoder().decode([Article].self, from: data)
} catch {
articles = []
}
}
}
}

"No value associated with key CodingKeys" Error when trying to get data from API - Swift

Hi I am trying to make a pokedex app and I have previously in my code used the same API and was successful but now when I call a different link from the API I am getting this error:
keyNotFound(CodingKeys(stringValue: "root", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"root\", intValue: nil) (\"root\").", underlyingError: nil))
I am trying to get a description for the pokemon.
This is my current code that isn't working:
func loadFlavor() {
guard let url = URL(string: "https://pokeapi.co/api/v2/pokemon-species/1/") else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
return
}
do {
let result = try JSONDecoder().decode(SpeciesResults.self, from: data)
DispatchQueue.main.async {
for typeEntry in result.root
{
self.descriptionText.text = typeEntry.flavor_text_entries.flavor_text
}
}
}
catch let error {
print(error)
}
}.resume()
}
And these are my structs:
struct SpeciesResults: Codable {
let root: [PokemonFlavorResults]
}
struct PokemonFlavorResults: Codable {
let flavor_text_entries: Zerodic
}
struct Zerodic: Codable {
let flavor_text: String
}
Issue:
You don't need SpeciesResults model because flavor_text_entries object is already at the root level.
Use camel-case for defining your variables. In Codable you can use convertFromSnakeCase as the decoder's keyDecodingStrategy.
Solution:
So, the Codable models must be,
struct PokemonFlavorResults: Codable {
let flavorTextEntries: [Zerodic]
}
struct Zerodic: Codable {
let flavorText: String
}
And parse the JSON data like so,
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let results = try decoder.decode(PokemonFlavorResults.self, from: data)
//use results here...
} catch {
print(error)
}

What is this function parameter for?

func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
load("jsonFilePath")
The function extracts the data form a JSON.
What is that 'parameter' type: T.Type = T.self for? If I remove this parameter the code still works. So what do I need it for?
This parameter is just a helper to use three kind of declarations as below,
1) let model: Model = self.load("Countries")
2) let model = self.load("Countries", as: Model.self)
3) let model = self.load("Countries") as Model
You can remove it from the method signature if you want to use the first kind of declaration.

Error when parsing JSON data (Swift 4 Playground)

My Swift Playground keeps returning
Error: The data couldn't be read because it isn't in the correct
format."
and I can't figure out what I'm doing wrong. Below is my code.
JSON Sample Data:
{
"meta": {
"name":"Tour of Honor Bonus Listing",
"version":"18.1.4"
},
"bonuses": [
{
"bonusCode":"AZ1",
"category":"ToH",
"name":"Anthem Tour of Honor Memorial",
"value":1,
"city":"Anthem",
"state":"AZ",
"flavor":"Flavor Text Goes Here"
}
]
}
Playground Code:
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
struct JsonFile: Codable {
struct Meta: Codable {
let name: String
let version: String
}
struct JsonBonuses: Codable {
let bonusCode: String
let category: String
let name: String
let value: Int
let city: String
let state: String
let flavor: String
}
let meta: Meta
let bonuses: [JsonBonuses]
}
let url = URL(string: "http://www.tourofhonor.com/BonusData.json")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error: \(error.localizedDescription)")
PlaygroundPage.current.finishExecution()
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print("Error: invalid HTTP response code")
PlaygroundPage.current.finishExecution()
}
guard let data = data else {
print("Error: missing data")
PlaygroundPage.current.finishExecution()
}
// feel free to uncomment this for debugging data
// print(String(data: data, encoding: .utf8))
do {
let decoder = JSONDecoder()
let posts = try decoder.decode([JsonFile].self, from: data)
print(posts.map { $0.meta.name })
PlaygroundPage.current.finishExecution()
}
catch {
print("Error: \(error.localizedDescription)")
PlaygroundPage.current.finishExecution()
}
}.resume()
I assume I have something in my Struct incorrect, but I can't figure out what it is.
(This paragraph is to make the submission tool happy because it says I have too much code and not enough other details. Apparently being direct and succinct is not compatible with the submission scanning function).
The struct is correct but the root object is not an array (remove the brackets)
let posts = try decoder.decode(JsonFile.self, from: data)
print(posts.bonuses.map{$0.name})

Swift is not printing or displaying name in App from a weather API?

if let jsonObj = jsonObj as? [String: Any],
let weatherDictionary = jsonObj["weather"] as? [String: Any],
let weather = weatherDictionary["description", default: "clear sky"] as?
NSDictionary {
print("weather")
DispatchQueue.main.async {
self.conditionsLabel.text = "\(weather)"
}
}
// to display weather conditions in "name" from Open Weather
"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}]
//No errors, but code is not printing or displaying in App.
I'm not sure how to help with your exact question unless you can provide some more code for context. However,
You might try using the built-in decoding that comes with Swift 4. Check it out here. Basically, you make a class that models the response object, like this:
struct Weather: Decodable {
var id: Int
var main: String
var description: String
var icon: String
}
Then decode it like so:
let decoder = JSONDecoder()
let weather = try decoder.decode(Weather.self, from: jsonObj)
And it magically decodes into the data you need! Let me know if that doesn't work, and comment if you have more code context for your problem that I can help with.
I put the complete demo here to show how to send a HTTP request and parse the JSON response.
Note, Configure ATS if you use HTTP request, rather than HTTPS request.
The demo URL is "http://samples.openweathermap.org/data/2.5/forecast?q=M%C3%BCnchen,DE&appid=b6907d289e10d714a6e88b30761fae22".
The JSON format is as below, and the demo shows how to get the city name.
{
cod: "200",
message: 0.0032,
cnt: 36,
list: [...],
city: {
id: 6940463,
name: "Altstadt",
coord: {
lat: 48.137,
lon: 11.5752
},
country: "none"
}
}
The complete demo is as below. It shows how to use URLSessionDataTask and JSONSerialization.
class WeatherManager {
static func sendRequest() {
guard let url = URL(string: "http://samples.openweathermap.org/data/2.5/forecast?q=M%C3%BCnchen,DE&appid=b6907d289e10d714a6e88b30761fae22") else {
return
}
// init dataTask
let dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
let name = WeatherManager.cityName(fromWeatherData: data)
print(name ?? "")
}
// send the request
dataTask.resume()
}
private static func cityName(fromWeatherData data: Data?) -> String? {
guard let data = data else {
print("data is nil")
return nil
}
do {
// convert Data to JSON object
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
print(jsonObject)
if let jsonObject = jsonObject as? [String: Any],
let cityDic = jsonObject["city"] as? [String: Any],
let name = cityDic["name"] as? String {
return name
} else {
return nil
}
} catch {
print("failed to get json object")
return nil
}
}
}