How can I iterate through a specific JSON item in Swift - 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)

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"
}
}

Blank Screen on Loading List in SwiftUI

I am trying to display a list in a view after a condition. If data is recieved from API than screen will load new view having list showing multiple fields. Here I am showing only one field in the list. The navigation code is working fine and data is also decoded however blank screen appears when clicking list button screen moves to next view but blank screen.
Here is the view in which I am showing list :
import SwiftUI
struct MyPriceList: View {
#StateObject var road = ListAPI()
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
}
struct MyPriceList_Previews: PreviewProvider {
static var previews: some View {
MyPriceList()
}
}
}
The following is the viewmodel in which I have decoded the JSON data and applied the navigation
import Foundation
class ListAPI : ObservableObject
{
#Published var priceRoad = [ResponseList]()
func getList()
{
// url building code //
let list = URLSession.shared.dataTask(with: urlRequest)
{
(data, response, error) in
if let error = error {
print("Error \(error)")
}
if let data = data
{
do
{
let jsonList = try JSONDecoder().decode(PriceList.self, from: data)
let panama = jsonList.response
for pan in panama
{
print(pan.packageName) //successfully printing
}
if jsonList.success==true
{
DispatchQueue.main.async
{
self.navigate = true
self.priceRoad = jsonList.response
}
}
else
{
DispatchQueue.main.async
{
self.errorMessage = jsonList.message
}
}
}
catch
{
print("error \(error)")
}
}
}
list.resume()
} }
This is the data model for Json
struct PriceList : Codable
{
let success: Bool
let message: String
let response: [ResponseList]
enum CodingKeys:String, CodingKey{
case response = "ResponseData"
case success = "IsSuccess"
case message = "Message"
}
}
struct ResponseList:Codable
{
let packageId: Int
let packageName: String
let price: Double
let discountedPrice: Double
let testType: String
let testPackageGroupId: Int?
let SampleType: [SampleTypeList]?
enum CodingKeys:String, CodingKey{
case packageId = "PackageId"
case packageName = "PackageName"
case price = "Price"
case discountedPrice = "DiscountedPrice"
case testType = "Type"
case testPackageGroupId = "TestPackageGroupId"
case SampleType = "SampleTypeList"
}}
struct SampleTypeList:Codable
{
let testSampleTypeId: String
let sampleName: String
let colourCode: String
enum CodingKeys:String, CodingKey{
case testSampleTypeId = "TestSampleTypeId"
case sampleName = "SampleName"
case colourCode = "ColourCode"
}
}
I need to display TestName as packageName, MRP as price, B2B as discountedPrice, and TestType as testType.
try this approach, calling road.getList() in .onAppear{}:
struct MyPriceList: View {
#StateObject var road = ListAPI()
var body: some View { // <-- here need a body
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
.onAppear {
road.getList() // <-- here load your data
}
}
}
and make PriceList and ResponseList Identifiable, like this:
struct PriceList : Identifiable, Codable {
let id = UUID()
// ...
}
struct ResponseList: Identifiable, Codable {
let id = UUID()
// ...
}
struct SampleTypeList: Identifiable, Codable {
let id = UUID()
// ...
}
Alternatively, in ListAPI, you could have init() { getList() }, instead of using .onAppear {road.getList()}

How to pass in data from Models, into Table View Controller and then to DetailViewController

