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()
})
}
}
Related
I am trying to create a list of text objects out of a function that returns an array of params. Everything seems to be working fine, getting the data, console shows the correct results, except the list itself which remains empty.
The function call:
import UIKit
import SwiftUI
struct SubdomainsList: View {
#State var SubDomains = VTData().funky(XDOMAIN: "giphy.com")
var body: some View {
VStack {
List{
Text("Subdomains")
ForEach(SubDomains, id: \.self) { SuDo in
Text(SuDo)
}
}
}
}
}
struct SubdomainsList_Previews: PreviewProvider {
static var previews: some View {
SubdomainsList()
}
}
The Json handlers:
struct VTResponse: Decodable {
let data: [VT]
}
struct VT: Decodable {
var id: String
}
The class:
class VTData {
func funky (XDOMAIN: String) -> Array<String>{
var arr = [""]
getDATA(XDOMAIN: "\(XDOMAIN)", userCompletionHandler: { (SubDomain) in
print(SubDomain)
arr.append(SubDomain)
return SubDomain
})
return arr
}
func getDATA(XDOMAIN: String, userCompletionHandler: #escaping ((String) -> String)) {
let token = "<TOKEN>"
guard let url = URL(string: "https://www.lalalla.com/subdomains") else {fatalError("Invalid URL")}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("\(token)", forHTTPHeaderField: "x-apikey")
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard let data = data else { return }
let decoder = JSONDecoder()
let result = try? decoder.decode(VTResponse.self, from: data)
if let result = result {
for SubDo in result.data {
let SubDomain = SubDo.id
userCompletionHandler(SubDomain)
}
}
else {
fatalError("Could not decode")
}
})
task.resume()
}
}
I'm getting no errors whatsoever, and the console output shows the correct results:
support.giphy.com
cookies.giphy.com
media3.giphy.com
i.giphy.com
api.giphy.com
developers.giphy.com
media.giphy.com
x-qa.giphy.com
media2.giphy.com
media0.giphy.com
It is also worth mentioning that when I add print(type(of: SubDomain)) to the code I'm getting a String rather than an array.
The preview:
preview
Any ideas?
try this approach, again, to extract the list of subdomain from your API, and display them in
a List using the asynchronous function getDATA(...):
class VTData {
// `func funky` is completely useless, remove it
func getDATA(XDOMAIN: String, completion: #escaping ([String]) -> Void) { // <-- here
let token = "<TOKEN>"
guard let url = URL(string: "https://www.virustotal.com/api/v3/domains/\(XDOMAIN)/subdomains") else {
print("Invalid URL")
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("\(token)", forHTTPHeaderField: "x-apikey")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else { return } // todo return some error msg
do {
let results = try JSONDecoder().decode(VTResponse.self, from: data)
return completion(results.data.map{ $0.id }) // <-- here
} catch {
print(error) // <-- here important
}
}.resume()
}
}
struct SubdomainsList: View {
#State var subDomains: [String] = [] // <--- here
var body: some View {
VStack {
List{
Text("Subdomains")
ForEach(subDomains, id: \.self) { SuDo in
Text(SuDo)
}
}
}
.onAppear {
// async function
VTData().getDATA(XDOMAIN: "giphy.com") { subs in // <--- here
subDomains = subs
}
}
}
}
hi everyone I'm newbie in Swift
I ask for help why the data is not displayed in List? Please help me a fix it.
I make a Model Data, and take a data from URl and parse it
My model:
struct ResponseData: Codable{
var userId: String
var id: String
var title: String
var body: String
}
My code in Content view:
import SwiftUI
struct ContentView: View {
#State var responseData = [ResponseData]()
var body: some View {
List(responseData, id: \.id ) { item in
Text("\(item.body)")
}
.task {
await loadData()
}
}
func loadData () async {
let urlString = "https://jsonplaceholder.typicode.com/posts"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {return}
guard error == nil else {return}
do {
let postData = try JSONDecoder().decode(ResponseData.self, from: data)
} catch let error {
print(error)
}
}.resume()
}
}
There are many issues in the code.
The most significant ones are
In the struct userId and id are Int.
The JSON root object is an array, please note the starting [.
You don't update the model after receiving the data.
As you are using async/await anyway take full advantage of the pattern.
The JSON API returns posts, so how about to name the struct Post?
struct Post: Codable, Identifiable {
let userId: Int
let id: Int
let title: String
let body: String
}
struct ContentView : View {
#State var posts = [Post]()
var body : some View {
List(posts) { post in
Text(post.body)
}
.task {
do {
posts = try await loadData()
} catch {
print(error)
}
}
}
func loadData() async throws -> [Post] {
let urlString = "https://jsonplaceholder.typicode.com/posts"
let url = URL(string: urlString)!
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
return try JSONDecoder().decode([Post].self, from: data)
}
}
I am having issues trying to get this data. I heard there is a trick. Can anyone create a simple call to view the data from this api? Would truly appreciate it. Been trying for a week. I cant for the life of me get this simple api call to work.
http://api.citybik.es/v2/networks
Model.swift
import Foundation
// MARK: - Welcome
struct Dataset: Codable {
let networks: [Network]
}
// MARK: - Network
struct Network: Codable {
let company: [String]
let href, id: String
let location: Location
let name: String
}
// MARK: - Location
struct Location: Codable {
let city, country: String
let latitude, longitude: Double
}
Contentview.swift
import SwiftUI
struct ContentView: View {
#State var results = [Network]()
func loadData() {
guard let url = URL(string: "http://api.citybik.es/v2/networks") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([Network].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
var body: some View {
List(results, id: \.name) { item in
VStack(alignment: .leading) {
Text("\(item.name)")
}
}.onAppear(perform: loadData)
}
}
copy the whole of the json from : "https://api.citybik.es/v2/networks"
into "https://app.quicktype.io/" and get the (correct) swift data structures from that.
Rename "Welcome" to "Response" and use that in your code.
use: "https://api.citybik.es/v2/networks" note the https.
EDIT: In your code:
struct ContentView: View {
#State var networks = [Network]()
var body: some View {
List(networks, id: \.id) { network in
VStack {
Text(network.name)
Text(network.location.city)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.citybik.es/v2/networks") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.networks = response.networks
}
return
}
}
}.resume()
}
}
Once you have all the data structures,
and if you are using Swift 5.5 for ios 15 or macos 12, you can use something like this:
struct ContentView: View {
#State var networks = [Network]()
var body: some View {
List(networks, id: \.id) { network in
VStack {
Text(network.name)
Text(network.location.city)
}
}
.task {
let response: Response? = await fetchNetworks()
if let resp = response {
networks = resp.networks
}
}
}
func fetchNetworks<T: Decodable>() async -> T? {
let url = URL(string: "https://api.citybik.es/v2/networks")!
let request = URLRequest(url: url)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
// throw URLError(.badServerResponse) // todo
print(URLError(.badServerResponse))
return nil
}
let results = try JSONDecoder().decode(T.self, from: data)
return results
}
catch {
return nil
}
}
}
I'm very new to swiftUI and have been working through the landscapes app tutorial.
I have been trying to switch the data source from a bundled JSON file to a remote JSON source but have so far been lost on how to integrate what I've learnt about the URLSession with the tutorials load code.
Apple's code:
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
// #Published var landmarks: [Landmark] = apiCall.getLocations(locations)
}
func load<T: Decodable>(_ filename: String) -> 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)")
}
}
What I have to load from the remote source:
struct Location: Codable, Identifiable {
let id = UUID()
let country: String
let name: String
}
class apiCall {
func getLocations(completion:#escaping ([Location]) -> ()) {
guard let url = URL(string: "https://overseer.cyou/heritage/heritageData.json") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let locations = try! JSONDecoder().decode([Location].self, from: data!)
print(locations)
DispatchQueue.main.async {
completion(locations)
}
}
.resume()
}
}
Can anyone show me how I go about doing this, ideally from a complete beginners point of view?
// framework support
import SwiftUI
import Combine
// List view setup
struct LocationsView: View {
#ObservedObject var viewModel = LocationModel()
var body: some View {
List(viewModel.locations) { location in
HStack {
VStack(alignment: .leading) {
Text(location.name)
.font(.headline)
Text(location.country)
.font(.subheadline)
}
}
}
}
}
// Location model
struct Location: Codable, Identifiable {
var id = UUID()
let country: String
let name: String
let locationId: Int = 0
enum CodingKeys: String, CodingKey {
case locationId = "id"
case country
case name
}
}
// Location view model class
class LocationModel: ObservableObject {
#Published var locations: [Location] = []
var cancellationToken: AnyCancellable?
init() {
getLocations()
}
}
extension LocationModel {
func getLocations() {
cancellationToken = self.request("https://overseer.cyou/heritage/heritageData.json")?
.mapError({ (error) -> Error in
print(error)
return error
})
.sink(receiveCompletion: { _ in },
receiveValue: {
self.locations = $0
})
}
// API request
private func request(_ path: String) -> AnyPublisher<[Location], Error>? {
guard let url = URL(string: path)
else { return nil }
let request = URLRequest(url: url)
return apiCall.run(request)
.map(\.value)
.eraseToAnyPublisher()
}
}
// API setup
struct apiCall {
struct Response<T> {
let value: T
let response: URLResponse
}
static func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<Response<T>, Error> {
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap { result -> Response<T> in
let value = try JSONDecoder().decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
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
}