Skip to content

Commit

Permalink
Implement Top Coins widget
Browse files Browse the repository at this point in the history
  • Loading branch information
ealymbaev committed Oct 19, 2023
1 parent 95e4281 commit e9c5380
Show file tree
Hide file tree
Showing 20 changed files with 640 additions and 92 deletions.
1 change: 1 addition & 0 deletions UnstoppableWallet/IntentExtension/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<array/>
<key>IntentsSupported</key>
<array>
<string>CoinPriceListIntent</string>
<string>SingleCoinPriceIntent</string>
</array>
</dict>
Expand Down
102 changes: 80 additions & 22 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions UnstoppableWallet/UnstoppableWallet/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
</dict>
<key>NSUserActivityTypes</key>
<array>
<string>CoinPriceListIntent</string>
<string>SingleCoinPriceIntent</string>
</array>
<key>OfficeMode</key>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import ThemeKit

struct HorizontalDivider: View {
private let color: Color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ struct ListSection<Content: View>: View {
_VariadicView.Tree(Layout()) {
content
}
.background(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).fill(listStyle.backgroundColor))
.clipShape(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous))
.overlay(RoundedRectangle(cornerRadius: .cornerRadius12).stroke(listStyle.borderColor, lineWidth: .heightOneDp))
.modifier(ListStyleModifier(listStyle: listStyle))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,36 @@ import SwiftUI
enum ListStyle {
case lawrence
case bordered
case transparent
}

struct ListStyleModifier: ViewModifier {
let listStyle: ListStyle

var backgroundColor: Color {
switch self {
case .lawrence: return .themeLawrence
case .bordered: return .clear
func body(content: Content) -> some View {
switch listStyle {
case .lawrence:
content
.background(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).fill(Color.themeLawrence))
.clipShape(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous))
case .bordered:
content
.clipShape(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous))
.overlay(RoundedRectangle(cornerRadius: .cornerRadius12).stroke(Color.themeSteel20, lineWidth: .heightOneDp))
case .transparent:
content
}
}
}

