Importing JSON from URL within a view - json

So, new to swift and was able to peice this together by reading and watching videos however i reached a point after countless hours searching for a solution..
Basically what the app does is scan's a qr code, parses the url it reads from the qr code to get a key, then I am appending that key to the api url, and i want to output the results from the api to the screen. however I am receiving an error Type '()' cannot conform to 'View' in xcode
Here is sample json data
[
{
"id": "160468",
"sport": "BASKETBALL",
"year": "2020",
"brand": "PANINI PRIZM",
"cardNumber": "278",
"playerName": "LaMELO BALL",
"extra": "",
"gradeName": "MINT",
"grade": "9",
"serial": "63585906",
"authDate": "1656406800",
"link": "https://www.example.com/certificate-verification?certificateNumber=63585906"
}
]
here is my contentview
import SwiftUI
import CodeScanner
extension URL {
var components: URLComponents? {
return URLComponents(url: self, resolvingAgainstBaseURL: false)
}
}
extension Array where Iterator.Element == URLQueryItem {
subscript(_ key: String) -> String? {
return first(where: { $0.name == key })?.value
}
}
struct Card: Decodable {
let sport: String
let year: String
let brand: String
let cardNumber: String
let playerName: String
let extra: String
let gradeName: String
let grade: String
let serial: String
}
struct ContentView: View {
#State var isPresentingScanner = false
#State var scannedCode: String = ""
var scannerSheet : some View {
CodeScannerView(
codeTypes: [.qr],
completion: { result in
if case let .success(code) = result {
self.scannedCode = code.string
self.isPresentingScanner = false
}
}
)
}
func getQueryStringParameter(url: String, param: String) -> String? {
guard let url = URLComponents(string: url) else { return nil }
return url.queryItems?.first(where: { $0.name == param })?.value
}
var body: some View {
VStack(spacing: 10) {
Image("logo-white")
.offset(y: -200)
if let urlComponents = URL(string: scannedCode)?.components,
let cert = urlComponents.queryItems?["certificateNumber"] {
//Text(cert)
let apihit = URL(string: "https://app.example.com/api.php?apikey=xxx&cert=\(cert)")!
//Text(apihit.absoluteString)
var request = URLRequest(url: apihit)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: apihit) { data, response, error in
if let data = data {
if let cards = try? JSONDecoder().decode([Card].self, from: data) {
print(cards)
} else {
print("Invalid Response")
}
} else if let error = error {
print("HTTP Request Failed \(error)")
}
}
}
Button("Scan QR Code") {
self.isPresentingScanner = true
}
.padding()
.background(Color(red: 0, green: 0, blue: 0.5))
.foregroundColor(.white)
.clipShape(Rectangle())
.cornerRadius(20)
.sheet(isPresented: $isPresentingScanner) {
self.scannerSheet
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have tried countless tutorials online, however none of them show how to do it within the view, which I believe is where it belongs because I don't get the actual json url until after I read the qr code..

To populate a link within a view, use Webkit.
First you need to import Webkit. Then, create a WebView which conforms to the UIViewRepresentable protocol.
Here is the Apple documentation. This is the easiest way to integrate web content into your app.
I hope this helps!

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()
})
}
}

SwiftUI Text View: When optional string is nil, expected default value is not working

