API not displaying data in Swift for iOS - ios

Currently not getting data from API to display.
Current code:
import SwiftUI
struct Response: Decodable {
var content: [Result]
}
struct Result : Decodable {
var code: String
var fire: String
var name: String
var police: String
var medical: String
}
struct ContentView: View {
#State private var content = [Result]()
var body: some View {
List(content, id: \.code) { item in
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.medical)
}
}
.onAppear(perform: loadData)
}
func loadData() {
let url = URL(string: "https://emergency-phone-numbers.herokuapp.com/country/gb")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { print(error); return }
do {
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.content = decodedResponse.content
}
} catch {
print(error)
}
}.resume()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Current error:
2021-01-21 19:21:07.094582+0000 iTunes API[39924:1098972] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
keyNotFound(CodingKeys(stringValue: "content", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "content", intValue: nil) ("content").", underlyingError: nil))
Please advise. Thanks in advance.

Please note that the content from https://emergency-phone-numbers.herokuapp.com/country/gb is:
{"code":"GB","fire":"999","police":"999","name":"United Kingdom","medical":"999"}
The problem is that there's no content on the response that you're getting, so the JSONDecoder is failing to parse the response. It seems the response you get contains content that fits on Result, so if you change your code to parse that, it works fine:
import SwiftUI
struct Response: Decodable {
var content: [Result]
}
struct Result : Decodable {
var code: String
var fire: String
var name: String
var police: String
var medical: String
}
struct ContentView: View {
#State private var content = [Result]()
var body: some View {
List(content, id: \.code) { item in
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.medical)
}
}
.onAppear(perform: loadData)
}
func loadData() {
let url = URL(string: "https://emergency-phone-numbers.herokuapp.com/country/gb")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { print(error); return }
do {
let decodedResponse = try JSONDecoder().decode(Result.self, from: data!)
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.content = [decodedResponse]
}
} catch {
print(error)
}
}.resume()
}
}
I guess, in the end, you probably wanted a collection of Results to fit on the content, so you might need to change something on that API of yours to return that collection, then your code would work fine.

Related

SwiftUI : Can't receive API response

