How to display a Rest API call in swiftUI? - swift

I am following a tutorial for a simple Rest API call for a swiftui app, but when trying to ping another api I am unable to decode and show the response.
The only things changed from the tutorial are the API call and changing the model id as the api doesn't return an id.
import SwiftUI
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.quote)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.kanye.rest") 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([TaskEntry].self, from: data) {
DispatchQueue.main.async {
print(response)
self.results = response
}
return
}
}
}.resume()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import Foundation
struct TaskEntry: Codable {
let id: UUID
let quote: String
}

If your Rest API does not return id then I assume your response is not decoded to your TaskEntry type.
Try the following
struct TaskEntry: Codable {
let id = UUID()
let quote: String
enum CodingKeys: String, CodingKey {
case quote = "quote"
}
}

If any field can be not provide by api, it shoud be optional value.
If not, it will not find value to pass keypath to your struct/class
struct TaskEntry: Codable {
let id: UUID? // mark as optional value
let quote: String
}

Related

Swift returning empty list from API

I'm learning swift and I wanted to pull some data from a django model to work with, so I was following a tutorial on YouTube. I copied the code on YouTube and I got his Breaking Bad API (https://breakingbadapi.com/api/quotes) to display on my simulator, however when I subbed in the URL to my API, I returned an empty list and my simulator displays only a blank screen.
I've tried using both http://127.0.0.1:8000/api/main_course/
and http://127.0.0.1:8000/api/main_course/?format=json
From my terminal I get 200 OK:
[14/Sep/2022 21:28:48] "GET /api/main_course/ HTTP/1.1" 200 1185
Here's my code:
import SwiftUI
struct Entree: Codable {
var id: Int
var menu: String
var name: String
var descripton: String
}
struct ContentView: View {
#State private var entrees = [Entree]()
var body: some View {
List(entrees, id: \.id) {entree in
Text(entree.name)
Text("Run")
}
.task {
await loadData()
print(entrees)
}
}
func loadData() async {
// create URL
guard let url = URL(string: "http://127.0.0.1:8000/api/main_course/") else {
print("URL Invalid")
return
}
// fetch data from that URL
do {
let (data, _) = try await URLSession.shared.data(from: url)
// decode that data
if let decodedResponse = try? JSONDecoder().decode([Entree].self, from: data) {
entrees = decodedResponse
}
}
catch {
print("Data invalid")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It seems that it is on the conversion.
Try using the same keys.
struct Entree: Codable {
var quoteId: Int
var quote: String
var author: String
var series: String
enum CodingKeys: String, CodingKey {
case quoteId = "quote_id"
}
}

Preview Crashed when I get data from api

I'm trying to fetch data from API according to a tutorial on YouTube and I does exactly as the video but the preview somehow crashed. But when I commented out the Api().getPosts , the preview is able to resume again. If that line of code is wrong how can I write it instead?
User Interface code:
import SwiftUI
struct ContentView: View {
#State var posts: [Post] = []
var body: some View {
VStack {
List(posts) { post in
Text(post.title)
}
.onAppear{
Api().getPosts { (posts) in
self.posts = posts
}
}
}//:VSTACK
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Api Service code:
import SwiftUI
struct Post: Codable, Identifiable {
var id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(completion: #escaping([Post]) -> ()) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return
print("Something occured!")
}
//CALL
URLSession.shared.dataTask(with: url) { data, response, error in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
print(posts)
}//:URLSESSION
.resume()
}
}
use this code for your Post model:
struct Post: Codable, Identifiable {
// let id = UUID() // <-- or this
var id: Int // <-- here
var title: String
var body: String
}

Fetching JSON, appending to array: Escaping closure captures mutating 'self' parameter

I have prepared a simple test project at Github to demo my problem:
I have a SwiftUI List and I try to display the var items:[String] in it.
When I only have a hardcoded array like below - it works fine and displays in iPhone:
items = (1...200).map { number in "Item \(number)" }
But when I try to fetch JSON web page and append results to items then I get the error:
Escaping closure captures mutating 'self' parameter
I understand that the line items.append(str) modifies the parent ContentView object out of dataTask closure and that is not good for some reason... but how to fix my code then?
import SwiftUI
struct TopResponse: Codable {
let data: [Top]
}
struct Top: Codable {
let uid: Int
let elo: Int
let given: String
let photo: String?
let motto: String?
let avg_score: Double?
let avg_time: String?
}
struct ContentView: View {
var items:[String];
init() {
items = (1...200).map { number in "Item \(number)" }
let url = URL(string: "https://slova.de/ws/top")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
let decoder = JSONDecoder()
guard let data = data else { return }
do {
let tops = try decoder.decode(TopResponse.self, from: data)
for (index, top) in tops.data.enumerated() {
let str = "\(index + 1): \(top.given)"
items.append(str) // this results in compile error!
}
} catch {
print("Error while parsing: \(error)")
}
}
task.resume()
}
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
}
}
Should I move the items out of the View maybe?
My final target is to have the JSON data in Core Data and then update/notify the List from it.
I have such an app in Android (structured as MVVM) and now I am trying to port it to SwiftUI, being a Swift newbie.
UPDATE:
I have added a view model file as suggested by achu (thanks!) and it kind of works, but the List is only updated with new items when I drag at it. And there is a warning
[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I will move the items to ViewModel and eventually move the service call to an APIManager class
EDIT: The UI update should be in the main thread. Added service call on ViewModel init().
struct TestView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List(viewModel.items, id: \.self) { item in
Text(item)
}
}
}
class TestViewModel: ObservableObject {
#Published var items: [String] = []
init() {
self.fetchData()
}
func fetchData() {
let url = URL(string: "https://slova.de/ws/top")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
let decoder = JSONDecoder()
guard let data = data else { return }
do {
let tops = try decoder.decode(TopResponse.self, from: data)
for (index, top) in tops.data.enumerated() {
let str = "\(index + 1): \(top.given)"
self.updateItems(str)
}
} catch {
print("Error while parsing: \(error)")
}
}
task.resume()
}
func updateItems(_ str: String) {
DispatchQueue.main.async {
self.items.append(str)
}
}
}