I have created quite a few classes and structs that have arrays from that data received from the end points of the Json API, and now I am in need of help passing in those arrays into a table view controller as well as a detail view controller that passes in the images and details from my model. Below are all of the models, "MealCategoryResponse", "MealDetailModel", "Meals" and MealDetailModel. I've included the Constants, as well as the ViewModels that i've created for the data received from my API end points. How can I create a TableViewController that will list all of my Model as well as when the arrays of meals are clicked, take me to detailViewController that has the details from my DetailModel? Please let me know if i need to add more information or if this question is not acceptable or if it needs to be edited. I am asking here because I have been trying to do this for the last days & I have covid.
Constants that i've created to get the Url's link
import Foundation
struct Constants {
struct Urls {
static let mealsCategoriesUrl = URL(string:
"https://www.themealdb.com/api/json/v1/1/categories.php")!
static func getMealNameCategoryUrl(_ strMeal: String) -> URL {
return URL(string:"https://www.themealdb.com/api/json/v1/1/filter.php?c=Beef")!
}
static func getMealByIdUrl(_ idMeal: String) -> URL { return URL(string:"https://www.themealdb.com/api/json/v1/1/lookup.php?i=52874")!
}
}
static func getImagesUrl(strMealThumb: String) -> URL {
return URL(string: "https://www.themealdb.com//images//category//beef.png")!
}
struct strMealThumbUrl {
static let strMealThumb = URL(string:"https://www.themealdb.com//images//media//meals//vtqxtu1511784197.jpg")!
}
struct strYoutubeUrl {
let strYoutube = URL (string:
"https://www.youtube.com/watch?v=nMyBC9staMU")!
}
}
Model with arrays
import Foundation
struct MealCategoryResponse: Decodable {
let categories: [MealCategory]
}
struct MealCategory: Decodable {
let idCategory: String //id
let strCategory: String //name
let strCategoryThumb: String //image//
let strCategoryDescription: String
}
import Foundation
struct Meals: Decodable {
let meals: [Meals]
let strMeal: String
let strMealThumb: String //image
let idMeal: String
}
import Foundation
struct MealDetailModel: Codable {
let meals: [MealDetail]
}
struct MealDetail: Codable {
let idMeal: String
let strMeal: String
let strCategory: String
let strInstructions: String
let strIngredient: String
let strMeasure: String
let strYoutube: String
let strMealThumb: String //image
let meals: [[String: String?]]
}
ViewModels for passing in the data from API to the Model
import Foundation
#MainActor
class MealsListModel: ObservableObject {
#Published public var mealsCategories: [CategoryMealViewModel] = []
private func loadCategories(strCategory: String) async {
do {
let mealCategoryResponse = try await
WebServiceRequest().parseData(url: Constants.Urls.mealsCategoriesUrl) { data in
return try? JSONDecoder().decode(MealCategoryResponse.self, from:data )
}
self.mealsCategories = mealCategoryResponse.categories.map(CategoryMealViewModel.init)
} catch {
print(ServiceError.decocorrupt)
}
}
}
import Foundation
import UIKit
//create View model for meals Category
struct CategoryMealViewModel: Identifiable {
let id = UUID()
private let mealCategory: MealCategory
init(_ mealCategory: MealCategory) {
self.mealCategory = mealCategory
}
var strCategory: String {
mealCategory.strCategory
}
var strCategoryThumb: URL? {
URL(string: mealCategory.strCategoryThumb)
}
}
import Foundation
import UIKit
#MainActor
class DetailViewModel: ObservableObject{
#Published var strMeal: String = ""
#Published var strInstructions: String
#Published var strIngredient: String
#Published var strMeasure: String
#Published var strMealThumb:URL?
init(strInstructions: String, strIngredient: String, strMeasure: String) {
self.strInstructions = strInstructions
self.strIngredient = strIngredient
self.strMeasure = strMeasure
func loadMealDetails(idMeal: String) async {
do {
let mealDetailResponse = try await WebServiceRequest().parseData(url: Constants.Urls.getMealByIdUrl(strMeal)) { data in
return try? JSONDecoder().decode(MealDetailModel.self, from:data )
}
let mealDetail = mealDetailResponse.meals
} catch {
print(error)
}
}
}
}

async request to Unsplash api not working correctly