struct ListStyleButtonModifier: ViewModifier {
let listStyle: ListStyle
let isPressed: Bool

var borderColor: Color {
switch self {
case .lawrence: return .clear
case .bordered: return .themeSteel20
func body(content: Content) -> some View {
switch listStyle {
case .lawrence: content.background(isPressed ? Color.themeLawrencePressed : Color.themeLawrence)
case .bordered, .transparent: content.background(isPressed ? Color.themeLawrencePressed : Color.clear)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ struct RowButtonStyle: ButtonStyle {

func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(configuration.isPressed ? Color.themeLawrencePressed : listStyle.backgroundColor)
.modifier(ListStyleButtonModifier(listStyle: listStyle, isPressed: configuration.isPressed))
}
}
1 change: 1 addition & 0 deletions UnstoppableWallet/Widget/AppWidgetBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import WidgetKit
struct AppWidgetBundle: WidgetBundle {
var body: some Widget {
SingleCoinPriceWidget()
TopCoinsWidget()
}
}
20 changes: 20 additions & 0 deletions UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
import SwiftUI
import WidgetKit

struct CoinPriceListEntry: TimelineEntry {
let date: Date
let title: String
let sortType: String
let items: [Item]

struct Item {
let uid: String
let icon: Image?
let code: String
let name: String
let price: String
let priceChange: String
let priceChangeType: PriceChangeType
}
}
107 changes: 107 additions & 0 deletions UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Foundation
import SwiftUI
import WidgetKit

struct CoinPriceListProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> CoinPriceListEntry {
let count: Int

switch context.family {
case .systemSmall, .systemMedium: count = 3
default: count = 6
}

return CoinPriceListEntry(
date: Date(),
title: "Top Coins",
sortType: "Highest Cap",
items: (1 ... count).map { index in
CoinPriceListEntry.Item(
uid: "coin\(index)",
icon: nil,
code: "COD\(index)",
name: "Coin Name \(index)",
price: "$1234",
priceChange: "1.23",
priceChangeType: .unknown
)
}
)
}

func getSnapshot(for _: CoinPriceListIntent, in context: Context, completion: @escaping (CoinPriceListEntry) -> Void) {
Task {
let entry = try await fetch(sortType: .highestCap, family: context.family)
completion(entry)
}
}

func getTimeline(for configuration: CoinPriceListIntent, in context: Context, completion: @escaping (Timeline<CoinPriceListEntry>) -> Void) {
Task {
let entry = try await fetch(sortType: configuration.sortBy, family: context.family)

if let nextUpdate = Calendar.current.date(byAdding: DateComponents(minute: 15), to: Date()) {
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
}

private func fetch(sortType: SortType, family: WidgetFamily) async throws -> CoinPriceListEntry {
let currency = CurrencyManager(storage: SharedLocalStorage()).baseCurrency
let apiProvider = ApiProvider(baseUrl: "https://api-dev.blocksdecoded.com")

let listType: ApiProvider.ListType
let listOrder: ApiProvider.ListOrder
let limit: Int

switch sortType {
case .highestCap, .lowestCap, .unknown: listType = .mcap
case .highestVolume, .lowestVolume: listType = .volume
case .topGainers, .topLosers: listType = .price
}

switch sortType {
case .highestCap, .highestVolume, .topGainers, .unknown: listOrder = .desc
case .lowestCap, .lowestVolume, .topLosers: listOrder = .asc
}

switch family {
case .systemSmall, .systemMedium: limit = 3
default: limit = 6
}

let coins = try await apiProvider.listCoins(type: listType, order: listOrder, limit: limit, currencyCode: currency.code)

return CoinPriceListEntry(
date: Date(),
title: "Top Coins",
sortType: title(sortType: sortType),
items: coins.map { coin in
let iconUrl = "https://cdn.blocksdecoded.com/coin-icons/32px/\(coin.uid)@3x.png"
let coinIcon = URL(string: iconUrl).flatMap { try? Data(contentsOf: $0) }.flatMap { UIImage(data: $0) }.map { Image(uiImage: $0) }

return CoinPriceListEntry.Item(
uid: coin.uid,
icon: coinIcon,
code: coin.code,
name: coin.name,
price: coin.price.flatMap { ValueFormatter.format(currency: currency, value: $0) } ?? "n/a",
priceChange: coin.priceChange24h.flatMap { ValueFormatter.format(percentValue: $0) } ?? "n/a",
priceChangeType: coin.priceChange24h.map { $0 >= 0 ? .up : .down } ?? .unknown
)
}
)
}

private func title(sortType: SortType) -> String {
switch sortType {
case .highestCap, .unknown: return "Highest Cap"
case .lowestCap: return "Lowest Cap"
case .highestVolume: return "Highest Volume"
case .lowestVolume: return "Lowest Volume"
case .topGainers: return "Top Gainers"
case .topLosers: return "Top Losers"
}
}
}
128 changes: 128 additions & 0 deletions UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Charts
import SwiftUI
import WidgetKit

struct CoinPriceListView: View {
var entry: CoinPriceListProvider.Entry

@Environment(\.widgetFamily) private var family

var body: some View {
switch family {
case .systemSmall: smallView()
case .systemMedium: mediumView()
default: largeView()
}
}

@ViewBuilder private func smallView() -> some View {
ListSection {
ForEach(entry.items, id: \.uid) { item in
HStack(spacing: .margin8) {
icon(image: item.icon)

VStack(spacing: 1) {
Text(item.price)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.themeSubhead1)
.foregroundColor(.themeLeah)
Text(item.priceChange)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.themeCaption)
.foregroundColor(item.priceChangeType.color)
}
}
.padding(.horizontal, .margin16)
.frame(maxHeight: .infinity)
}
}
.listStyle(.transparent)
.frame(maxHeight: .infinity)
.padding(.vertical, .margin4)
}

@ViewBuilder private func mediumView() -> some View {
ListSection {
ForEach(entry.items, id: \.uid) { item in
row(item: item)
}
}
.listStyle(.transparent)
.frame(maxHeight: .infinity)
.padding(.vertical, .margin4)
}

@ViewBuilder private func largeView() -> some View {
VStack(spacing: 0) {
HStack(spacing: .margin16) {
Text(entry.title)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.themeSubhead1)
.foregroundColor(.themeLeah)

Text(entry.sortType)
.frame(maxWidth: .infinity, alignment: .trailing)
.font(.themeSubhead2)
.foregroundColor(.themeGray)
}
.padding(.margin16)

HorizontalDivider()

ListSection {
ForEach(entry.items, id: \.uid) { item in
row(item: item)
}
}
.listStyle(.transparent)
.frame(maxHeight: .infinity)
}
.padding(.vertical, .margin4)
}

@ViewBuilder private func icon(image: Image?) -> some View {
if let image = image {
image
.resizable()
.scaledToFit()
.frame(width: .iconSize32, height: .iconSize32)
} else {
Circle()
.fill(Color.themeGray)
.frame(width: .iconSize32, height: .iconSize32)
}
}

@ViewBuilder private func row(item: CoinPriceListEntry.Item) -> some View {
HStack(spacing: .margin16) {
icon(image: item.icon)

VStack(spacing: 1) {
HStack(spacing: .margin16) {
Text(item.code.uppercased())
.frame(maxWidth: .infinity, alignment: .leading)
.font(.themeSubhead1)
.foregroundColor(.themeLeah)

Text(item.price)
.frame(maxWidth: .infinity, alignment: .trailing)
.font(.themeSubhead1)
.foregroundColor(.themeLeah)
}

HStack(spacing: .margin16) {
Text(item.name)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.themeSubhead2)
.foregroundColor(.themeGray)

Text(item.priceChange)
.font(.themeSubhead2)
.foregroundColor(item.priceChangeType.color)
}
}
}
.padding(.horizontal, .margin16)
.frame(maxHeight: .infinity)
}
}
Loading

0 comments on commit e9c5380

Please sign in to comment.