I'm trying to get a response from this link : https://zsr.octane.gg/players/5f5ae840c6cbf591c568a477 but it won't work and I can't figure why.
There is my ContentView.swift :
struct ContentView: View {
#State private var players = [Player]()
var body: some View {
List(players, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.country)
}
}.task {
await loadData()
}
}
func loadData() async {
guard let url = URL(string: "https://zsr.octane.gg/players/5f5ae840c6cbf591c568a477") else {
print("URL invalide")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
players = decodedResponse.players
}
} catch {
print("Invalid data")
}
}
}
My Response struct :
struct Response: Codable {
var players: [Player]
}
Player struct :
struct Player: Codable {
var _id: String
var slug: String
var tag: String
var name: String
var country: String
var team: Team
var accounts: [Account]
var revelant: Bool
}
Team struct :
struct Team: Codable {
var _id: String
var slug: String
var name: String
var region: String
var image: String
var relevant: Bool
}
Account struct :
struct Account: Codable {
var platform: String
var id: String
}
Edit: the error that I have from the do catch is :
Invalid data with error: keyNotFound(CodingKeys(stringValue: "revelant", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "revelant", intValue: nil) ("revelant").", underlyingError: nil))
I followed a tutorial which works well but when I replace the link and use my own structs nothing happens when I launch the app.
Thanks for your help.
Your response is a dictionary not an array to to log error use try instead of try? and log the error inside the catch part , this snippet is after the edit and it shows currently info of 1 player
One player
func loadData() async {
guard let url = URL(string: "https://zsr.octane.gg/players/5f5ae840c6cbf591c568a477") else {
print("URL invalide")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decodedResponse = try JSONDecoder().decode(Player.self, from: data)
players = [decodedResponse]
} catch {
print("Invalid data with error: ",error)
}
}
All players
May be you mean to drop 5f5ae840c6cbf591c568a477 from guard let url = URL(string: "https://zsr.octane.gg/players/5f5ae840c6cbf591c568a477") else { to get all players , check here complete working demo
import SwiftUI
struct ContentView: View {
#State private var players = [Player]()
var body: some View {
List(players, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.name ?? "")
.font(.headline)
Text(item.country ?? "")
}
}.task {
await loadData()
}
}
func loadData() async {
guard let url = URL(string: "https://zsr.octane.gg/players") else {
print("URL invalide")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decodedResponse = try JSONDecoder().decode(Response.self, from: data)
players = decodedResponse.players
} catch {
print("Invalid data",error)
}
}
}
struct Response: Codable {
var players: [Player]
}
struct Player: Codable {
var _id: String
var slug: String
var tag: String
var name: String?
var country: String?
var accounts: [Account]?
}
struct Team: Codable {
var _id: String
var slug: String
var name: String
var region: String
var image: String
var relevant: Bool
}
struct Account: Codable {
var platform: String
var id: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Note: a variable that may exist in a model or not should be marked as optional

How to pass data to a sub view from ContentView in SwiftUI

Forgive me if this doesn't make sense, I am a total beginner at Swift. I am creating a recipe app that pulls data from an API and lists it out in navigation links. When the user clicks on the recipe I want it to move to sub view and display information from the API such as recipe name, image, ingredients, and have a button with a link to the webpage.
I was able to get the data pulled into the list with navigation links. However, now I do not know how to go about setting up the recipe details sub view with all of the information I listed above.
This is where I call the API:
class RecipeService {
func getRecipes(_ completion: #escaping (Result<[Recipe], Error>) -> ()) {
let url = URL(string: "http://www.recipepuppy.com/api")!
URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error {
return completion(.failure(error))
}
guard let data = data else {
return completion(.failure(NSError(domain: "", code: -1, userInfo: nil)))
}
do {
let response = try JSONDecoder().decode(RecipesResponses.self, from: data)
completion(.success(response.results))
} catch {
completion(.failure(error))
}
}.resume()
}
}
This is where I take in the recipe responses:
struct RecipesResponses: Codable {
let title: String
let version: Double
let href: String
let results: [Recipe]
}
struct Recipe: Codable {
let title, href, ingredients, thumbnail: String
var detail: URL {
URL(string: href)!
}
var thumb: URL {
URL(string: thumbnail)!
}
}
This is my recipe ViewModel:
class RecipeViewModel: ObservableObject {
#Published var recipes = [Recipe]()
#Published var isLoading = false
private let service = RecipeService()
init() {
loadData()
}
private func loadData() {
isLoading = true
service.getRecipes{ [weak self] result in
DispatchQueue.main.async {
self?.isLoading = false
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let recipes):
self?.recipes = recipes
}
}
}
}
}
This is my view where I list out the API responses:
struct ListView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
NavigationView {
List(viewModel.recipes, id: \.href) { recipe in
NavigationLink (destination: RecipeDetailView()) {
HStack{
CachedImageView(recipe.thumb)
.mask(Circle())
.frame(width: 80)
VStack(alignment: .leading) {
Text(recipe.title)
.font(.largeTitle)
.foregroundColor(.black)
.padding()
}
}
.buttonStyle(PlainButtonStyle())
}
}
.navigationBarTitle(Text("All Recipes"))
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
This is the view where I would like to list out the recipe details and link to the webpage. This is where I am struggling to be able to pull the API data into:
struct RecipeDetailView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
Text("Detail View")
}
}
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView()
}
}
Images of app
You can change RecipeDetailView to accept a Recipe as a parameter:
struct RecipeDetailView: View {
var recipe : Recipe
var body: some View {
Text(recipe.title)
Link("Webpage", destination: recipe.detail)
//etc
}
}
Then, in your NavigationLink, pass the Recipe to it:
NavigationLink(destination: RecipeDetailView(recipe: recipe)) {
One thing I'd warn you about is force unwrapping the URLs in Recipe using ! -- you should know that if you ever get an invalid/malformed URL, this style of unwrapping will crash the app.
Update, to show you what the preview might look like:
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView(recipe: Recipe(title: "Recipe name", href: "https://google.com", ingredients: "Stuff", thumbnail: "https://linktoimage.com"))
}
}

Why is my #AppStorage not working on SwiftUI?

