Showing posts with label MVVM. Show all posts
Showing posts with label MVVM. Show all posts

Friday, 16 August 2024

Swift UI app to exchange currency using openexchangerates.org Api using MVVM

 

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

}