I've been having some trouble with my swift package called UnsplashSwiftUI
Before WWDC, I was having some trouble which caused my View to reload (as you can see on the main branch) but when async/await was announced, it seemed to be the perfect opportunity for my package.
I am working on the package with async/await on the development branch.
However, I am now having some trouble with the async API request.
Here's my minimally reproducible example, I get the printed error 'Failed to fetch image' from the catch block of my async function getURL(). I also tried calling the task with async inside.
//From this
.task {
await getURL()
}
//To this
.task {
async {
await getURL()
}
}
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
VStack {
UnsplashRandom(clientId: "TSozaArCYtCWcXnnUkh4KvKJ5ZfmVOn_FYbIVVn76Ew")
.frame(width: 500, height: 500)
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
import SwiftUI
#available(iOS 15, OSX 12, *)
public struct UnsplashRandom: View {
//MARK: Parameters
//Required parameters
var clientId: String //Unsplash API access key
#State private var unsplashData: UnsplashData? = nil
#State private var requestURL: URL? = nil
//MARK: Init
public init(clientId: String) {
self.clientId = clientId
let url = URL(string: "https://api.unsplash.com/")!
guard var components = URLComponents(url: url.appendingPathComponent("photos/random"), resolvingAgainstBaseURL: true)
else { fatalError("Couldn't append path component")}
components.queryItems = [URLQueryItem(name: "client_id", value: clientId)]
_requestURL = State(initialValue: components.url!)
}
//MARK: Body
public var body: some View {
//MARK: Main View
ZStack(alignment: .bottomTrailing) {
//MARK: Remote Image
AsyncImage (url: URL(string: unsplashData?.urls!.raw! ?? "https://images.unsplash.com/photo-1626643590239-4d5051bafbcc?ixid=MnwxOTUzMTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2MjY5Njc0MjI&ixlib=rb-1.2.1")!)
.aspectRatio(contentMode: .fit)
}
.task {
await getURL()
}
}
func getURL() async {
do {
let (data, _) = try await URLSession.shared.data(from: requestURL!)
unsplashData = try JSONDecoder().decode(UnsplashData.self, from: data)
} catch {
print("Failed to fetch image")
}
}
}
import Foundation
// MARK: - UnsplashData
struct UnsplashData: Codable {
let id: String?
let createdAt, updatedAt, promotedAt: Date?
let width, height: Int?
let color, blurHash: String?
let unsplashDataDescription: String?
let altDescription: String?
let urls: Urls?
let links: UnsplashDataLinks?
let categories: [String]?
let likes: Int?
let likedByUser: Bool?
let currentUserCollections: [String]?
let sponsorship: JSONNull?
let user: User?
let exif: Exif?
let location: Location?
let views, downloads: Int?
enum CodingKeys: String, CodingKey {
case id
case createdAt = "created_at"
case updatedAt = "updated_at"
case promotedAt = "promoted_at"
case width, height, color
case blurHash = "blur_hash"
case unsplashDataDescription = "description"
case altDescription = "alt_description"
case urls, links, categories, likes
case likedByUser = "liked_by_user"
case currentUserCollections = "current_user_collections"
case sponsorship, user, exif, location, views, downloads
}
}
// MARK: - Exif
struct Exif: Codable {
let make, model, exposureTime, aperture: String?
let focalLength: String?
let iso: Int?
enum CodingKeys: String, CodingKey {
case make, model
case exposureTime = "exposure_time"
case aperture
case focalLength = "focal_length"
case iso
}
}
// MARK: - UnsplashDataLinks
struct UnsplashDataLinks: Codable {
let linksSelf, html, download, downloadLocation: String?
enum CodingKeys: String, CodingKey {
case linksSelf = "self"
case html, download
case downloadLocation = "download_location"
}
}
// MARK: - Location
struct Location: Codable {
let title, name, city, country: String?
let position: Position?
}
// MARK: - Position
struct Position: Codable {
let latitude, longitude: Double?
}
// MARK: - Urls
struct Urls: Codable {
let raw, full, regular, small: String?
let thumb: String?
}
// MARK: - User
struct User: Codable {
let id: String?
let updatedAt: Date?
let username, name, firstName, lastName: String?
let twitterUsername: String?
let portfolioURL: String?
let bio: String?
let location: String?
let links: UserLinks?
let profileImage: ProfileImage?
let instagramUsername: String?
let totalCollections, totalLikes, totalPhotos: Int?
let acceptedTos: Bool?
enum CodingKeys: String, CodingKey {
case id
case updatedAt = "updated_at"
case username, name
case firstName = "first_name"
case lastName = "last_name"
case twitterUsername = "twitter_username"
case portfolioURL = "portfolio_url"
case bio, location, links
case profileImage = "profile_image"
case instagramUsername = "instagram_username"
case totalCollections = "total_collections"
case totalLikes = "total_likes"
case totalPhotos = "total_photos"
case acceptedTos = "accepted_tos"
}
}
// MARK: - UserLinks
struct UserLinks: Codable {
let linksSelf, html, photos, likes: String?
let portfolio, following, followers: String?
enum CodingKeys: String, CodingKey {
case linksSelf = "self"
case html, photos, likes, portfolio, following, followers
}
}
// MARK: - ProfileImage
struct ProfileImage: Codable {
let small, medium, large: String?
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public func hash(into hasher: inout Hasher) {
// No-op
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
In your models, UnsplashData and User, replace Date? with String?.
After that, this is how I tested my answer:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var unsplashData: UnsplashData?
var body: some View {
VStack {
if let unsplash = unsplashData {
Text("user is \(unsplash.user?.name ?? "no name")")
} else {
Text("testing testing")
}
}
.task {
await getUnsplashData()
}
}
func getUnsplashData() async {
let fetchResponse: UnsplashData? = await fetchIt()
if let theResponse = fetchResponse {
self.unsplashData = theResponse
print("\n-----> getUnsplashData: \(theResponse)")
}
}
func fetchIt<T: Decodable>() async -> T? {
let url = URL(string: "https://api.unsplash.com/photos/random?client_id=TSozaArCYtCWcXnnUkh4KvKJ5ZfmVOn_FYbIVVn76Ew")!
let request = URLRequest(url: url)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
// throw URLError(.badServerResponse) // todo
print(URLError(.badServerResponse))
return nil
}
let results = try JSONDecoder().decode(T.self, from: data)
return results
}
catch {
return nil
}
}
}

How to display a Rest API call in swiftUI?

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
}