NIL value decoding nested JSON - SwiftUI - json

I'm getting nil while decoding a "fairly simple" nested JSON, what am I missing here?
The NetworkController part (along with the whole ContentView) works perfectly with "plain" JSONs (i.e. {"greetings": "hi"}), yet I'm still getting nil with nested JSON Structures
Here's my JSON:
index.json
{
"products": {
"pasta": 81.22,
"pizza": 13.81,
"apples": 0,
"sausages": 2.36,
"potatoes": 0.55,
"cucmbers": 2.06,
"oranges": 0
}
}
Here's my Code:
DataStruct.swift
struct ProductWrapper: Decodable {
var products: Product
}
struct Product: Decodable {
var pasta: Double
var pizza: Double
var apples: Double
var sausages: Double
var potatoes: Double
var cucmbers: Double
var oranges: Double
}
ContentView.swift
import SwiftUI
class ProductsList: ObservableObject {
#Published var prods: ProductWrapper? = nil
func fetchProducts() {
NetworkController.fetchProducts { prods in
self.prods = prods
}
}
}
struct ContentView: View {
#ObservedObject var DecodedProducts = ProductsList()
var body: some View {
VStack{
HStack{
Text("Pasta: ")
Text(String("\(self.DecodedProducts.prods?.products.pasta)")).fontWeight(.bold).foregroundColor(.red)
}
HStack{
Text("Pizza: ")
Text(String("\(self.DecodedProducts.prods?.products.pizza)")).fontWeight(.bold).foregroundColor(.blue)
}
HStack{
Text("Apples: ")
Text(String("\(self.DecodedProducts.prods?.products.apples)")).fontWeight(.bold).foregroundColor(.green)
}
}.padding()
}
}
NetworkManager.swift
import Foundation
struct NetworkController {
static func fetchProducts(completion: #escaping ((ProductWrapper) -> Void)) {
if let url = URL(string: "http://localhost:1234/index.json") {
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, resp, err) in
do {
if let products = data {
let prods = try JSONDecoder().decode(ProductWrapper.self, from: products)
DispatchQueue.main.async() {
completion(prods)
}
} else {
print("Error Found")
}
} catch let err as NSError {
print(err.localizedDescription)
}
}.resume()
}
}
}
Thanks in Advance!

Found the problem;
That was just a dumb me...
Forgot to call the fetchProducts() in the onAppear *facepalm*
.onAppear{
self.DecodedProducts.fetchProducts()
}
Here's the correct code:
ContentView.swift
import SwiftUI
class ProductsList: ObservableObject {
#Published var prods: ProductWrapper? = nil
func fetchProducts() {
NetworkController.fetchProducts { prods in
self.prods = prods
}
}
}
struct ContentView: View {
#ObservedObject var DecodedProducts = ProductsList()
var body: some View {
VStack{
HStack{
Text("Pasta: ")
Text(String("\(self.DecodedProducts.prods?.products.pasta ?? 0)")).fontWeight(.bold).foregroundColor(.red)
}
HStack{
Text("Pizza: ")
Text(String("\(self.DecodedProducts.prods?.products.pizza ?? 0)")).fontWeight(.bold).foregroundColor(.blue)
}
HStack{
Text("Apples: ")
Text(String("\(self.DecodedProducts.prods?.products.apples ?? 0)")).fontWeight(.bold).foregroundColor(.green)
}
}.onAppear{
self.DecodedProducts.fetchProducts()
}
}
}

Related

SwiftUI NavigationLink cannot find 'json' in scope

