import SwiftUI
import Combine
// View
struct CurrencyConverterView1: View {
@StateObject private var viewModel = CurrencyConverterView1Model()
var body: some View {
VStack {
Picker("Select Currency", selection: $viewModel.selectedCurrency) {
ForEach(viewModel.currencies, id: \.self) { currency in
Text(currency).tag(currency)
}
}
.pickerStyle(MenuPickerStyle())
TextField("Enter Amount", text: $viewModel.amount)
.keyboardType(.decimalPad)
.padding()
.border(Color.gray)
if let results = viewModel.convertedResults {// list view if data added
List(results, id: \.currency) { result in
HStack {
Text(result.currency)
Spacer()
Text(String(format: "%.2f", result.amount))
}
}
}
}
.onAppear {
viewModel.loadRates() // call api to fetch data
}
}
}
// View Model
class CurrencyConverterView1Model: ObservableObject {
@Published var currencies: [String] = []
@Published var selectedCurrency: String = "USD"
@Published var amount: String = "1"
@Published var convertedResults: [ConversionResult1]?
private var rates: [String: Double] = [:]
private let dataService = ExchangeRatesService()
func loadRates() {// Check if data outdated
if dataService.isDataStale() { // last time check
dataService.fetchRates { [weak self] rates in
DispatchQueue.main.async {
self?.rates = rates
self?.currencies = Array(rates.keys)
self?.convertAmount()
}
}
} else { // Load from local storage
rates = dataService.loadRatesFromStorage()
currencies = Array(rates.keys)
convertAmount()
}
}
func convertAmount() {
guard let inputAmount = Double(amount) else { return }
convertedResults = rates.map { currency, rate in
ConversionResult1(currency: currency, amount: inputAmount * rate)
}
}
}
//Model
struct ConversionResult1 {
let currency: String
let amount: Double
}
//Service class that handles fetching and local storage
class ExchangeRatesService {
private let apiURL = "https://openexchangerates.org/api/latest.json"
private let appID = "Get yOur own key from the website"
private let lastFetchKey = "LastFetchDate"
private let storageKey = "SavedRates"
func fetchRates(completion: @escaping ([String: Double]) -> Void) {
guard let url = URL(string: "\(apiURL)?app_id=\(appID)") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
if let json = try? JSONDecoder().decode(RatesResponse.self, from: data) {
self.saveRatesToStorage(json.rates)
self.saveLastFetchDate()
completion(json.rates)
}
}
}.resume()
}
func isDataStale() -> Bool {// chekc if data is old
if let lastFetch = UserDefaults.standard.object(forKey: lastFetchKey) as? Date {
return Date().timeIntervalSince(lastFetch) > 1800 // 30 minutes = 60*30secs
}
return true
}
func loadRatesFromStorage() -> [String: Double] { // load existing data from userdefults
return UserDefaults.standard.dictionary(forKey: storageKey) as? [String: Double] ?? [:]
}
private func saveRatesToStorage(_ rates: [String: Double]) {
UserDefaults.standard.set(rates, forKey: storageKey)
}
private func saveLastFetchDate() {
UserDefaults.standard.set(Date(), forKey: lastFetchKey)
}
}
struct RatesResponse: Codable {
let rates: [String: Double]
}
#Preview {
CurrencyConverterView1()
}