I'm trying to set up the #AppStorage wrapper in my project.
I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the #AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update #AppStorage with the newest Text data, (which would delete the past Poem stored).
Currently, the code below builds but does not display anything in the Text(currentPoemTitle).
Data Model
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
TestView
import SwiftUI
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage("currentPoemtTitle") var currentPoemTitle = ""
#AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
var body: some View {
VStack{
Text(currentPoemTitle)
Button("Fetch next text") {
fetch.getPoem()
}
}.onAppear{
if let poem = fetch.poems.first {
currentPoemTitle = "\(poem.title)"
currentPoemAuthor = "\(poem.author)"
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
What am I missing? Thanks.
Here are a few code edits to get you going.
I added AppStorageKeys to manage the #AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")
Your question asked how to update the #AppStorage with the data, and the simple solution is to add the #AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.
The purpose of using #ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of #AppStorage, you make the #ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the #ObservedObject values directly, instead of relying on #AppStorage. I'm not sure if you want this, but it would remove the need for the #AppStorage variables entirely.
I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)
Code:
import Foundation
import SwiftUI
import Combine
struct AppStorageKeys {
static let poemTitle = "current_poem_title"
static let poemAuthor = "current_poem_author"
}
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
#AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
init() {
getPoem2()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
guard let poemData = data else {
print("No data")
return
}
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
self.updateFirstPoem()
}
} catch {
print("Error")
}
}
.resume()
}
func getPoem2() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to [Poem]
.decode(type: [Poem].self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("poetry fetch completion: \(result)")
} receiveValue: { (value) in
// if success, will return [Poem]
// here you can update your view
self.poems = value
self.updateFirstPoem()
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
func updateFirstPoem() {
if let firstPoem = self.poems.first {
self.poemTitle = firstPoem.title
self.poemAuthor = firstPoem.author
}
}
}
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
var body: some View {
VStack(spacing: 10){
Text("App Storage:")
Text(currentPoemTitle)
Text(currentPoemAuthor)
Divider()
Text("Observed Object:")
Text(fetch.poems.first?.title ?? "")
Text(fetch.poems.first?.author ?? "")
Button("Fetch next text") {
fetch.getPoem()
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}

How do i call the parsed data from the GET request that have a longer nested JSON structure in Swift?

NOTE: Forgive my cluelessness, i am still new in regards to this. The full code is posted at the bottom.
ISSUE: It seems that when i have a short nest, i am able to call it for my #Published property however when i try an api request with a longer nest, like this. and type Decodable structs that follows the structure of the GET request
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
I am not able to call it for the #Published var dataSet1 = [TripTest]()
self.dataSet1 = tripJSON.TripList.Trip.Leg
I get an error message, that says "Value of type '[TravelDetail]' has no member 'Leg'
I am not sure why, however it works when i use [TravelDetail]() instead of [TripTest]() in the #Published var and stop at Trip before Leg for the dataSet1, then it seems to at least build successfully. But now i am not able to get the name and type information from the request
Full code
import SwiftUI
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
class TripViewModel: ObservableObject {
#Published var dataSet1 = [TripTest]()
init() {
let urlString = "http://xmlopen.rejseplanen.dk/bin/rest.exe/trip?originId=8600790&destId=6553&format=json"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
guard let data = data else { return }
do {
let tripJSON = try
JSONDecoder().decode(TripScheduleTest.self, from: data)
print(data)
DispatchQueue.main.async {
self.dataSet1 = tripJSON.TripList.Trip.Leg
}
} catch {
print("JSON Decode error: ", error)
}
}.resume()
}
}
struct TripView: View {
#ObservedObject var vm = TripViewModel()
var body: some View {
List(vm.dataSet1, id: \.self) { day in
Text("Test")
.font(.system(size: 12, weight: .bold))
Text(" \(day.name)")
.font(.system(size: 12))
}
}
}
Trip is an array (note the [])
You need to get one item of the array by index for example
tripJSON.TripList.Trip.first?.Leg
To assign the value to a non-optional array write
self.dataSet1 = tripJSON.TripList.Trip.first?.Leg ?? []

Getting JSON from a public API to render in a list view in SwiftUI

I'm trying to get the data from a Rest Api to download and render in a list view in SwiftUI.
I think I manage to get the JSON to download and assign to all the relevant Structs correctly but nothing displays in the list view on the simulator when I go to build it.
I'm not even sure I need to have the 'Enum CodingKeys in there.
Can anyone point out where I may be going wrong?
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all, I try to find which error of your code and recognize the launch.details of response data somewhere have null. So that I just mark this property Optional and it work.
For more details, you can refer below code :
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
At catch line, get the error message to know exactly what's happen
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
Hope this help!
Any messages in console? I think you need to add NSAppTransportSecurity>NSAllowsArbitraryLoads>YES in your .plist
Transport security has blocked a cleartext HTTP
With the help from Trai Nguyen, I managed to get the simulator to display the data.
All I was missing was to ensure the variable named 'details' had an optional property (by adding on the "?") - like so:
public var details: String?
and then making sure when I render the text in my view, I had to give it a value to insert if the return is null - like so:
Text(launch.details ?? "No Data Found")
Here is the complete code that works for me:
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details ?? "No Data Found")
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
}
/// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources