Consume data from blockchain API - swift

I'm not sure what I'm doing wrong, I've been at it for an hour, I'm trying to print 1 or 2 lines from
https://api.covalenthq.com/v1/1/address/0x8f299f2908c9Cd71e723E7059Ac52eaea3638b2E/balances_v2/?&key=ckey_4eeea29a22c14701a9844f01151
but I get an error. I tried to debug it and it seems like my model and it's call is wrong, what's going on?
import SwiftUI
struct FetchingAPI: View {
#State private var results = [Result]()
var body: some View {
Text("Fetching API")
.onAppear(perform: loadData)
}
var semaphore = DispatchSemaphore (value: 0)
func loadData() {
guard let url = URL(string: "https://api.covalenthq.com/v1/1/address/0x8f299f2908c9Cd71e723E7059Ac52eaea3638b2E/balances_v2/?&key=ckey_4eeea29a22c14701a9844f01151") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url, timeoutInterval: Double.infinity)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
print(String(data: data, encoding: .utf8)!)
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// AFTER HERE THE DATA IS [] NON EXISTENT
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.results
}
// everything is good, so we can exit
return
}
} else {
print(String(describing: error))
semaphore.signal()
return
}
semaphore.signal()
}
task.resume()
semaphore.wait()
}
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
var data: Int
var address: String
var quote_currency: String
}
}
EDIT: Adding error message after adding suggested try catch:
I updated the code to be:
do {
if let data = data {
// print(String(data: data, encoding: .utf8)!)
let decodedResponse = try JSONDecoder().decode(Response.self, from: data)
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.results
}
// everything is good, so we can exit
return
}
} catch {
print("Unexpected error: \(error).")
semaphore.signal()
return
}
ERROR Message:
Unexpected error: keyNotFound(CodingKeys(stringValue: "updatedAt", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \"updatedAt\", intValue: nil) (\"updatedAt\").", underlyingError: nil)).

Here's how I solved it without so much code:
struct ContentView: View {
#State var address:String = ""
var body: some View {
VStack(){
Text("address: \(address)")
Button(action: {
makeApiRequest()
}, label: {
Text("make api request")
})
}
}
func makeApiRequest(){
let url = URLRequest(url: URL(string: "https://api.covalenthq.com/v1/1/address/0x8f299f2908c9Cd71e723E7059Ac52eaea3638b2E/balances_v2/?&key=ckey_4eeea29a22c14701a9844f01151")!)
URLSession.shared.dataTask(with: url) { data, responce, error in
if let data = data {
if let decodeResponce = try? JSONDecoder().decode(apiResponce.self, from: data){
address = decodeResponce.data.address
}
}
}.resume()
print(url)
}
}
struct apiResponce: Codable {
let data:apiData
let error:Bool
}
struct apiData: Codable {
let address:String
let quote_currency:String
}

Related

Swift iOS App - No Console Messages Cannot Make URLSession Call to GraphQL Enpoint

