Need help targeting a nested json object SwiftUI *Question clarified* - json

Im currently hitting a wall as I'm trying to display information from an array within a nested json object. I can't understand where I am going wrong, so any help will be greatly appreciated. **The current error I am receiving is : Fatal error: Index out of range
I believe my problem may be in regards to how I am targeting the information. I have outlined using "!! ... !!" the key areas where I believe the error is being made.
I am confused because of how the json object is nested. If I'm correct, the heiarchy for my desired target is: object(main object) -> object(DATA) -> array(newReleases) -> String(prId). With that being said Im under the impression my self.posts = results.data.newReleases would then be targeting the newReleases array directly(which I want), for it to then be printed in the contentView Text(networkManager.posts[0].prID)
Content View where the list is declared:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List{
Text(networkManager.posts[0].prID)
}
.navigationTitle("Json Test")
}
.onAppear{
self.networkManager.fetchData()
}
}
}
Heres a photo of the JSON Data formatted.
JSON Data
This is how I have the data defined in my app:
// MARK: - Results
struct Results: Codable {
let data: DATAClass
enum CodingKeys: String, CodingKey {
case data = "DATA"
}
}
// MARK: - DATAClass
struct DATAClass: Codable {
let newReleases, exclusives, preorders, backIssues: [BackIssue]
}
// MARK: - BackIssue
struct BackIssue: Codable {
let totalcount: String
let sectionName: SectionName
let sectionLink, prID, prParentid, prTtle: String
let prPrice, prLprice, prSimg, prBimg: String
enum CodingKeys: String, CodingKey {
case totalcount
case sectionName = "section_name"
case prID = "pr_id"
case prTtle = "pr_ttle"
case prPrice = "pr_price"
}
}
}
enum SectionName: String, Codable {
case featuredNewReleases = "FEATURED_NEW_RELEASES"
case recommendedBackIssues = "RECOMMENDED_BACK_ISSUES"
case recommendedPreOrders = "RECOMMENDED_PRE_ORDERS"
}
Heres where I am attempting to decode the json object:
#Published var posts = !! [BackIssue]() !!
func fetchData() {
if let url = URL(string:" https://www.midtowncomics.com/wcfmt/services/product.svc/load-featured-sections?apiKey=&mtUser=&mtPass=&sh_id=76367&pgn=home&app_id") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
!! self.posts = results.data.newReleases !!
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}

Never get an item by hard-coded index in the rendering area of an SwiftUI view. If the array is empty the code crashes reliably.
Get the item safely by replacing
Text(networkManager.posts[0].prID)
with
Text(networkManager.posts.first?.prID ?? "n/a")
Better is a ForEach loop which is skipped if the array is empty.

Related

JSON decoding struct has a key, but Xcode calls that there is no member with that key in SwiftUI

I would like to get the text value of the repo name from GitHub. The structs are decoded successfully, but I can't seem to get one value that is important to me (name).
Can someone help, please?
Swift files with code:
DECODER
class NetworkingManager: ObservableObject{
#Published var repos = [Repository]()
init() {
loadData()
}
func loadData() {
guard let url = URL(string: "https://api.github.com/users/jacobtoye/repos") else { return }
URLSession.shared.dataTask(with: url) {(data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
do {
self.repos = try JSONDecoder().decode([Repository].self, from: data)
} catch {
fatalError("Failed to decode given URL")
}
}
}.resume()
}
}
APIKEYS
import Foundation
struct Repository: Decodable, Identifiable {
let id: Int
let name: String
let owner: Owner
enum CodingKeys: String, CodingKey {
case id, owner
case name
}
}
struct Owner : Decodable, Identifiable {
let id: Int
let reposUrl : String
enum CodingKeys: String, CodingKey, CaseIterable {
case id
case reposUrl = "repos_url"
}
}
CONTENT VIEW
import SwiftUI
struct ContentView: View {
#StateObject var netManager = NetworkingManager()
var body: some View {
Text("\(netManager.repos.name)")
}
}
The error I get is: value of type [Repository] has no member 'name'.
In the final project I want to use this value as a navigationTitle, so ForEach solution from here: SwiftUI value of type [x] has no member [y], but the struct has the member doesn't seem to be viable for me.
netManager.repos is an array of Repository, it does not have a name, as the error tells you. You must select a single repo and get the name from that.
For example
Text("\(netManager.repos.first?.name ?? "no name")")

