diff --git a/ios/Widgets/PriceWidget/PriceIntent.swift b/ios/Widgets/PriceWidget/PriceIntent.swift index aabd4af85..99c248ccf 100644 --- a/ios/Widgets/PriceWidget/PriceIntent.swift +++ b/ios/Widgets/PriceWidget/PriceIntent.swift @@ -6,6 +6,7 @@ import AppIntents import SwiftUI +@available(iOS 16.0, *) struct PriceIntent: AppIntent { // MARK: - Intent Metadata @@ -60,7 +61,7 @@ struct PriceIntent: AppIntent { price: "N/A", lastUpdated: "--", currencySymbol: getCurrencySymbol(for: selectedFiatCurrency.rawValue), - dataSource: "Error fetching data" + dataSource: "Error fetching data") return .result( diff --git a/ios/Widgets/PriceWidget/PriceWidgetEntryView.swift b/ios/Widgets/PriceWidget/PriceWidgetEntryView.swift index ba12cbdd9..7e7aa287d 100644 --- a/ios/Widgets/PriceWidget/PriceWidgetEntryView.swift +++ b/ios/Widgets/PriceWidget/PriceWidgetEntryView.swift @@ -9,7 +9,7 @@ import SwiftUICore -@available(iOS 14.0, *) +@available(iOS 16.0, *) struct PriceWidgetEntryView: View { let entry: PriceWidgetEntry diff --git a/ios/Widgets/Shared/Views/PriceView.swift b/ios/Widgets/Shared/Views/PriceView.swift index 2f48a417f..6ea556f3a 100644 --- a/ios/Widgets/Shared/Views/PriceView.swift +++ b/ios/Widgets/Shared/Views/PriceView.swift @@ -9,137 +9,182 @@ import SwiftUI import WidgetKit -@available(iOS 14.0, *) +@available(iOS 16.0, *) struct PriceView: View { - var entry: PriceWidgetEntry - - var body: some View { - switch entry.family { - case .accessoryInline, .accessoryCircular, .accessoryRectangular: - getView(for: entry.family) - default: - defaultView.background(Color(UIColor.systemBackground)) + var entry: PriceWidgetEntry + + var body: some View { + switch entry.family { + case .accessoryInline, .accessoryCircular, .accessoryRectangular: + if #available(iOSApplicationExtension 16.0, *) { + wrappedView(for: getView(for: entry.family), family: entry.family) + } else { + getView(for: entry.family) + } + default: + defaultView.background(Color(UIColor.systemBackground)) + } + } + + private func getView(for family: WidgetFamily) -> some View { + switch family { + case .accessoryCircular: + return AnyView(accessoryCircularView) + case .accessoryInline: + return AnyView(accessoryInlineView) + case .accessoryRectangular: + return AnyView(accessoryRectangularView) + default: + return AnyView(defaultView) + } + } + + @ViewBuilder + private func wrappedView(for content: Content, family: WidgetFamily) -> some View { + if #available(iOSApplicationExtension 16.0, *) { + ZStack { + if family == .accessoryRectangular { + AccessoryWidgetBackground() + .background(Color(UIColor.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } else { + AccessoryWidgetBackground() } + content + } + } else { + content } - - @ViewBuilder - private func getView(for family: WidgetFamily) -> some View { - switch family { - case .accessoryCircular: - accessoryCircularView - case .accessoryInline: - accessoryInlineView - case .accessoryRectangular: - accessoryRectangularView - default: - defaultView + } + + private var accessoryCircularView: some View { + let priceString = formattedPriceString(from: entry.currentMarketData?.rate) + let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate) + + return VStack(alignment: .center, spacing: 4) { + Text("BTC") + .font(.caption) + .minimumScaleFactor(0.1) + Text(priceString) + .font(.body) + .minimumScaleFactor(0.1) + .lineLimit(1) + if let priceChangePercentage = priceChangePercentage { + Text(priceChangePercentage) + .font(.caption2) + .foregroundColor(priceChangePercentage.contains("-") ? .red : .green) + } + } + .widgetURL(URL(string: "bluewallet://marketprice")) + } + + private var accessoryInlineView: some View { + let priceString = formattedCurrencyString(from: entry.currentMarketData?.rate) + let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate) + + return HStack { + Text(priceString) + .font(.body) + .minimumScaleFactor(0.1) + if let priceChangePercentage = priceChangePercentage { + Image(systemName: priceChangePercentage.contains("-") ? "arrow.down" : "arrow.up") + .foregroundColor(priceChangePercentage.contains("-") ? .red : .green) + } + } + } + + private var accessoryRectangularView: some View { + let currentPrice = formattedCurrencyString(from: entry.currentMarketData?.rate) + + return VStack(alignment: .leading, spacing: 4) { + Text("Bitcoin (\(Currency.getUserPreferredCurrency()))") + .font(.caption) + .foregroundColor(.secondary) + HStack { + Text(currentPrice) + .font(.caption) + .fontWeight(.bold) + if let currentMarketDataRate = entry.currentMarketData?.rate, + let previousMarketDataRate = entry.previousMarketData?.rate, + currentMarketDataRate != previousMarketDataRate { + Image(systemName: currentMarketDataRate > previousMarketDataRate ? "arrow.up" : "arrow.down") } + } + + if let previousMarketDataPrice = entry.previousMarketData?.price, Int(entry.currentMarketData?.rate ?? 0) != Int(entry.previousMarketData?.rate ?? 0) { + Text("From \(previousMarketDataPrice)") + .font(.caption) + .foregroundColor(.secondary) + } + + Text("at \(entry.currentMarketData?.formattedDate ?? "--")") + .font(.caption2) + .foregroundColor(.secondary) } - - private var accessoryCircularView: some View { - let priceString = (entry.currentMarketData?.rate ?? 0).formattedPriceString() - let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate) - - return VStack(alignment: .center, spacing: 4) { - Text("BTC") - .font(.caption) - .minimumScaleFactor(0.1) - Text(priceString) - .font(.body) - .minimumScaleFactor(0.1) - .lineLimit(1) - if let priceChangePercentage = priceChangePercentage { - Text(priceChangePercentage) - .font(.caption2) - .foregroundColor(priceChangePercentage.contains("-") ? .red : .green) - } + .padding(.all, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(UIColor.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + private var defaultView: some View { + VStack(alignment: .trailing, spacing: nil, content: { + Text("Last Updated").font(Font.system(size: 11, weight: .regular)).foregroundColor(Color(UIColor.lightGray)) + HStack(alignment: .lastTextBaseline, spacing: nil, content: { + Text(entry.currentMarketData?.formattedDate ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01).transition(.opacity) + }) + Spacer() + VStack(alignment: .trailing, spacing: 16, content: { + HStack(alignment: .lastTextBaseline, spacing: nil, content: { + Text(entry.currentMarketData?.price ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 28, weight: .bold)).minimumScaleFactor(0.01).transition(.opacity) + }) + if let previousMarketDataPrice = entry.previousMarketData?.price, let currentMarketDataRate = entry.currentMarketData?.rate, let previousMarketDataRate = entry.previousMarketData?.rate, previousMarketDataRate > 0, currentMarketDataRate != previousMarketDataRate { + HStack(alignment: .lastTextBaseline, spacing: nil, content: { + Image(systemName: currentMarketDataRate > previousMarketDataRate ? "arrow.up" : "arrow.down") + Text("from").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01) + Text(previousMarketDataPrice).lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01) + }).transition(.slide) } - .widgetURL(URL(string: "bluewallet://marketprice")) - } - - private var accessoryInlineView: some View { - let priceString = (entry.currentMarketData?.rate ?? 0).formattedCurrencyString() - let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate) - - return HStack { - Text(priceString) - .font(.body) - .minimumScaleFactor(0.1) - if let priceChangePercentage = priceChangePercentage { - Image(systemName: priceChangePercentage.contains("-") ? "arrow.down" : "arrow.up") - .foregroundColor(priceChangePercentage.contains("-") ? .red : .green) - } - } - } - - private var accessoryRectangularView: some View { - let currentPrice = (entry.currentMarketData?.rate ?? 0).formattedCurrencyString() - - return VStack(alignment: .leading, spacing: 4) { - Text("Bitcoin (\(Currency.getUserPreferredCurrency()))") - .font(.caption) - .foregroundColor(.secondary) - HStack { - Text(currentPrice) - .font(.caption) - .fontWeight(.bold) - if let currentRate = entry.currentMarketData?.rate, - let previousRate = entry.previousMarketData?.rate, - currentRate != previousRate { - Image(systemName: currentRate > previousRate ? "arrow.up" : "arrow.down") - } - } - - if let previousPrice = entry.previousMarketData?.price, - let currentRate = entry.currentMarketData?.rate, - let previousRate = entry.previousMarketData?.rate, - currentRate != previousRate { - Text("From \(previousPrice)") - .font(.caption) - .foregroundColor(.secondary) - } - - Text("at \(entry.currentMarketData?.formattedDate ?? "--")") - .font(.caption2) - .foregroundColor(.secondary) - } - .padding(.all, 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(UIColor.systemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - - private var defaultView: some View { - VStack(alignment: .trailing, spacing: nil) { - Text("Last Updated").font(Font.system(size: 11, weight: .regular)).foregroundColor(Color(UIColor.lightGray)) - HStack(alignment: .lastTextBaseline, spacing: nil) { - Text(entry.currentMarketData?.formattedDate ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01).transition(.opacity) - } - Spacer() - VStack(alignment: .trailing, spacing: 16) { - HStack(alignment: .lastTextBaseline, spacing: nil) { - Text(entry.currentMarketData?.price ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 28, weight: .bold)).minimumScaleFactor(0.01).transition(.opacity) - } - if let previousPrice = entry.previousMarketData?.price, - let currentRate = entry.currentMarketData?.rate, - let previousRate = entry.previousMarketData?.rate, - previousRate > 0, currentRate != previousRate { - HStack(alignment: .lastTextBaseline, spacing: nil) { - Image(systemName: currentRate > previousRate ? "arrow.up" : "arrow.down") - Text("from").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01) - Text(previousPrice).lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01) - }.transition(.slide) - } - } - }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing).padding() - } - - private func formattedPriceChangePercentage(currentRate: Double?, previousRate: Double?) -> (change: Double, formattedString: String)? { - guard let currentRate = currentRate, let previousRate = previousRate, previousRate > 0 else { return nil } - let change = ((currentRate - previousRate) / previousRate) * 100 - guard change != 0 else { return nil } - let formattedString = String(format: "%+.1f%%", change) - return (change, formattedString) - } - } + }) + }).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .trailing).padding() + } + + private func formattedPriceString(from rate: Double?) -> String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + numberFormatter.maximumFractionDigits = 0 + return numberFormatter.string(from: NSNumber(value: rate ?? 0)) ?? "--" + } + + private func formattedCurrencyString(from rate: Double?) -> String { + let numberFormatter = NumberFormatter() + numberFormatter.maximumFractionDigits = 0 + numberFormatter.numberStyle = .currency + numberFormatter.currencySymbol = fiatUnit(currency: Currency.getUserPreferredCurrency())?.symbol + return numberFormatter.string(from: NSNumber(value: rate ?? 0)) ?? "--" + } + + private func formattedPriceChangePercentage(currentRate: Double?, previousRate: Double?) -> String? { + guard let currentRate = currentRate, let previousRate = previousRate, previousRate > 0 else { return nil } + let change = ((currentRate - previousRate) / previousRate) * 100 + return change == 0 ? nil : String(format: "%+.1f%%", change) + } +} + +@available(iOS 16.0, *) +struct PriceView_Previews: PreviewProvider { + static var previews: some View { + Group { + PriceView(entry: PriceWidgetEntry(date: Date(), family: .systemSmall, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData)) + .previewContext(WidgetPreviewContext(family: .systemSmall)).padding() + if #available(iOSApplicationExtension 16.0, *) { + PriceView(entry: PriceWidgetEntry(date: Date(), family: .accessoryCircular, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData)) + .previewContext(WidgetPreviewContext(family: .accessoryCircular)) + PriceView(entry: PriceWidgetEntry(date: Date(), family: .accessoryInline, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData)) + .previewContext(WidgetPreviewContext(family: .accessoryInline)) + PriceView(entry: PriceWidgetEntry(date: Date(), family: .accessoryRectangular, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData)) + .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) + } + } + } }