I'm new to SwiftUI and have worked through the server requests and JSON. I now need to programmatically transition to a new view which is where I get stuck with a "Cannot find 'json' in scope" error on the NavigationLink in ContentView.swift. I've watched videos and read articles but nothing quite matches, and everything I try just seems to make things worse.
JSON response from server
{"status":{"errno":0,"errstr":""},
"data":[
{"home_id":1,"name":"Dave's House","timezone":"Australia\/Brisbane"},
{"home_id":2,"name":"Mick's House","timezone":"Australia\/Perth"},
{"home_id":3,"name":"Jim's House","timezone":"Australia\/Melbourne"}
]}
JSON Struct file
import Foundation
struct JSONStructure: Codable {
struct Status: Codable {
let errno: Int
let errstr: String
}
struct Home: Codable, Identifiable {
var id = UUID()
let home_id: Int
let name: String
let timezone: String
}
let status: Status
let data: [Home]
}
ContentView file
import SwiftUI
struct ContentView: View {
#State private var PushViewAfterAction = false
var body: some View {
NavigationLink(destination: ListView(json: json.data), isActive: $PushViewAfterAction) {
EmptyView()
}.hidden()
Button(action: {
Task {
await performAnAction()
}
}, label: {
Text("TEST")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue.cornerRadius(10))
.foregroundColor(.white)
.font(.headline)
})
}
func performAnAction() {
PushViewAfterAction = true
return
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ListView file
import SwiftUI
struct ListView: View {
#State var json: JSONStructure
var body: some View {
VStack {
List (self.json.data) { (home) in
HStack {
Text(home.name).bold()
Text(home.timezone)
}
}
}.onAppear(perform: {
guard let url: URL = URL(string: "https://... ***removed*** ") else {
print("invalid URL")
return
}
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// check if response is okay
guard let data = data, error == nil else { // check for fundamental networking error
print((error?.localizedDescription)!)
return
}
let httpResponse = (response as? HTTPURLResponse)!
if httpResponse.statusCode != 200 { // check for http errors
print("httpResponse Error: \(httpResponse.statusCode)")
return
}
// convert JSON response
do {
self.json = try JSONDecoder().decode(JSONStructure.self, from: data)
} catch {
print(error.localizedDescription)
print(String(data: data, encoding: String.Encoding.utf8)!)
}
print(json)
if (json.status.errno != 0) {
print(json.status.errstr)
}
print("1. \(json.data[0].name)), \(json.data[0].timezone)")
print("2. \(json.data[1].name)), \(json.data[1].timezone)")
}).resume()
})
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
I've tried to keep the code to a minimum for clarity.
It's because there is no "json" in ContentView, you need to pass json object to ListView, but since you load json in ListView, then you need to initialize json in ListView like:
struct ListView: View {
#State var json: JSONStructure = JSONStructure(status: JSONStructure.Status(errno: 0, errstr: ""), data: [JSONStructure.Home(home_id: 0, name: "", timezone: "")])
var body: some View {
and remove it form NavigationLink in ContentView like:
NavigationLink(destination: ListView(), isActive: $PushViewAfterAction) {
or you could build your JSONStructure to accept optional like:
import Foundation
struct JSONStructure: Codable {
struct Status: Codable {
let errno: Int?
let errstr: String?
init() {
errno = nil
errstr = nil
}
}
struct Home: Codable, Identifiable {
var id = UUID()
let home_id: Int?
let name: String?
let timezone: String?
init() {
home_id = nil
name = nil
timezone = nil
}
}
let status: Status?
let data: [Home]
init() {
status = nil
data = []
}
}
but then you need to check for optionals or provide default value like:
struct ListView: View {
#State var json: JSONStructure = JSONStructure()
var body: some View {
VStack {
List (self.json.data) { (home) in
HStack {
Text(home.name ?? "Could not get name").bold()
Text(home.timezone ?? "Could not get timeZone")
}
}
}.onAppear(perform: {
guard let url: URL = URL(string: "https://... ***removed*** ") else {
print("invalid URL")
return
}
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// check if response is okay
guard let data = data, error == nil else { // check for fundamental networking error
print((error?.localizedDescription)!)
return
}
let httpResponse = (response as? HTTPURLResponse)!
if httpResponse.statusCode != 200 { // check for http errors
print("httpResponse Error: \(httpResponse.statusCode)")
return
}
// convert JSON response
do {
self.json = try JSONDecoder().decode(JSONStructure.self, from: data)
} catch {
print(error.localizedDescription)
print(String(data: data, encoding: String.Encoding.utf8)!)
}
print(json)
if (json.status?.errno != 0) {
print(json.status?.errstr)
}
print("1. \(json.data[0].name)), \(json.data[0].timezone)")
print("2. \(json.data[1].name)), \(json.data[1].timezone)")
}).resume()
})
}
}

How to solve error: 'Missing arguments for parameters 'data', 'detailedData' in call'

I'm trying to make a detailed view of a contact from a contacts list. This is the WIP of that view. I am getting the error;
Missing arguments for parameters 'data', 'detailedData' in call
on the line with ContactDetail() in the ContactDetail_Previews struct.
I think I understand that this is because something is missing from the variables data and detailedData, but my confusion comes from how I use similar code for the actual list view of all the contacts, with no such error. I have pasted the code for the whole list view below the code for the detailed view.
Any help would be appreciated!
Contact Detail Code:
import SwiftUI
struct ContactDetail: View {
var data: Response_Detailed.Contact_Detailed
#ObservedObject var detailedData: getData_Detailed
var body: some View {
VStack {
Text(data.first_name + " " + data.last_name)
Text(data.phone_number)
Text(data.birthday)
Text(data.address)
Text(data.updated_date)
Text(data.create_date)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.detailedData.updateDetailed_Data()
}
}
}
}
}
class getData_Detailed: ObservableObject {
#Published var data = [Response_Detailed.Contact_Detailed]()
#Published var id = 1
init() {
updateDetailed_Data()
}
func updateDetailed_Data() {
let url = "DATABASE_LINK\(id)"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { (data, _, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
do {
let json = try JSONDecoder().decode(Response_Detailed.self, from: data!)
let oldData = self.data
DispatchQueue.main.async {
self.data = oldData + json.data
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(json)
print(String(data: data, encoding: .utf8)!)
}
}
catch {
print(error.localizedDescription)
}
}.resume()
}
}
struct ContactDetail_Previews: PreviewProvider {
static var previews: some View {
ContactDetail()
}
}
struct Response_Detailed: Codable {
struct Contact_Detailed: Codable, Identifiable {
public let id: Int
public let first_name: String
public let last_name: String
public let birthday: String
public let phone_number: String
public let create_date: String
public let updated_date: String
public let address: String
}
public let data: [Contact_Detailed]
enum CodingKeys : String, CodingKey {
case data = "data"
}
}
Contacts List View Code: (Note the same error comes up on the line with NavigationLink.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
HStack {
ContactsList()
.navigationBarTitle("Contacts")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20.0)
}
}
}
}
}
}
struct ContactsList: View {
#ObservedObject var listData = getData()
var body: some View {
List(0..<listData.data.count, id: \.self) {i in
NavigationLink(destination: ContactDetail()) {
if i == self.listData.data.count - 1 {
cellView(data: self.listData.data[i], isLast: true, listData: self.listData)
}
else {
cellView(data: self.listData.data[i], isLast: false, listData: self.listData)
}
}
}
}
}
struct cellView: View {
var data: Response.Contact
var isLast: Bool
#ObservedObject var listData: getData
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if self.isLast {
Text(data.first_name + " " + data.last_name)
.fontWeight(/*#START_MENU_TOKEN#*/.bold/*#END_MENU_TOKEN#*/)
.font(.title2)
.padding(/*#START_MENU_TOKEN#*/[.leading, .bottom, .trailing], 5.0/*#END_MENU_TOKEN#*/)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.listData.updateData()
}
}
}
else {
Text(data.first_name + " " + data.last_name)
.fontWeight(/*#START_MENU_TOKEN#*/.bold/*#END_MENU_TOKEN#*/)
.font(.title2)
.padding(/*#START_MENU_TOKEN#*/[.leading, .bottom, .trailing], 5.0/*#END_MENU_TOKEN#*/)
}
}
.padding(.top, 10)
}
}
class getData: ObservableObject {
#Published var data = [Response.Contact]()
#Published var limit = 15
#Published var skip = 0
init() {
updateData()
}
func updateData() {
let url = "DATABASE_LINK?skip=\(skip)&limit=\(limit)"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { (data, _, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
do {
let json = try JSONDecoder().decode(Response.self, from: data!)
let oldData = self.data
DispatchQueue.main.async {
self.data = oldData + json.data
self.limit += 15
self.skip += 15
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(json)
print(String(data: data, encoding: .utf8)!)
}
}
catch {
print(error.localizedDescription)
}
}.resume()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Response: Codable {
struct Contact: Codable, Identifiable {
public let id: Int
public let first_name: String
public let last_name: String
public let updated_date: String
}
struct Pagination_Data: Codable {
public let skip: Int
public let limit: Int
public let total: Int
}
public let data: [Contact]
public let pagination: Pagination_Data
enum CodingKeys : String, CodingKey {
case data = "data"
case pagination = "pagination"
}
}
Your ContactsList is giving the only variable an initial value.
#ObservedObject var listData = getData()
Not IAW Apple documentation but the = getData() is the initial value
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
BTW you should change it to
#StateObject var listData = getData()
Your ContactDetail View has two variables without an initial value
struct ContactDetail: View {
var data: Response_Detailed.Contact_Detailed
#ObservedObject var detailedData: getData_Detailed
No = sign so a struct creates an init that looks like this
init(data: Response_Detailed.Contact_Detailed, detailedData: getData_Detailed)
So in your Preview you need to provide the initial values
ContactDetail(data: /.../, detailedData: /.../)
The /.../ symbolizes where you will provide sample data

Why is my JSON code in Swift not parsing?

Disclaimer: Very basic question below. I am trying to learn the basics of IOS development.
I'm currently trying to parse data from an API to a SwiftUI project and am not able to successfully do so.
The code goes as follows:
import SwiftUI
struct Poem: Codable {
let title, author: String
let lines: [String]
let linecount: String
}
class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct ContentView: View {
#ObservedObject var fetch = FetchPoem()
let joined = fetch.poem.lines.joined(separator: "\n")
var body: some View {
Text(fetch.poem.title)
.padding()
Text( \(joined) )
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The build currently fails. It's throwing me the following errors:
Initializer 'init(_:)' requires that 'Binding<Subject>' conform to 'StringProtocol'
Referencing subscript 'subscript(dynamicMember:)' requires wrapper 'ObservedObject<FetchPoem>.Wrapper'
Value of type 'FetchPoem' has no dynamic member 'poem' using key path from root type 'FetchPoem'
Moreover, I am attempting to append the array "Lines" into one main String variable "Joined". However, I am not sure this works... The error is "String interpolation can only appear inside a string literal". Would love some help if anyone knows...
Any ideas? All help is appreciated.
** Edited Code - Q2
import SwiftUI
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct ContentView: View {
#ObservedObject var fetch = FetchPoem()
var body: some View {
VStack {
if let poem = fetch.poems.first {
Button("Refresh") {getPoem}
Text("\(poem.author): \(poem.title)").bold()
Divider()
ScrollView {
VStack {
ForEach(poem.lines, id: \.self) {
Text($0)
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Most probably you wanted this (because poems is an array) - tested with Xcode 12.1 / iOS 14.1
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
// ...
struct ContentView: View {
#ObservedObject var fetch = FetchPoem()
var body: some View {
List {
ForEach(fetch.poems, id: \.self) {
Text("\($0.author): \($0.title)")
}
}
}
}
... , and next might be wrapping that into NavigationView/NavigationLink with shown each poem in desination view.
Of showing lines as follows:
var body: some View {
VStack {
if let poem = fetch.poems.first {
Text("\(poem.author): \(poem.title)").bold()
Divider()
ScrollView {
VStack {
ForEach(poem.lines, id: \.self) {
Text($0)
}
}
}
}
}
}

Parse a Json from a url with entry values and list them in SwiftUI

I'm stuck on this.
I have a json that I'm parsing and that Json has an entry value and I want to do a swift list based in user input that will trigger from the external json different responses.
I have the following code so far
import SwiftUI
import Combine
import Foundation
var lokas : String = "ABCY"
struct ContentView: View {
#State var name: String = ""
#ObservedObject var fetcher = Fetcher()
var body: some View {
VStack {
TextField("Enter Loka id", text: $name)
List(fetcher.allLokas) { vb in
VStack (alignment: .leading) {
Text(movie.device)
Text(String(vb.seqNumber))
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class Fetcher: ObservableObject {
#Published var allLokas = [AllLoka_Data]()
init(){
load(devices:lokas)
}
func load(devices:String) {
let url = URL(string: "https://xxx.php?device=\(devices)&hours=6")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([AllLoka_Data].self, from: d)
DispatchQueue.main.async {
self.allLokas = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct AllLoka_Data: Codable {
var date: String
var time: String
var unix_time: Int
var seqNumber : Int
}
// Now conform to Identifiable
extension AllLoka_Data: Identifiable {
var id: Int { return unix_time }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
But I just added a Global variable to test it, but how do I pass the variable name to make the function work ?
Thank you

Empty List from JSON in SwiftUI

I'm having some trouble fetching data from the PiHole API;
This is the JSON format (from the url http://pi.hole/admin/api.php?summary):
{
"domains_being_blocked": "1,089,374",
"dns_queries_today": "34,769",
"ads_blocked_today": "11,258",
"ads_percentage_today": "32.4",
"unique_domains": "9,407",
"queries_forwarded": "17,972",
"queries_cached": "5,539",
"clients_ever_seen": "35",
"unique_clients": "23",
"dns_queries_all_types": "34,769",
"reply_NODATA": "1,252",
"reply_NXDOMAIN": "625",
"reply_CNAME": "10,907",
"reply_IP": "21,004",
"privacy_level": "0",
"status": "enabled",
"gravity_last_updated": {
"file_exists": true,
"absolute": 1588474361,
"relative": {
"days": "0",
"hours": "14",
"minutes": "18"
}
}
}
This is my code:
ContentView.swift
import SwiftUI
struct NetworkController {
static func fetchData(completion: #escaping (([PiHole.Stat]) -> Void)) {
if let url = URL(string: "http://pi.hole/admin/api.php?summary") {
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
let stat = try? JSONDecoder().decode(PiHole.self, from: data)
completion(stat?.stats ?? [])
}
}.resume()
}
}
}
class ContentViewModel: ObservableObject {
#Published var messages: [PiHole.Stat] = []
func fetchData() {
NetworkController.fetchData { messages in
DispatchQueue.main.async {
self.messages = messages
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
List {
ForEach(viewModel.messages, id: \.self) { stat in
Text(stat.domains_being_blocked)
}
}.onAppear{
self.viewModel.fetchData()
}
}
}
Data.swift
struct PiHole: Decodable {
var stats: [Stat]
struct Stat: Decodable, Hashable {
var domains_being_blocked: String
var ads_percentage_today: String
var ads_blocked_today: String
var dns_queries_today: String
}
}
Everything seems okay, no errors, yet when I run it, the simulator only shows an empty list
In Playground I can retrieve those data just fine:
import SwiftUI
struct PiHoleTest: Codable {
let domains_being_blocked: String
let ads_blocked_today: String
}
let data = try! Data.init(contentsOf: URL.init(string: "http://pi.hole/admin/api.php?summary")!)
do {
let decoder: JSONDecoder = JSONDecoder.init()
let user: PiHoleTest = try decoder.decode(PiHoleTest.self, from: data)
print("In Blocklist \(user.domains_being_blocked)")
print("Blocked Today: \(user.ads_blocked_today) ")
} catch let e {
print(e)
}
The Output:
In Blocklist 1,089,374
Blocked Today: 11,258
What am I doing wrong? Or better, is there another way to fetch these stats?
Thanks in Advance!
The issue was related to the structure. Your JSON decoded were not an array. So PiHole struct was unnecessary. I can tested and this code is working now.
import SwiftUI
struct NetworkController {
static func fetchData(completion: #escaping ((Stat) -> Void)) {
if let url = URL(string: "http://pi.hole/admin/api.php?summary") {
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let stat = try JSONDecoder().decode(Stat.self, from: data)
DispatchQueue.main.async() {
completion(stat)
}
return
} else {
print("Error Found")
}
} catch let error as NSError {
print(error.localizedDescription)
}
}.resume()
}
}
}
class ContentViewModel: ObservableObject {
#Published var stat: Stat? = nil
func fetchData() {
NetworkController.fetchData { stat in
self.stat = stat
}
}
}
struct TestView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
List {
Text(viewModel.stat?.domains_being_blocked ?? "No Data")
Text(viewModel.stat?.ads_blocked_today ?? "No Data")
Text(viewModel.stat?.ads_percentage_today ?? "No Data")
Text(viewModel.stat?.dns_queries_today ?? "No Data")
}.onAppear{
self.viewModel.fetchData()
}
}
}
struct Stat: Decodable, Hashable {
var domains_being_blocked: String
var ads_percentage_today: String
var ads_blocked_today: String
var dns_queries_today: String
}