Errors while trying to create a list based on JSON in SwiftUI

I need my JSON data to be translated to a list as an output for an app I am creating.
I have found this tutorial, which seems to be using some old elements like "BindableObject":
https://www.youtube.com/watch?v=ri1A032zfLo
As far as I've checked several times, I went word for word in the NetworkingManager file being described from 1:52 in the video (just changing the names and the URL). My code:
import Foundation
import SwiftUI
import Combine
/*BindableObject was renamed to ObservableObject */
class NetworkingManager: ObservableObject{
var didChange: PassthroughSubject <NetworkingManager, Never>
var gitHubList = Root(items: []){
didSet{
didChange.send(self)
}
}
init() {
guard let url = URL(string: "https://api.github.com/search/repositories?q=CoreData&per_page=20") else { return }
URLSession.shared.dataTask(with: url) {(data, _, _) in guard let data = data else { return }
let gitHubList = try! JSONDecoder().decode(GitHubAPIlist.self, from: data)
DispatchQueue.main.async {
self.gitHubList = gitHubList
}
}.resume()
}
}
I get 2 errors:
'self' captured by a closure before all members were initialized
on the line with URLSession and
Return from initializer without initializing all stored properties
on the line with the } after the .resume() command
Are there some obsolete syntaxes in the code or am I missing something?
try something like this approach, to get you data from github. Works very well for me:
class NetworkingManager: ObservableObject{
#Published var gitHubList: [Item] = []
init() {
loadData()
}
func loadData() {
guard let url = URL(string: "https://api.github.com/search/repositories?q=CoreData&per_page=20") else { return }
URLSession.shared.dataTask(with: url) {(data, _, _) in
guard let data = data else { return }
do {
let response = try JSONDecoder().decode(Root.self, from: data)
DispatchQueue.main.async {
self.gitHubList = response.items
}
} catch {
print("error: \(error)")
}
}.resume()
}
}
struct Root: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [Item]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct Item: Identifiable, Codable {
let keysURL, statusesURL, issuesURL: String
let id: Int
let url: String
let pullsURL: String
// ... more properties
enum CodingKeys: String, CodingKey {
case keysURL = "keys_url"
case statusesURL = "statuses_url"
case issuesURL = "issues_url"
case id
case url
case pullsURL = "pulls_url"
}
}
struct ContentView: View {
#StateObject var netManager = NetworkingManager()
var body: some View {
List {
ForEach(netManager.gitHubList) { item in
Text(item.url)
}
}
}
}
You declared the subject but didn't initialize it
var didChange = PassthroughSubject<NetworkingManager, Never>()
However actually you don't need the subject, mark gitHubList as #Published (and delete the subject).
And if you are only interested in the items declare
#Published var gitHubList = [Repository]()
and assign
DispatchQueue.main.async {
self.gitHubList = gitHubList.items
}
The #Published property wrapper will update the view.
In the view declare NetworkingManager as #StateObject
#StateObject private var networkManager = NetworkingManager()
and in the rendering area refer to
List(networkManager.gitHubList...
Notes:
Forget the tutorial , the video is more than two years old which are ages in terms of Swift(UI)'s evolution speed.
Ignoring errors and force unwrapping the result of the JSON decoder is very bad practice. Handle the potential URLSession error and catch and handle the potential decoding error.

json file is missing/ struct is wrong

I have been trying to get this code to work for like 6 hours. I get the error: "failed to convert The data couldn’t be read because it is missing." I don't know while the File is missing is there something wrong in my models(structs). Do I need to write a struct for very json dictionary? Currently I have only made those JSON dictionaries to a struct, which I actually need. The full JSON file can be found at https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-12-22&offset=-05:00 . I want to be able to print the time of the sunrise, sunset and solar noon as well as the elevation of the sun at solar noon. It's currently 1 am and I am desperate. Good Night!
class ViewController: NSViewController {
#IBOutlet weak var sunriseField: NSTextField!
#IBOutlet weak var sunsetField: NSTextField!
#IBOutlet weak var daylengthField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-12-22&offset=-05:00"
getData(from: url)
// Do any additional setup after loading the view.
}
private func getData(from url: String) {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: {data, response, error in
guard let data = data, error == nil else {
print("something went wrong")
return
}
var result: MyTime?
do {
result = try JSONDecoder().decode(MyTime.self, from: data)
}
catch {
print("failed to convert \(error.localizedDescription)")
}
guard let json = result else {
return
}
let sunrise1 = json.sunrise.time
DispatchQueue.main.async { [weak self] in
self?.sunriseField.stringValue = sunrise1
}
print(json)
})
task.resume()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
struct MyData : Codable {
let location : Location
let meta : Meta
}
struct MyTime : Codable {
let solarnoon : Solarnoon
let sunset : Sunset
let sunrise : Sunrise
}
struct Location : Codable {
let height : String
let time : [MyTime]
let longitude : String
let latitude : String
}
struct Meta : Codable {
let licenseurl : String
}
struct Solarnoon : Codable {
let desc : String
let time : String
let elevation : String
}
struct Sunrise : Codable {
let desc : String
let time : String
}
struct Sunset : Codable {
let time : String
let desc : String
}
You don't really have a SwiftUI class, but that is a different question. I am going to work on fixing getData(). I have tried to comment it extensively, but let me know if you have any questions.
private func getData(from url: String) {
// Personally I like converting the string to a URL to unwrap it and make sure it is valid:
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
let config = URLSessionConfiguration.default
// This will hold the request until you have internet
config.waitsForConnectivity = true
URLSession.shared.dataTask(with: url) { data, response, error in
// A check for a bad response
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
print("Bad Server Response")
return
}
if let data = data {
// You can print(data) here that will shown you the number of bytes returned for debugging.
//This work needs to be done on the main thread:
DispatchQueue.main.async {
let decoder = JSONDecoder()
if let json = try? decoder.decode(MetDecoder.self, from: data){
print(json)
//At this point, you have your data in a struct
self.sunriseTime = json.dailyData?.solarData?.first?.sunrise?.time
}
}
}
}
.resume()
}
With regard to your structs, you only need them for the data you are trying to parse. If you don't need it, don't worry about it. I would make this a separate class named MetDecoder or something that makes sense to you and indicates the decoder for your JSON. You will also note that I changed the names of some of the variables. You can do that so long as you use a CodingKeys enum to translate your JSON to your struct as in the case of dailyData = "location", etc. This is ugly JSON, and I am not sure why the Met decided everything should be a string, but this decoder is tested and it works:
import Foundation
// MARK: - MetDecoder
struct MetDecoder: Codable {
let dailyData: DailyData?
enum CodingKeys: String, CodingKey {
case dailyData = "location"
}
}
// MARK: - Location
struct DailyData: Codable {
let solarData: [SolarData]?
enum CodingKeys: String, CodingKey {
case solarData = "time"
}
}
// MARK: - Time
struct SolarData: Codable {
let sunrise, sunset: RiseSet?
let solarnoon: Position?
let date: String?
enum CodingKeys: String, CodingKey {
case sunrise, sunset, solarnoon, date
}
}
// MARK: - HighMoon
struct Position: Codable {
let time: String?
let desc, elevation, azimuth: String?
}
// MARK: - Moonrise
struct RiseSet: Codable {
let time: String?
let desc: String?
}
You should see what the National Weather Service does to us in the US to get the JSON. Lastly, when working on JSON I find the following pages VERY helpful:
JSON Formatter & Validator which will help you parse out the wall of text that gets returned in a browser, and
quicktype which will parse JSON into a programming language like Swift. I will warn you that the parsing can give some very ugly structs in Swift, but it gives you a nice start. I used both sites for this answer.
Apple's new framework, Combine, helps to simplify the code needed for async fetch requests. I have used the MetDecoder in #Yrb's response above (you can accept his answer) and altered the getData() function. Just make sure you import Combine at the top.
import Combine
var sunriseTime: String?
var sunsetTime: String?
var solarNoonTime: String?
var solarNoonElevation: String?
func getData() {
let url = URL(string: "https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-12-22&offset=-05:00")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to MetDecoder
.decode(type: MetDecoder.self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("completion: \(result)")
} receiveValue: { (value) in
// if success, will return MetDecoder
// here you can update your view
print("value: \(value)")
if let solarData = value.dailyData?.solarData?.first {
self.sunriseTime = solarData.sunrise?.time
self.sunsetTime = solarData.sunset?.time
self.solarNoonTime = solarData.solarnoon?.time
self.solarNoonElevation = solarData.solarnoon?.elevation
}
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
from the json data (2nd entry), looks like you need at least:
struct MyTime : Codable {
let solarnoon : Solarnoon?
let sunset : Sunset?
let sunrise : Sunrise?
}
and you need:
var result: MyData?
do {
result = try JSONDecoder().decode(MyData.self, from: data)
}
catch {
print("----> error failed to convert \(error)")
}

How do I parse this JSON data In Swift 5?

My app has a few different view controllers that receive JSON data from my web service and parses it into table views. This one isn't working.
Here is a sample of the JSON data I am trying to parse into a TableViewController
"content_4_4":{"Sku":"W-22","Qty":"1","Desc":"Panel","Condition":""},"content_4_5":{"Sku":"W-15","Qty":"1","Desc":"Desk 44\" long","Condition":""},"content_4_6":{"Sku":"W-18","Qty":"1","Desc":"End Return Panel","Condition":""},"content_4_7":{"Sku":"W-25","Qty":"1","Desc":"End Return Panel","Condition":""},"content_4_8":{"Sku":"W-19","Qty":"1","Desc":"Header w/lights, transformer","Condition":""}
Here is the codable struct I am using to model the data.
struct Components: Codable {
var result: [Component]
}
struct Component: Codable {
var Sku: String
var Qty: String
var Desc: String
var Condition: String
}
Here is how I am trying to parse the json into components
let decoder = JSONDecoder()
if let jsonUnits = try? decoder.decode(Components.self, from: data) {
UnitComponents = jsonUnits.result
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Nothing is showing up in my reusable cell. Since I can see the data I'm sure I'm parsing it wrong or something in the data could be breaking the parser. Perhaps the forward slashes or the presence of the titles ex. "content_4_4" is breaking the parser. Unsure. Any help appreciated.
You need
var unitComponents = [Component]()
do {
let jsonUnits = try JSONDecoder().decode([String:Component].self, from: data)
unitComponents = Array(jsonUnits.values)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print(error)
}
struct Component: Codable {
let sku, qty, desc, condition: String
enum CodingKeys: String, CodingKey {
case sku = "Sku"
case qty = "Qty"
case desc = "Desc"
case condition = "Condition"
}
}

Syntax for accessing struct property with enum type in JSON model

I am trying to access a url string contained within some JSON data.
The string is contained within the "urls" array with type "detail" as can be seen below.
JSON DATA
I used quicktype to construct my model as below:
struct Response: Codable {
let data: DataClass
}
struct DataClass: Codable {
let results: [Result]
}
struct Result: Codable {
let name: String
let description: String
let thumbnail: Thumbnail
let urls: [URLElement]
}
struct Thumbnail: Codable {
let path: String
let thumbnailExtension: Extension
enum CodingKeys: String, CodingKey {
case path
case thumbnailExtension = "extension"
}
}
enum Extension: String, Codable {
case jpg = "jpg"
}
struct URLElement: Codable {
let type: URLType
let url: String
}
enum URLType: String, Codable {
case comiclink = "comiclink"
case detail = "detail"
case wiki = "wiki"
}
I have tried accessing it by declaring it like so...
var urlelement: URLElement!
override func viewDidLoad() {
super.viewDidLoad()
let detailurl = urlelement.url
print(detailurl)
... but it always returns an empty string. Any suggestions will be most welcome. Thanks!
First Download the JSON then user JSONDecoder
let url = URL(string: "your url")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("error \(error.localizedDescription)")
return
}
guard let data = data else { return }
do {
let response = try JSONDecoder().decode(Response.self, from: data)
// use this
response?.data.results.forEach({ (rsl) in
rsl.urls.forEach({ (element) in
print(element.type, element.url)
})
})
// or this one
for rsl in response!.data.results {
for element in rsl.urls {
print(element.type, element.url)
}
}
} catch let error {
print("error while decoding the json \(error.localizedDescription)")
}
}.resume()