diff --git a/Gem.xcodeproj/project.pbxproj b/Gem.xcodeproj/project.pbxproj index af6e9b6f..40351582 100644 --- a/Gem.xcodeproj/project.pbxproj +++ b/Gem.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 481152C82B566AC200377C75 /* Gemstone in Frameworks */ = {isa = PBXBuildFile; productRef = 481152C72B566AC200377C75 /* Gemstone */; }; 832553982C0F27F000C9CA0C /* ChartScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832553972C0F27F000C9CA0C /* ChartScene.swift */; }; 8325539A2C0FB11C00C9CA0C /* CurrencySceneViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832553992C0FB11C00C9CA0C /* CurrencySceneViewModelTests.swift */; }; + 83A704272C17CA78005B77F4 /* BuyAssetInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A704262C17CA78005B77F4 /* BuyAssetInputViewModel.swift */; }; 83D824022C173FAF0023CA0C /* NetworkSelectorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D824012C173FAF0023CA0C /* NetworkSelectorViewModel.swift */; }; 83D824042C176A0D0023CA0C /* ChainFilterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D824032C176A0D0023CA0C /* ChainFilterable.swift */; }; C30952B4299C39D70004C0F9 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C30952B3299C39D70004C0F9 /* App.swift */; }; @@ -253,6 +254,7 @@ 487A27E82B632CCE00BEEADB /* Gemstone */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Gemstone; sourceTree = ""; }; 832553972C0F27F000C9CA0C /* ChartScene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartScene.swift; sourceTree = ""; }; 832553992C0FB11C00C9CA0C /* CurrencySceneViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySceneViewModelTests.swift; sourceTree = ""; }; + 83A704262C17CA78005B77F4 /* BuyAssetInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyAssetInputViewModel.swift; sourceTree = ""; }; 83D824012C173FAF0023CA0C /* NetworkSelectorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSelectorViewModel.swift; sourceTree = ""; }; 83D824032C176A0D0023CA0C /* ChainFilterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainFilterable.swift; sourceTree = ""; }; C30952B0299C39D70004C0F9 /* Gem.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gem.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -799,11 +801,11 @@ C366791B2A1860E500F1D74D /* QRScanner */, C36679142A0FF81000F1D74D /* GemAPI */, C3549B4C29CA3A9000B4BE01 /* Store */, - C3CF3BD929BBE40300E96586 /* Style */, C3CF3BA529B5262100E96586 /* Keystore */, C3CF3BA229B3F29C00E96586 /* WalletCore */, C3CF3B7329A98C3400E96586 /* Blockchain */, C3CF3B7529A98C5D00E96586 /* Primitives */, + C3CF3BD929BBE40300E96586 /* Style */, C3CF3B8629AC2B5500E96586 /* Components */, C3D1C4E32A2AB5B6006E8EEA /* Signer */, ); @@ -1023,6 +1025,7 @@ isa = PBXGroup; children = ( C3CF3BC629B7CB4900E96586 /* BuyAssetViewModel.swift */, + 83A704262C17CA78005B77F4 /* BuyAssetInputViewModel.swift */, C3D1C50B2A44FD2B006E8EEA /* FiatQuoteViewModel.swift */, C36679122A0E249100F1D74D /* FiatProvidersViewModel.swift */, ); @@ -1842,6 +1845,7 @@ C3549B3B29C5167100B4BE01 /* WalletDetailScene.swift in Sources */, D8D203BB2ACC878200261CA2 /* ChartService.swift in Sources */, D810F3742AF1AB520006D9C6 /* StakeScene.swift in Sources */, + 83A704272C17CA78005B77F4 /* BuyAssetInputViewModel.swift in Sources */, C3E99BBA2A70739D005DF35F /* TransactionView.swift in Sources */, C34C7D132A006E9F009EEC21 /* SecretPhraseViewableModel.swift in Sources */, C3A7CB8829CD8D3100431341 /* WalletService.swift in Sources */, diff --git a/Gem/Assets/Scenes/AddTokenScene.swift b/Gem/Assets/Scenes/AddTokenScene.swift index dd391e6f..e442f303 100644 --- a/Gem/Assets/Scenes/AddTokenScene.swift +++ b/Gem/Assets/Scenes/AddTokenScene.swift @@ -78,7 +78,7 @@ struct AddTokenScene: View { Button(Localized.Wallet.Import.action, action: onSelectImportToken) .disabled(model.state.isLoading) .frame(maxWidth: Spacing.scene.button.maxWidth) - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) } .padding(.bottom, Spacing.scene.bottom) .background(Colors.grayBackground) diff --git a/Gem/Assets/Scenes/SelectAssetScene.swift b/Gem/Assets/Scenes/SelectAssetScene.swift index 3c4f6cfe..c68ef270 100644 --- a/Gem/Assets/Scenes/SelectAssetScene.swift +++ b/Gem/Assets/Scenes/SelectAssetScene.swift @@ -119,7 +119,11 @@ struct SelectAssetScene: View { ) ) case .buy: - BuyAssetScene(model: BuyAssetViewModel(assetAddress: input.assetAddress)) + BuyAssetScene( + model: BuyAssetViewModel( + assetAddress: input.assetAddress, + input: .default) + ) case .swap: SwapScene(model: SwapViewModel( wallet: model.wallet, diff --git a/Gem/Assets/ViewModels/AddTokenViewModel.swift b/Gem/Assets/ViewModels/AddTokenViewModel.swift index 8ea40334..80016fa2 100644 --- a/Gem/Assets/ViewModels/AddTokenViewModel.swift +++ b/Gem/Assets/ViewModels/AddTokenViewModel.swift @@ -6,6 +6,7 @@ import SwiftUI import Settings import Components +// TODO: - SelectAssetSceneNavigationStack, inject chain & address in SelectAssetSceneNavigationStack class AddTokenViewModel: ObservableObject { let wallet: Wallet let service: AddTokenService diff --git a/Gem/Connections/Scenes/ConnectionProposalScene.swift b/Gem/Connections/Scenes/ConnectionProposalScene.swift index b93d76f2..a4eff4e3 100644 --- a/Gem/Connections/Scenes/ConnectionProposalScene.swift +++ b/Gem/Connections/Scenes/ConnectionProposalScene.swift @@ -37,7 +37,7 @@ struct ConnectionProposalScene: View { Text(model.buttonTitle) } } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .padding(.bottom, Spacing.scene.bottom) .frame(maxWidth: Spacing.scene.button.maxWidth) diff --git a/Gem/Connections/Scenes/SignMessageScene.swift b/Gem/Connections/Scenes/SignMessageScene.swift index a629a589..105d78a9 100644 --- a/Gem/Connections/Scenes/SignMessageScene.swift +++ b/Gem/Connections/Scenes/SignMessageScene.swift @@ -28,7 +28,7 @@ struct SignMessageScene: View { Text(model.buttonTitle) } } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .padding(.bottom, Spacing.scene.bottom) .frame(maxWidth: Spacing.scene.button.maxWidth) } diff --git a/Gem/Fiat/Scenes/BuyAssetScene.swift b/Gem/Fiat/Scenes/BuyAssetScene.swift index 9c055661..06b5154e 100644 --- a/Gem/Fiat/Scenes/BuyAssetScene.swift +++ b/Gem/Fiat/Scenes/BuyAssetScene.swift @@ -4,126 +4,182 @@ import Style import Components struct BuyAssetScene: View { - @StateObject var model: BuyAssetViewModel var body: some View { VStack { List { - Section { } header: { - VStack(alignment: .center) { - AmountView( - title: "$\(Int(model.amount))", - subtitle: model.cryptoAmountText(quote: model.quote) - ) - - ZStack(alignment: .center) { - Grid(alignment: .center) { - ForEach(model.amounts, id: \.self) { amounts in - GridRow(alignment: .center) { - ForEach(amounts, id: \.self) { amount in - VStack(alignment: .center) { - Button("$\(amount)") { - Task { - await buy(amount) - } - } - .buttonStyle(BlueButton(paddingHorizontal: 12, paddingVertical: 12)) - .font(.callout) - } - } - } - .padding(.all, 4) - } - } - } - .padding(.top, 8) - } - .padding(.top, 20) - } - .frame(maxWidth: .infinity) - .textCase(nil) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets()) - - Section { - if let quote = model.quote { - if model.quotes.count > 1 { - NavigationLink(value: Scenes.FiatProviders()) { - ListItemView(title: Localized.Common.provider, subtitle: quote.provider.name) - } - } else { - ListItemView(title: Localized.Common.provider, subtitle: quote.provider.name) - } - ListItemView(title: Localized.Buy.rate, subtitle: model.rateText(quote: quote)) - } else if let quoteError = model.quoteError { - Text(quoteError.localizedDescription) - .multilineTextAlignment(.center) - } else if model.quoteLoading == false { - Text(Localized.Buy.noResults) - .multilineTextAlignment(.center) - } - } header: { - VStack { - if model.quoteLoading { - ProgressView() - .progressViewStyle(.circular) - } - } - .frame(maxWidth: .infinity) - } + amountSelectorSection + providerSection } Spacer() - - Button(Localized.Common.continue, action: { - buyNext() - }) - .disabled(model.quote == nil) - .padding(.bottom, Spacing.scene.bottom) + StatefullButton( + text: Localized.Common.continue, + viewState: model.state, + action: openBuyUrl + ) + .disabled(model.shouldDisalbeContinueButton) .frame(maxWidth: Spacing.scene.button.maxWidth) - .buttonStyle(BlueButton()) } .padding(.bottom, Spacing.scene.bottom) .background(Colors.grayBackground) + .frame(maxWidth: .infinity) .navigationTitle(model.title) .task { - guard model.quote == nil else { - return - } - await model.getQuotes(asset: model.asset, amount: Double(model.defaultaAmount)) + await onTask() } .navigationDestination(for: Scenes.FiatProviders.self) {_ in FiatProvidersScene( model: FiatProvidersViewModel( - currentQuote: model.quote!, + buyAssetInput: model.input, asset: model.asset, - quotes: model.quotes, - selectQuote: { - model.quote = $0 - } + selectQuote: onSelectQuote(_:) ) ) } - .frame(maxWidth: .infinity) } - - func buy(_ amount: Int) async { - await model.getQuotes(asset: model.asset, amount: Double(amount)) +} + +// MARK: - UI Components + +extension BuyAssetScene { + private var amountSelectorSection: some View { + Section { + + } header: { + amountSelectorView + .padding(.top, 20) + } + .textCase(nil) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) } - - func buyNext() { - guard - let quote = model.quote, - let url = URL(string: quote.redirectUrl) else { return } - + + @ViewBuilder + private var providerSection: some View { + Section { + if !model.state.isLoading { + providerView + } + } header: { + VStack { + if model.state.isLoading { + ProgressView() + .progressViewStyle(.circular) + } + } + .frame(maxWidth: .infinity) + } + } + + @ViewBuilder + private var providerView: some View { + switch model.state { + case .noData: + Text(Localized.Buy.noResults) + .multilineTextAlignment(.center) + case .loading: + EmptyView() + case .loaded: + if let quote = model.input.quote { + if model.input.quotes.count > 1 { + NavigationLink(value: Scenes.FiatProviders()) { + ListItemView(title: Localized.Common.provider, subtitle: quote.provider.name) + } + } else { + ListItemView(title: Localized.Common.provider, subtitle: quote.provider.name) + } + ListItemView(title: Localized.Buy.rate, subtitle: model.rateText(for: quote)) + } else { + EmptyView() + } + case .error(let error): + Text(error.localizedDescription) + .multilineTextAlignment(.center) + } + } + + @ViewBuilder + private var amountSelectorView: some View { + VStack(alignment: .center) { + AmountView( + title: "$\(Int(model.amount))", + subtitle: model.cryptoAmountText(for: model.input.quote) + ) + + ZStack(alignment: .center) { + Grid(alignment: .center) { + ForEach(model.amounts, id: \.self) { amounts in + GridRow(alignment: .center) { + ForEach(amounts, id: \.self) { amount in + VStack(alignment: .center) { + Button("$\(Int(amount))") { + onSelectNew(amount: amount) + } + .buttonStyle(.blue(paddingHorizontal: 12, paddingVertical: 12)) + .font(.callout) + } + } + } + .padding(.all, 4) + } + } + } + .padding(.top, 8) + } + } +} + +// MARK: - Actions + +extension BuyAssetScene { + private func onTask() async { + guard model.input.quote == nil else { return } + await getQuotesAsync(amount: model.amount) + } + + private func onSelectContinue() { + openBuyUrl() + } + + private func onSelectNew(amount: Double) { + getQuotes(amount: amount) + } + + private func onSelectQuote(_ quote: FiatQuote) { + model.input.quote = quote + } +} + +// MARK: - Effects + +extension BuyAssetScene { + private func getQuotes(amount: Double) { + Task { + await getQuotesAsync(amount: amount) + } + } + + private func getQuotesAsync(amount: Double) async { + await model.getQuotes(for: model.asset, amount: amount) + } + + private func openBuyUrl() { + guard let quote = model.input.quote, + let url = URL(string: quote.redirectUrl) else { return } + + // TODO: - use new @Environment(\.openURL) var openURL, insead use UIKit UIApplication UIApplication.shared.open(url, options: [:]) } } -//struct BuyAssetScene_Previews: PreviewProvider { -// static var previews: some View { -// NavigationStack { -// BuyAssetScene(model: BuyAssetViewModel(assetAddress: <#T##AssetAddress#>)) -// }.navigationBarTitleDisplayMode(.inline) -// } -//} +// MARK: - Previews + +#Preview { + @StateObject var model = BuyAssetViewModel(assetAddress: .init(asset: .main, address: .empty), input: .default) + + return NavigationStack { + BuyAssetScene(model: model) + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/Gem/Fiat/ViewModel/BuyAssetInputViewModel.swift b/Gem/Fiat/ViewModel/BuyAssetInputViewModel.swift new file mode 100644 index 00000000..6b686552 --- /dev/null +++ b/Gem/Fiat/ViewModel/BuyAssetInputViewModel.swift @@ -0,0 +1,29 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Primitives + +struct BuyAssetInputViewModel { + var amount: Double + + var quote: FiatQuote? + var quotes: [FiatQuote] +} + +// MARK: - Static + +extension BuyAssetInputViewModel { + static var `default`: BuyAssetInputViewModel { + return BuyAssetInputViewModel(amount: defaultAmount, quote: nil, quotes: []) + } + + static private var defaultAmount: Double { + return 50 + } + + static var availableDefaultAmounts: [[Double]] { + return [ + [50, 100, 200], + [250, 500, 1000] + ] + } +} diff --git a/Gem/Fiat/ViewModel/BuyAssetViewModel.swift b/Gem/Fiat/ViewModel/BuyAssetViewModel.swift index 5b2f9576..64c38957 100644 --- a/Gem/Fiat/ViewModel/BuyAssetViewModel.swift +++ b/Gem/Fiat/ViewModel/BuyAssetViewModel.swift @@ -2,67 +2,72 @@ import Foundation import Primitives import SwiftUI import GemAPI +import Components +import Style class BuyAssetViewModel: ObservableObject { - private let assetAddress: AssetAddress - private let fiatService: GemAPIFiatService = GemAPIService() + private let fiatService: GemAPIFiatService - @Published var amount: Double = 0 - @Published var quote: FiatQuote? - @Published var quotes: [FiatQuote] = [] - @Published var quoteLoading: Bool = false - @Published var quoteError: Error? + @Published var input: BuyAssetInputViewModel + @Published var state: StateViewType = .loading init( - assetAddress: AssetAddress + assetAddress: AssetAddress, + fiatService: GemAPIFiatService = GemAPIService(), + input: BuyAssetInputViewModel ) { self.assetAddress = assetAddress + self.fiatService = fiatService + self.input = input } - + var asset: Asset { assetAddress.asset } - + var address: String { assetAddress.address } - + var title: String { - return Localized.Buy.title(asset.name) + Localized.Buy.title(asset.name) + } + + var amounts: [[Double]] { + BuyAssetInputViewModel.availableDefaultAmounts } - - var defaultaAmount: Int { - return 50 + + var amount: Double { + input.amount } - - var amounts: [[Int]] { - return [ - [50, 100, 200], - [250, 500, 1000] - ] + + var shouldDisalbeContinueButton: Bool { + state.isLoading || state.isNoData } - - func cryptoAmountText(quote: FiatQuote?) -> String { - if let quote = quote { - return "~\(quote.cryptoAmount.rounded(toPlaces: 4)) \(asset.symbol)" - } - return " " +} + +// MARK: - Business Logic + +extension BuyAssetViewModel { + func cryptoAmountText(for quote: FiatQuote?) -> String { + guard let quote = quote else { return " " } + return "~\(quote.cryptoAmount.rounded(toPlaces: 4)) \(asset.symbol)" } - - func rateText(quote: FiatQuote) -> String { - return "1 \(asset.symbol) ~ $\((quote.fiatAmount / quote.cryptoAmount).rounded(toPlaces: 2))" + + func rateText(for quote: FiatQuote) -> String { + let rate = (quote.fiatAmount / quote.cryptoAmount).rounded(toPlaces: 2) + return "1 \(asset.symbol) ~ $\(rate)" } - - func getQuotes(asset: Asset, amount: Double) async { - //TODO: Refactor to use state managment - DispatchQueue.main.async { - self.amount = amount - self.quoteLoading = true - self.quote = nil - self.quoteError = nil + + func getQuotes(for asset: Asset, amount: Double) async { + await MainActor.run { + self.input.amount = amount + self.input.quote = nil + self.input.quotes = [] + self.state = .loading } - + do { let quotes = try await fiatService.getQuotes( asset: asset, @@ -73,20 +78,20 @@ class BuyAssetViewModel: ObservableObject { walletAddress: address ) ) - DispatchQueue.main.async { - self.quotes = quotes - self.quote = quotes.first + + await MainActor.run { + if !quotes.isEmpty { + let inputViewModel = BuyAssetInputViewModel(amount: amount, quote: quotes.first, quotes: quotes) + self.input = inputViewModel + self.state = .loaded(true) + } else { + self.state = .noData + } } - } catch { - NSLog("getQuotes error: \(error)") - DispatchQueue.main.async { - self.quoteError = error + await MainActor.run { + self.state = .error(error) } } - - DispatchQueue.main.async { - self.quoteLoading = false - } } } diff --git a/Gem/Fiat/ViewModel/FiatProvidersViewModel.swift b/Gem/Fiat/ViewModel/FiatProvidersViewModel.swift index 88f951f9..bb36999e 100644 --- a/Gem/Fiat/ViewModel/FiatProvidersViewModel.swift +++ b/Gem/Fiat/ViewModel/FiatProvidersViewModel.swift @@ -4,12 +4,11 @@ import Foundation import Primitives struct FiatProvidersViewModel { - let currentQuote: FiatQuote + let buyAssetInput: BuyAssetInputViewModel let asset: Asset - let quotes: [FiatQuote] + var selectQuote: ((FiatQuote) -> Void)? - var quotesViewModel: [FiatQuoteViewModel] { - return quotes.map { FiatQuoteViewModel(asset: asset, quote: $0) } + return buyAssetInput.quotes.map { FiatQuoteViewModel(asset: asset, quote: $0) } } } diff --git a/Gem/Onboarding/Scenes/CreateWalletScene.swift b/Gem/Onboarding/Scenes/CreateWalletScene.swift index f8001c16..27b4c610 100644 --- a/Gem/Onboarding/Scenes/CreateWalletScene.swift +++ b/Gem/Onboarding/Scenes/CreateWalletScene.swift @@ -23,7 +23,7 @@ struct CreateWalletScene: View { } Spacer() Button(Localized.Common.continue, action: continueAction) - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .frame(maxWidth: Spacing.scene.button.maxWidth) } .padding(.bottom, Spacing.scene.bottom) diff --git a/Gem/Onboarding/Scenes/ImportWalletScene.swift b/Gem/Onboarding/Scenes/ImportWalletScene.swift index d0a63e6a..4f99409e 100644 --- a/Gem/Onboarding/Scenes/ImportWalletScene.swift +++ b/Gem/Onboarding/Scenes/ImportWalletScene.swift @@ -114,7 +114,7 @@ struct ImportWalletScene: View { } .accessibilityIdentifier("import_wallet") .frame(maxWidth: Spacing.scene.button.maxWidth) - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) } .padding(.bottom, Spacing.scene.bottom) .background(Colors.grayBackground) diff --git a/Gem/Onboarding/Scenes/VerifyPhraseWalletScene.swift b/Gem/Onboarding/Scenes/VerifyPhraseWalletScene.swift index a36dab52..3a6a82c8 100644 --- a/Gem/Onboarding/Scenes/VerifyPhraseWalletScene.swift +++ b/Gem/Onboarding/Scenes/VerifyPhraseWalletScene.swift @@ -30,7 +30,7 @@ struct VerifyPhraseWalletScene: View { Button { } label: { Text(row.word) } - .buttonStyle(ColorButton.lightGray(paddingHorizontal: Spacing.small, paddingVertical: Spacing.tiny)) + .buttonStyle(.lightGray(paddingHorizontal: Spacing.small, paddingVertical: Spacing.tiny)) .disabled(true) .fixedSize() } else { @@ -39,7 +39,7 @@ struct VerifyPhraseWalletScene: View { } label: { Text(row.word) } - .buttonStyle(ColorButton.blue(paddingHorizontal: Spacing.small, paddingVertical: Spacing.tiny)) + .buttonStyle(.blueGrayPressed(paddingHorizontal: Spacing.small, paddingVertical: Spacing.tiny)) .fixedSize() } } @@ -51,7 +51,7 @@ struct VerifyPhraseWalletScene: View { Spacer() Button(Localized.Common.continue, action: importWallet) .disabled(model.isContinueDisabled) - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .frame(maxWidth: Spacing.scene.button.maxWidth) } .frame(maxWidth: Spacing.scene.content.maxWidth) diff --git a/Gem/Onboarding/Views/WordSuggestionView.swift b/Gem/Onboarding/Views/WordSuggestionView.swift index 4746f71e..fd400251 100644 --- a/Gem/Onboarding/Views/WordSuggestionView.swift +++ b/Gem/Onboarding/Views/WordSuggestionView.swift @@ -20,9 +20,7 @@ struct WordSuggestionView: View { Button(word) { selectWord?(word) } - .buttonStyle( - ColorButton.blue(paddingHorizontal: 10, paddingVertical: 6) - ) + .buttonStyle(.blue(paddingHorizontal: 10, paddingVertical: 6)) .fixedSize(horizontal: true, vertical: false) } } diff --git a/Gem/Swap/Views/SwapFooterView.swift b/Gem/Swap/Views/SwapFooterView.swift index fc7fd8d4..b58d1bfb 100644 --- a/Gem/Swap/Views/SwapFooterView.swift +++ b/Gem/Swap/Views/SwapFooterView.swift @@ -24,7 +24,7 @@ struct SwapFooterView: View { Button(action: action) { Text(Localized.Wallet.swap) } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) case false: Text(Localized.Swap.approveTokenPermission(tokenName)) .font(.system(size: 12)) @@ -36,7 +36,7 @@ struct SwapFooterView: View { Text(Localized.Swap.approveToken(tokenName)) } } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) } case .error(let error): StateErrorView(error: error, message: "") diff --git a/Gem/Transfer/Scenes/AmountScene.swift b/Gem/Transfer/Scenes/AmountScene.swift index 4685fc51..a6bb23d8 100644 --- a/Gem/Transfer/Scenes/AmountScene.swift +++ b/Gem/Transfer/Scenes/AmountScene.swift @@ -125,7 +125,7 @@ struct AmountScene: View { AnyView( HStack { Button(Localized.Transfer.max, action: useMax) - .buttonStyle(ColorButton.lightGray(paddingHorizontal: Spacing.medium, paddingVertical: Spacing.small)) + .buttonStyle(.lightGray(paddingHorizontal: Spacing.medium, paddingVertical: Spacing.small)) .fixedSize() } ) @@ -143,7 +143,7 @@ struct AmountScene: View { }) .padding(.bottom, Spacing.scene.bottom) .frame(maxWidth: Spacing.scene.button.maxWidth) - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) } .background(Colors.grayBackground) .navigationTitle(model.title) diff --git a/Gem/Transfer/Scenes/ConfirmTransferScene.swift b/Gem/Transfer/Scenes/ConfirmTransferScene.swift index d012eeea..8989e634 100644 --- a/Gem/Transfer/Scenes/ConfirmTransferScene.swift +++ b/Gem/Transfer/Scenes/ConfirmTransferScene.swift @@ -76,7 +76,7 @@ struct ConfirmTransferScene: View { Text(model.buttonTitle) } } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .padding(.bottom, Spacing.scene.bottom) .frame(maxWidth: Spacing.scene.button.maxWidth) case .error(_, let error): @@ -91,7 +91,7 @@ struct ConfirmTransferScene: View { Button(role: .none) {} label: { Text(title) } - .buttonStyle(ColorButton.gray()) + .buttonStyle(.gray()) .padding(.bottom, Spacing.scene.bottom) .frame(maxWidth: Spacing.scene.button.maxWidth) } diff --git a/Gem/Transfer/Scenes/RecipientScene.swift b/Gem/Transfer/Scenes/RecipientScene.swift index 958f24db..bdb3ef3f 100644 --- a/Gem/Transfer/Scenes/RecipientScene.swift +++ b/Gem/Transfer/Scenes/RecipientScene.swift @@ -101,7 +101,7 @@ struct RecipientScene: View { Button(Localized.Common.continue, action: { Task { next() } }) - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .padding(.bottom, Spacing.scene.bottom) .frame(maxWidth: Spacing.scene.button.maxWidth) } diff --git a/Gem/Wallet/Scenes/ReceiveScene.swift b/Gem/Wallet/Scenes/ReceiveScene.swift index a3f92c64..b76654e6 100644 --- a/Gem/Wallet/Scenes/ReceiveScene.swift +++ b/Gem/Wallet/Scenes/ReceiveScene.swift @@ -34,7 +34,7 @@ struct ReceiveScene: View { .frame(maxWidth: .infinity) Spacer() Button(Localized.Common.copy, action: copyAddress) - .buttonStyle(BlueButton(paddingHorizontal: 8, paddingVertical: 6)) + .buttonStyle(.blue(paddingHorizontal: 8, paddingVertical: 6)) .fixedSize() }.padding(8) } @@ -49,7 +49,7 @@ struct ReceiveScene: View { }) { Text(Localized.Common.share) } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .padding(.bottom, Spacing.scene.bottom) .frame(maxWidth: Spacing.scene.button.maxWidth) } diff --git a/Gem/Wallet/Scenes/WalletScene.swift b/Gem/Wallet/Scenes/WalletScene.swift index 39a1e401..96bde9b6 100644 --- a/Gem/Wallet/Scenes/WalletScene.swift +++ b/Gem/Wallet/Scenes/WalletScene.swift @@ -170,7 +170,10 @@ struct WalletScene: View { } case .buy: BuyAssetScene( - model: BuyAssetViewModel(assetAddress: selectType.assetAddress) + model: BuyAssetViewModel( + assetAddress: selectType.assetAddress, + input: .default + ) ) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Gem/Welcome/Scenes/WelcomeScene.swift b/Gem/Welcome/Scenes/WelcomeScene.swift index 9f9b520c..8d5463d3 100644 --- a/Gem/Welcome/Scenes/WelcomeScene.swift +++ b/Gem/Welcome/Scenes/WelcomeScene.swift @@ -33,12 +33,12 @@ struct WelcomeScene: View { Button(Localized.Wallet.createNewWallet) { isPresentingCreateWalletSheet.toggle() } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .accessibilityIdentifier("welcome_create") Button(Localized.Wallet.importExistingWallet) { isPresentingImportWalletSheet.toggle() } - .buttonStyle(ClearButton()) + .buttonStyle(.blue()) .accessibilityIdentifier("welcome_import") } .frame(maxWidth: Spacing.scene.button.maxWidth) diff --git a/Packages/Components/Sources/Buttons/StateButton.swift b/Packages/Components/Sources/Buttons/StateButton.swift new file mode 100644 index 00000000..cb21905e --- /dev/null +++ b/Packages/Components/Sources/Buttons/StateButton.swift @@ -0,0 +1,87 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import SwiftUI +import Style + +public struct StatefullButton: View { + public static let defaultTextStyle = TextStyle(font: .body.weight(.semibold), color: Colors.whiteSolid) + + public let textValue: TextValue + public var styleState: StatefulButtonStyle.State + + private let action: () -> Void + + public init( + text: String, + textStyle: TextStyle = StatefullButton.defaultTextStyle, + viewState: StateViewType, + action: @escaping () -> Void + ) { + + let styleState: StatefulButtonStyle.State + switch viewState { + case .noData: + styleState = .disabled + case .loaded, .error: + styleState = .normal + case .loading: + styleState = .loading + } + + self.init(text: text, textStyle: textStyle, styleState: styleState, action: action) + } + + public init( + text: String, + textStyle: TextStyle = StatefullButton.defaultTextStyle, + styleState: StatefulButtonStyle.State, + action: @escaping () -> Void + ) { + self.textValue = TextValue(text: text, style: textStyle) + self.styleState = styleState + self.action = action + } + + public init( + textValue: TextValue, + styleState: StatefulButtonStyle.State, + action: @escaping () -> Void + ) { + self.textValue = textValue + self.styleState = styleState + self.action = action + } + + public var body: some View { + Button( + action: action, + label: { + Text(textValue.text) + .textStyle(textValue.style) + } + ) + .buttonStyle(.statefullBlue(state: styleState)) + } +} + +// MARK: - Preview + +#Preview { + List { + Section(header: Text("Normal State")) { + StatefullButton(text: "Submit", styleState: .normal, action: {}) + } + + Section(header: Text("Loading State")) { + StatefullButton(text: "Submit", styleState: .loading, action: {}) + } + + Section(header: Text("Disabled State")) { + StatefullButton(text: "Submit", styleState: .normal, action: {}) + .disabled(true) + + StatefullButton(text: "Submit", styleState: .disabled, action: {}) + } + } + .padding() +} diff --git a/Packages/Components/Sources/Lists/ListItemView.swift b/Packages/Components/Sources/Lists/ListItemView.swift index 095d93df..a2819c73 100644 --- a/Packages/Components/Sources/Lists/ListItemView.swift +++ b/Packages/Components/Sources/Lists/ListItemView.swift @@ -224,8 +224,8 @@ extension ListItemView { let longTitleExtra = "Long Title Extra Long Title Extra Long Title Extra Long Title Extra" let longSubtitleExtra = "Long Subtitle Extra Long Subtitle Extra Long Subtitle Extra" - let defaultTextStyle = TextStyle.Preview.body - let extraTextStyle = TextStyle.Preview.footnote + let defaultTextStyle = TextStyle.body + let extraTextStyle = TextStyle.footnote let tagTextStyleWhite = TextStyle(font: .footnote, color: .white, background: .gray) let tagTextStyleBlue = TextStyle(font: Font.system(.footnote), color: .blue, background: .blue.opacity(0.2)) diff --git a/Packages/Components/Sources/StateView/StateEmptyView.swift b/Packages/Components/Sources/StateView/StateEmptyView.swift index 910935fd..53cda7cf 100644 --- a/Packages/Components/Sources/StateView/StateEmptyView.swift +++ b/Packages/Components/Sources/StateView/StateEmptyView.swift @@ -44,9 +44,15 @@ public struct StateEmptyView: View { .foregroundStyle(Colors.gray) VStack(spacing: description == nil ? 0 : Spacing.tiny) { - Text(title.text) - .textStyle(title.style) - .multilineTextAlignment(.center) + HStack(spacing: 0.0) { + Text(title.text) + .textStyle(title.style) + .multilineTextAlignment(.center) + if description == nil && image == nil { + Spacer() + } + } + .frame(maxWidth: .infinity) if let description { Text(description.text) @@ -62,40 +68,44 @@ public struct StateEmptyView: View { // MARK: - Previews #Preview { - VStack { - StateEmptyView( - title: "No Results Found", - description: "Try adjusting your search or filter to find what you're looking for.", - image: Image(systemName: SystemImage.searchNoResults) - ) - Divider() + List { + Section(header: Text("Full View with Title, Description, and Image")) { + StateEmptyView( + title: "No Results Found", + description: "Try adjusting your search or filter to find what you're looking for.", + image: Image(systemName: SystemImage.searchNoResults) + ) + } - StateEmptyView( - title: "No Results Found", - image: Image(systemName: SystemImage.searchNoResults) - ) - Divider() + Section(header: Text("View with Title and Image")) { + StateEmptyView( + title: "No Results Found", + image: Image(systemName: SystemImage.searchNoResults) + ) + } - StateEmptyView( - title: "No Results Found", - titleTextStyle: .title, - description: "Try adjusting your search or filter to find what you're looking for.", - descriptionTextStyle: .body, - image: Image(systemName: SystemImage.searchNoResults) - ) - Divider() + Section(header: Text("View with Custom Title and Description Styles")) { + StateEmptyView( + title: "No Results Found", + titleTextStyle: .title, + description: "Try adjusting your search or filter to find what you're looking for.", + descriptionTextStyle: .body, + image: Image(systemName: SystemImage.searchNoResults) + ) + } - StateEmptyView( - title: "No Results Found", - description: "Try adjusting your search or filter to find what you're looking for." - ) - Divider() + Section(header: Text("View with Title and Description")) { + StateEmptyView( + title: "No Results Found", + description: "Try adjusting your search or filter to find what you're looking for." + ) + } - // Only Message - StateEmptyView( - title: "No Results Found" - ) - Divider() + Section(header: Text("View with Only Title")) { + StateEmptyView( + title: "No Results Found" + ) + } } .padding() } diff --git a/Packages/Components/Sources/StateView/StateErrorView.swift b/Packages/Components/Sources/StateView/StateErrorView.swift index a1b3fb04..af14bf6b 100644 --- a/Packages/Components/Sources/StateView/StateErrorView.swift +++ b/Packages/Components/Sources/StateView/StateErrorView.swift @@ -27,7 +27,7 @@ public struct StateErrorView: View { Button(message) { action?() } - .buttonStyle(BlueButton()) + .buttonStyle(.blue()) .frame(width: 180) } } diff --git a/Packages/Components/Sources/StateView/StateViewType.swift b/Packages/Components/Sources/StateView/StateViewType.swift index 3d058b23..8208ba1b 100644 --- a/Packages/Components/Sources/StateView/StateViewType.swift +++ b/Packages/Components/Sources/StateView/StateViewType.swift @@ -21,4 +21,11 @@ public enum StateViewType { default: false } } + + public var value: T? { + guard case .loaded(let t) = self else { + return nil + } + return t + } } diff --git a/Packages/Style/Sources/ButtonStyle.swift b/Packages/Style/Sources/ButtonStyle.swift index b38447d4..dbaadc8b 100644 --- a/Packages/Style/Sources/ButtonStyle.swift +++ b/Packages/Style/Sources/ButtonStyle.swift @@ -2,143 +2,306 @@ import SwiftUI -public struct BlueButton: ButtonStyle { - - @Environment(\.isEnabled) var isEnabled - +public struct ColorButtonStyle: ButtonStyle { let paddingHorizontal: CGFloat let paddingVertical: CGFloat - + let foregroundStyle: Color + let foregroundStylePressed: Color + let background: Color + let backgroundPressed: Color + public init( - paddingHorizontal: CGFloat = 16, - paddingVertical: CGFloat = 16 + paddingHorizontal: CGFloat, + paddingVertical: CGFloat, + foregroundStyle: Color, + foregroundStylePressed: Color, + background: Color, + backgroundPressed: Color ) { self.paddingHorizontal = paddingHorizontal self.paddingVertical = paddingVertical + self.foregroundStyle = foregroundStyle + self.foregroundStylePressed = foregroundStylePressed + self.background = background + self.backgroundPressed = backgroundPressed } - - public func backgroundColor(isPressed: Bool ) -> Color { - if isPressed { - return Colors.blueDark - } -// else if !isEnabled { -// return Colors.gray -// } - return Colors.blue - } - + public func makeBody(configuration: Configuration) -> some View { configuration.label .frame(minWidth: 0, maxWidth: .infinity) - .padding(.horizontal, paddingHorizontal) .padding(.vertical, paddingVertical) - .foregroundColor(Colors.whiteSolid) - .background(backgroundColor(isPressed: configuration.isPressed)) - .cornerRadius(12) + .padding(.horizontal, paddingHorizontal) + .foregroundStyle(foregroundStyle(configuration: configuration)) + .background(background(configuration: configuration)) .fontWeight(.semibold) } + + private func background(configuration: Configuration) -> some View { + return RoundedRectangle(cornerRadius: 12) + .fill(configuration.isPressed ? backgroundPressed : background) + } + + private func foregroundStyle(configuration: Configuration) -> some ShapeStyle { + return configuration.isPressed ? foregroundStylePressed : foregroundStyle + } } -public struct ClearButton: ButtonStyle { - +// MARK: - ColorButtonStyle Static + +extension ButtonStyle where Self == ColorButtonStyle { + public static func blue( + paddingHorizontal: CGFloat = Spacing.medium, + paddingVertical: CGFloat = Spacing.medium) -> ColorButtonStyle + { + return ColorButtonStyle( + paddingHorizontal: paddingHorizontal, + paddingVertical: paddingVertical, + foregroundStyle: Colors.whiteSolid, + foregroundStylePressed: Colors.whiteSolid, + background: Colors.blue, + backgroundPressed: Colors.blueDark + ) + } + + public static func blueGrayPressed( + paddingHorizontal: CGFloat = Spacing.medium, + paddingVertical: CGFloat = Spacing.medium) -> ColorButtonStyle { + return ColorButtonStyle( + paddingHorizontal: paddingHorizontal, + paddingVertical: paddingVertical, + foregroundStyle: Colors.whiteSolid, + foregroundStylePressed: Colors.whiteSolid, + background: Colors.blue, + backgroundPressed: Colors.gray + ) + } + + public static func gray( + paddingHorizontal: CGFloat = Spacing.medium, + paddingVertical: CGFloat = Spacing.medium) -> ColorButtonStyle { + return ColorButtonStyle( + paddingHorizontal: paddingHorizontal, + paddingVertical: paddingVertical, + foregroundStyle: Colors.whiteSolid, + foregroundStylePressed: Colors.whiteSolid, + background: Colors.grayLight, + backgroundPressed: Colors.gray + ) + } + + public static func lightGray( + paddingHorizontal: CGFloat = Spacing.medium, + paddingVertical: CGFloat = Spacing.medium) -> ColorButtonStyle { + return ColorButtonStyle( + paddingHorizontal: paddingHorizontal, + paddingVertical: paddingVertical, + foregroundStyle: Colors.whiteSolid, + foregroundStylePressed: Colors.whiteSolid, + background: Colors.grayVeryLight, + backgroundPressed: Colors.grayLight + ) + } + + public static func white( + paddingHorizontal: CGFloat = Spacing.medium, + paddingVertical: CGFloat = Spacing.medium) -> ColorButtonStyle { + return ColorButtonStyle( + paddingHorizontal: paddingHorizontal, + paddingVertical: paddingVertical, + foregroundStyle: Colors.whiteSolid, + foregroundStylePressed: Colors.whiteSolid, + background: Colors.white, + backgroundPressed: Colors.grayVeryLight + ) + } +} + +public struct ClearButtonStyle: ButtonStyle { public init() {} - + public func makeBody(configuration: Configuration) -> some View { configuration.label .fontWeight(.semibold) - .foregroundColor(configuration.isPressed ? Colors.gray : Colors.black) + .foregroundStyle(configuration.isPressed ? Colors.gray : Colors.black) } } -public struct ColorButton: ButtonStyle { - - let paddingHorizontal: CGFloat - let paddingVertical: CGFloat - let foregroundColor: Color - let foregroundColorPressed: Color - let backgroundColor: Color - let backgroundColorPressed: Color - +// MARK: - ClearButtonStyle Static + +extension ButtonStyle where Self == ClearButtonStyle { + public static var clear: ClearButtonStyle { ClearButtonStyle() } +} + +public struct StatefulButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled: Bool + public static let maxButtonHeight: CGFloat = 50 + + public enum State { + case normal, loading, disabled + } + + var state: State + let foregroundStyle: Color + let foregroundStylePressed: Color + let background: Color + let backgroundPressed: Color + let backgroundDisabled: Color + + private var isGrayBackgroundSate: Bool { + state == .disabled || state == .loading || !isEnabled + } + public init( - paddingHorizontal: CGFloat, - paddingVertical: CGFloat, - foregroundColor: Color, - foregroundColorPressed: Color, - backgroundColor: Color, - backgroundColorPressed: Color + state: State, + foregroundStyle: Color, + foregroundStylePressed: Color, + background: Color, + backgroundPressed: Color, + backgroundDisabled: Color ) { - self.paddingHorizontal = paddingHorizontal - self.paddingVertical = paddingVertical - self.foregroundColor = foregroundColor - self.foregroundColorPressed = foregroundColorPressed - self.backgroundColor = backgroundColor - self.backgroundColorPressed = backgroundColorPressed + self.foregroundStyle = foregroundStyle + self.foregroundStylePressed = foregroundStylePressed + self.background = background + self.backgroundPressed = backgroundPressed + self.state = state + self.backgroundDisabled = backgroundDisabled } - - public static func gray( - paddingHorizontal: CGFloat = 16, - paddingVertical: CGFloat = 16 - ) -> ColorButton { - return Self( - paddingHorizontal: paddingHorizontal, - paddingVertical: paddingVertical, - foregroundColor: Colors.whiteSolid, - foregroundColorPressed: Colors.whiteSolid, - backgroundColor: Colors.grayLight, - backgroundColorPressed: Colors.gray - ) + + public func makeBody(configuration: Configuration) -> some View { + ZStack { + backgorundView + .frame(height: StatefulButtonStyle.maxButtonHeight) + switch state { + case .normal, .disabled: + configuration.label + .lineLimit(1) + .foregroundStyle(foregroundStyle(configuration: configuration)) + .padding(.horizontal, Spacing.medium) + .frame(minWidth: 0, maxWidth: .infinity) + .frame(height: StatefulButtonStyle.maxButtonHeight) + .background(labelBackground(configuration: configuration)) + case .loading: + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: Colors.whiteSolid)) + } + } + .frame(maxWidth: .infinity) + .disabled(isGrayBackgroundSate) } - - public static func blue( - paddingHorizontal: CGFloat = 16, - paddingVertical: CGFloat = 16 - ) -> ColorButton { - return Self( - paddingHorizontal: paddingHorizontal, - paddingVertical: paddingVertical, - foregroundColor: Colors.whiteSolid, - foregroundColorPressed: Colors.whiteSolid, - backgroundColor: Colors.blue, - backgroundColorPressed: Colors.gray - ) + + private var backgorundView: some View { + RoundedRectangle(cornerRadius: 12) + .fill(isGrayBackgroundSate ? backgroundDisabled : background) } - - public static func lightGray( - paddingHorizontal: CGFloat = 16, - paddingVertical: CGFloat = 16 - ) -> ColorButton { - return Self( - paddingHorizontal: paddingHorizontal, - paddingVertical: paddingVertical, - foregroundColor: Colors.gray, - foregroundColorPressed: Colors.whiteSolid, - backgroundColor: Colors.grayVeryLight, - backgroundColorPressed: Colors.grayLight - ) + + private func backgorundPressedView(configuration: Configuration) -> some View { + RoundedRectangle(cornerRadius: 12) + .fill(configuration.isPressed ? backgroundPressed : background) } - - public static func white( - paddingHorizontal: CGFloat = 16, - paddingVertical: CGFloat = 16 - ) -> ColorButton { - return Self( - paddingHorizontal: paddingHorizontal, - paddingVertical: paddingVertical, - foregroundColor: Colors.gray, - foregroundColorPressed: Colors.whiteSolid, - backgroundColor: Colors.white, - backgroundColorPressed: Colors.grayVeryLight + + private func labelBackground(configuration: Configuration) -> some View { + Group { + if !isGrayBackgroundSate { + backgorundPressedView(configuration: configuration) + } else { + backgorundView + } + } + } + + private func foregroundStyle(configuration: Configuration) -> some ShapeStyle { + return configuration.isPressed ? foregroundStylePressed : foregroundStyle + } +} + +// MARK: - StatefulButtonStyle Static + +extension ButtonStyle where Self == StatefulButtonStyle { + public static func statefullBlue(state: StatefulButtonStyle.State) -> StatefulButtonStyle { + StatefulButtonStyle( + state: state, + foregroundStyle: Colors.whiteSolid, + foregroundStylePressed: Colors.whiteSolid, + background: Colors.blue, + backgroundPressed: Colors.blueDark, + backgroundDisabled: Colors.gray ) } - - public func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(.vertical, paddingVertical) - .padding(.horizontal, paddingHorizontal) - .frame(minWidth: 0, maxWidth: .infinity) - .foregroundColor(configuration.isPressed ? foregroundColorPressed : foregroundColor) - .background(configuration.isPressed ? backgroundColorPressed : backgroundColor) - .cornerRadius(12) - .fontWeight(.semibold) +} + + +// MARK: - Previews + +#Preview { + struct StatefulButtonPreviewWrapper: View { + let text: String + @State var state: StatefulButtonStyle.State + + var body: some View { + Button(action: { + state = .loading + Task { + try await Task.sleep(nanoseconds: 1000000000 * 3) + state = .normal + } + }) { + Text(text) + } + .buttonStyle(.statefullBlue(state: state)) + } + } + + return List { + Section(header: Text("Regular Buttons")) { + Button(action: {}) { + Text("Blue Button") + } + .buttonStyle(.blue()) + + Button(action: {}) { + Text("Blue BIG TEXT TEXT Button Blue BIG TEXT TEXT Button Blue BIG TEXT TEXT Button") + } + .buttonStyle(.blue()) + + Button(action: {}) { + Text("Blue Gray Pressed Button") + } + .buttonStyle(.blueGrayPressed()) + + Button(action: {}) { + Text("Gray Button") + } + .buttonStyle(.gray()) + + Button(action: {}) { + Text("Light Gray Button") + } + .buttonStyle(.lightGray()) + + Button(action: {}) { + Text("White Button") + } + .buttonStyle(.white()) + + Button(action: {}) { + Text("Clear Button") + } + .buttonStyle(.clear) + .frame(maxWidth: Spacing.scene.button.maxWidth) + } + + Section(header: Text("Stateful Buttons")) { + StatefulButtonPreviewWrapper(text: "Stateful Button", state: .normal) + + StatefulButtonPreviewWrapper(text: "BIG TEXT Disabled Button BIG TEXT Disabled Button", state: .normal) + .disabled(true) + + StatefulButtonPreviewWrapper(text: "Stateful Button", state: .disabled) + .disabled(true) + + StatefulButtonPreviewWrapper(text: "Stateful Button", state: .loading) + .disabled(true) + } } + .padding() } diff --git a/Packages/Style/Sources/Previews/TextStyle+Preview.swift b/Packages/Style/Sources/Previews/TextStyle+Preview.swift deleted file mode 100644 index e1a3ca41..00000000 --- a/Packages/Style/Sources/Previews/TextStyle+Preview.swift +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -import SwiftUI - -/* - That is temporary solution to have ability watch previews via xcode. - Curretly xcode can't recognize shared colors. Gem uses colors from Assets. - This extension is same TextStyle, but with apple colors instead of Colors struct from Style - - [Do not use this TextStyle outside previews of Packages, this colors only for shared packages] - */ - -public extension TextStyle { - enum Preview { - public static let title = TextStyle(font: .title, color: .primary) - public static let headline = TextStyle(font: .headline, color: .primary) - public static let subheadline = TextStyle(font: .subheadline, color: .secondary) - public static let body = TextStyle(font: .body, color: .primary) - public static let callout = TextStyle(font: .callout, color: .primary) - public static let calloutSecondary = TextStyle(font: .callout, color: .secondary) - public static let footnote = TextStyle(font: .footnote, color: .secondary) - public static let caption = TextStyle(font: .caption, color: .secondary) - public static let largeTitle = TextStyle(font: .largeTitle, color: .primary) - public static let boldTitle = TextStyle(font: .title.bold(), color: .primary) - public static let highlighted = TextStyle(font: .headline, color: .white, background: Colors.blue) - } -} diff --git a/Packages/Style/Sources/SystemImage.swift b/Packages/Style/Sources/SystemImage.swift index 749da408..6a2ff60f 100644 --- a/Packages/Style/Sources/SystemImage.swift +++ b/Packages/Style/Sources/SystemImage.swift @@ -33,71 +33,45 @@ public struct SystemImage { // MARK: - Previews #Preview { - struct SystemImageView: View { - let imageName: String - let symbolName: String - - var body: some View { - VStack { - if !imageName.isEmpty { - Image(systemName: imageName) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 50, height: 50) - .padding() - } else { - Rectangle() - .fill(Color.gray.opacity(0.2)) - .frame(width: 50, height: 50) - .overlay( - Text("None") - .font(.caption) - .foregroundColor(.gray) - ) - } - Text(symbolName) - .font(.caption) - .multilineTextAlignment(.center) - .padding(.top, 4) - } - .padding() - } - } - let symbols = [ - (SystemImage.settings, "settings"), - (SystemImage.qrCode, "qrCode"), - (SystemImage.paste, "paste"), - (SystemImage.copy, "copy"), - (SystemImage.chevronDown, "chevronDown"), - (SystemImage.checklist, "checklist"), - (SystemImage.clear, "clear"), - (SystemImage.hide, "hide"), - (SystemImage.list, "list"), - (SystemImage.faceid, "faceid"), - (SystemImage.touchid, "touchid"), - (SystemImage.network, "network"), - (SystemImage.globe, "globe"), - (SystemImage.share, "share"), - (SystemImage.lock, "lock"), - (SystemImage.none, "none"), - (SystemImage.delete, "delete"), - (SystemImage.checkmark, "checkmark"), - (SystemImage.ellipsis, "ellipsis"), - (SystemImage.info, "info"), - (SystemImage.eyeglasses, "eyeglasses"), - (SystemImage.lockOpen, "lockOpen"), - (SystemImage.plus, "plus"), - (SystemImage.eye, "eye"), - (SystemImage.searchNoResults, "no results") + (SystemImage.settings, "Settings"), + (SystemImage.qrCode, "QR Code"), + (SystemImage.paste, "Paste"), + (SystemImage.copy, "Copy"), + (SystemImage.chevronDown, "Chevron Down"), + (SystemImage.checklist, "Checklist"), + (SystemImage.clear, "Clear"), + (SystemImage.hide, "Hide"), + (SystemImage.list, "List"), + (SystemImage.faceid, "Face ID"), + (SystemImage.touchid, "Touch ID"), + (SystemImage.network, "Network"), + (SystemImage.globe, "Globe"), + (SystemImage.share, "Share"), + (SystemImage.lock, "Lock"), + (SystemImage.none, "None"), + (SystemImage.delete, "Delete"), + (SystemImage.checkmark, "Checkmark"), + (SystemImage.ellipsis, "Ellipsis"), + (SystemImage.info, "Info"), + (SystemImage.eyeglasses, "Eyeglasses"), + (SystemImage.lockOpen, "Lock Open"), + (SystemImage.plus, "Plus"), + (SystemImage.eye, "Eye"), + (SystemImage.searchNoResults, "No Results") ] - return ScrollView { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 20) { - ForEach(symbols, id: \.1) { symbol in - SystemImageView(imageName: symbol.0, symbolName: symbol.1) + return List { + ForEach(symbols, id: \.1) { symbol in + Section(header: Text(symbol.1)) { + Image(systemName: symbol.0) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Sizing.list.image, height: Sizing.list.image) + .padding(Spacing.extraSmall) } } - .padding() } + .listStyle(InsetGroupedListStyle()) + .padding() } diff --git a/Packages/Style/Sources/TextStyle.swift b/Packages/Style/Sources/TextStyle.swift index 71ea4fce..9b2c7168 100644 --- a/Packages/Style/Sources/TextStyle.swift +++ b/Packages/Style/Sources/TextStyle.swift @@ -19,11 +19,9 @@ public struct TextStyle { } } -// MARK: - +// MARK: - TextStyle Static extension TextStyle { - // if you extend this and want to check previews in packages, please also - // add same apple color value in /Previews/TextStyle+Preview public static let title = TextStyle(font: .title, color: Colors.black) public static let headline = TextStyle(font: .headline, color: Colors.black) public static let subheadline = TextStyle(font: .subheadline, color: Colors.secondaryText) @@ -62,25 +60,25 @@ extension Text { #Preview { VStack(spacing: 16) { Text("Title") - .textStyle(TextStyle.Preview.title) + .textStyle(.title) Text("Headline") - .textStyle(TextStyle.Preview.headline) + .textStyle(.headline) Text("Subheadline") - .textStyle(TextStyle.Preview.subheadline) + .textStyle(.subheadline) Text("Body text that provides additional information.") - .textStyle(TextStyle.Preview.body) + .textStyle(.body) Text("Callout") - .textStyle(TextStyle.Preview.callout) + .textStyle(.callout) Text("Footnote") - .textStyle(TextStyle.Preview.footnote) + .textStyle(.footnote) Text("Caption") - .textStyle(TextStyle.Preview.caption) + .textStyle(.caption) Text("Large Title") - .textStyle(TextStyle.Preview.largeTitle) + .textStyle(.largeTitle) Text("Bold Title") - .textStyle(TextStyle.Preview.boldTitle) + .textStyle(.boldTitle) Text("Highlighted Text") - .textStyle(TextStyle.Preview.highlighted) + .textStyle(.highlighted) } .padding() } diff --git a/core b/core index 857f928e..6a14716c 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 857f928e522607ef112c88e50695012044dae4a4 +Subproject commit 6a14716ce650b04781f63724f90371f7df858e05