How could you access the GitHub API in Swift? - 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()
}
}

Related

How to use sink to assign to an array

Working on a demo I switched to using Combine, but just cannot seem to find a way to assign the values that I get from json decoding in sink to point to an array , below is my code , as you can see in the commented out code using URLSession it was much easier …thanks
Currently I just see the default record
struct NewsItem: Decodable {
let id: Int
let title: String
let strap: String
let url: URL
let main_image: URL
let published_date: Date
static let shared = NewsItem(id: 0, title: "", strap: "", url: URL(string: "https://www.hackingwithswift.com/articles/239/wwdc21-wrap-up-and-recommended-talks")!, main_image: URL(string: "https://www.hackingwithswift.com/resize/300/uploads/wwdc-21#2x.jpg")!, published_date: Date())
}
struct CardView: View {
#State private var news = [NewsItem]()
#State private var request = Set<AnyCancellable>()
var body: some View {
List {
ForEach(news, id:\.id) { news in
Text(news.title)
Text("\(news.published_date)")
Link("Goto Link", destination: news.url)
AsyncImage(url: news.main_image)
.frame(width: 50, height: 50)
}
}
.onAppear {
Task {
await fetchData()
}
}
}
func fetchData() async {
let url = URL(string: "https://www.hackingwithswift.com/samples/headlines.json")!
// URLSession.shared.dataTask(with: url) { data, response, error in
// if let error = error {
// print(error.localizedDescription)
// } else if let data = data {
// let json = JSONDecoder()
//
// json.dateDecodingStrategy = .iso8601
// do {
// let user = try json.decode([NewsItem].self, from: data)
// news = user
// } catch {
// print(error.localizedDescription)
// }
// }
// }.resume()
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [NewsItem].self, decoder: JSONDecoder())
.replaceError(with: [NewsItem.shared])
.sink(receiveValue: { item in
news = item
})
.store(in: &request)
}
}
You are seeing the default output because you are replacing all errors. Use at least print to look at the error before replacing it.
Turned out the issue here was the decoding of the Date. Applying the proper decoding strategy fixed the issue.
func fetchData() async {
//create custom decoder and apply dateDecodingStrategy
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let url = URL(string: "https://www.hackingwithswift.com/samples/headlines.json")!
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
// use the custom decoder
.decode(type: [NewsItem].self, decoder: decoder)
// if an error occures at least print it
.mapError({ error in
print(error)
return error
})
.replaceError(with: [NewsItem.shared])
.sink(receiveValue: { item in
news = item
})
.store(in: &request)
}
If you want to use Combine you need an ObservableObject and assign the end of the pipeline to an #Published var. When the object is deinit it will cancel the pipeline automatically.
The advantatage of async/await and .task is we don't need objects anymore and the task is cancelled when the UIView (that SwiftUI manages) dissapears.

What is wrong with this API call for SwiftUI

I have been trying to make an API call with swiftui, but I keep running into threading errors when I run the code. This is the current program:
import SwiftUI
struct Post: Codable, Identifiable {
var id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(){
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
print(posts)
}
.resume()
}
}
// Content view file
import SwiftUI
struct PostList: View {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
.onAppear{
Api().getPosts()
}
}
}
struct PostList_Previews: PreviewProvider {
static var previews: some View {
PostList()
}
}
I got this code verbatim from a swift tutorial, but I am getting errors from it. Any help would be greatly appreciated!
the problem happen because in this line:
let posts = try! JSONDecoder().decode([Post].self, from: data!)
you are making force unwrap try! and swift can't decode your data into [Post] because your model is wrong, change for this:
struct Post: Codable {
var userId: Int
var id: Int
var title: String
var body: String
}
your app will compile, and please avoid to use force unwrap.
To "fix" your error, use var id: Int in your Post model.
Also use the following code, that is more robust than the tutorial code you have been using: see this SO post: Preview Crashed when I get data from api
struct ContentView: View {
#State var posts: [Post] = []
var body: some View {
VStack {
List(posts) { post in
Text(post.title)
}
.onAppear{
Api.shared.getPosts { posts in // <-- here
self.posts = posts
}
}
}
}
}
struct Post: Codable, Identifiable {
var id: Int // <-- here
var title: String
var body: String
}
class Api {
static let shared = Api() // <-- here
func getPosts(completion: #escaping([Post]) -> ()) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return
print("bad url")
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { // <-- here
print("no data") // todo deal with no data
completion([])
return
}
do {
let posts = try JSONDecoder().decode([Post].self, from: data)
DispatchQueue.main.async {
completion(posts)
}
print(posts)
} catch {
print("\(error)")
}
}
.resume()
}
}

Google Books API GET Request returns blank screen