Having trouble figuring out why code won’t display default value when variable is nil. Here’s the context below. Any pointers would be greatly appreciated.
Thanks!
EXAMPLE OF DATA FROM JSON API:
NOTE: image_url is just the base name, not the full path or file extension.
[
{
"id": 1,
"title": "Autumn in New York",
"image_url": ""
}
]
DATA MODEL:
import Foundation
struct Challenge: Codable, Hashable, Identifiable {
let id: Int
let title: String
let imageURL: String?
private enum CodingKeys: String, CodingKey {
case id
case title
case imageURL = "image_url"
}
}
CODE FOR VIEW AND VIEW MODEL:
import SwiftUI
struct JSONChallengeV2: View {
#State private var challenge: [Challenge] = []
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 5) {
ScrollView(.horizontal, showsIndicators: true) {
HStack() {
ForEach (challenge) { challenge in
NavigationLink(
destination:
PlayerView(),
label: {
// PROBLEMS OCCUR IN THIS VIEW (see view code below)
JSONChallengeRowView(challenge: challenge)
})
}
}
}
}
.onAppear {
getData()
}
}
}
func getData() {
let url = URL(string: "https://example.com/jsonapi") // EXAMPLE ONLY
URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
guard data != nil else {
print("No data")
return
}
let decoder = JSONDecoder()
do {
let loaded = try decoder.decode([Challenge].self, from: data!)
challenge = loaded
} catch {
print("Can't decode data")
}
}.resume()
}
}
CODE FOR SUB-VIEW ("JSONChallengeRowView" referenced in above view):
import SwiftUI
struct JSONChallengeRowView: View {
var challenge: Challenge
var body: some View {
let thumbnailPrefix = "https://example.com/" // EXAMPLE ONLY
let thumbnailSuffix = "-001.jpg"
VStack(alignment: .leading) {
// WORKS: Hardcoding a known image base (i.e., "autumn-default":
RemoteImageView(url: ("\(thumbnailPrefix)\(String(describing: "autumn-default"))\(thumbnailSuffix)"))
.scaledToFit()
.cornerRadius(10)
Link("Go", destination: (URL(string: "\(thumbnailPrefix)\("autumn-default")\(thumbnailSuffix)") ?? URL(string: "https://google.com"))!)
// DOESN'T WORK: build succeeds but no default image appears when no "imageURL" value can be found:
RemoteImageView(url: ("\(thumbnailPrefix)\(String(describing: challenge.imageURL ?? "autumn-default" ))\(thumbnailSuffix)"))
.scaledToFit()
.cornerRadius(10)
Link("Go", destination: URL(string: "\(thumbnailPrefix)\(String(describing: challenge.imageURL ?? "autumn-default"))\(thumbnailSuffix)")!)
// AND WHILE THESE WORK:
Text("\(challenge.title)")
Text(challenge.title)
// THESE SIMILARLY DISPLAY NOTHING (despite values in the "title" variable, used as a default value here for testing only):
Text("\(challenge.imageURL ?? challenge.title)")
Text(challenge.imageURL ?? challenge.title)
}
}
}
Nick's point about image_url containing an empty string, not nil, pointed me in the right direction, and with a tip from my Swift learner's forum elsewhere, I used this modification to fix the sections that weren't working:
challenge.imageURL.isEmpty ? "autumn-default" : "\(challenge.imageURL)"
For example, here:
RemoteImageView(url: "\(thumbnailPrefix)" + (challenge.imageURL.isEmpty ? "autumn-default" : "\(challenge.imageURL)") + "\(thumbnailInfix)" + (challenge.imageURL.isEmpty ? "autumn-default" : "\(challenge.imageURL)") + "\(thumbnailSuffix)")
I also applied this syntax to the Link and Text views successfully.
Thanks again, everyone!

SwiftUI - JSON from GitHub REST API fails to parse

I am attempting to create an app in SwiftUI using GitHub's REST API. I am attempting to only retrieve only the login name and profile picture of a given user's followers and then populate a List.
FollowersView.swift
import SwiftUI
struct Follower: Codable, Hashable {
public var login: String
public var avatar_url: String
}
struct Response: Codable {
var followers: [Follower] = [Follower]()
}
struct FollowersView: View {
#EnvironmentObject var followerInfo: FollowerInfo
#State var followers: [Follower] = [Follower]()
init() {
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.white]
}
var body: some View {
NavigationView {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
List(followers, id: \.self) { follower in
HStack(spacing: 10) {
Image(decorative: "\(follower.avatar_url).png")
.resizable()
.frame(width: 75, height: 75)
HStack(spacing: 5) {
Image(systemName: "person")
Text("\(follower.login)").fontWeight(.heavy)
}
}
}
.onAppear(perform: loadData)
}
.navigationBarTitle("\(followerInfo.followerUsername)")
}
}
func loadData() {
guard let url = URL(string: "https://api.github.com/users/\(followerInfo.followerUsername)/followers") 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.followers = decodedResponse.followers
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown Error")")
}.resume()
}
}
struct FollowersView_Previews: PreviewProvider {
static var previews: some View {
FollowersView()
}
}
The code gets to print("Fetch failed: \(error?.localizedDescription ?? "Unknown Error")") and prints "Unknown Error" before crashing. Upon inspecting the data that comes back, it comes up with nothing, at least not that I can tell anyway. (I'm not entirely accustomed to the Xcode debugger). I double checked the API response in a browser and the fields in the response in the browser and the names of the properties in the Codable struct match. So, I'm not exactly sure what's going on here. Any suggestions on how I can fix this issue?
If I understood your problem correctly. Update your Codable as shown below:
import Foundation
// MARK: - FollowerElement
struct FollowerElement: Codable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
let url, htmlURL, followersURL: String
let followingURL, gistsURL, starredURL: String
let subscriptionsURL, organizationsURL, reposURL: String
let eventsURL: String
let receivedEventsURL: String
let type: String
let siteAdmin: Bool
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
case url
case htmlURL = "html_url"
case followersURL = "followers_url"
case followingURL = "following_url"
case gistsURL = "gists_url"
case starredURL = "starred_url"
case subscriptionsURL = "subscriptions_url"
case organizationsURL = "organizations_url"
case reposURL = "repos_url"
case eventsURL = "events_url"
case receivedEventsURL = "received_events_url"
case type
case siteAdmin = "site_admin"
}
}
typealias Follower = [FollowerElement]
You can remove properties you don't need from above code.
Now update your dataTask method
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Fetch failed: \(error.localizedDescription)")
} else {
if let data = data {
do {
let follower = try JSONDecoder().decode(Follower.self, from: data)
DispatchQueue.main.async {
print(follower.count)
}
} catch {
print(error)
}
}
}
}.resume()

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
}