JSON with SwiftUI using Array

I am new to SwiftUI and only used UIKit before. I tried to use JSON to show a title but all tutorial videos work with lists. I dont want to use any list with JSON which shows all data. Only want to fetch for example the second or a specific array for title.
How can I remove the list in SwiftUI?
My View:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
HStack {
Text(String(post.points))
Text(post.title)
}}
.navigationBarTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NetworkManager:
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fetchData() {
if let url = URL(string: "https://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
And my struct files for Json:
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let points: Int
let title: String
}
I dont want to use any list with JSON which shows all data. Only want
to fetch for example the second or a specific array for title.
You can use a computed property to access the specific element (and its title) from the posts array:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
// return the title of the second item in the `posts` array
var title: String {
guard networkManager.posts.count >= 2 else {
// decide what to do when data is not yet loaded or count is <= 1
return "Loading..."
}
return networkManager.posts[1].title
}
var body: some View {
NavigationView {
Text(title)
.navigationBarTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}

How can I iterate through a specific JSON item in Swift

I've done my best to implement other peoples similar questions from around the internet but haven't been successful. I am working on a simple app that displays the top 50 cryptocurrencies. The information I will show will be the symbol(BTC, ETH...) and price. For now I am just trying to show the symbol.
I am able to get the symbol for each coin individually by using Text(self.fetcher.publishedCoins?.data.coins[0].symbol ?? "") and changing the array index. Obviously I don't want to do that 50 times so I tried implementing ForEach but couldn't figure it out. Here's where I'm at...
ContentView.swift
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
#ObservedObject var fetcher = CoinFetcher()
var body: some View {
NavigationView {
List {
//Text(self.fetcher.publishedCoins?.data.coins[0].symbol ?? "Error Updating")
//Attempting to iterate through Coin.symbol
ForEach(self.fetcher.publishedCoins?.data.coins[Coin] ?? "") { select in
Text(select.symbol)
}
}
}
}}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
LoadJSON.swift
import Foundation
public class CoinFetcher: ObservableObject {
#Published var publishedCoins: Top?
init() {
loadJSON()
}
func loadJSON() {
let url = URL(string: "https://api.coinranking.com/v1/public/coins")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let retrievedData = data {
let webData = try JSONDecoder().decode(Top.self, from: retrievedData)
print(Top.self)
DispatchQueue.main.async {
self.publishedCoins = webData
}
} else {
print("No data loaded")
}
} catch {
print ("Error here")
}
}.resume()
}
}
Coins.swift
import Foundation
// MARK: - Top
struct Top: Codable {
let status: String
let data: Data
}
// MARK: - Data
struct Data: Codable {
let coins: [Coin]
}
// MARK: - Coin
struct Coin: Codable {
let id: Int
let uuid: String
let slug: String
let symbol: String
let name: String
let confirmedSupply: Bool
let volume: Int
let marketCap: Int
let price: String
let circulatingSupply: Double
let totalSupply: Double
let approvedSupply: Bool
let change: Double
let rank: Int
let history: [String?]
enum CodingKeys: String, CodingKey {
case id, uuid, slug, symbol, name, confirmedSupply, volume, marketCap, price, circulatingSupply, totalSupply, approvedSupply, change, rank, history
}
}
Thank you for your help!
If you can conform Coin to Hashable:
struct Coin: Codable, Hashable { ... }
you can try the following:
NavigationView {
List {
ForEach(self.fetcher.publishedCoins?.data.coins ?? [], id:\.self) { coin in
Text(coin.symbol)
}
}
}
Note that as your data can change you need to use a dynamic ForEach loop (with an explicit id parameter)