From 303cad1076af8cd9f713457b1a83e55c604a5298 Mon Sep 17 00:00:00 2001 From: Terry Yiu <963907+tyiu@users.noreply.github.com> Date: Mon, 11 Nov 2024 00:42:50 +0100 Subject: [PATCH] Create separate CurrencyPickerView and tighten up ContentView --- Sources/SatsPrice/ContentView.swift | 136 ++++++------------ Sources/SatsPrice/CurrencyPickerView.swift | 84 +++++++++++ .../SatsPrice/Resources/Localizable.xcstrings | 23 ++- Sources/SatsPrice/SatsPriceApp.swift | 2 +- Sources/SatsPrice/SatsViewModel.swift | 31 +++- 5 files changed, 166 insertions(+), 110 deletions(-) create mode 100644 Sources/SatsPrice/CurrencyPickerView.swift diff --git a/Sources/SatsPrice/ContentView.swift b/Sources/SatsPrice/ContentView.swift index 118f220..62bfb71 100644 --- a/Sources/SatsPrice/ContentView.swift +++ b/Sources/SatsPrice/ContentView.swift @@ -8,70 +8,23 @@ import SwiftUI public struct ContentView: View { @ObservedObject private var satsViewModel = SatsViewModel() - @State private var priceSource: PriceSource - - @State private var expandAddCurrencySection: Bool = false - private let dateFormatter: DateFormatter - private let priceFetcherDelegator: PriceFetcherDelegator - - init(_ priceSource: PriceSource) { + init() { dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .short - - self.priceSource = priceSource - priceFetcherDelegator = PriceFetcherDelegator(priceSource) - } - - @MainActor - func updatePrice() async { - do { - let currencies = Set([satsViewModel.currentCurrency] + satsViewModel.currencyValueStrings.keys) - let prices = try await priceFetcherDelegator.convertBTC(toCurrencies: Array(currencies)) - - satsViewModel.currencyPrices = prices - satsViewModel.updateCurrencyValueStrings() - } catch { - satsViewModel.clearCurrencyValueStrings() - } - satsViewModel.lastUpdated = Date.now } public var addCurrencyView: some View { - DisclosureGroup("Add Currency", isExpanded: $expandAddCurrencySection) { - Picker("Currency", selection: $satsViewModel.selectedCurrency) { - ForEach(satsViewModel.currencies, id: \.self) { currency in - Group { - if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: currency.identifier) { - Text("\(currency.identifier) - \(localizedCurrency)") - } else { - Text(currency.identifier) - } - } - .tag(currency.identifier) - } + NavigationLink( + destination: { + CurrencyPickerView(satsViewModel: satsViewModel) + }, + label: { + Text("Change Currencies") } -#if os(iOS) || SKIP - .pickerStyle(.navigationLink) -#endif - - let selectedCurrency = satsViewModel.selectedCurrency - if selectedCurrency == satsViewModel.currentCurrency || satsViewModel.currencyValueStrings.keys.contains(selectedCurrency) { - Text("\(selectedCurrency.identifier) has already been added") - .foregroundStyle(.secondary) - } else { - Button("Add \(selectedCurrency.identifier)") { - satsViewModel.currencyValueStrings[selectedCurrency] = "" - expandAddCurrencySection = false - - Task { - await updatePrice() - } - } - } - } + ) } public func selectedCurrencyBinding(_ currency: Locale.Currency) -> Binding { @@ -89,7 +42,7 @@ public struct ContentView: View { NavigationStack { Form { Section { - Picker("Price Source", selection: $priceSource) { + Picker("Price Source", selection: $satsViewModel.priceSource) { ForEach(PriceSource.allCases, id: \.self) { Text($0.description) } @@ -97,14 +50,14 @@ public struct ContentView: View { HStack { TextField("1 BTC to \(satsViewModel.currentCurrency.identifier)", text: satsViewModel.btcToCurrencyString(for: satsViewModel.currentCurrency)) - .disabled(priceSource != .manual) + .disabled(satsViewModel.priceSource != .manual) #if os(iOS) || SKIP .keyboardType(.decimalPad) #endif - if priceSource != .manual { + if satsViewModel.priceSource != .manual { Button(action: { Task { - await updatePrice() + await satsViewModel.updatePrice() } }) { Image(systemName: "arrow.clockwise.circle") @@ -114,31 +67,27 @@ public struct ContentView: View { } header: { Text("1 BTC to \(satsViewModel.currentCurrency.identifier)") } footer: { - if priceSource != .manual, let lastUpdated = satsViewModel.lastUpdated { + if satsViewModel.priceSource != .manual, let lastUpdated = satsViewModel.lastUpdated { Text("Last updated: \(dateFormatter.string(from: lastUpdated))") } } Section { - TextField("Sats", text: $satsViewModel.satsString) + HStack { + Text("Sats") + TextField("Sats", text: $satsViewModel.satsString) #if os(iOS) || SKIP - .keyboardType(.numberPad) + .keyboardType(.numberPad) #endif - } header: { - Text("Sats") - } footer: { - if satsViewModel.exceedsMaximum { - Text("2100000000000000 sats is the maximum.") } - } - Section { - TextField("BTC", text: $satsViewModel.btcString) + HStack { + Text("BTC") + TextField("BTC", text: $satsViewModel.btcString) #if os(iOS) || SKIP - .keyboardType(.decimalPad) + .keyboardType(.decimalPad) #endif - } header: { - Text("BTC") + } } footer: { if satsViewModel.exceedsMaximum { Text("21000000 BTC is the maximum.") @@ -146,38 +95,41 @@ public struct ContentView: View { } Section { - TextField(satsViewModel.currentCurrency.identifier, text: satsViewModel.currencyValueString(for: satsViewModel.currentCurrency)) + HStack { + Text(satsViewModel.currentCurrency.identifier) + TextField(satsViewModel.currentCurrency.identifier, text: satsViewModel.currencyValueString(for: satsViewModel.currentCurrency)) #if os(iOS) || SKIP - .keyboardType(.decimalPad) + .keyboardType(.decimalPad) #endif - } header: { - Text(satsViewModel.currentCurrency.identifier) + } } - if priceSource != .manual { - ForEach(satsViewModel.currencyValueStrings.sorted { $0.key.identifier < $1.key.identifier }.filter { $0.key != satsViewModel.currentCurrency }, id: \.key.identifier) { currencyAndPrice in - Section { - TextField(currencyAndPrice.key.identifier, text: satsViewModel.currencyValueString(for: currencyAndPrice.key)) + Section { + if satsViewModel.priceSource != .manual { + ForEach(satsViewModel.selectedCurrencies.sorted { $0.identifier < $1.identifier }.filter { $0 != satsViewModel.currentCurrency }, id: \.identifier) { currency in + HStack { + Text(currency.identifier) + TextField(currency.identifier, text: satsViewModel.currencyValueString(for: currency)) #if os(iOS) || SKIP - .keyboardType(.decimalPad) + .keyboardType(.decimalPad) #endif - } header: { - Text(currencyAndPrice.key.identifier) + } + .tag(currency.identifier) } - .tag(currencyAndPrice.key.identifier) } + } + if satsViewModel.priceSource != .manual { addCurrencyView } } .task { - await updatePrice() + await satsViewModel.updatePrice() } - .onChange(of: priceSource) { newPriceSource in + .onChange(of: satsViewModel.priceSource) { newPriceSource in satsViewModel.lastUpdated = nil - priceFetcherDelegator.priceSource = newPriceSource Task { - await updatePrice() + await satsViewModel.updatePrice() } } #if os(macOS) @@ -188,9 +140,5 @@ public struct ContentView: View { } #Preview { -#if DEBUG - ContentView(.fake) -#else - ContentView(.coinbase) -#endif + ContentView() } diff --git a/Sources/SatsPrice/CurrencyPickerView.swift b/Sources/SatsPrice/CurrencyPickerView.swift new file mode 100644 index 0000000..0c9efb1 --- /dev/null +++ b/Sources/SatsPrice/CurrencyPickerView.swift @@ -0,0 +1,84 @@ +// This is free software: you can redistribute and/or modify it +// under the terms of the GNU General Public License 3.0 +// as published by the Free Software Foundation https://fsf.org +// +// CurrencyPickerView.swift +// sats-price +// +// Created by Terry Yiu on 11/10/24. +// + +import SwiftUI + +struct CurrencyPickerView: View { + @ObservedObject var satsViewModel: SatsViewModel + + var body: some View { + let currentCurrency = satsViewModel.currentCurrency + + List { + Section("Current Currency") { + let currentCurrency = satsViewModel.currentCurrency + if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: currentCurrency.identifier) { + Text("\(currentCurrency.identifier) - \(localizedCurrency)") + } else { + Text(currentCurrency.identifier) + } + } + + if !satsViewModel.selectedCurrencies.isEmpty { + Section("Selected Currencies") { + ForEach(satsViewModel.selectedCurrencies.filter { $0 != currentCurrency }.sorted { $0.identifier < $1.identifier }, id: \.identifier) { currency in + Button( + action: { + satsViewModel.selectedCurrencies.remove(currency) + }, + label: { + HStack { + Group { + if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: currency.identifier) { + Text("\(currency.identifier) - \(localizedCurrency)") + } else { + Text(currency.identifier) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + + Image(systemName: "checkmark") + } + } + ) + .buttonStyle(.plain) + } + } + } + + Section("Currencies") { + ForEach(satsViewModel.currencies.filter { $0 != currentCurrency && !satsViewModel.selectedCurrencies.contains($0) }, id: \.identifier) { currency in + Button( + action: { + satsViewModel.selectedCurrencies.insert(currency) + }, + label: { + if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: currency.identifier) { + Text("\(currency.identifier) - \(localizedCurrency)") + } else { + Text(currency.identifier) + } + } + ) + .buttonStyle(.plain) + } + } + } + .onDisappear(perform: { + Task { + await satsViewModel.updatePrice() + } + }) + } +} + +#Preview { + CurrencyPickerView(satsViewModel: SatsViewModel()) +} diff --git a/Sources/SatsPrice/Resources/Localizable.xcstrings b/Sources/SatsPrice/Resources/Localizable.xcstrings index ce3f616..33f0df2 100644 --- a/Sources/SatsPrice/Resources/Localizable.xcstrings +++ b/Sources/SatsPrice/Resources/Localizable.xcstrings @@ -13,29 +13,23 @@ } } } - }, - "%@ has already been added" : { - }, "1 BTC to %@" : { }, "21000000 BTC is the maximum." : { - }, - "2100000000000000 sats is the maximum." : { - - }, - "Add %@" : { - - }, - "Add Currency" : { - }, "BTC" : { }, - "Currency" : { + "Change Currencies" : { + + }, + "Currencies" : { + + }, + "Current Currency" : { }, "Last updated: %@" : { @@ -46,6 +40,9 @@ }, "Sats" : { + }, + "Selected Currencies" : { + } }, "version" : "1.0" diff --git a/Sources/SatsPrice/SatsPriceApp.swift b/Sources/SatsPrice/SatsPriceApp.swift index d78e7a0..3d12d0e 100644 --- a/Sources/SatsPrice/SatsPriceApp.swift +++ b/Sources/SatsPrice/SatsPriceApp.swift @@ -19,7 +19,7 @@ public struct RootView : View { } public var body: some View { - ContentView(.coinbase) + ContentView() .task { logger.log("Welcome to Skip on \(androidSDK != nil ? "Android" : "Darwin")!") logger.warning("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat") diff --git a/Sources/SatsPrice/SatsViewModel.swift b/Sources/SatsPrice/SatsViewModel.swift index 13ab3b7..473e2d4 100644 --- a/Sources/SatsPrice/SatsViewModel.swift +++ b/Sources/SatsPrice/SatsViewModel.swift @@ -15,9 +15,13 @@ import SwiftUI class SatsViewModel: ObservableObject { @Published var lastUpdated: Date? + @Published var priceSourceInternal: PriceSource = .coinbase + let priceFetcherDelegator = PriceFetcherDelegator(.coinbase) + @Published var satsStringInternal: String = "" @Published var btcStringInternal: String = "" @Published var selectedCurrency: Locale.Currency = Locale.current.currency ?? Locale.Currency("USD") + @Published var selectedCurrencies = Set() @Published var currencyValueStrings: [Locale.Currency: String] = [:] var currencyPrices: [Locale.Currency: Decimal] = [:] @@ -26,7 +30,6 @@ class SatsViewModel: ObservableObject { var currencies: [Locale.Currency] { let commonISOCurrencyCodes = Set(Locale.commonISOCurrencyCodes) - let currentCurrency = Locale.current.currency ?? Locale.Currency("USD") if commonISOCurrencyCodes.contains(currentCurrency.identifier) { return Locale.commonISOCurrencyCodes.map { Locale.Currency($0) } } else { @@ -37,6 +40,30 @@ class SatsViewModel: ObservableObject { } } + var priceSource: PriceSource { + get { + priceSourceInternal + } + set { + priceSourceInternal = newValue + priceFetcherDelegator.priceSource = newValue + } + } + + @MainActor + func updatePrice() async { + do { + let currencies = Set([currentCurrency] + selectedCurrencies) + let prices = try await priceFetcherDelegator.convertBTC(toCurrencies: Array(currencies)) + + currencyPrices = prices + updateCurrencyValueStrings() + } catch { + clearCurrencyValueStrings() + } + lastUpdated = Date.now + } + var satsString: String { get { satsStringInternal @@ -89,7 +116,7 @@ class SatsViewModel: ObservableObject { func updateCurrencyValueStrings(excludedCurrency: Locale.Currency? = nil) { if let btc { - let currencies = Set([currentCurrency] + currencyValueStrings.keys) + let currencies = Set([currentCurrency] + selectedCurrencies) .filter { $0 != excludedCurrency } for currency in currencies {