Good day, All
So I am learning/practicing Network calls. I came across a video by Paul Hudson where he makes a call to the Itunes API using the same code I am trying to use here. However, I am trying to make a call to the Google Books API. My call (code below) is not working, it (returns a blank screen) I am not sure why. I am of course using variables from the URL I am trying to call/make a request from.
import SwiftUI
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
var id: Int
var etag: String
}
struct ContentView: View {
#State private var results = [Result]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.etag)
}
}
.task {
await loadData()
// ????
}
}
func loadData() async {
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=flowers+inauthor:keyes") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.results
}
} catch {
print("Invalid data")
}
}
}
As previously mentioned, this code was tested using the Itunes API and it worked flawlessly. I am not sure what is causing the issue or what can fix it. I will keep searching and practicing on my end.
Thank you!
the reason your are getting a blank screen, is because your Response and Result struct do not match the json data you get from
the api. Look carefully at the json data and you will see the difference. Try something like this:
struct Response: Codable {
let items: [Result] // <--- here
}
struct Result: Codable, Identifiable { // <--- here
var id: String // <--- here
var etag: String
}
struct ContentView: View {
#State private var results = [Result]()
var body: some View {
List(results) { item in // <--- here
VStack(alignment: .leading) {
Text(item.etag)
}
}
.task {
await loadData()
}
}
func loadData() async {
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=flowers+inauthor:keyes") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.items // <--- here
}
} catch {
print("Invalid data")
}
}
}

Consume data from blockchain API

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
}

Consume #Published values on the main thread?

Is there a way to specify that count should only publish on the main thread? I've seen docs that talk about setting up your Publisher using receive(on:), but in this case the #Publisher wrapper hides that logic.
import SwiftUI
import Combine
class MyCounter: ObservableObject {
#Published var count = 0
public static let shared = MyCounter()
private init() { }
}
struct ContentView: View {
#ObservedObject var state = MyCounter.shared
var body: some View {
return VStack {
Text("Current count: \(state.count)")
Button(action: increment) {
HStack(alignment: .center) {
Text("Increment")
.foregroundColor(Color.white)
.bold()
}
}
}
}
private func increment() {
NetworkUtils.count()
}
}
public class NetworkUtils {
public static func count() {
guard let url = URL.parse("https://www.example.com/counter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response as? HTTPURLResponse {
let statusCode = response.statusCode
if statusCode >= 200 && statusCode < 300 {
do {
guard let responseData = data else {
return
}
guard let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] else {
return
}
if let newCount = json["new_count"] as? Int{
MyCounter.shared.count = newCount
}
} catch {
print("Caught error")
}
}
}
}
task.resume()
}
}
As you can see from my updated example, This is a simple SwiftUI view that has a button that when clicked makes a network call. The network call is asynchronous. When the network call returns, the ObservableObject MyCounter is updated on a background thread. I would like to know if there is a way to make the ObservableObject publish the change on the main thread. The only way I know to accomplish this now is to wrap the update logic in the network call closure like this:
DispatchQueue.main.async {
MyCounter.shared.count = newCount
}
Instead of using URLSession.shared.dataTask(with: request), you can use URLSession.shared.dataTaskPublisher(for: request) (docs) which will allow you to create a Combine pipeline. Then you can chain the receive(on:) operator as part of your pipeline.
URLSession.shared.dataTaskPublisher(for: request)
.map { response in ... }
...
.receive(on: RunLoop.main)
...
Also check out heckj's examples, I've found them to be very useful.
If you try to set value marked #Published from a background thread you will see this error:
Publishing changes from background threads is not allowed; make sure
to publish values from the main thread (via operators like receive
So you have to make sure anywhere you set the value that it this done on the main thread, the values will always be published on the main thread.
The Combine way to accomplish this (for API that do not provide Publishers) could be replacing
MyCounter.shared.count = newCount
with
Just(newCount).receive(on: RunLoop.main).assign(to: &MyCounter.shared.$count)
And here is how we can do it using Modern Concurrency async/await syntax.
import SwiftUI
final class MyCounter: ObservableObject {
#Published var count = 0
public static let shared = MyCounter()
private init() {}
#MainActor func setCount(_ newCount: Int) {
count = newCount
}
}
struct ContentView: View {
#ObservedObject var state = MyCounter.shared
var body: some View {
return VStack {
Text("Current count: \(state.count)")
Button(action: increment) {
HStack(alignment: .center) {
Text("Increment")
.bold()
}
}
.buttonStyle(.bordered)
}
}
private func increment() {
Task {
await NetworkUtils.count()
}
}
}
class NetworkUtils {
static func count() async {
guard let url = URL(string: "https://www.example.com/counter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
do {
let (data, response) = try await URLSession.shared.data(for: request)
await MyCounter.shared.setCount(Int.random(in: 0...100)) // FIXME: Its just for demo
if let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 { throw URLError(.badServerResponse) }
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Expected [String: Any]"))
}
if let newCount = json["new_count"] as? Int {
await MyCounter.shared.setCount(newCount)
}
} catch {
print("Caught error :\(error.localizedDescription)")
}
}
}