I work with this API and I figured out how to parse all the data from it, except one value – payload weight. The problem is I have to parse it by id – "leo", but I don't understand how to do this.
This is my code:
// MARK: - API
class InfoApi {
func getRockets(completion: #escaping ([RocketInfo]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let rocketsInfo = try JSONDecoder().decode([RocketInfo].self, from: data!)
DispatchQueue.main.async {
completion(rocketsInfo)
}
} catch {
print(error.localizedDescription)
}
}
.resume()
}
}
// MARK: - MODEL
struct RocketInfo: Codable, Identifiable {
let id = UUID()
let name: String
let firstFlight: String
let country: String
let costPerLaunch: Int
let firstStage: StageInfo
let payloadWeights: [Payload]
enum CodingKeys: String, CodingKey {
case id
case name
case firstFlight = "first_flight"
case country
case costPerLaunch = "cost_per_launch"
case firstStage = "first_stage"
case payloadWeights = "payload_weights"
}
// MARK: - STAGE
struct StageInfo: Codable {
let engines: Int
let fuelAmountTons: Double
let burnTimeSec: Int?
enum CodingKeys: String, CodingKey {
case engines
case fuelAmountTons = "fuel_amount_tons"
case burnTimeSec = "burn_time_sec"
}
static let firstStage = StageInfo(engines: 1, fuelAmountTons: 44.3, burnTimeSec: 169)
static let secondStage = StageInfo(engines: 1, fuelAmountTons: 3.30, burnTimeSec: 378)
}
// MARK: - PAYLOAD
struct Payload: Codable {
let id: String
let kg: Int
let lb: Int
static let payloadWeights = Payload(id: "leo", kg: 450, lb: 992)
}
// MARK: - EXAMPLE
static let example = RocketInfo(name: "Falcon 1", firstFlight: "2006-03-24", country: "Republic of the Marshall Islands", costPerLaunch: 6700000, firstStage: StageInfo.firstStage, payloadWeights: [Payload.payloadWeights])
}
// MARK: - CONTENT VIEW
struct ParametersView: View {
#State var rockets: [RocketInfo] = []
var body: some View {
List(rockets) { rocket in
VStack(spacing: 20) {
HStack {
Text("First flight of \(rocket.name)")
Spacer()
Text("\(rocket.firstFlight)")
}
HStack {
Text("Payload of \(rocket.name)")
Spacer()
Text("\(rocket.payloadWeights[0].kg)") //<-- Here I try to parse a payload weight value
}
}
}
.onAppear {
InfoApi().getRockets { rockets in
self.rockets = rockets
}
}
}
}
// MARK: - PREVIEW
struct ParametersView_Previews: PreviewProvider {
static var previews: some View {
ParametersView()
}
}
I can access payload weight value by pointing an index of the first element of the Payload array in API, but I want to figure out how I can get this value by special id – "Leo".
In API it looks this way:
You can use first(where:) to search through the array and return the first element matching a condition (in this case, matching a certain id):
if let leo = rocket.payloadWeights.first(where: { $0.id == "leo" }) {
Text("\(leo.kg)") //<-- Here I try to parse a payload weight value
}
Related
Building a crystal app. Displaying a list, showing details on click.
Been looking into ObservableObject, Binding, etc.
Tried #State in CrystalView but got lost pretty quickly.
What's the easiest way to pass data around views? Watched a few videos, still confused.
How do I pass crystals[key] into CrystalView()?
struct ContentView: View {
#State private var crystals = [String:Crystal]()
var body: some View {
Text("Crystals").font(.largeTitle)
NavigationView {
List {
ForEach(Array(crystals.keys), id:\.self) { key in
HStack {
NavigationLink(destination: CrystalView()) {
Text(key)
}
}
}
}.onAppear(perform:loadData)
}
}
func loadData() {
guard let url = URL(string: "https://lit-castle-74820.herokuapp.com/api/crystals") else { return }
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data else { return }
do {
let decoded = try JSONDecoder().decode([String:Crystal].self, from: data)
DispatchQueue.main.async {
print(decoded)
self.crystals = decoded
}
} catch let jsonError as NSError {
print("JSON decode failed: \(jsonError)")
}
}.resume()
}
}
struct Crystal: Codable, Identifiable {
var id = UUID()
let composition, formation, colour: String
let metaphysical: [String]
}
struct CrystalView: View {
var body: some View {
Text("crystal")
}
}
try this approach, works well for me:
struct Crystal: Codable, Identifiable {
var id = UUID()
let composition, formation, colour: String
let metaphysical: [String]
// -- here, no `id`
enum CodingKeys: String, CodingKey {
case composition,formation,colour,metaphysical
}
}
struct CrystalView: View {
#State var crystal: Crystal? // <-- here
var body: some View {
Text("\(crystal?.composition ?? "no data")")
}
}
struct ContentView: View {
#State private var crystals = [String:Crystal]()
var body: some View {
Text("Crystals").font(.largeTitle)
NavigationView {
List {
ForEach(Array(crystals.keys), id:\.self) { key in
HStack {
NavigationLink(destination: CrystalView(crystal: crystals[key])) { // <-- here
Text(key)
}
}
}
}.onAppear(perform: loadData)
}
}
func loadData() {
guard let url = URL(string: "https://lit-castle-74820.herokuapp.com/api/crystals") else { return }
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data else { return }
do {
let decoded = try JSONDecoder().decode([String:Crystal].self, from: data)
DispatchQueue.main.async {
print("decoded: \(decoded)")
self.crystals = decoded
}
} catch let jsonError as NSError {
print("JSON decode failed: \(jsonError)")
}
}.resume()
}
}
Something like this:
struct ContentView: View {
#State private var crystals: [Crystal] = []
var body: some View {
NavigationView {
List {
ForEach(crystals) { crystal in
NavigationLink(destination: CrystalView(crystal: crystal)) {
Text(crystal.name)
}
}
}
.navigationTitle("Crystals")
// initial detail view
Text("Select a crystal")
}
.task {
crystals = try? await fetchCrystals()
}
}
func fetchCrystals() async throws -> [Crystal] {
let (data, _) = try await URLSession.shared.data(from: "https://lit-castle-74820.herokuapp.com/api/crystals")
let decoder = JSONDecoder()
return try decoder.decode([Crystal].self, from: data) // you might want to convert this downloaded struct into a more suitable struct for the app.
}
}
struct CrystalView: View {
let crystal: Crystal
var body: some View {
Text(crystal.composition)
.navigationTitle(crystal.name)
}
}
Elaborating the Problem in depth with code
I have a data model (API response) using which I am creating a list. The list items should also display their details in detail view. The problem is details of list items are coming from a different API other than the API used for creating the list. The id of one item is passed to API which in response provides the details of the item.
This is the data model(including items only problem specific):
struct TrackSample : Codable, Identifiable {
let id = UUID()
let success : Bool
let message : String
let trackResponse : [TrackResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case trackResponse = "ResponseData"
}}
struct TrackResponse : Codable, Identifiable {
let id = UUID()
let patientID : String
let name : String
let testCount : Int
//..some more//
enum CodingKeys: String, CodingKey {
case patientID = "PatientId"
case name = "Name"
case status = "Status"
case testCount = "NoOfTests"
}}
ViewModel to fetch API response ( TrackResource() is a different class which implements the networking call):
class TrackViewModel : ObservableObject
{
#Published var trackReport = [TrackResponse]()
#Published var navigate:Bool = false
//other var
private let trackResource = TrackResource()
func getTrack()
{
//code to get data based on which button is clicked in Options
screen.
There are five options to choose:
if else statement follows//
centerID = "100"
let trackRequest = TrackRequest(CenterId:centerID,
SearchText:searchText, StartDate:startDate, EndDate:endDate)
trackResource.track(trackRequest: trackRequest)
{
response in
if(response?.success==true)
{
DispatchQueue.main.async {
self.navigate = true
self.trackReport = response?.trackResponse ?? []
}
}
else
{
DispatchQueue.main.async {
self.errorMessage = response?.message ?? "No Data"
// self.isPresentingAlert
}
}}}}
The view YdaySample which represents the list :
struct YdaySample: View {
#ObservedObject var tracking : TrackViewModel
var body: some View {
NavigationView{
List{
ForEach(tracking.trackReport)
{ truck in
NavigationLink(destination: TrackDetail(track:truck))
{
YdayRow(truck: truck)
}
}
if(tracking.trackReport.isEmpty){
Text("No Record Found !")
//formatting
}}}}}
struct YdaySample_Previews: PreviewProvider {
static var previews: some View {
YdaySample(tracking: TrackViewModel())
}}
The YdayRow() :
struct YdayRow: View {
var truck : TrackResponse
var body: some View {
VStack{
HStack{
Text(truck.name)
//formatting//
}
HStack{
Text("P.Id:")
//formatting//
Text(truck.patientID)
//formatting//
Spacer()
Text("Total Test:")
//formatting//
Text("\(truck.testCount)")
//formatting//
}}}}
struct YdayRow_Previews: PreviewProvider {
static var previews: some View {
YdayRow(truck: TrackResponse(patientID: "1", name: "test",
status: "null", crmNo: "null",
recordDate: "somedate", uniqueID: "null", testCount: 4))
}
}
TrackDetail() updated:
struct TrackDetail: View {
var track: TrackResponse
#State var patientDetail: DetailResponse
var body: some View {
VStack{
HStack{
Text(track.name)
//formatting
}.frame(maxWidth: .infinity, alignment: .center)
HStack{
Text("P.Id:")
//formatting
Text(track.patientId)
//formatting
}
List{ForEach(patientDetail.detailResponse)
{detail in
HStack
{ Text("Barcode: ")
Text(detail.barcode)
}
}
}.onAppear{
Task{
do{
try await getDetail()
}catch{
Alert(title: Text("Error"),message: Text("Not Found"),
dismissButton: .cancel())
}}}}}
func getDetail() async throws{
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "xx.xxx.xx.xx"
urlComponents.path = "/api/reports/getalltests"
urlComponents.queryItems = [URLQueryItem(name: "centerId", value:
("\(668)")),URLQueryItem(name: "patientId", value: "\
(track.patientId)")]
let url = urlComponents.url
var request = URLRequest(url: url!)
print(request)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-
Type")
request.setValue("Basic xcvgjhalddjdj",forHTTPHeaderField:
"Authorization")
// Send HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data,
response, error) in
// Check if Error took place
if let error = error {
print("Error took place \(error)")
return
}
// Read HTTP Response Status code
if let response = response as? HTTPURLResponse {
print("Response HTTP Status code: \(response.statusCode)")
}
if let data = data
{
let dataString = String(data:data,encoding: .utf8)
print(dataString!)
do{
let json = try JSONDecoder().decode(DetailResponse.self,
from: data)
print(json)
DispatchQueue.main.async {
self.patientDetail = json
}
}catch{
print("error \(error)")
}
}
};task.resume()
}}
struct TrackDetail_Previews: PreviewProvider {
static var previews: some View {
TrackDetail(track: TrackResponse(patientId: "4193716", name: "Dummy
Report HCV RNA", status: "null", crmNo: "null", recordDate: "2022-
04-15", uniqueId: "null", testCount: 10), patientDetail:
DetailResponse(success: false, message: "mess", detailResponse:
[]))
}}
print(request) is printing right URL as desired also the patientID is correct in url (http://xxx..x.x/api/reports/getalltests/centerId=668&patientId=(tapped id))
But it is throwing error in decoding saying "Authrization has been denied for this request" error keynotfound(codingkeys(stringvalue: "ResponseData", intvalue:nil), Swift.decodingerror.context(codingpath: [],debugdescription: "No value associated with key CodingKeys(stringvalue: "ResponseData", intvalue:nil)("ResponseData", underlyingError:nil)
struct DetailResponse : Codable{
let success : Bool ?
let message : String
let detailResponse : [PatResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case patResponse = "ResponseData"
}}
struct PatResponse : Codable, Identifiable {
var barcode: String
var:id String {barcode}
let testname : String?
let packageName : String?
let status: String?
let sampleRecievedTime: String?
let recordDate: String
enum CodingKeys: String, CodingKey {
case packageName = "PackageName"
case testname = "TestName"
case status = "Status"
case sampleRecievedTime = "SampleRecievedTime"
case recordDate = "RecordDate"
case barcode = "Barcode"
}}
///////**************///////////////////
The detail view is showing name and ID as they are coming from TrackResponse
but status and barcode are not as they are from different API.
When the user click/tap an item in list, the patientID and centerId is sent as query param in GET request which in response will provide the details associated with the given patientId (center ID is constant). How to implement this?
How about something like this?
struct TrackDetail: View{
var track: TrackResponse
#State var details: TrackDetails?
var body: some View{
HStack{
//Detail implementation
}.onAppear{
Task{
do{
try await doStuff()
}catch{
//Alert
}
}
}
}
func doStuff() async throws{
// pull Details from Api
}
}
I have been having trouble displaying my JSON into my content view. I can decode the data and save it into a dictionary as I have printed and seen. However when its time to display it in ContentView with a ForEach. I'm getting this error Cannot convert value of type '[String : String]' to expected argument type 'Binding' Below is my code for my ContentView, Struct and ApiCall. I have read other solutions on stackoverflow and tried them all but they do not work.
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
let country = api.storedData.countries
VStack(alignment: .leading) {
ForEach(country.id, id: \.self) { country in
HStack(alignment: .top) {
Text("\(country.countries)")
}
}
.onAppear {
api.loadData()
}
}
}
}
My ApiCall class which loads the data, as well as the struct.
// MARK: - Country
struct Country: Codable, Identifiable {
let id = UUID()
var countries: [String: String]
enum CodingKeys: String, CodingKey {
case countries = "countries"
}
}
class APICALL: ObservableObject {
#Published var storedData = Country(countries: [:])
func loadData() {
let apikey = ""
guard let url = URL(string:"https://countries-cities.p.rapidapi.com/location/country/list?rapidapi-key=\(apikey)") 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(Country.self, from: data) {
DispatchQueue.main.async {
self.storedData.countries = response.countries
print(self.storedData.countries)
}
return
}
}
}
.resume()
}
}
Any Point in the right direction would be absolutely helpful.
you could try this approach to display your countries data:
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
VStack(alignment: .leading) {
// -- here --
ForEach(Array(api.storedData.countries.enumerated()), id: \.0) { index, country in
HStack(alignment: .top) {
Text("\(country.key) \(country.value)")
}
}
.onAppear {
api.loadData()
}
}
}
}
you can also use this, if you prefer:
ForEach(api.storedData.countries.sorted(by: >), id: \.key) { key, value in
HStack(alignment: .top) {
Text("\(key) \(value)")
}
}
I am creating custom data and I want to save them into CoreData when I make favorites. In order to do that I use the Combine framework by subscribing CoreData values back into my custom data. The problem is when I try to map both CoreData and custom data, there is something wrong and I couldn't display even my custom data on the canvas. To be honest, I don't even know what I am doing because most of the ViewModel codes are based on Nick's tutorial video (from the Swiftful Thinking Youtube channel). Please help me with what is wrong with my codes. Thanks in advance.
I create my CoreData with a name "DataContainer" with entity name "DataEntity". In DataEntity, there are three attributes:
'id' with a type "Integer32"
'isFavorite' with a type "Boolean"
'timestamp' with a type "Date"
import Foundation
import CoreData
// This is CoreData class without using Singleton
class CoreDataManager {
private let container: NSPersistentContainer
#Published var savedEntities: [DataEntity] = []
init() {
container = NSPersistentContainer(name: "DataContainer")
container.loadPersistentStores { _, error in
if let error = error {
print("Error loading CoreData! \(error)")
}
}
fetchData()
}
// MARK: Privates
private func fetchData() {
let request = NSFetchRequest<DataEntity>(entityName: "DataEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching DataEntity! \(error)")
}
}
// Add to CoreData
private func addFavorite(dataID: DataArray, onTappedFavorite: Bool) {
let newFavorite = DataEntity(context: container.viewContext)
newFavorite.id = Int32(dataID.id)
newFavorite.isFavorite = onTappedFavorite
applyChanges()
}
// Update time
private func updateTime() {
let newTime = DataEntity(context: container.viewContext)
newTime.timestamp = Date()
}
// Save to CoreData
private func save() {
do {
try container.viewContext.save()
} catch let error {
print("Error saving to CoreData! \(error)")
}
}
private applyChanges() {
save()
updateTime()
fetchData()
}
private func update(entity: DataEntity, updateFavorite: Bool) {
entity.isFavorite = updateFavorite
applyChanges()
}
private func delete(entity: DataEntity) {
container.viewContext.delete(entity)
applyChanges()
}
// MARK: Public
func updateFavorite(dataID: DataArray, onTappedFavorite: Bool) {
// Checking the data is already taken
if let entity = savedEntities.first(where: { $0.id == dataID.id }) {
if onTappedFavorite {
update(entity: entity, updateFavorite: onTappedFavorite)
} else {
delete(entity: entity)
}
} else {
addFavorite(dataID: dataID, onTappedFavorite: onTappedFavorite)
}
}
}
This will be my Model:
import Foundation
import SwiftUI
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
let isFavorite: Bool
func updateFavorite(favorited: Bool) -> DataArray {
return DataArray(id: id, cities: cities, name1: name1, name2: name2, isFavorite: favorited)
}
}
public struct ListDataArray {
static let dot = [
DataArray(id: 1,
cities: "Baltimore"
name1: "John",
name2: "Mike",
isFavorite: False),
DataArray(id: 2,
cities: "Frederick"),
name1: "Joe",
name2: "Swift",
isFavorite: False),
DataArray(id: 3,
cities: "Catonsville"
name1: "Susan",
name2: "Oliver",
isFavorite: False),
// There will be a lot of data
]
}
This will be my Home ViewModel:
import Foundation
import SwiftUI
import Combine
class Prospect: ObservableObject {
#Published var datas: [DataArray] = []
private let coreDataServices = CoreDataManager()
private var cancellables = Set<AnyCancellable>()
init() {
fetchDataArrays()
fetchCoreData()
}
private func fetchDataArrays() {
let items = ListDataArray.dot
datas = items
}
private func fetchCoreData() {
coreDataServices.$savedEntities
.map({ (coreData) -> [DataArray] in
// Here is something wrong when I check and try to convert CoreData to DataArray
let arrays: [DataArray] = []
return arrays
.compactMap { (data) -> DataArray? in
guard let entity = coreData.first(where: { $0.id == data.id }) else {
return nil
}
return data.updateFavorite(favorited: entity.isFavorite)
}
})
.sink {[weak self] (receivedEntities) in
self?.datas = receivedEntities
}
.store(in: &cancellables)
}
func updateFavoriteData(dataID: DataArray, isFavorite: Bool) {
coreDataServices.updateFavorite(dataID: dataID, onTappedFavorite: isFavorite)
}
// To View Favorite
#Published var showFavorite: Bool = false
}
This is my View:
import SwiftUI
struct Home: View {
#EnvironmentObject var items: Prospect
var body: some View {
ScrollView {
LazyVStack {
ForEach(items.datas) { data in
VStack {
HStack {
Button {
//Action for making favorite or unfavorite
items.updateFavoriteData(dataID: data, isFavorite: data.isFavorite)
} label: {
Image(systemName: data.isFavorite ? "suit.heart.fill" : "suit.heart")
}
Spacer()
Button {
items.showFavorite.toggle()
} label: {
Image(systemName: "music.note.house.fill")
}
.sheet(isPresented: $items.showFavorite) {
FavoriteView()
.environmentObject(items)
}
}
Text("\(data.id)")
.font(.title3)
Text(data.cities)
.font(.subheadline)
Spacer()
}
padding()
}
.padding()
}
}
}
}
struct FavoriteView: View {
#EnvironmentObject var items: Prospect
var body: some View {
VStack {
List {
ForEach(items.datas) { data in
if data.isFavorite {
VStack(spacing: 10) {
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
.font(.body)
}
}
.padding()
}
Spacer()
}
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(Prospect())
}
}
Here is a different approach
//extend DataEntity
extension DataEntity{
//Have a dataArray variable that links with your array
var dataArray: DataArray?{
ListDataArray.dot.first(where: {
$0.id == self.id
})
}
}
//extend DataArray
extension DataArray{
//have a data entity variable that retrieves the CoreData object
var dataEntity: DataEntity{
//Use DataEntity Manager to map
//Find or Create
return DataEntityManager().retrieve(dataArray: self)
}
}
//Have an entity manager
class DataEntityManager{
let container = PersistenceController.previewAware
//convenience to create
func create(id: Int32) -> DataEntity{
let entity = DataEntity(context: container.container.viewContext)
entity.id = id
save()
return entity
}
//for updating to centralize work
func update(entity: DataEntity){
//I think this is what you intend to update the timestamp when the value changes
entity.timestamp = Date()
//get the array variable
var dataArry = entity.dataArray
//See if they match to prevent loops
if dataArry?.isFavorite != entity.isFavorite{
//if they dont update the array
dataArry = dataArry?.updateFavorite(favorited: entity.isFavorite)
}else{
//Leave alone
}
save()
}
//for updating to centralize work
func update(dataArray: DataArray){
//get the entity
let entity = dataArray.dataEntity
//See if they match to prevent loops
if entity.isFavorite != dataArray.isFavorite{
//if they dont update the entity
DataEntityManager().update(entity: entity)
}else{
//leave alone
}
}
func retrieve(dataArray: DataArray) -> DataEntity{
let request: NSFetchRequest = DataEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %#", dataArray.id)
do{
let result = try controller.container.viewContext.fetch(request).first
//This is risky because it will create a new one
//You can handle it differently if you prefer
let new = create(id: Int32(dataArray.id))
update(entity: new)
return result ?? new
}catch{
print(error)
//This is risky because it will create a new one
//You can handle it differently if you prefer
let new = create(id: Int32(dataArray.id))
update(entity: new)
return new
}
}
func save() {
do{
try container.container.viewContext.save()
}catch{
print(error)
}
}
}
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
var isFavorite: Bool
//Mostly your method
mutating func updateFavorite(favorited: Bool) -> DataArray {
//get the new value
isFavorite = favorited
//update the entity
DataEntityManager().update(dataArray: self)
//return the new
return self
}
}
With this you can now access the matching variable using either object
dataEntity.dataArray
or
dataArray.dataEntity
Remember to update using the methods in the manager or the array so everything stays in sync.
Something to be aware of. CoreData objects are ObservableObjects where ever you want to see changes for the DataEntity you should wrap them in an #ObservedObject
I have a currency API that returns a JSON object containing a strange arrangement: the base currency is used as a label. Typical currency APIs have labels like "base", "date", "success", and "rates", but this API doesn't have any of those.
{
"usd": {
"aed": 4.420217,
"afn": 93.3213,
"all": 123.104693,
"amd": 628.026474,
"ang": 2.159569,
"aoa": 791.552347,
"ars": 111.887966,
"aud": 1.558363,
"awg": 2.164862,
"azn": 2.045728,
"bam": 1.9541,
"bbd": 2.429065,
"bch": 0.001278
}
}
The "usd" (US dollars) at the top is called the base or home currency. At the moment the storage structure and state parameter are hardcoded to "usd" which prevents using exchange rates with other base currencies. The exchange rate API works great for a base currency of US dollars.
I need help modifying things so that I can download the exchange rates with different base currencies. For example, can I use a string variable in the storage model and state parameter? Any enlightenment will be greatly appreciated.
struct RateResult: Codable {
let usd: [String: Double]
}
#State private var results = RateResult(usd: [:])
struct ContentView: View {
var body: some View {
}
func UpdateRates() {
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = baseCurrency.baseCur.baseS // usd
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) 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 decodedResponse = try? JSONDecoder().decode(RateResult.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse
// save off currency exchange rates
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Currency Fetch Failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
import SwiftUI
//You can't use the standard Codable for this. You have to make your own.
class BaseCurrency: Codable {
let id = UUID()
var baseCurrencies: [String : [String: Double]] = [:]
required public init(from decoder: Decoder) throws {
do{
print(#function)
let baseContainer = try decoder.singleValueContainer()
let base = try baseContainer.decode([String : [String: Double]].self)
for key in base.keys{
baseCurrencies[key] = base[key]
}
}catch{
print(error)
throw error
}
}
//#State should never be used outside a struct that is a View
}
struct CurrencyView: View {
#StateObject var vm: CurrencyViewModel = CurrencyViewModel()
var body: some View {
VStack{
List{
if vm.results != nil{
ForEach(vm.results!.baseCurrencies.sorted{$0.key < $1.key}, id: \.key) { key, baseCurrency in
DisclosureGroup(key){
ForEach(baseCurrency.sorted{$0.key < $1.key}, id: \.key) { key, rate in
HStack{
Text(key)
Text(rate.description)
}
}
}
}
}else{
Text("waiting...")
}
}
//To select another rate to go fetch
RatesPickerView().environmentObject(vm)
}.onAppear(){
vm.UpdateRates()
}
}
}
struct RatesPickerView: View {
#EnvironmentObject var vm: CurrencyViewModel
var body: some View {
if vm.results != nil{
//You can probaly populate this picker with the keys in
// baseCurrency.baseCur.baseS
Picker("rates", selection: $vm.selectedBaseCurrency){
ForEach((vm.results!.baseCurrencies.first?.value.sorted{$0.key < $1.key})!, id: \.key) { key, rate in
Text(key).tag(key)
}
}
}else{
Text("waiting...")
}
}
}
class CurrencyViewModel: ObservableObject{
#Published var results: BaseCurrency?
#Published var selectedBaseCurrency: String = "usd"{
didSet{
UpdateRates()
}
}
init() {
//If you can .onAppear you don't need it here
//UpdateRates()
}
func UpdateRates() {
print(#function)
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = selectedBaseCurrency // usd
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do{
let decodedResponse = try JSONDecoder().decode(BaseCurrency.self, from: data)
DispatchQueue.main.async {
if self.results == nil{
//Assign a new base currency
self.results = decodedResponse
}else{ //merge the existing with the new result
for base in decodedResponse.baseCurrencies.keys{
self.results?.baseCurrencies[base] = decodedResponse.baseCurrencies[base]
}
}
//update the UI
self.objectWillChange.send()
}
}catch{
//Error thrown by a try
print(error)//much more informative than error?.localizedDescription
}
}
if error != nil{
//data task error
print(error!)
}
}.resume()
}
}
struct CurrencyView_Previews: PreviewProvider {
static var previews: some View {
CurrencyView()
}
}