Skip to content

Commit

Permalink
Implement Watchlist widget
Browse files Browse the repository at this point in the history
  • Loading branch information
ealymbaev committed Oct 19, 2023
1 parent 1cc015b commit 49ab649
Show file tree
Hide file tree
Showing 20 changed files with 279 additions and 92 deletions.
28 changes: 28 additions & 0 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions UnstoppableWallet/UnstoppableWallet/Core/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ class App {
let logRecordStorage = LogRecordStorage(dbPool: dbPool)
logRecordManager = LogRecordManager(storage: logRecordStorage)

currencyManager = CurrencyManager(storage: SharedLocalStorage())
let sharedLocalStorage = SharedLocalStorage()
currencyManager = CurrencyManager(storage: sharedLocalStorage)

marketKit = try MarketKit.Kit.instance(
hsApiBaseUrl: AppConfig.marketApiUrl,
Expand Down Expand Up @@ -248,7 +249,7 @@ class App {
feeRateProviderFactory = FeeRateProviderFactory()

let favoriteCoinRecordStorage = FavoriteCoinRecordStorage(dbPool: dbPool)
favoritesManager = FavoritesManager(storage: favoriteCoinRecordStorage)
favoritesManager = FavoritesManager(storage: favoriteCoinRecordStorage, sharedStorage: sharedLocalStorage)

let appVersionRecordStorage = AppVersionRecordStorage(dbPool: dbPool)
appVersionStorage = AppVersionStorage(storage: appVersionRecordStorage)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import RxSwift
import RxCocoa
import RxSwift
import WidgetKit

class FavoritesManager {
private let storage: FavoriteCoinRecordStorage
private let sharedStorage: SharedLocalStorage

private let coinUidsUpdatedRelay = PublishRelay<()>()
private let coinUidsUpdatedRelay = PublishRelay<Void>()

init(storage: FavoriteCoinRecordStorage) {
init(storage: FavoriteCoinRecordStorage, sharedStorage: SharedLocalStorage) {
self.storage = storage
self.sharedStorage = sharedStorage

syncSharedStorage()
}

private func syncSharedStorage() {
sharedStorage.set(value: allCoinUids, for: AppWidgetConstants.keyFavoriteCoinUids)
WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind)
}
}

extension FavoritesManager {

var coinUidsUpdatedObservable: Observable<()> {
var coinUidsUpdatedObservable: Observable<Void> {
coinUidsUpdatedRelay.asObservable()
}

Expand All @@ -25,11 +33,13 @@ extension FavoritesManager {
func add(coinUid: String) {
storage.save(favoriteCoinRecord: FavoriteCoinRecord(coinUid: coinUid))
coinUidsUpdatedRelay.accept(())
syncSharedStorage()
}

func add(coinUids: [String]) {
storage.save(favoriteCoinRecords: coinUids.map { FavoriteCoinRecord(coinUid: $0) })
coinUidsUpdatedRelay.accept(())
syncSharedStorage()
}

func removeAll() {
Expand All @@ -39,10 +49,10 @@ extension FavoritesManager {
func remove(coinUid: String) {
storage.deleteFavoriteCoinRecord(coinUid: coinUid)
coinUidsUpdatedRelay.accept(())
syncSharedStorage()
}

func isFavorite(coinUid: String) -> Bool {
storage.favoriteCoinRecordExists(coinUid: coinUid)
}

}
1 change: 1 addition & 0 deletions UnstoppableWallet/Widget/AppWidgetBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ struct AppWidgetBundle: WidgetBundle {
var body: some Widget {
SingleCoinPriceWidget()
TopCoinsWidget()
WatchlistWidget()
}
}
7 changes: 7 additions & 0 deletions UnstoppableWallet/Widget/AppWidgetConstants.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum AppWidgetConstants {
static let singleCoinPriceWidgetKind: String = "io.horizontalsystems.unstoppable.SingleCoinPriceWidget"
static let topCoinsWidgetKind: String = "io.horizontalsystems.unstoppable.TopCoinsWidget"
static let watchlistWidgetKind: String = "io.horizontalsystems.unstoppable.WatchlistWidget"

static let keyFavoriteCoinUids = "favorite_coin_uids"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import WidgetKit

struct CoinPriceListEntry: TimelineEntry {
let date: Date
let title: String
let mode: CoinPriceListMode
let sortType: String
let maxItemCount: Int
let items: [Item]

struct Item {
Expand Down
39 changes: 27 additions & 12 deletions UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import SwiftUI
import WidgetKit

struct CoinPriceListProvider: IntentTimelineProvider {
let mode: CoinPriceListMode

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

Expand All @@ -13,8 +15,9 @@ struct CoinPriceListProvider: IntentTimelineProvider {

return CoinPriceListEntry(
date: Date(),
title: "Top Coins",
mode: mode,
sortType: "Highest Cap",
maxItemCount: count,
items: (1 ... count).map { index in
CoinPriceListEntry.Item(
uid: "coin\(index)",
Expand Down Expand Up @@ -48,7 +51,8 @@ struct CoinPriceListProvider: IntentTimelineProvider {
}

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

let listType: ApiProvider.ListType
Expand All @@ -71,24 +75,35 @@ struct CoinPriceListProvider: IntentTimelineProvider {
default: limit = 6
}

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

switch mode {
case .topCoins:
coins = try await apiProvider.listCoins(type: listType, order: listOrder, limit: limit, currencyCode: currency.code)
case .watchlist:
let coinUids: [String]? = storage.value(for: AppWidgetConstants.keyFavoriteCoinUids)

if let coinUids, !coinUids.isEmpty {
coins = try await apiProvider.listCoins(uids: coinUids, type: listType, order: listOrder, limit: limit, currencyCode: currency.code)
} else {
coins = []
}
}

return CoinPriceListEntry(
date: Date(),
title: "Top Coins",
mode: mode,
sortType: title(sortType: sortType),
maxItemCount: limit,
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(
CoinPriceListEntry.Item(
uid: coin.uid,
icon: coinIcon,
icon: coin.image,
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
price: coin.formattedPrice(currency: currency),
priceChange: coin.formattedPriceChange,
priceChangeType: coin.priceChangeType
)
}
)
Expand Down
118 changes: 73 additions & 45 deletions UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,34 @@ struct CoinPriceListView: View {
}

@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)
}
list(verticalPadding: .margin4) { 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)
}
list(verticalPadding: .margin4) { 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)
Text(entry.mode.title)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.themeSubhead1)
.foregroundColor(.themeLeah)
Expand All @@ -69,27 +57,56 @@ struct CoinPriceListView: View {

HorizontalDivider()

ListSection {
ForEach(entry.items, id: \.uid) { item in
row(item: item)
}
list(verticalPadding: 0) { 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)
@ViewBuilder private func list(verticalPadding: CGFloat, rowBuilder: @escaping (CoinPriceListEntry.Item) -> some View) -> some View {
if entry.mode.isWatchlist, entry.items.isEmpty {
VStack(spacing: .margin16) {
switch family {
case .systemLarge:
ZStack {
Circle()
.fill(Color.themeRaina)
.frame(width: 100, height: 100)

Image("rate_48")
.renderingMode(.template)
.foregroundColor(.themeGray)
}
default:
EmptyView()
}

Text("Your watchlist is empty.")
.multilineTextAlignment(.center)
.font(.themeSubhead2)
.foregroundColor(.themeGray)
}
.frame(maxHeight: .infinity)
.padding(.margin16)
} else {
Circle()
.fill(Color.themeGray)
.frame(width: .iconSize32, height: .iconSize32)
GeometryReader { proxy in
ListSection {
ForEach(entry.items, id: \.uid) { item in
rowBuilder(item)
.padding(.horizontal, .margin16)
.frame(maxHeight: .infinity)
.frame(maxHeight: proxy.size.height / CGFloat(entry.maxItemCount))
}

if entry.items.count < entry.maxItemCount {
Spacer()
}
}
.listStyle(.transparent)
}
.frame(maxHeight: .infinity)
.padding(.vertical, verticalPadding)
}
}

Expand Down Expand Up @@ -122,7 +139,18 @@ struct CoinPriceListView: View {
}
}
}
.padding(.horizontal, .margin16)
.frame(maxHeight: .infinity)
}

@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)
}
}
}
8 changes: 6 additions & 2 deletions UnstoppableWallet/Widget/Misc/ApiProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ class ApiProvider {
return try await networkManager.fetch(url: "\(baseUrl)/v1/coins", method: .get, parameters: parameters, headers: headers)
}

func listCoins(type: ListType, order: ListOrder, limit: Int, currencyCode: String) async throws -> [Coin] {
let parameters: Parameters = [
func listCoins(uids: [String]? = nil, type: ListType, order: ListOrder, limit: Int, currencyCode: String) async throws -> [Coin] {
var parameters: Parameters = [
"order": order.rawValue,
"limit": limit,
"currency": currencyCode.lowercased(),
]

if let uids {
parameters["uids"] = uids.joined(separator: ",")
}

return try await networkManager.fetch(url: "\(baseUrl)/v1/coins/top-movers-by/\(type.rawValue)", method: .get, parameters: parameters, headers: headers)
}

Expand Down
18 changes: 18 additions & 0 deletions UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
enum CoinPriceListMode {
case topCoins
case watchlist

var title: String {
switch self {
case .topCoins: return "Top Coins"
case .watchlist: return "Watchlist"
}
}

var isWatchlist: Bool {
switch self {
case .watchlist: return true
default: return false
}
}
}
Loading

0 comments on commit 49ab649

Please sign in to comment.