Data immediately deallocated

I am currently working on a Headline UI Element for my app and I am having an issue with my data being immediately deallocated. I have been trying to get the console to print any data it is receiving, but I am not even seen that in the console. What I am trying to achieve is for the app to connect to its targeted "CDN" and then pull data for the headlines. Using PHP I am preforming a SQL Query to generate an array that the app will then feed off of. When running said scrip the following array is generated.
{"id":"1","title":"Meet ergoWare","header_image":"https://gba.ergoware.io/cache/content/topstory/ergo_news_01.svg","summary":"GBA's New Ergonomic Portal!"}
Stunning array yeah, so the next part is it will then be read and compiled in the app to create headline cards, but the issue is I cannot get the data to to load. I know there is something I am missing, but without the compiler pointing me in the direction I need, I'm fumbling around with it. So here is the Swift code.
import SwiftUI
struct Response: Codable {
var results: [Article]
}
struct Article: Codable, Identifiable {
var id: Int
var title: String
var header_image: String
var summary: String
}
struct HeadlineUI: View {
#State var results = [Article]()
var CDNLink: String
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 20) {
ForEach(results) { result in
CardView(image: result.header_image, summary: result.summary, heading: result.title)
}
.onAppear(perform: { self.loadData() })
}
}
}
func loadData() {
guard let url = URL(string: CDNLink) 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
print(self.results)
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
struct HeadlineUI_Previews: PreviewProvider {
static var previews: some View {
HeadlineUI(CDNLink: "https://gba.ergoware.io/cdn?funct=fetchNews")
}
}
So what should happen is that it connects to the "CDN" and reads the array. That information then is plugged in and for each index of the results, defined by the id, a card should display with the top story image, article header and a short summary. I am starting to get a little more comfortable with Swift, but little hiccups like this keeps breaking my confidence down haha. Thanks for any teachable moments you can provide.
I found it, follow these steps:
According to your class Response, your service should return this JSON:
{
"results" : [{
"id":1,
"title":"Meet ergoWare",
"header_image":"https://gba.ergoware.io/cache/content/topstory/ergo_news_01.svg",
"summary":"GBA's New Ergonomic Portal!"
}]
}
Give it a try using this url
You should place .onAppear in your HStack not in your ForEach. Here is a the full example:
struct Response: Codable {
var results: [Article]
}
struct Article: Codable, Identifiable {
var id: Int
var title: String
var header_image: String
var summary: String
}
struct HeadlineUI: View {
#State var results = [Article]()
var CDNLink: String
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 20) {
Text("Response:")
ForEach(results) { result in
Text(result.title)
}
}.onAppear(perform: { self.loadData() })
}
}
func loadData() {
guard let url = URL(string: CDNLink) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
return
} else {
do {
let decodedResponse = try JSONDecoder().decode(Response.self, from: data!)
print(decodedResponse)
self.results = decodedResponse.results
} catch let err {
print("Error parsing: \(err)")
}
}
}.resume()
}
}