I've been trying to get a list of entities from a graphql endpoint but I can't figure it out. Also, the console in my Xcode v13.4 isn't showing anything even though I have some print() statements in the code, so that's not helping - I've found where it is at the bottom of the window but it's always blank.
My View to get the data is below, the DetailView is the link following a link from the main ContentView. the loadData function content, I got the code from Postman after testing the graphql call.
import SwiftUI
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
//array of properties
struct Response: Codable {
var data: Properties
}
struct Properties: Codable {
var properties: Nodes
}
struct Nodes: Codable {
var nodes: [Result]
}
struct Result: Codable {
var propertyId: Int
var title: String
}
struct DetailView: View {
#State private var results = [Result]()
var body: some View {
List(results, id: \.propertyId) { property in
VStack(alignment: .leading) {
Text(property.title)
.font(.headline)
}
}
.task{
await loadData()
}
}
func loadData() async {
let semaphore = DispatchSemaphore (value: 0)
let parameters = "{\"query\":\"{\\n properties {\\n nodes {\\n title(format: RENDERED)\\n propertyId\\n }\\n }\\n}\",\"variables\":{}}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "http://DOMAIN/graphql")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
do {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.data.properties.nodes
}
semaphore.signal()
}
task.resume()
semaphore.wait()
} catch {
print("Invalid data")
}
}
}
The output of the graphql call is
{
"data": {
"properties": {
"nodes": [
{
"title": "MY TITLE",
"propertyId": 00001
}
]
}
}
}
EDIT
Swapped try? for try! as suggested
if let decodedResponse = try! JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.data.properties.nodes
}
error: Initializer for conditional binding must have Optional type, not 'Response'
I managed to get it all working with the below...
import SwiftUI
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
//array of properties
struct Response: Codable {
var data: Properties
}
struct Properties: Codable {
var properties: Nodes
}
struct Nodes: Codable {
var nodes: [Result]
}
struct Result: Codable {
var propertyId: Int
var title: String
}
struct DetailView: View {
#State private var results = [Result]()
#State private var test = "one"
var body: some View {
List(results, id: \.propertyId) { property in
VStack(alignment: .leading) {
Text(property.title)
.font(.headline)
}
}.task{
await loadData()
}
}
func loadData() async {
let semaphore = DispatchSemaphore (value: 0)
let parameters = "{\"query\":\"{\\n properties {\\n nodes {\\n title(format: RENDERED)\\n propertyId\\n }\\n }\\n}\",\"variables\":{}}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://DOMAIN/graphql")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
do {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
let decodedResponse = try! JSONDecoder().decode(Response.self, from: data)
results = decodedResponse.data.properties.nodes
semaphore.signal()
}
task.resume()
semaphore.wait()
} catch {
print("Invalid data \(error)")
}
}
}
Much to learn :)

Why is this Swift web scraper not working?

