Dynamically Changing the Base Currency - swiftui

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()
}
}

Related

Cant view the list of devices

I have a json-result that I get from my own "api". In this I have a list of different devices, that I like to view in a list.
When I add the ForEach, then I get the following error:
Generic struct 'List' requires that 'some AccessibilityRotorContent' conform to 'View'
The JsonResponse:
[{"name":"Tormek-T8","topHorizontal":55,"topVertical":55,"frontHorizontal":44,"frontVertical":44},{"name":"SH-332","topHorizontal":77,"topVertical":77,"frontHorizontal":88,"frontVertical":88}]
Can it be, that there are numbers in this JSON-String and not all is a String?
In my very simple code I had made a struct for the single Device and try to pull it from the url. Why I would get this error? Because the response of the JSON is not nil.
struct Device: Hashable, Codable {
let name: String
let topHorizontal: Double
let topVertical: Double
let frontHorizontal: Double
let frontVertical: Double
}
class ViewModel: ObservableObject {
#Published var devices: [Device] = []
func fetch() {
guard let url = URL(string: "https://cdn.rowoco.de/grindcalculator/devices") else {
return
}
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else {
return
}
do {
let devices = try JSONDecoder().decode([Device].self, from: data)
DispatchQueue.main.async {
self?.devices = devices
}
} catch {
print(error)
}
}
task.resume()
}
}
struct CdnDevicesListView: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.devices, id: \.self) { device in
device.name
}
}
.navigationTitle("cdn devices")
.onAppear {
viewModel.fetch()
}
}
}
}
ForEach's content needs to be a View.
device.name is not a View.

SwiftUI Object details on list click

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

#EnvironmentVariable not being passed to child views

I have 2 views - which I want to navigate between, and have a viewModel object shared between them as an EnvironmentObject. I keep getting the "A View.environmentObject(_:) for TidesViewModel may be missing as an ancestor of this view." error - but none of the solutions I have found seem to work. Please find below my code. The following is the first view:
import SwiftUI
struct ContentView: View {
#ObservedObject var tidesViewModel: TidesViewModel = TidesViewModel()
var body: some View {
NavigationView
{
List
{
ForEach (tidesViewModel.stations.indices) {
stationid in
HStack
{
NavigationLink(destination: TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id))
{
Text(tidesViewModel.stations[stationid].properties.Name)
}
}
}
}
}.environmentObject(tidesViewModel)
}
}
and below is the child view - which throws the error.
import SwiftUI
struct TideDataView: View {
#EnvironmentObject var tidesViewModel : TidesViewModel
var stationId: String
init(stationId: String) {
self.stationId = stationId
getTidesForStation(stationId: stationId)
}
var body: some View {
List
{
ForEach (tidesViewModel.tides.indices)
{
tideIndex in
Text(tidesViewModel.tides[tideIndex].EventType)
}
}
}
func getTidesForStation(stationId: String)
{
tidesViewModel.getTidalData(forStation: stationId)
}
}
For completeness - below is the Observable object being passed:
import Foundation
import SwiftUI
class TidesViewModel: ObservableObject
{
private var tideModel: TideModel = TideModel()
var currentStation: Feature?
init()
{
readStations()
}
var stations: [Feature]
{
tideModel.features
}
var tides: [TidalEvent]
{
tideModel.tides
}
func readStations()
{
let stationsData = readLocalFile(forName: "stations")
parseStations(jsonData: stationsData!)
}
private func readLocalFile(forName name: String) -> Data? {
do {
if let bundlePath = Bundle.main.path(forResource: name,
ofType: "json"),
let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
return jsonData
}
} catch {
print(error)
}
return nil
}
private func parseStations(jsonData: Data) {
do {
let decodedData: FeatureCollection = try JSONDecoder().decode(FeatureCollection.self,
from: jsonData)
//print(decodedData)
tideModel.features = decodedData.features
} catch let jsonError as NSError{
print(jsonError.userInfo)
}
}
func getTidalData(forStation stationId: String)
{
let token = "f43c068141bb417fb88909be5f68781b"
guard let url = URL(string: "https://admiraltyapi.azure-api.net/uktidalapi/api/V1/Stations/" + stationId + "/TidalEvents") else {
fatalError("Invalid URL")
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue(token, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else{ return }
do{
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let decodedData = try decoder.decode([TidalEvent].self, from: data)
DispatchQueue.main.async {
self.tideModel.tides = decodedData
}
}catch let error{
print(error)
}
}.resume()
}
}
You need to attach the modifier .environmentObject() directly to the view that will receive it. So, in your case, attach it to TideDataView, not to the NavigationView around it.
Your code would look like this:
NavigationLink {
TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id)
.environmentObject(tidesViewModel)
} label: {
Text(tidesViewModel.stations[stationid].properties.Name)
}
// Delete the modifier .environmentObject() attached to the NavigationView

How to set a value received by hitting the first endpoint to a filter in the second endpoint?