I am having trouble scraping an image HTML link with a code I found on youtube (https://www.youtube.com/watch?v=0jTyKu9DGm8&list=PLYjXqILgs9uPwYlmSrIkNj2O3dwPCcoBK&index=2). The code works perfectly fine in a playground, but there is something wrong with my implementation into an Xcode project. (More like: im not sure how to implement it into my project :) )
When I ran this code on a Playground it pulled the link that I needed exactly as I needed it to be outputted.
import Foundation
let url = URL(string: "https://guide.michelin.com/th/en/bangkok-
region/bangkok/restaurant/somtum-khun-kan")
let task = URLSession.shared.dataTask(with: url!) { (data, resp, error) in
guard let data = data else {
print("data was nil")
return
}
guard let htmlString = String(data: data, encoding: String.Encoding.utf8) else {
print("can not cast data into string")
return
}
let leftSideOfTheString = """
image":"
"""
let rightSideOfTheString = """
","#type
"""
guard let leftRange = htmlString.range(of: leftSideOfTheString) else {
print("can not find left range of string")
return
}
guard let rightRange = htmlString.range(of: rightSideOfTheString) else {
print("can not find right range of string")
return
}
let rangeOfValue = leftRange.upperBound..<rightRange.lowerBound
print(htmlString[rangeOfValue])
}
task.resume()
I then put the same exact code into a structure containing the code as a parameter and method, like so:
struct ImageLink {
let url = URL(string: "https://guide.michelin.com/th/en/bangkok-region/bangkok/restaurant/somtum-khun-kan")
func getImageLink() {
let task = URLSession.shared.dataTask(with: url!) { (data, resp, error) in
guard let data = data else {
print("data was nil")
return
}
guard let htmlString = String(data: data, encoding: String.Encoding.utf8) else {
print("can not cast data into string")
return
}
let leftSideOfTheString = """
image":"
"""
let rightSideOfTheString = """
","#type
"""
guard let leftRange = htmlString.range(of: leftSideOfTheString) else {
print("can not find left range of string")
return
}
guard let rightRange = htmlString.range(of: rightSideOfTheString) else {
print("can not find right range of string")
return
}
let rangeOfValue = leftRange.upperBound..<rightRange.lowerBound
print(htmlString[rangeOfValue])
}
task.resume()
}
}
Finally, to check if the code would give me the right link, I made an instance in a View and made a button printing the getImageLink() function like bellow. You'll see in commented out code that I tried displaying the image both by hard coding its link and by inserting the function call. The former worked as expected, the latter did not work.
import SwiftUI
struct WebPictures: View {
var imageLink = ImageLink()
var body: some View {
VStack {
//AsyncImage(url: URL(string: "\(imageLink.getImageLink())"))
//AsyncImage(url: URL(string: "https://axwwgrkdco.cloudimg.io/v7/__gmpics__/c8735576e7d24c09b45a4f5d56f739ba?width=1000"))
Button {
print(imageLink.getImageLink())
} label: {
Text("Print Html")
}
}
}
}
When I click the button to print the link I get the following message:
()
2022-05-16 17:21:30.030264+0800 MichelinRestaurants[35477:925525] [boringssl]
boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
https://axwwgrkdco.cloudimg.io/v7/__gmpics__/c8735576e7d24c09b45a4f5d56f739ba?width=1000
And if I click the button for a second time only this gets printed:
()
https://axwwgrkdco.cloudimg.io/v7/__gmpics__/c8735576e7d24c09b45a4f5d56f739ba?width=1000
If anybody knows how to help me out here that would be much appreciated!!
This fails because you do not wait until your func has pulled the link. You are in an async context here. One possible solution:
//Make a class in instead of a struct and inherit from ObservableObject
class ImageLink: ObservableObject {
let url = URL(string: "https://guide.michelin.com/th/en/bangkok-region/bangkok/restaurant/somtum-khun-kan")
//Create a published var for your view to get notified when the value changes
#Published var imageUrlString: String = ""
func getImageLink() {
let task = URLSession.shared.dataTask(with: url!) { (data, resp, error) in
guard let data = data else {
print("data was nil")
return
}
guard let htmlString = String(data: data, encoding: String.Encoding.utf8) else {
print("can not cast data into string")
return
}
let leftSideOfTheString = """
image":"
"""
let rightSideOfTheString = """
","#type
"""
guard let leftRange = htmlString.range(of: leftSideOfTheString) else {
print("can not find left range of string")
return
}
guard let rightRange = htmlString.range(of: rightSideOfTheString) else {
print("can not find right range of string")
return
}
let rangeOfValue = leftRange.upperBound..<rightRange.lowerBound
print(htmlString[rangeOfValue])
//Assign the scrapped link to the var
imageUrlString = htmlString[rangeOfValue]
}
task.resume()
}
}
And the view:
struct WebPictures: View {
//Observe changes from your imagelink class
#StateObject var imageLink = ImageLink()
var body: some View {
VStack {
AsyncImage(url: URL(string: imageLink.imageUrlString)) // assign imageurl to asyncimage
//AsyncImage(url: URL(string: "https://axwwgrkdco.cloudimg.io/v7/__gmpics__/c8735576e7d24c09b45a4f5d56f739ba?width=1000"))
Button {
imageLink.getImageLink()
} label: {
Text("Print Html")
}
}
}
}
Update:
In order to get the link when the view appears call it this way:
VStack {
AsyncImage(url: URL(string: imageLink.imageUrlString))
}
.onAppear{
if imageLink.imageUrlString.isEmpty{
imageLink.getImageLink()
}
}

How could you access the GitHub API in Swift?

I'd like to make an update detection system in my macOS SwiftUI app by pulling the latest release from GitHub via the API and then comparing the tag. How would I go about accessing the API from Swift? I've tried using the methods from here, medium.com, here, swifttom.com and here, steveclarkapps.com but none of them accomplish what I'm trying to do.
For the first method, the code functions with the provided example API, but doesn't work with the GitHub API and it returns this error instead:
Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
Method 2 suffers the same issue.
I couldn't even get enough of method 3's code working to try it.
Here's my adapted code based off of the medium.com method:
Model.swift
import Foundation
struct TaskEntry: Codable {
let id: Int
let tag_name: String
let name: String
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") 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 response = try! JSONDecoder().decode([TaskEntry].self, from: data) /*{*/
DispatchQueue.main.async {
self.results = response
}
return
/*}*/
}
}.resume()
}
}
Commented out code and variable names that seem irrelevant are just leftovers.
OS: macOS Big Sur 11.6
Xcode version: 13.0
Open this in your browser:
https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest
You will notice it is not an array but an object. You should be decoding an object like this:
JSONDecoder().decode(TaskEntry.self, from: data)
Edit:
This requires you to change your view. Notice this is no longer a List because you are no longer fetching an array but a single item:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var entry: TaskEntry? = nil
var body: some View {
VStack(alignment: .leading) {
if let entry = entry {
Text("\(entry.id)")
Text(entry.name)
Text(entry.tagName)
} else {
ProgressView()
}
}
.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode(TaskEntry.self, from: data)
DispatchQueue.main.async {
self.entry = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}
NOTICE: I did some other improvements as well
Use JSONDecoder to convert from snake case to camel case
Added do catch block so your app doesn't crash
Check for errors before decoding
Added loading indicator (had to put something in the else)
However,
As our discussion you are probably calling the wrong endpoint. That endpoint is not returning an array but a single object, you can tell this because the JSON response begins with { rather than [
I've adjusted my answer to change the endpoint I believe you should be calling:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var results: [TaskEntry]? = nil
var body: some View {
if let results = results {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}
} else {
VStack(alignment: .leading) {
ProgressView()
.onAppear(perform: loadData)
}
}
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode([TaskEntry].self, from: data)
DispatchQueue.main.async {
self.results = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}

how can I init struct: Codable without default value

I want to get Json from API, and use Codable protocol, but when I init published var, I get an error.
struct Search: Codable {
let result: [String]
}
class SearchViewModel: ObservableObject {
#Published var data = Search()
func loadData(search: String) {
var urlComps = URLComponents(string: getUrl)
let queryItems = [URLQueryItem(name: "result", value: search)]
urlComps!.queryItems = queryItems
let url = urlComps!.url!.absoluteString
guard let Url = URL(string: url) else { return }
URLSession.shared.dataTask(with: Url) { (data, res, err) in
do {
if let data = data {
let decoder = JSONDecoder()
let result = try decoder.decode(Search.self, from: data)
DispatchQueue.main.async {
self.data = result
}
} else {
print("there's no Data😭")
}
} catch (let error) {
print("Error!")
print(error.localizedDescription)
}
}.resume()
}
}
Change
#Published var data:Search?

Swift ui macos #Published nil or Int

I have the following variable, I would like it to take nil as an initial value and then an Int value.
#Published var status: Int = 0
To better understand place all the reference code:
struct ServerMessage: Decodable {
let token: String
}
class Http: ObservableObject {
#Published var status: Int = 0
#Published var authenticated = false
func req(url: String, httpMethod: String, body: [String: String]?) {
guard let url = URL(string: url) else { return }
let httpBody = try! JSONSerialization.data(withJSONObject: body ?? [])
var request = URLRequest(url: url)
request.httpMethod = httpMethod
request.httpBody = httpBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
print("Error: \(String(describing: error))")
return
}
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 400: do {
print("Error: 400")
DispatchQueue.main.async {
self.status = 400
}
return
}
case 401: do {
print("Error: 401")
DispatchQueue.main.async {
self.status = 401
}
return
}
default: do {}
}
}
do {
if let data = data {
let results = try JSONDecoder().decode(ServerMessage.self, from: data)
DispatchQueue.main.async {
self.authenticated = true
}
print("Ok.", results)
} else {
print("No data.")
}
} catch {
print("Error:", error)
}
}.resume()
}
}
Use:
self.http.req(
url: "",
httpMethod: "POST",
body: ["email": "", "password": ""]
)
Make it optional (with all following corrections in place of usage)
#Published var status: Int? = nil // << I like explicit initialising
Update: possible variant of usage in View
Text("\(http.status ?? 0)") // << it is Int, so ?? "" is not valid
but probably it is more appropriate (due to no sense to show unknown status field)
if http.status != nil {
Text("\(http.status!)")
}