I'm currently developing an application using SwiftUI.
I want to show two kinds of data in two lists in a view.
(1)start_date
(2)the temperatures of each day from start_date(1) to today
in the case of my code, (1)start_date is shown well, but (2)the temperatures have a problem because each list should show different data but they show the same data in each list...
Although I can check when each method is called, they make deferent data in the console like below, the simulator shows the same data...
The result in the console:
temp_info
25.7
24.9
temp_info
25.6
25.7
24.9
24.1
23.5
25.7
26.4
23.7
23.0
24.4
26.1
How could I resolve this problem?
Here are the codes:
JsonModel.swift
import Foundation
struct DbVegetableInfos: Codable,Identifiable {
var id: Int
var start_date: String
}
struct WeatherAveInfos:Codable,Identifiable {
var id: Int
var ave_temp: Float
}
AppState.swift
import SwiftUI
import Foundation
import Combine
import UIKit
class AppState: ObservableObject {
#Published var arrayDbVegetableInfos:[DbVegetableInfos]?
#Published var weatherAveInfos:[WeatherAveInfos]?
func makeGetCallVegetableInfos() {
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxx", forHTTPHeaderField: "authorization")
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.arrayDbVegetableInfos = try JSONDecoder().decode([DbVegetableInfos].self, from: responseData)
}catch{
print("Error: did not decode")
return
}
}
}
task.resume()
}
}
func makeGetCallWeatherAveTemp(start_date:String ) {
// Set up the URL request
let endpoint: String = "https://sample.com/api/weather_ave/?start_date=\(start_date)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxx", forHTTPHeaderField: "authorization")
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET")
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.weatherAveInfos = try JSONDecoder().decode([WeatherAveInfos].self, from: responseData)
print("temp_info")
for info in self.weatherAveInfos!{
print(info.ave_temp)
}
}catch{
print("Error: did not decode")
return
}
}
}
task.resume()
}
HomeView.swift
import SwiftUI
struct HomeView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView{
VStack{
ForEach(appState.arrayDbVegetableInfos ?? []){ info in
VStack{
VegetableInfoRow(info:info)
}.background(Color(.secondarySystemFill))
.cornerRadius(10)
.padding(.top)
.padding(.leading)
.padding(.bottom)
}
}.onAppear(){
appState.makeGetCallVegetableInfos()
}
}
}
}
VegetableInfoRow.swift
import SwiftUI
struct VegetableInfoRow: View {
#EnvironmentObject var appState: AppState
var info:DbVegetableInfos
var body: some View {
ScrollView(.horizontal) {
HStack{
VStack{
VStack{
Text("start_date:").padding()
Text(stringToStringDate(stringDate: info.start_date, format: "yyyy-MM-dd"))
}
}
Divider()
.padding()
VStack{
VStack{
Text("progress_temperature:").padding()
ForEach(appState.weatherAveInfos ?? []){ info in
Text(String(info.ave_temp))
}
}
}
}
}.onAppear(){
appState.makeGetCallWeatherAveTemp(start_date: info.start_date)
}
}
}
func stringToStringDate(stringDate: String, format:String) -> String {
let formatter: DateFormatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = "yyyy/MM/dd"
let newDate = formatter.date(from: stringDate)!
formatter.dateFormat = format
return formatter.string(from: newDate)
}
Xcode:Version 12.0.1

Why doesn't this SwiftUI View update when changing a #State variable?

I've got a SwiftUI View that takes a Hacker News API submission ID, and then fetches the details for that item in fetchStory().
When fetchStory() completed its HTTP call, it updates the #State private var url on the View, however the View never re-renders to show the new value -- it always shows the initial empty value.
Why?
import Foundation
import SwiftUI
struct StoryItem: Decodable {
let title: String
let url: String?
}
struct StoryView: View {
public var _storyId: Int
#State private var url: String = "";
init(storyId: Int) {
self._storyId = storyId
self.fetchStory()
}
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self._storyId))")
Text("URL is: \(self.url)") // this never changes!
}
}
private func fetchStory() {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self._storyId).json")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else {
print(String(error.debugDescription))
return
}
do {
let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)
if let storyUrl = item.url {
self.url = storyUrl
} else {
print("No url")
}
} catch {
print(error)
}
}
task.resume()
}
}
struct StoryView_Previews: PreviewProvider {
static var previews: some View {
StoryView(storyId: 22862053)
}
}
something like this:
import SwiftUI
struct StoryItem: Decodable {
let title: String
let url: String?
}
class ObservedStoryId: ObservableObject {
#Published var storyId: String = ""
init(storyId: String) {
self.storyId = storyId
}
}
struct StoryView: View {
#ObservedObject var storyId: ObservedStoryId
#State var url: String = ""
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self.storyId.storyId))")
Text("URL is: \(self.url)")
Text(self.storyId.storyId)
}.onReceive(storyId.$storyId) { _ in self.fetchStory() }
}
private func fetchStory() {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self.storyId.storyId).json")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else {
print(String(error.debugDescription))
return
}
do {
let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)
if let storyUrl = item.url {
self.url = storyUrl
} else {
print("No url")
}
} catch {
print(error)
}
}
task.resume()
}
}
and call it like this:
struct ContentView: View {
#ObservedObject var storyId = ObservedStoryId(storyId: "22862053")
var body: some View {
StoryView(storyId: storyId)
}
}
to fix your problem:
init(storyId: Int) {
self._storyId = storyId
}
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self._storyId))")
Text("URL is: \(self.url)") // this now works
}.onAppear(perform: fetchStory)
}
why does this works and not your code,
my guess is this: "self.url" can only be updated/changed within the special SwiftUI View functions, such as onAppear(), elsewhere it does not change a thing.