├── Resources
├── icon.afdesign
├── screen-dark-01.png
├── screen-dark-02.png
├── screen-white-01.png
└── screen-white-02.png
├── VirtualCoin
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Icon-1024.png
│ │ ├── Icon-120.png
│ │ ├── Icon-121.png
│ │ ├── Icon-152.png
│ │ ├── Icon-167.png
│ │ ├── Icon-180.png
│ │ ├── Icon-20.png
│ │ ├── Icon-29.png
│ │ ├── Icon-40.png
│ │ ├── Icon-41.png
│ │ ├── Icon-42.png
│ │ ├── Icon-58.png
│ │ ├── Icon-59.png
│ │ ├── Icon-60.png
│ │ ├── Icon-76.png
│ │ ├── Icon-80.png
│ │ ├── Icon-81.png
│ │ ├── Icon-87.png
│ │ └── Contents.json
│ ├── GreenPastel.colorset
│ │ └── Contents.json
│ ├── RedPastel.colorset
│ │ └── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── BackgroundLabel.colorset
│ │ └── Contents.json
│ └── WidgetBackground.colorset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── vcoin.xcdatamodeld
│ ├── .xccurrentversion
│ ├── vcoin-20210604.xcdatamodel
│ │ └── contents
│ ├── vcoin-20210606.xcdatamodel
│ │ └── contents
│ ├── vcoin-20190712.xcdatamodel
│ │ └── contents
│ └── vcoin.xcdatamodel
│ │ └── contents
├── ViewModels
│ ├── SideBarNavigationItem.swift
│ ├── ViewState.swift
│ ├── AlertViewModel.swift
│ ├── MarketViewModel.swift
│ ├── ExchangeViewModel.swift
│ └── CoinViewModel.swift
├── VirtualCoin.entitlements
├── Extensions
│ ├── DispatchQueue.swift
│ ├── View.swift
│ └── Color.swift
├── Notifications
│ ├── NotificationsError.swift
│ ├── PriceAlert.swift
│ └── Notifications.swift
├── Views
│ ├── ComponentViews
│ │ ├── LoadingView.swift
│ │ ├── InitialsPlaceholder.swift
│ │ ├── ViewControllerResolver.swift
│ │ ├── ErrorView.swift
│ │ ├── SearchBar.swift
│ │ ├── NoDataView.swift
│ │ ├── CoinImageView.swift
│ │ ├── ExchangeDetailView.swift
│ │ ├── AlertDetailView.swift
│ │ └── ChartView.swift
│ ├── AppViews
│ │ ├── AppView.swift
│ │ ├── TabsView.swift
│ │ └── SideBarsView.swift
│ ├── ScreenViews
│ │ ├── MarketsView.swift
│ │ ├── ThirdPartyView.swift
│ │ ├── AddAlertView.swift
│ │ ├── AddExchangeView.swift
│ │ ├── EditExchangeView.swift
│ │ ├── EditAlertView.swift
│ │ ├── SettingsView.swift
│ │ └── CoinView.swift
│ ├── ListRowsViews
│ │ ├── MarketRowView.swift
│ │ ├── CoinRowView.swift
│ │ ├── AlertRowView.swift
│ │ └── ExchangeRowView.swift
│ └── TabsViews
│ │ ├── CoinsView.swift
│ │ ├── ExchangesView.swift
│ │ ├── AlertsView.swift
│ │ └── FavouritesView.swift
├── Services
│ ├── ApplicationStateService.swift
│ └── PricesService.swift
├── Info.plist
├── Cache
│ └── MemoryCache.swift
├── VirtualCoinApp.swift
└── Previews
│ └── PreviewData.swift
├── VirtualCoinWidget
├── en.lproj
│ └── VirtualCoinWidget.strings
├── CoinSymbol.swift
├── WidgetEntry.swift
├── WidgetViewModel.swift
├── Extensions
│ ├── CurrentValueLineType+ChartColors.swift
│ └── ChartVisualType+ChartColors.swift
├── VirtualCoinWidget.swift
├── VirtualCoinWidgetEntryView.swift
├── Info.plist
├── Provider.swift
├── Base.lproj
│ └── VirtualCoinWidget.intentdefinition
├── Views
│ ├── SmallWidgetView.swift
│ ├── LargeWidgetView.swift
│ └── MediumWidgetView.swift
├── PreviewData.swift
└── DataFetcher.swift
├── VirtualCoin.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── mczachurski.xcuserdatad
│ │ ├── xcschemes
│ │ └── xcschememanagement.plist
│ │ └── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
└── xcshareddata
│ └── xcschemes
│ └── VirtualCoin.xcscheme
├── VirtualCoinKit
├── Models
│ ├── ResponseArray.swift
│ ├── ChartValue.swift
│ ├── ChartTimeRange.swift
│ ├── CurrencyRate.swift
│ ├── Market.swift
│ ├── Coin.swift
│ └── Currency.swift
├── Extensions
│ ├── Date.swift
│ ├── NumberFormatter.swift
│ └── Double.swift
├── VirtualCoinKit.h
├── Info.plist
└── Errors
│ └── RestClientError.swift
├── CoreData
├── Entities
│ ├── Alert+CoreDataClass.swift
│ ├── Favourite+CoreDataClass.swift
│ ├── Settings+CoreDataClass.swift
│ ├── ExchangeItem+CoreDataClass.swift
│ ├── Settings+CoreDataProperties.swift
│ ├── Favourite+CoreDataProperties.swift
│ ├── ExchangeItem+CoreDataProperties.swift
│ └── Alert+CoreDataProperties.swift
├── PropertyWrappers
│ └── Setting.swift
├── ExchangeItemsHandler.swift
├── SettingsHandler.swift
├── FavouritesHandler.swift
├── AlertsHandler.swift
└── CoreDataHandler.swift
├── VirtualCoinWidgetExtension.entitlements
├── VirtualCoinKitTests
├── Info.plist
└── VirtualCoinKitTests.swift
├── VirtualCoinTests
├── Info.plist
└── VirtualCoinTests.swift
├── VirtualCoinUITests
├── Info.plist
└── VirtualCoinUITests.swift
├── README.md
├── .swiftlint.yml
└── .gitignore
/Resources/icon.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/Resources/icon.afdesign
--------------------------------------------------------------------------------
/Resources/screen-dark-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/Resources/screen-dark-01.png
--------------------------------------------------------------------------------
/Resources/screen-dark-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/Resources/screen-dark-02.png
--------------------------------------------------------------------------------
/Resources/screen-white-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/Resources/screen-white-01.png
--------------------------------------------------------------------------------
/Resources/screen-white-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/Resources/screen-white-02.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/en.lproj/VirtualCoinWidget.strings:
--------------------------------------------------------------------------------
1 | "gpCwrM" = "Configuration";
2 |
3 | "tVvJ9c" = "Configuration for vCoin app";
4 |
5 |
--------------------------------------------------------------------------------
/VirtualCoin/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-120.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-121.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-121.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-152.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-167.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-180.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-20.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-29.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-41.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-42.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-58.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-59.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-59.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-60.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-80.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-81.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-81.png
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mczachurski/vcoin/HEAD/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Icon-87.png
--------------------------------------------------------------------------------
/VirtualCoin.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/CoinSymbol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | struct CoinOrder {
10 | let coinId: String
11 | let order: Int32
12 | }
13 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Models/ResponseArray.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public class Response: Decodable {
10 | public var data: T
11 | }
12 |
--------------------------------------------------------------------------------
/CoreData/Entities/Alert+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | @objc(Alert)
11 | public class Alert: NSManagedObject {
12 | }
13 |
--------------------------------------------------------------------------------
/CoreData/Entities/Favourite+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | @objc(Favourite)
11 | public class Favourite: NSManagedObject {
12 | }
13 |
--------------------------------------------------------------------------------
/CoreData/Entities/Settings+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | @objc(Settings)
11 | public class Settings: NSManagedObject {
12 | }
13 |
--------------------------------------------------------------------------------
/VirtualCoin/vcoin.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | vcoin-20210606.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/VirtualCoin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/WidgetEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import WidgetKit
8 |
9 | struct WidgetEntry: TimelineEntry {
10 | let date: Date
11 | let viewModels: [WidgetViewModel]
12 | }
13 |
--------------------------------------------------------------------------------
/CoreData/Entities/ExchangeItem+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | @objc(ExchangeItem)
11 | public class ExchangeItem: NSManagedObject {
12 | }
13 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Models/ChartValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public struct ChartValue: Decodable {
10 | public var priceUsd: String
11 | public var time: Int
12 | }
13 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Extensions/Date.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | extension Date {
10 | var unixTimestamp: Int64 {
11 | return Int64(self.timeIntervalSince1970 * 1_000)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/VirtualCoin/ViewModels/SideBarNavigationItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | enum SideBarNavigationItem {
10 | case favourites
11 | case currencies
12 | case exchanges
13 | case alerts
14 | }
15 |
--------------------------------------------------------------------------------
/VirtualCoin/VirtualCoin.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.dev.mczachurski.vcoin
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/VirtualCoinWidgetExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.dev.mczachurski.vcoin
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Models/ChartTimeRange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public enum ChartTimeRange: String {
10 | case hour = "Hour", day = "Day", week = "Week", month = "Month", year = "Year"
11 |
12 | public static let allValues = [hour, day, week, month, year]
13 | }
14 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Models/CurrencyRate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public struct CurrencyRate: Decodable {
10 | public var id: String
11 | public var symbol: String
12 | public var currencySymbol: String
13 | public var type: String
14 | public var rateUsd: String
15 | }
16 |
--------------------------------------------------------------------------------
/CoreData/Entities/Settings+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | public extension Settings {
11 | @nonobjc
12 | class func fetchRequest() -> NSFetchRequest {
13 | return NSFetchRequest(entityName: "Settings")
14 | }
15 |
16 | @NSManaged var currency: String
17 | }
18 |
--------------------------------------------------------------------------------
/VirtualCoin/Extensions/DispatchQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public extension DispatchQueue {
10 | static func runOnMain(_ handler: @escaping () -> Void) {
11 | if Thread.isMainThread {
12 | handler()
13 | } else {
14 | DispatchQueue.main.async(execute: handler)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/VirtualCoin/Extensions/View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | extension View {
10 | func animate(using animation: Animation = Animation.easeInOut(duration: 0.5), _ action: @escaping () -> Void) -> some View {
11 | onAppear {
12 | withAnimation(animation) {
13 | action()
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/WidgetViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | struct WidgetViewModel: Identifiable, Hashable {
10 | let id: String
11 | let order: Int32
12 | let rank: Int
13 | let symbol: String
14 | let name: String
15 | let priceUsd: Double
16 | let changePercent24Hr: Double
17 | let price: Double
18 | var chart: [Double]
19 | }
20 |
--------------------------------------------------------------------------------
/CoreData/Entities/Favourite+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | public extension Favourite {
11 | @nonobjc
12 | class func fetchRequest() -> NSFetchRequest {
13 | return NSFetchRequest(entityName: "Favourite")
14 | }
15 |
16 | @NSManaged var coinId: String
17 | @NSManaged var order: Int32
18 | }
19 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Extensions/NumberFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public extension NumberFormatter {
10 | static var amountFormatter: NumberFormatter = {
11 | let formatter = NumberFormatter()
12 |
13 | formatter.numberStyle = .decimal
14 | formatter.maximumFractionDigits = 10
15 |
16 | return formatter
17 | }()
18 | }
19 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Models/Market.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public struct Market: Decodable {
10 | public var exchangeId: String
11 | public var baseId: String?
12 | public var quoteId: String?
13 | public var baseSymbol: String?
14 | public var quoteSymbol: String?
15 | public var volumeUsd24Hr: String?
16 | public var priceUsd: String?
17 | public var volumePercent: String?
18 | }
19 |
--------------------------------------------------------------------------------
/VirtualCoinKit/VirtualCoinKit.h:
--------------------------------------------------------------------------------
1 | //
2 | // VirtualCoinKit.h
3 | // VirtualCoinKit
4 | //
5 | // Created by Marcin Czachurski on 18/04/2021.
6 | //
7 |
8 | #import
9 |
10 | //! Project version number for VirtualCoinKit.
11 | FOUNDATION_EXPORT double VirtualCoinKitVersionNumber;
12 |
13 | //! Project version string for VirtualCoinKit.
14 | FOUNDATION_EXPORT const unsigned char VirtualCoinKitVersionString[];
15 |
16 | // In this header, you should import all the public headers of your framework using statements like #import
17 |
18 |
19 |
--------------------------------------------------------------------------------
/VirtualCoin/Notifications/NotificationsError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public enum NotificationsError: Error {
10 | case notRecognizedCurrencySymbol
11 | }
12 |
13 | extension NotificationsError: LocalizedError {
14 | public var errorDescription: String? {
15 | switch self {
16 | case .notRecognizedCurrencySymbol:
17 | return NSLocalizedString("Not recognized currency symbol.", comment: "")
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/VirtualCoin/Notifications/PriceAlert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | enum Processing {
10 | case inQueue, processing, finished
11 | }
12 |
13 | class PriceAlert {
14 | var currency: String
15 | var coinId: String
16 |
17 | var price: Double?
18 | var processing: Processing
19 |
20 | init(currency: String, coinId: String) {
21 | self.currency = currency
22 | self.coinId = coinId
23 | self.processing = Processing.inQueue
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Models/Coin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public struct Coin: Decodable {
10 | public var id: String
11 | public var rank: String
12 | public var symbol: String
13 | public var name: String
14 | public var supply: String?
15 | public var maxSupply: String?
16 | public var marketCapUsd: String?
17 | public var volumeUsd24Hr: String?
18 | public var priceUsd: String?
19 | public var changePercent24Hr: String?
20 | public var vwap24Hr: String?
21 | }
22 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Extensions/CurrentValueLineType+ChartColors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import LightChart
9 |
10 | extension CurrentValueLineType {
11 | static var red: CurrentValueLineType {
12 | get {
13 | CurrentValueLineType.dash(color: .redPastel, lineWidth: 1, dash: [5])
14 | }
15 | }
16 |
17 | static var green: CurrentValueLineType {
18 | get {
19 | CurrentValueLineType.dash(color: .greenPastel, lineWidth: 1, dash: [5])
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/CoreData/Entities/ExchangeItem+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | public extension ExchangeItem {
11 | @nonobjc
12 | class func fetchRequest() -> NSFetchRequest {
13 | return NSFetchRequest(entityName: "ExchangeItem")
14 | }
15 |
16 | @NSManaged var amount: Double
17 | @NSManaged var coinId: String
18 | @NSManaged var coinSymbol: String
19 | @NSManaged var currency: String
20 | }
21 |
22 |
23 | extension ExchangeItem: Identifiable {
24 | }
25 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/LoadingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct LoadingView: View {
10 | var body: some View {
11 | VStack {
12 | Spacer()
13 | ProgressView()
14 | .progressViewStyle(CircularProgressViewStyle())
15 | Spacer()
16 | }
17 | }
18 | }
19 |
20 | struct LoadingView_Previews: PreviewProvider {
21 | static var previews: some View {
22 | LoadingView()
23 | .previewLayout(.fixed(width: 32, height: 32))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CoreData/Entities/Alert+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | public extension Alert {
11 | @nonobjc
12 | class func fetchRequest() -> NSFetchRequest {
13 | return NSFetchRequest(entityName: "Alert")
14 | }
15 |
16 | @NSManaged var alertSentDate: Date?
17 | @NSManaged var coinId: String
18 | @NSManaged var coinSymbol: String
19 | @NSManaged var currency: String
20 | @NSManaged var isEnabled: Bool
21 | @NSManaged var isPriceLower: Bool
22 | @NSManaged var price: Double
23 | }
24 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/VirtualCoinWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import WidgetKit
8 | import SwiftUI
9 |
10 | @main
11 | struct VirtualCoinWidget: Widget {
12 | let kind: String = "VirtualCoinWidget"
13 |
14 | var body: some WidgetConfiguration {
15 | StaticConfiguration(kind: kind, provider: Provider()) { entry in
16 | VirtualCoinWidgetEntryView(entry: entry)
17 | }
18 | .configurationDisplayName("vCoin")
19 | .description("Shows coin prices")
20 | .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/CoreData/PropertyWrappers/Setting.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import SwiftUI
9 |
10 | @propertyWrapper public struct Setting: DynamicProperty {
11 | private let keyPath: KeyPath
12 |
13 | public var wrappedValue: T {
14 | get {
15 | let settingsHandler = SettingsHandler()
16 | let settings = settingsHandler.getDefaultSettings()
17 |
18 | return settings[keyPath: keyPath]
19 | }
20 | }
21 |
22 | public init(_ keyPath: KeyPath) {
23 | self.keyPath = keyPath
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/AppViews/AppView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct AppView: View {
11 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass
12 |
13 | var body: some View {
14 | if horizontalSizeClass == .compact {
15 | TabsView()
16 | } else {
17 | NavigationView {
18 | SideBarsView()
19 | FavouritesView()
20 | }
21 | }
22 | }
23 | }
24 |
25 | struct AppView_Previews: PreviewProvider {
26 | static var previews: some View {
27 | AppView()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/VirtualCoinWidgetEntryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import WidgetKit
8 | import SwiftUI
9 |
10 | struct VirtualCoinWidgetEntryView : View {
11 | @Environment(\.widgetFamily) var family: WidgetFamily
12 |
13 | var entry: Provider.Entry
14 |
15 | @ViewBuilder
16 | var body: some View {
17 | switch family {
18 | case .systemSmall: SmallWidgetView(viewModels: entry.viewModels)
19 | case .systemMedium: MediumWidgetView(viewModels: entry.viewModels)
20 | case .systemLarge: LargeWidgetView(viewModels: entry.viewModels)
21 | default: Text("Define favourites")
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/VirtualCoin/ViewModels/ViewState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public enum ViewState: Equatable {
10 | case iddle
11 | case loading
12 | case loaded
13 | case error(Error)
14 |
15 | static public func ==(lhs: ViewState, rhs: ViewState) -> Bool {
16 | switch (lhs, rhs) {
17 | case (.error, .error):
18 | return true
19 | case (.loaded, .loaded):
20 | return true
21 | case (.loading,.loading):
22 | return true
23 | case (.iddle,.iddle):
24 | return true
25 | default:
26 | return false
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/VirtualCoinKitTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/VirtualCoinTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/VirtualCoinUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/VirtualCoin/Extensions/Color.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | public extension Color {
10 | static var main: Color {
11 | return Color("AccentColor")
12 | }
13 |
14 | static func main(opacity: Double) -> Color {
15 | return Color.main.opacity(opacity)
16 | }
17 |
18 | static var greenPastel: Color {
19 | return Color("GreenPastel")
20 | }
21 |
22 | static var redPastel: Color {
23 | return Color("RedPastel")
24 | }
25 |
26 | static var backgroundLabel: Color {
27 | return Color("BackgroundLabel")
28 | }
29 |
30 | static func backgroundLabel(opacity: Double) -> Color {
31 | return Color.backgroundLabel.opacity(opacity)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/GreenPastel.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "108",
9 | "green" : "169",
10 | "red" : "0"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "108",
27 | "green" : "169",
28 | "red" : "0"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/RedPastel.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "53",
9 | "green" : "54",
10 | "red" : "255"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "53",
27 | "green" : "54",
28 | "red" : "255"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x08",
9 | "green" : "0x65",
10 | "red" : "0xF0"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.031",
27 | "green" : "0.396",
28 | "red" : "0.941"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/BackgroundLabel.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/WidgetBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/VirtualCoin/ViewModels/AlertViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import VirtualCoinKit
9 |
10 | public class AlertViewModel: Identifiable, ObservableObject {
11 | @Published public var alert: Alert
12 | @Published public var coinViewModel: CoinViewModel
13 | @Published public var currency: Currency
14 |
15 | init(coinViewModel: CoinViewModel, alert: Alert, currency: Currency) {
16 | self.alert = alert
17 | self.coinViewModel = coinViewModel
18 | self.currency = currency
19 | }
20 |
21 | public func setCoinViewModel(_ coinViewModel: CoinViewModel) {
22 | self.coinViewModel = coinViewModel
23 | }
24 |
25 | public func setCurrency(_ currency: Currency) {
26 | self.currency = currency
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Models/Currency.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public struct Currency {
10 | public var id: String
11 | public var symbol: String
12 | public var locale: String
13 | public var name: String
14 |
15 | public init(id: String = "", symbol: String = "", locale: String = "", name: String = "") {
16 | self.id = id
17 | self.symbol = symbol
18 | self.locale = locale
19 | self.name = name
20 | }
21 | }
22 |
23 | extension Currency: Hashable {
24 | public static func == (lhs: Currency, rhs: Currency) -> Bool {
25 | return lhs.symbol == rhs.symbol
26 | }
27 |
28 | public func hash(into hasher: inout Hasher) {
29 | hasher.combine(self.symbol)
30 | }
31 | }
32 |
33 | extension Currency: Identifiable {
34 | }
35 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | vCoin
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSExtension
24 |
25 | NSExtensionPointIdentifier
26 | com.apple.widgetkit-extension
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/VirtualCoinTests/VirtualCoinTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import XCTest
8 | @testable import VirtualCoin
9 |
10 | class VirtualCoinTests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 | }
15 |
16 | override func tearDownWithError() throws {
17 | // Put teardown code here. This method is called after the invocation of each test method in the class.
18 | }
19 |
20 | func testExample() throws {
21 | // This is an example of a functional test case.
22 | // Use XCTAssert and related functions to verify your tests produce the correct results.
23 | }
24 |
25 | func testPerformanceExample() throws {
26 | // This is an example of a performance test case.
27 | self.measure {
28 | // Put the code you want to measure the time of here.
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/VirtualCoinKitTests/VirtualCoinKitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import XCTest
8 | @testable import VirtualCoinKit
9 |
10 | class VirtualCoinKitTests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 | }
15 |
16 | override func tearDownWithError() throws {
17 | // Put teardown code here. This method is called after the invocation of each test method in the class.
18 | }
19 |
20 | func testExample() throws {
21 | // This is an example of a functional test case.
22 | // Use XCTAssert and related functions to verify your tests produce the correct results.
23 | }
24 |
25 | func testPerformanceExample() throws {
26 | // This is an example of a performance test case.
27 | self.measure {
28 | // Put the code you want to measure the time of here.
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/InitialsPlaceholder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import SwiftUI
9 |
10 | struct InitialsPlaceholder: View {
11 | var text: String
12 |
13 | var body: some View {
14 | ZStack {
15 | Circle()
16 | .strokeBorder(Color.main,lineWidth: 2)
17 | .background(Circle().foregroundColor(Color.main(opacity: 0.4)))
18 | .frame(width: 32, height: 32)
19 | Text(String(text.first ?? "?") )
20 | .foregroundColor(.white)
21 | }
22 | }
23 | }
24 |
25 | struct InitialsPlaceholder_Previews: PreviewProvider {
26 | static var previews: some View {
27 | Group {
28 | InitialsPlaceholder(text: "Bitcoin")
29 | .preferredColorScheme(.dark)
30 | InitialsPlaceholder(text: "DegeCoin")
31 | .preferredColorScheme(.light)
32 | }
33 | .previewLayout(.fixed(width: 32, height: 32))
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/VirtualCoin.xcodeproj/xcuserdata/mczachurski.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | VirtualCoin.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | VirtualCoinKit.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 2
16 |
17 | VirtualCoinWidgetExtension.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 1
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 | F8916835262B38AD007335D7
26 |
27 | primary
28 |
29 |
30 | F891684B262B38AE007335D7
31 |
32 | primary
33 |
34 |
35 | F8916856262B38AE007335D7
36 |
37 | primary
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/CoreData/ExchangeItemsHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | class ExchangeItemsHandler {
11 | func createExchangeItemEntity() -> ExchangeItem {
12 | let context = CoreDataHandler.shared.container.viewContext
13 | return ExchangeItem(context: context)
14 | }
15 |
16 | func deleteExchangeItemEntity(exchangeItem: ExchangeItem) {
17 | let context = CoreDataHandler.shared.container.viewContext
18 | context.delete(exchangeItem)
19 | }
20 |
21 | func getExchangeItems() -> [ExchangeItem] {
22 | var exchangeItems: [ExchangeItem] = []
23 |
24 | let context = CoreDataHandler.shared.container.viewContext
25 | let fetchRequest = NSFetchRequest(entityName: "ExchangeItem")
26 | do {
27 | if let list = try context.fetch(fetchRequest) as? [ExchangeItem] {
28 | exchangeItems = list
29 | }
30 | } catch {
31 | print("Error during fetching ExchangeItem")
32 | }
33 |
34 | return exchangeItems
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/MarketsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct MarketsView: View {
10 | @Environment(\.presentationMode) var presentationMode
11 | public var markets: [MarketViewModel]
12 |
13 | var body: some View {
14 | NavigationView {
15 | List(markets) { market in
16 | MarketRowView(market: market)
17 | }
18 | .navigationBarTitle(Text("Markets"), displayMode: .inline)
19 | .navigationBarItems(trailing: Button(action: {
20 | presentationMode.wrappedValue.dismiss()
21 | }) {
22 | Text("Done").bold()
23 | })
24 | }
25 | .listStyle(PlainListStyle())
26 | }
27 | }
28 |
29 | struct MarketsView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | Group {
32 | MarketsView(markets: [PreviewData.getMarketViewModel()])
33 | .preferredColorScheme(.dark)
34 |
35 | MarketsView(markets: [PreviewData.getMarketViewModel()])
36 | .preferredColorScheme(.light)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/VirtualCoin/ViewModels/MarketViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import VirtualCoinKit
9 |
10 | public class MarketViewModel: Identifiable, ObservableObject {
11 | public let id: String
12 | public let baseSymbol: String
13 | public let quoteSymbol: String
14 | public let priceUsd: Double
15 | public let price: Double
16 |
17 | init(market: Market, rateUsd: Double) {
18 | self.id = market.exchangeId
19 | self.baseSymbol = market.baseSymbol ?? ""
20 | self.quoteSymbol = market.quoteSymbol ?? ""
21 |
22 | if let priceUsd = market.priceUsd, let price = Double(priceUsd) {
23 | self.priceUsd = price
24 | self.price = price / rateUsd
25 | } else {
26 | self.priceUsd = 0
27 | self.price = 0
28 | }
29 | }
30 |
31 | init(id: String, baseSymbol: String?, quoteSymbol: String?, priceUsd: Double, price: Double) {
32 | self.id = id
33 | self.baseSymbol = baseSymbol ?? ""
34 | self.quoteSymbol = quoteSymbol ?? ""
35 | self.priceUsd = priceUsd
36 | self.price = price
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Errors/RestClientError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public enum RestClientError: Error {
10 | case badUrl
11 | case serverError
12 | case emptyDataError
13 | case numberBadFormat
14 | case networkFailure(Error)
15 | case badDataFormat(Error)
16 | }
17 |
18 | extension RestClientError: LocalizedError {
19 | public var errorDescription: String? {
20 | switch self {
21 | case .badUrl:
22 | return NSLocalizedString("Bad URL to coincap.io API.", comment: "")
23 | case .serverError:
24 | return NSLocalizedString("Server returns unexpected error.", comment: "")
25 | case .emptyDataError:
26 | return NSLocalizedString("Server returns empty data result.", comment: "")
27 | case .numberBadFormat:
28 | return NSLocalizedString("Server returns number which cannot be deserialized.", comment: "")
29 | case .networkFailure(let error):
30 | return NSLocalizedString(error.localizedDescription, comment: "")
31 | case .badDataFormat(let error):
32 | return NSLocalizedString(error.localizedDescription, comment: "")
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ListRowsViews/MarketRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct MarketRowView: View {
10 | @StateObject var market: MarketViewModel
11 | @Setting(\.currency) private var currencySymbol: String
12 |
13 | var body: some View {
14 | HStack {
15 | VStack(alignment: .leading) {
16 | Text(market.id)
17 | .font(.headline)
18 | Text("\(market.baseSymbol) / \(market.quoteSymbol)")
19 | .font(.footnote)
20 | .foregroundColor(.accentColor)
21 | }
22 |
23 | Spacer()
24 |
25 | Text(market.price.toFormattedPrice(currency: currencySymbol))
26 | .font(.footnote)
27 | }
28 | }
29 | }
30 |
31 | struct MarketRowView_Previews: PreviewProvider {
32 | static var previews: some View {
33 | Group {
34 | MarketRowView(market: PreviewData.getMarketViewModel())
35 | .preferredColorScheme(.dark)
36 |
37 | MarketRowView(market: PreviewData.getMarketViewModel())
38 | .preferredColorScheme(.light)
39 | }
40 | .previewLayout(.fixed(width: 360, height: 70))
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/CoreData/SettingsHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 | import UIKit
10 |
11 | class SettingsHandler {
12 | func getDefaultSettings() -> Settings {
13 | var settingsList: [Settings] = []
14 |
15 | let context = CoreDataHandler.shared.container.viewContext
16 | let fetchRequest = NSFetchRequest(entityName: "Settings")
17 | do {
18 | if let list = try context.fetch(fetchRequest) as? [Settings] {
19 | settingsList = list
20 | }
21 | } catch {
22 | print("Error during fetching favourites")
23 | }
24 |
25 | if let settings = settingsList.first {
26 | if settings.currency.count == 0 {
27 | settings.currency = "USD"
28 | }
29 |
30 | return settings
31 | } else {
32 | let settings = self.createSettingsEntity()
33 | settings.currency = "USD"
34 | CoreDataHandler.shared.save()
35 |
36 | return settings
37 | }
38 | }
39 |
40 | private func createSettingsEntity() -> Settings {
41 | let context = CoreDataHandler.shared.container.viewContext
42 | return Settings(context: context)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/ViewControllerResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | final class ViewControllerResolver: UIViewControllerRepresentable {
10 | let onResolve: (UIViewController) -> Void
11 |
12 | init(onResolve: @escaping (UIViewController) -> Void) {
13 | self.onResolve = onResolve
14 | }
15 |
16 | func makeUIViewController(context: Context) -> ParentResolverViewController {
17 | ParentResolverViewController(onResolve: onResolve)
18 | }
19 |
20 | func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
21 | }
22 |
23 | class ParentResolverViewController: UIViewController {
24 | let onResolve: (UIViewController) -> Void
25 |
26 | init(onResolve: @escaping (UIViewController) -> Void) {
27 | self.onResolve = onResolve
28 | super.init(nibName: nil, bundle: nil)
29 | }
30 |
31 | required init?(coder: NSCoder) {
32 | fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
33 | }
34 |
35 | override func didMove(toParent parent: UIViewController?) {
36 | super.didMove(toParent: parent)
37 |
38 | if let parent = parent {
39 | onResolve(parent)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Extensions/ChartVisualType+ChartColors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import LightChart
9 |
10 | extension ChartVisualType {
11 | static var red: ChartVisualType {
12 | get {
13 | ChartVisualType.customFilled(color: .redPastel,
14 | lineWidth: 2,
15 | fillGradient: LinearGradient(gradient: .init(colors: [.redPastel.opacity(0.3), .redPastel.opacity(0.01)]),
16 | startPoint: .init(x: 0.5, y: 1),
17 | endPoint: .init(x: 0.5, y: 0)))
18 | }
19 | }
20 |
21 | static var green: ChartVisualType {
22 | get {
23 | ChartVisualType.customFilled(color: .greenPastel,
24 | lineWidth: 2,
25 | fillGradient: LinearGradient(gradient: .init(colors: [.greenPastel.opacity(0.3), .greenPastel.opacity(0.01)]),
26 | startPoint: .init(x: 0.5, y: 1),
27 | endPoint: .init(x: 0.5, y: 0)))
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/ErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ErrorView: View {
10 | public var error: Error
11 | public var refreshAction: (() -> Void)?
12 |
13 | var body: some View {
14 | VStack {
15 | Image(systemName: "exclamationmark.triangle.fill")
16 | .resizable()
17 | .foregroundColor(.accentColor)
18 | .frame(width: 64, height: 64, alignment: .center)
19 |
20 | Text("\(error.localizedDescription)")
21 | .multilineTextAlignment(.center)
22 | if let action = refreshAction {
23 | Button("Refresh", action: action)
24 | .padding(.top, 10)
25 | }
26 | }
27 | }
28 | }
29 |
30 | struct ErrorView_Previews: PreviewProvider {
31 |
32 | enum PreviewError: Error, LocalizedError {
33 | case unknown
34 |
35 | public var errorDescription: String? {
36 | switch self {
37 | case .unknown:
38 | return NSLocalizedString("Bad URL to coincap.io API.", comment: "")
39 | }
40 | }
41 | }
42 |
43 | static var previews: some View {
44 | ErrorView(error: PreviewError.unknown) {
45 | }
46 | .previewLayout(.fixed(width: 360, height: 200))
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/SearchBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | class SearchBar: NSObject, ObservableObject {
10 | @Published var text: String = ""
11 | let searchController: UISearchController = UISearchController(searchResultsController: nil)
12 |
13 | override init() {
14 | super.init()
15 | self.searchController.obscuresBackgroundDuringPresentation = false
16 | self.searchController.searchResultsUpdater = self
17 | }
18 | }
19 |
20 | extension SearchBar: UISearchResultsUpdating {
21 | func updateSearchResults(for searchController: UISearchController) {
22 |
23 | // Publish search bar text changes.
24 | if let searchBarText = searchController.searchBar.text {
25 | self.text = searchBarText
26 | }
27 | }
28 | }
29 |
30 | struct SearchBarModifier: ViewModifier {
31 | let searchBar: SearchBar
32 |
33 | func body(content: Content) -> some View {
34 | content
35 | .overlay(
36 | ViewControllerResolver { viewController in
37 | viewController.navigationItem.searchController = self.searchBar.searchController
38 | }
39 | .frame(width: 0, height: 0)
40 | )
41 | }
42 | }
43 |
44 | extension View {
45 | func add(_ searchBar: SearchBar) -> some View {
46 | return self.modifier(SearchBarModifier(searchBar: searchBar))
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Provider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import WidgetKit
8 | import SwiftUI
9 | import Intents
10 |
11 | struct Provider: TimelineProvider {
12 | typealias Entry = WidgetEntry
13 |
14 | func placeholder(in context: Context) -> WidgetEntry {
15 | return WidgetEntry(date: Date(), viewModels: PreviewData.getWidgetViewModels())
16 | }
17 |
18 | func getSnapshot(in context: Context, completion: @escaping (WidgetEntry) -> ()) {
19 | let entry = WidgetEntry(date: Date(), viewModels: PreviewData.getWidgetViewModels())
20 | completion(entry)
21 | }
22 |
23 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
24 |
25 | let dataFetcher = DataFetcher()
26 | dataFetcher.getCoins { widgetViewModelsResult in
27 | var entries: [WidgetEntry] = []
28 | let currentDate = Date()
29 |
30 | switch widgetViewModelsResult {
31 | case .success(let widgetViewModels):
32 | entries.append(WidgetEntry(date: currentDate, viewModels: widgetViewModels))
33 | break
34 | case .failure:
35 | break
36 | }
37 |
38 | let nextUpdateDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
39 | let timeline = Timeline(entries: entries, policy: .after(nextUpdateDate))
40 | completion(timeline)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/VirtualCoinUITests/VirtualCoinUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import XCTest
8 |
9 | class VirtualCoinUITests: XCTestCase {
10 |
11 | override func setUpWithError() throws {
12 | // Put setup code here. This method is called before the invocation of each test method in the class.
13 |
14 | // In UI tests it is usually best to stop immediately when a failure occurs.
15 | continueAfterFailure = false
16 |
17 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
18 | }
19 |
20 | override func tearDownWithError() throws {
21 | // Put teardown code here. This method is called after the invocation of each test method in the class.
22 | }
23 |
24 | func testExample() throws {
25 | // UI tests must launch the application that they test.
26 | let app = XCUIApplication()
27 | app.launch()
28 |
29 | // Use recording to get started writing UI tests.
30 | // Use XCTAssert and related functions to verify your tests produce the correct results.
31 | }
32 |
33 | func testLaunchPerformance() throws {
34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
35 | // This measures how long it takes to launch your application.
36 | measure(metrics: [XCTApplicationLaunchMetric()]) {
37 | XCUIApplication().launch()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/VirtualCoin/Services/ApplicationStateService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public class ApplicationStateService: ObservableObject {
10 | public static let shared = ApplicationStateService()
11 |
12 | @Published public var coins: [CoinViewModel] = []
13 | @Published public var markets: [MarketViewModel] = []
14 | @Published public var favourites: [CoinViewModel] = []
15 | @Published public var currencyRateUsd: Double = 1.0
16 |
17 | public var selectedExchangeViewModel: ExchangeViewModel?
18 | public var selectedAlertViewModel: AlertViewModel?
19 | }
20 |
21 | extension ApplicationStateService {
22 | public func removeFromFavourites(coinViewModel: CoinViewModel) {
23 | self.favourites = self.favourites.filter { $0 !== coinViewModel }
24 | }
25 |
26 | public func addToFavourites(coinViewModel: CoinViewModel) {
27 | self.favourites.append(coinViewModel)
28 |
29 | self.favourites = self.favourites.sorted(by: { lhs, rhs in
30 | lhs.rank < rhs.rank
31 | })
32 | }
33 | }
34 |
35 | extension ApplicationStateService {
36 | public static var preview: ApplicationStateService = {
37 | let applicationStateService = ApplicationStateService()
38 |
39 | applicationStateService.coins = PreviewData.getCoinsViewModel()
40 | applicationStateService.favourites = PreviewData.getCoinsViewModel()
41 | applicationStateService.markets = PreviewData.getMarketsViewModel()
42 |
43 | return applicationStateService
44 | }()
45 | }
46 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/AppViews/TabsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct TabsView: View {
11 |
12 | var body: some View {
13 | TabView {
14 |
15 | // Favourites view.
16 | NavigationView {
17 | FavouritesView()
18 | }
19 | .tabItem {
20 | Image(systemName: "star.fill")
21 | Text("Favourites")
22 | }
23 |
24 | // All currencies view.
25 | NavigationView {
26 | CoinsView()
27 | }
28 | .tabItem {
29 | Image(systemName: "bitcoinsign.circle.fill")
30 | Text("All currencies")
31 | }
32 |
33 | // Exchanges view.
34 | NavigationView {
35 | ExchangesView()
36 | }
37 | .tabItem {
38 | Image(systemName: "arrow.triangle.2.circlepath.circle.fill")
39 | Text("Exchanges")
40 | }
41 |
42 | // Alerts view.
43 | NavigationView {
44 | AlertsView()
45 | }
46 | .tabItem {
47 | Image(systemName: "bell.fill")
48 | Text("Alerts")
49 | }
50 | }
51 | }
52 | }
53 |
54 | struct TabsView_Previews: PreviewProvider {
55 | static var previews: some View {
56 | TabsView()
57 | .environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/VirtualCoinKit/Extensions/Double.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public extension Double {
10 | func toFormattedAmount() -> String {
11 | return NumberFormatter.amountFormatter.string(from: NSNumber(value: self)) ?? ""
12 | }
13 |
14 | func toFormattedPrice(currency: String) -> String {
15 | return self.toFormattedPrice(currency: currency, maximumFractionDigits: 4)
16 | }
17 |
18 | func toFormattedPrice(currency: String, maximumFractionDigits: Int) -> String {
19 | let currencyFormatter = NumberFormatter()
20 | currencyFormatter.usesGroupingSeparator = true
21 | currencyFormatter.maximumFractionDigits = maximumFractionDigits
22 | currencyFormatter.numberStyle = NumberFormatter.Style.currency
23 |
24 | var locale = "en-US"
25 | if let currency = Currencies.allCurrenciesDictionary[currency] {
26 | locale = currency.locale
27 | }
28 |
29 | currencyFormatter.locale = Locale(identifier: locale)
30 | let priceString = currencyFormatter.string(from: NSNumber(value: self))
31 | return priceString ?? ""
32 | }
33 |
34 | func toFormattedPercent() -> String {
35 | return String(self.rounded(toPlaces: 2)) + "%"
36 | }
37 |
38 | var absoluteValue: Double {
39 | if self > 0.0 {
40 | return self
41 | } else {
42 | return -1 * self
43 | }
44 | }
45 |
46 | func rounded(toPlaces places: Int) -> Double {
47 | let divisor = pow(10.0, Double(places))
48 | return (self * divisor).rounded() / divisor
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ListRowsViews/CoinRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct CoinRowView: View {
11 | @ObservedObject var coin: CoinViewModel
12 | @Setting(\.currency) private var currencySymbol: String
13 |
14 | var body: some View {
15 |
16 | HStack {
17 | CoinImageView(coin: coin)
18 |
19 | VStack(alignment: .leading) {
20 | Text(coin.name)
21 | .font(.headline)
22 | Text(coin.symbol)
23 | .font(.subheadline)
24 | .foregroundColor(.accentColor)
25 | }
26 |
27 | Spacer()
28 |
29 | VStack(alignment: .trailing) {
30 | Text(coin.price.toFormattedPrice(currency: currencySymbol))
31 | .font(.subheadline)
32 | .foregroundColor(coin.changePercent24Hr > 0 ?.greenPastel : .redPastel)
33 |
34 | Text(coin.changePercent24Hr.toFormattedPercent())
35 | .font(.caption)
36 | .foregroundColor(.gray)
37 | }
38 | }
39 | }
40 | }
41 |
42 | struct CoinRowView_Previews: PreviewProvider {
43 | static var previews: some View {
44 | Group {
45 | CoinRowView(coin: PreviewData.getCoinViewModel())
46 | .preferredColorScheme(.dark)
47 |
48 | CoinRowView(coin: PreviewData.getCoinViewModel())
49 | .preferredColorScheme(.light)
50 | }
51 | .previewLayout(.fixed(width: 360, height: 70))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/CoreData/FavouritesHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | class FavouritesHandler {
11 | func createFavouriteEntity() -> Favourite {
12 | let context = CoreDataHandler.shared.container.viewContext
13 | return Favourite(context: context)
14 | }
15 |
16 | func deleteFavouriteEntity(coinId: String) {
17 | let favourites = self.getFavourites()
18 | let filtered = favourites.filter { favourite -> Bool in
19 | return favourite.coinId == coinId
20 | }
21 |
22 | let context = CoreDataHandler.shared.container.viewContext
23 | for favourite in filtered {
24 | context.delete(favourite)
25 | }
26 | }
27 |
28 | func isFavourite(coinId: String) -> Bool {
29 | let favourites = self.getFavourites()
30 |
31 | let exists = favourites.contains { favourite -> Bool in
32 | return favourite.coinId == coinId
33 | }
34 |
35 | return exists
36 | }
37 |
38 | func getFavourites() -> [Favourite] {
39 | var favourites: [Favourite] = []
40 |
41 | let context = CoreDataHandler.shared.container.viewContext
42 | let fetchRequest = NSFetchRequest(entityName: "Favourite")
43 | let sortDescriptor = NSSortDescriptor(key: "order", ascending: true)
44 | fetchRequest.sortDescriptors = [ sortDescriptor ]
45 |
46 | do {
47 | if let list = try context.fetch(fetchRequest) as? [Favourite] {
48 | favourites = list
49 | }
50 | } catch {
51 | print("Error during fetching favourites")
52 | }
53 |
54 | return favourites
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/NoDataView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 |
8 | import SwiftUI
9 |
10 | struct NoDataView: View {
11 | var title: String
12 | var subtitle: String
13 | var action: (() -> Void)?
14 |
15 | @State var opacity = 0.0
16 |
17 | public init(title: String, subtitle: String) {
18 | self.title = title
19 | self.subtitle = subtitle
20 | }
21 |
22 | public init(title: String, subtitle: String, action: @escaping () -> Void) {
23 | self.title = title
24 | self.subtitle = subtitle
25 | self.action = action
26 | }
27 |
28 | var body: some View {
29 | VStack {
30 | Text(title)
31 | .font(.title)
32 | .foregroundColor(.gray)
33 |
34 | if let action = self.action {
35 | Button(action: action, label: {
36 | Text(subtitle)
37 | .foregroundColor(Color.white)
38 | .padding()
39 | })
40 | .frame(height: 40)
41 | .background(
42 | RoundedRectangle(cornerRadius: 20, style: .continuous)
43 | .fill(Color.main)
44 | )
45 | } else {
46 | Text(subtitle)
47 | .foregroundColor(.gray)
48 | }
49 | }
50 | .opacity(self.opacity)
51 | .animate {
52 | self.opacity = 1
53 | }
54 | }
55 | }
56 |
57 | struct NoDataView_Previews: PreviewProvider {
58 | static var previews: some View {
59 | NoDataView(title: "No data", subtitle: "Please add new favourite") {
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/CoinImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import URLImage
9 |
10 | struct CoinImageView: View {
11 | var coin: CoinViewModel
12 |
13 | var body: some View {
14 | if let imageUrl = URL(string: coin.imageUrl) {
15 | URLImage(imageUrl,
16 | empty: {
17 | InitialsPlaceholder(text: coin.name)
18 | .frame(width: 32, height: 32, alignment: .center)
19 | },
20 | inProgress: { progress in
21 | InitialsPlaceholder(text: coin.name)
22 | .frame(width: 32, height: 32, alignment: .center)
23 | },
24 | failure: { error, retry in
25 | InitialsPlaceholder(text: coin.name)
26 | .frame(width: 32, height: 32, alignment: .center)
27 | },
28 | content: { image in
29 | image
30 | .resizable()
31 | .frame(width: 32, height: 32, alignment: .center)
32 | }
33 | )
34 | } else {
35 | InitialsPlaceholder(text: coin.name)
36 | .frame(width: 32, height: 32, alignment: .center)
37 | }
38 | }
39 | }
40 |
41 | struct CoinImageView_Previews: PreviewProvider {
42 | static var previews: some View {
43 | Group {
44 | CoinImageView(coin: PreviewData.getCoinViewModel())
45 | .preferredColorScheme(.dark)
46 |
47 | CoinImageView(coin: PreviewData.getCoinViewModel())
48 | .preferredColorScheme(.light)
49 | }
50 | .previewLayout(.fixed(width: 32, height: 32))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Base.lproj/VirtualCoinWidget.intentdefinition:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | INEnums
6 |
7 | INIntentDefinitionModelVersion
8 | 1.2
9 | INIntentDefinitionNamespace
10 | 88xZPY
11 | INIntentDefinitionSystemVersion
12 | 21A5284e
13 | INIntentDefinitionToolsBuildVersion
14 | 13A5192j
15 | INIntentDefinitionToolsVersion
16 | 13.0
17 | INIntents
18 |
19 |
20 | INIntentCategory
21 | information
22 | INIntentDescription
23 | Configuration for vCoin app
24 | INIntentDescriptionID
25 | tVvJ9c
26 | INIntentEligibleForWidgets
27 |
28 | INIntentIneligibleForSuggestions
29 |
30 | INIntentName
31 | Configuration
32 | INIntentResponse
33 |
34 | INIntentResponseCodes
35 |
36 |
37 | INIntentResponseCodeName
38 | success
39 | INIntentResponseCodeSuccess
40 |
41 |
42 |
43 | INIntentResponseCodeName
44 | failure
45 |
46 |
47 |
48 | INIntentTitle
49 | Configuration
50 | INIntentTitleID
51 | gpCwrM
52 | INIntentType
53 | Custom
54 | INIntentVerb
55 | View
56 |
57 |
58 | INTypes
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/VirtualCoin/ViewModels/ExchangeViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import VirtualCoinKit
9 |
10 | public class ExchangeViewModel: Identifiable, ObservableObject {
11 | @Published public var priceUsd = 0.0
12 | @Published public var price = 0.0
13 | @Published public var exchangeItem: ExchangeItem
14 | @Published public var coinViewModel: CoinViewModel
15 | @Published public var currency: Currency
16 |
17 | init(coinViewModel: CoinViewModel, exchangeItem: ExchangeItem, currency: Currency) {
18 | self.exchangeItem = exchangeItem
19 | self.coinViewModel = coinViewModel
20 | self.currency = currency
21 |
22 | self.recalculatePrice()
23 | }
24 |
25 | public func setCoinViewModel(_ coinViewModel: CoinViewModel) {
26 | self.coinViewModel = coinViewModel
27 | self.recalculatePrice()
28 | }
29 |
30 | public func setCurrency(_ currency: Currency) {
31 | self.currency = currency
32 | self.recalculatePrice()
33 | }
34 |
35 | private func recalculatePrice() {
36 | self.priceUsd = self.coinViewModel.priceUsd * self.exchangeItem.amount
37 |
38 | let coinCapClient = CoinCapClient()
39 | coinCapClient.getCurrencyRate(for: self.currency.id) { result in
40 | switch result {
41 | case .success(let currencyRate):
42 | let currencyRateUsd = Double(currencyRate.rateUsd) ?? 1.0
43 |
44 | DispatchQueue.runOnMain {
45 | self.price = self.priceUsd / currencyRateUsd
46 | }
47 | break
48 | case .failure(let error):
49 | // TODO: Show something in UI.
50 | print(error)
51 | break
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/ThirdPartyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ThirdPartyView: View {
10 | var body: some View {
11 | List {
12 | VStack(alignment: .leading) {
13 | Link("https://coincap.io",
14 | destination: URL(string: "https://coincap.io")!)
15 | Spacer()
16 | Text("API for cryptocurrency pricing.")
17 | .font(.footnote)
18 | }
19 |
20 | VStack(alignment: .leading) {
21 | Link("https://github.com/pichukov/LightChart",
22 | destination: URL(string: "https://github.com/pichukov/LightChart")!)
23 | Spacer()
24 | Text("Lightweight SwiftUI package with line charts implementation.")
25 | .font(.footnote)
26 | }
27 |
28 | VStack(alignment: .leading) {
29 | Link("https://github.com/dmytro-anokhin/url-image",
30 | destination: URL(string: "https://github.com/dmytro-anokhin/url-image")!)
31 | Spacer()
32 | Text("SwiftUI view that displays an image downloaded from provided URL. ")
33 | .font(.footnote)
34 | }
35 | }
36 | .navigationBarTitle(Text("Third party"), displayMode: .inline)
37 | }
38 | }
39 |
40 | struct ThirdPartyView_Previews: PreviewProvider {
41 | static var previews: some View {
42 | Group {
43 | NavigationView {
44 | ThirdPartyView()
45 | }
46 | .preferredColorScheme(.dark)
47 |
48 | NavigationView {
49 | ThirdPartyView()
50 | }
51 | .preferredColorScheme(.light)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/AppViews/SideBarsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct SideBarsView: View {
11 | @State private var selectedItem: SideBarNavigationItem? = .favourites
12 |
13 | var body: some View {
14 | List {
15 |
16 | // Favourites view.
17 | NavigationLink(
18 | destination:FavouritesView(),
19 | tag: SideBarNavigationItem.favourites,
20 | selection: $selectedItem
21 | ) {
22 | Label("Favourites", systemImage: "star.fill")
23 | }
24 |
25 | // All currencies view.
26 | NavigationLink(
27 | destination: CoinsView(),
28 | tag: SideBarNavigationItem.currencies,
29 | selection: $selectedItem
30 | ) {
31 | Label("All currencies", systemImage: "bitcoinsign.circle.fill")
32 | }
33 |
34 | // Exchanges view.
35 | NavigationLink(
36 | destination: ExchangesView(),
37 | tag: SideBarNavigationItem.exchanges,
38 | selection: $selectedItem
39 | ) {
40 | Label("Exchanges", systemImage: "arrow.triangle.2.circlepath.circle.fill")
41 | }
42 |
43 | // Alerts view.
44 | NavigationLink(
45 | destination: AlertsView(),
46 | tag: SideBarNavigationItem.alerts,
47 | selection: $selectedItem
48 | ) {
49 | Label("Alerts", systemImage: "bell.fill")
50 | }
51 |
52 | }
53 | .listStyle(SidebarListStyle())
54 | .navigationTitle("vCoin")
55 | }
56 | }
57 |
58 | struct SideBarsView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | SideBarsView()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vCoin (Virtual Coin)
2 |
3 | **vCoin** is lightweight application for keeping track of cryptocurrency prices.
4 |
5 | Main features:
6 | - more then 1000 cryptocurrencies
7 | - 5 dynamic charts (hour, day, week, month, year)
8 | - prices displayed in more then 100 currencies (USD, EUR, GBP, CHF, etc.)
9 | - minimal design
10 | - dark/white themes
11 | - widget presenting current prices (taking into account user favorite currencies)
12 |
13 |
14 |
15 | You can downlad app from AppStore.
16 |
17 | **vCoin** uses [https://coincap.io](https://coincap.io) API. All endpoints are described here: [https://docs.coincap.io/](https://docs.coincap.io/).
18 |
19 | ## Screenshots for App Store
20 |
21 | After running simulator run one of the following command to change status bar icons.
22 |
23 | **iPhone 6.5" Display**
24 |
25 | ```bash
26 | xcrun simctl status_bar "iPhone 11 Pro Max" override --time 9:41 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100
27 | ```
28 |
29 | **iPhone 5.5" Display**
30 |
31 | ```bash
32 | xcrun simctl status_bar "iPhone 8 Plus" override --time 9:41 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100
33 | ```
34 |
35 | **iPad Pro (3rd Gen) 12.9" Display**
36 |
37 | ```bash
38 | xcrun simctl status_bar "iPad Pro (12.9-inch) (5th generation)" override --time 9:41 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100
39 | ```
40 |
41 | ## Contributing
42 | You can fork and clone repository. Execute 'carthage update' and build. Do your changes and pull a request. Enjoy!
43 |
--------------------------------------------------------------------------------
/VirtualCoin/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BGTaskSchedulerPermittedIdentifiers
6 |
7 | dev.mczachurski.vcoin.fetch
8 |
9 | CFBundleDevelopmentRegion
10 | $(DEVELOPMENT_LANGUAGE)
11 | CFBundleDisplayName
12 | vCoin
13 | CFBundleExecutable
14 | $(EXECUTABLE_NAME)
15 | CFBundleIdentifier
16 | $(PRODUCT_BUNDLE_IDENTIFIER)
17 | CFBundleInfoDictionaryVersion
18 | 6.0
19 | CFBundleName
20 | $(PRODUCT_NAME)
21 | CFBundlePackageType
22 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
23 | CFBundleShortVersionString
24 | $(MARKETING_VERSION)
25 | CFBundleVersion
26 | $(CURRENT_PROJECT_VERSION)
27 | LSRequiresIPhoneOS
28 |
29 | NSUserActivityTypes
30 |
31 | ConfigurationIntent
32 |
33 | UIApplicationSceneManifest
34 |
35 | UIApplicationSupportsMultipleScenes
36 |
37 |
38 | UIApplicationSupportsIndirectInputEvents
39 |
40 | UIBackgroundModes
41 |
42 | fetch
43 |
44 | UILaunchScreen
45 |
46 | UIRequiredDeviceCapabilities
47 |
48 | armv7
49 |
50 | UISupportedInterfaceOrientations
51 |
52 | UIInterfaceOrientationPortrait
53 | UIInterfaceOrientationLandscapeLeft
54 | UIInterfaceOrientationLandscapeRight
55 |
56 | UISupportedInterfaceOrientations~ipad
57 |
58 | UIInterfaceOrientationPortrait
59 | UIInterfaceOrientationPortraitUpsideDown
60 | UIInterfaceOrientationLandscapeLeft
61 | UIInterfaceOrientationLandscapeRight
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/VirtualCoin/vcoin.xcdatamodeld/vcoin-20210604.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Views/SmallWidgetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import WidgetKit
8 | import SwiftUI
9 | import VirtualCoinKit
10 |
11 | struct SmallWidgetView: View {
12 | var viewModels: [WidgetViewModel]
13 |
14 | @Setting(\.currency) private var currencySymbol: String
15 |
16 | var body: some View {
17 | VStack {
18 | ForEach(viewModels.prefix(3).indices, id: \.self) { index in
19 | VStack {
20 | HStack {
21 | VStack(alignment: .leading) {
22 | Text("\(viewModels[index].name)")
23 | .font(.caption2)
24 | Text("\(viewModels[index].symbol)")
25 | .foregroundColor(Color.gray)
26 | .font(.caption2)
27 | }
28 |
29 | Spacer()
30 |
31 | VStack(alignment: .trailing){
32 | Text("\(viewModels[index].price.toFormattedPrice(currency: currencySymbol))")
33 | .font(.caption2)
34 | Text("\(viewModels[index].changePercent24Hr.toFormattedPercent())")
35 | .foregroundColor(viewModels[index].changePercent24Hr > 0 ?.greenPastel : .redPastel)
36 | .font(.caption2)
37 | }
38 | }
39 |
40 | if index + 1 < (viewModels.capacity > 3 ? 3 : viewModels.capacity) {
41 | Divider()
42 | }
43 | }
44 | }
45 | }
46 | .padding()
47 | }
48 | }
49 |
50 | struct SmallWidgetView_Previews: PreviewProvider {
51 | static var previews: some View {
52 | Group {
53 | VirtualCoinWidgetEntryView(entry: WidgetEntry(date: Date(), viewModels: PreviewData.getWidgetViewModels()))
54 | .previewContext(WidgetPreviewContext(family: .systemSmall))
55 | .environment(\.colorScheme, .light)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/AddAlertView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct AddAlertView: View {
11 | @Environment(\.presentationMode) var presentationMode
12 |
13 | @State private var price: NSNumber?
14 | @State private var isPriceLower: Bool = true
15 | @State private var selectedCurrency = Currencies.getDefaultCurrency()
16 | @State private var selectedCoin = CoinViewModel(symbol: "BTC")
17 |
18 | var body: some View {
19 | NavigationView {
20 | AlertDetailView(price: $price, isPriceLower: $isPriceLower, currency: $selectedCurrency, coin: $selectedCoin)
21 | .navigationBarTitle(Text("Exchange"), displayMode: .inline)
22 | .navigationBarItems(
23 | leading: Button(action: {
24 | presentationMode.wrappedValue.dismiss()
25 | }) {
26 | Text("Cancel").bold()
27 | },
28 | trailing: Button(action: {
29 | self.saveSettings()
30 | presentationMode.wrappedValue.dismiss()
31 | }) {
32 | Text("Done").bold()
33 | })
34 | }
35 | }
36 |
37 | private func saveSettings() {
38 | let alertsHandler = AlertsHandler()
39 | let alert = alertsHandler.createAlertEntity()
40 |
41 | alert.currency = self.selectedCurrency.symbol
42 | alert.coinId = self.selectedCoin.id
43 | alert.coinSymbol = self.selectedCoin.symbol
44 | alert.price = self.price?.doubleValue ?? 0
45 | alert.isEnabled = true
46 | alert.isPriceLower = self.isPriceLower
47 |
48 | CoreDataHandler.shared.save()
49 | }
50 | }
51 |
52 | struct AddAlertView_Previews: PreviewProvider {
53 | static var previews: some View {
54 | Group {
55 | AddAlertView()
56 | .environmentObject(ApplicationStateService.preview)
57 | .preferredColorScheme(.dark)
58 |
59 | AddAlertView()
60 | .environmentObject(ApplicationStateService.preview)
61 | .preferredColorScheme(.light)
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | included: # paths to include during linting. `--path` is ignored if present.
2 | - CoreData
3 | - VirtualCoin
4 | - VirtualCoinKit
5 | analyzer_rules:
6 | - unused_import
7 | - unused_private_declaration
8 | opt_in_rules:
9 | - anyobject_protocol
10 | - array_init
11 | - attributes
12 | - class_delegate_protocol
13 | - closure_end_indentation
14 | - closure_body_length
15 | - closure_spacing
16 | - collection_alignment
17 | - contains_over_first_not_nil
18 | - empty_count
19 | - empty_string
20 | - empty_xctest_method
21 | - explicit_init
22 | - extension_access_modifier
23 | - fallthrough
24 | - fatal_error_message
25 | - file_name
26 | - first_where
27 | - force_unwrapping
28 | - identical_operands
29 | - joined_default_parameter
30 | - let_var_whitespace
31 | - last_where
32 | - literal_expression_end_indentation
33 | - lower_acl_than_parent
34 | - mark
35 | - modifier_order
36 | - multiline_arguments
37 | - multiline_literal_brackets
38 | - multiline_parameters
39 | - nimble_operator
40 | - number_separator
41 | - operator_usage_whitespace
42 | - overridden_super_call
43 | - override_in_extension
44 | - pattern_matching_keywords
45 | - private_action
46 | - private_outlet
47 | - prohibited_super_call
48 | - quick_discouraged_call
49 | - quick_discouraged_focused_test
50 | - quick_discouraged_pending_test
51 | - redundant_nil_coalescing
52 | - redundant_type_annotation
53 | - single_test_class
54 | - sorted_first_last
55 | - sorted_imports
56 | - static_operator
57 | - unavailable_function
58 | - unneeded_parentheses_in_closure_argument
59 | - vertical_parameter_alignment_on_call
60 | - vertical_whitespace_closing_braces
61 | - vertical_whitespace_opening_braces
62 | - xct_specific_matcher
63 | - yoda_condition
64 | line_length: 180
65 | file_header:
66 | required_pattern: |
67 | \/\/
68 | \/\/ .*?\.swift
69 | \/\/ VCoin
70 | \/\/
71 | \/\/ Created by .*? on \d{1,2}\/\d{1,2}\/\d{2}\.
72 | \/\/ Copyright © \d{4} Marcin Czachurski\. All rights reserved\.
73 | \/\/
74 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji)
75 | identifier_name:
76 | excluded: # excluded via string array
77 | - id
78 | - Id
79 | - x
80 | - y
81 |
--------------------------------------------------------------------------------
/VirtualCoin/vcoin.xcdatamodeld/vcoin-20210606.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/AddExchangeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 | import NumericText
10 |
11 | struct AddExchangeView: View {
12 | @Environment(\.presentationMode) var presentationMode
13 |
14 | @State private var amount: NSNumber?
15 | @State private var selectedCurrency = Currencies.getDefaultCurrency()
16 | @State private var selectedCoin = CoinViewModel(symbol: "BTC")
17 |
18 | var body: some View {
19 | NavigationView {
20 | ExchangeDetailView(amount: self.$amount, currency: self.$selectedCurrency, coin: self.$selectedCoin)
21 | .navigationBarTitle(Text("Exchange"), displayMode: .inline)
22 | .navigationBarItems(
23 | leading: Button(action: {
24 | presentationMode.wrappedValue.dismiss()
25 | }) {
26 | Text("Cancel").bold()
27 | },
28 | trailing: Button(action: {
29 | self.saveSettings()
30 | presentationMode.wrappedValue.dismiss()
31 | }) {
32 | Text("Done").bold()
33 | })
34 | }
35 | }
36 |
37 | private func saveSettings() {
38 | let exchangeItemHandler = ExchangeItemsHandler()
39 | let exchangeItem = exchangeItemHandler.createExchangeItemEntity()
40 |
41 | exchangeItem.currency = self.selectedCurrency.symbol
42 | exchangeItem.coinId = self.selectedCoin.id
43 | exchangeItem.coinSymbol = self.selectedCoin.symbol
44 | exchangeItem.amount = self.amount?.doubleValue ?? 0
45 |
46 | CoreDataHandler.shared.save()
47 | }
48 | }
49 |
50 | struct AddExchangeView_Previews: PreviewProvider {
51 | static var previews: some View {
52 | Group {
53 | EditExchangeView(exchangeViewModel: PreviewData.getExchangeViewModel())
54 | .environmentObject(ApplicationStateService.preview)
55 | .preferredColorScheme(.dark)
56 |
57 | EditExchangeView(exchangeViewModel: PreviewData.getExchangeViewModel())
58 | .environmentObject(ApplicationStateService.preview)
59 | .preferredColorScheme(.light)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ListRowsViews/AlertRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct AlertRowView: View {
11 | @ObservedObject public var alertViewModel: AlertViewModel
12 |
13 | @State var isEnabled: Bool = true
14 | let onDetail: () -> Void
15 |
16 | var body: some View {
17 | Button(action: onDetail) {
18 | HStack {
19 | CoinImageView(coin: alertViewModel.coinViewModel)
20 | VStack(alignment: .leading) {
21 | Text("\(alertViewModel.coinViewModel.name)")
22 | .font(.headline)
23 | Text(alertViewModel.alert.coinSymbol)
24 | .font(.subheadline)
25 | .foregroundColor(.accentColor)
26 | HStack {
27 | Text(alertViewModel.alert.isPriceLower ? "Lower than" : "Greather than")
28 | .foregroundColor(.gray)
29 | .font(.subheadline)
30 | Text("\(alertViewModel.alert.price.toFormattedPrice(currency: alertViewModel.alert.currency))")
31 | .font(.subheadline)
32 | Text(alertViewModel.alert.currency)
33 | .font(.subheadline)
34 | .foregroundColor(.accentColor)
35 | }
36 | }
37 |
38 | Spacer()
39 |
40 | VStack(alignment: .trailing) {
41 | Toggle("", isOn: $isEnabled)
42 | .toggleStyle(SwitchToggleStyle(tint: .accentColor))
43 | .labelsHidden()
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | struct AlertRowView_Previews: PreviewProvider {
51 | static var previews: some View {
52 | Group {
53 | AlertRowView(alertViewModel: PreviewData.getAlertViewModel()) {
54 | }
55 | .preferredColorScheme(.dark)
56 |
57 | AlertRowView(alertViewModel: PreviewData.getAlertViewModel()) {
58 | }
59 | .preferredColorScheme(.light)
60 | }
61 | .previewLayout(.fixed(width: 360, height: 70))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/CoreData/AlertsHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 |
10 | class AlertsHandler {
11 | private let timeInterval: TimeInterval = -1 * 24 * 60 * 60
12 |
13 | func createAlertEntity() -> Alert {
14 | let context = CoreDataHandler.shared.container.viewContext
15 | return Alert(context: context)
16 | }
17 |
18 | func deleteAlertEntity(alert: Alert) {
19 | let context = CoreDataHandler.shared.container.viewContext
20 | context.delete(alert)
21 | }
22 |
23 | func getActiveAlerts() -> [Alert] {
24 | var alerts: [Alert] = []
25 |
26 | let context = CoreDataHandler.shared.container.viewContext
27 | let fetchRequest = NSFetchRequest(entityName: "Alert")
28 |
29 | let calendar = Calendar(identifier: Calendar.Identifier.gregorian)
30 |
31 | var date = Date()
32 | date = date.addingTimeInterval(self.timeInterval)
33 | let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
34 |
35 | guard let minimumAlertDate = calendar.date(from: components) else {
36 | return alerts
37 | }
38 |
39 | let predicate = NSPredicate(format: "isEnabled == YES && (alertSentDate == nil || alertSentDate < %@)",
40 | argumentArray: [minimumAlertDate]
41 | )
42 |
43 | fetchRequest.predicate = predicate
44 |
45 | do {
46 | if let list = try context.fetch(fetchRequest) as? [Alert] {
47 | alerts = list
48 | }
49 | } catch {
50 | print("Error during fetching Alert")
51 | }
52 |
53 | return alerts
54 | }
55 |
56 | func getAlerts(coinSymbol: String) -> [Alert] {
57 | var alerts: [Alert] = []
58 |
59 | let context = CoreDataHandler.shared.container.viewContext
60 |
61 | let fetchRequest = NSFetchRequest(entityName: "Alert")
62 | let predicate = NSPredicate(format: "coinSymbol == %@", coinSymbol)
63 | fetchRequest.predicate = predicate
64 |
65 | do {
66 | if let list = try context.fetch(fetchRequest) as? [Alert] {
67 | alerts = list
68 | }
69 | } catch {
70 | print("Error during fetching Alert")
71 | }
72 |
73 | return alerts
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ListRowsViews/ExchangeRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct ExchangeRowView: View {
11 | @ObservedObject public var exchangeViewModel: ExchangeViewModel
12 |
13 | let onDetail: () -> Void
14 |
15 | var body: some View {
16 | Button(action: onDetail) {
17 | ZStack {
18 | HStack {
19 | CoinImageView(coin: exchangeViewModel.coinViewModel)
20 | VStack(alignment: .leading) {
21 | Text("\(exchangeViewModel.coinViewModel.name)")
22 | .font(.headline)
23 | Text(exchangeViewModel.exchangeItem.coinSymbol)
24 | .font(.subheadline)
25 | .foregroundColor(.accentColor)
26 | Text("\(exchangeViewModel.exchangeItem.amount.toFormattedAmount())")
27 | .font(.subheadline)
28 | }
29 |
30 | Spacer()
31 |
32 | VStack(alignment: .trailing) {
33 | Text("\(exchangeViewModel.currency.name)")
34 | .font(.headline)
35 | Text(exchangeViewModel.exchangeItem.currency)
36 | .font(.subheadline)
37 | .foregroundColor(.accentColor)
38 | Text(exchangeViewModel.price.toFormattedPrice(currency: exchangeViewModel.exchangeItem.currency))
39 | .font(.subheadline)
40 | }
41 | }
42 | HStack {
43 | Spacer()
44 | Image(systemName: "arrow.left.arrow.right")
45 | Spacer()
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
52 | struct ExchangeRowView_Previews: PreviewProvider {
53 | static var previews: some View {
54 | Group {
55 | ExchangeRowView(exchangeViewModel: PreviewData.getExchangeViewModel()) {
56 | }
57 | .preferredColorScheme(.dark)
58 |
59 | ExchangeRowView(exchangeViewModel: PreviewData.getExchangeViewModel()) {
60 | }
61 | .preferredColorScheme(.light)
62 | }
63 | .previewLayout(.fixed(width: 360, height: 70))
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/VirtualCoin/vcoin.xcdatamodeld/vcoin-20190712.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/VirtualCoin/vcoin.xcdatamodeld/vcoin.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/VirtualCoin/Cache/MemoryCache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | /// Memory cache based on article: https://www.swiftbysundell.com/articles/caching-in-swift/
10 | final class MemoryCache {
11 | private let wrapped = NSCache()
12 | private let dateProvider: () -> Date
13 | private let entryLifetime: TimeInterval
14 |
15 | init(dateProvider: @escaping () -> Date = Date.init,
16 | entryLifetime: TimeInterval = 12 * 60 * 60) {
17 | self.dateProvider = dateProvider
18 | self.entryLifetime = entryLifetime
19 | }
20 |
21 | func insert(_ value: Value, forKey key: Key) {
22 | let date = dateProvider().addingTimeInterval(entryLifetime)
23 | let entry = Entry(value: value, expirationDate: date)
24 | wrapped.setObject(entry, forKey: WrappedKey(key))
25 | }
26 |
27 | func value(forKey key: Key) -> Value? {
28 | guard let entry = wrapped.object(forKey: WrappedKey(key)) else {
29 | return nil
30 | }
31 |
32 | guard dateProvider() < entry.expirationDate else {
33 | // Discard values that have expired
34 | removeValue(forKey: key)
35 | return nil
36 | }
37 |
38 | return entry.value
39 | }
40 |
41 | func removeValue(forKey key: Key) {
42 | wrapped.removeObject(forKey: WrappedKey(key))
43 | }
44 | }
45 |
46 | private extension MemoryCache {
47 | final class WrappedKey: NSObject {
48 | let key: Key
49 |
50 | init(_ key: Key) { self.key = key }
51 |
52 | override var hash: Int { return key.hashValue }
53 |
54 | override func isEqual(_ object: Any?) -> Bool {
55 | guard let value = object as? WrappedKey else {
56 | return false
57 | }
58 |
59 | return value.key == key
60 | }
61 | }
62 | }
63 |
64 | private extension MemoryCache {
65 | final class Entry {
66 | let value: Value
67 | let expirationDate: Date
68 |
69 | init(value: Value, expirationDate: Date) {
70 | self.value = value
71 | self.expirationDate = expirationDate
72 | }
73 | }
74 | }
75 |
76 | extension MemoryCache {
77 | subscript(key: Key) -> Value? {
78 | get { return value(forKey: key) }
79 | set {
80 | guard let value = newValue else {
81 | // If nil was assigned using our subscript,
82 | // then we remove any value for that key:
83 | removeValue(forKey: key)
84 | return
85 | }
86 |
87 | insert(value, forKey: key)
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/VirtualCoin/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "Icon-60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "Icon-58.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "Icon-87.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "Icon-80.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "Icon-120.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "Icon-121.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "Icon-180.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "Icon-20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "Icon-41.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "Icon-29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "Icon-59.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "Icon-42.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "Icon-81.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "Icon-76.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "Icon-152.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "Icon-167.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "Icon-1024.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/osx,swift,xcode,windows
3 |
4 | ### Carthage ###
5 | # Carthage
6 | #
7 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
8 | Carthage/Checkouts
9 | Carthage/Build
10 | Cartfile.resolved
11 |
12 |
13 | ### OSX ###
14 | *.DS_Store
15 | .AppleDouble
16 | .LSOverride
17 |
18 | # Icon must end with two \r
19 | Icon
20 |
21 | # Thumbnails
22 | ._*
23 |
24 | # Files that might appear in the root of a volume
25 | .DocumentRevisions-V100
26 | .fseventsd
27 | .Spotlight-V100
28 | .TemporaryItems
29 | .Trashes
30 | .VolumeIcon.icns
31 | .com.apple.timemachine.donotpresent
32 |
33 | # Directories potentially created on remote AFP share
34 | .AppleDB
35 | .AppleDesktop
36 | Network Trash Folder
37 | Temporary Items
38 | .apdisk
39 |
40 | ### Swift ###
41 | # Xcode
42 | #
43 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
44 |
45 | ## Build generated
46 | build/
47 | DerivedData/
48 |
49 | ## Various settings
50 | *.pbxuser
51 | !default.pbxuser
52 | *.mode1v3
53 | !default.mode1v3
54 | *.mode2v3
55 | !default.mode2v3
56 | *.perspectivev3
57 | !default.perspectivev3
58 | xcuserdata/
59 |
60 | ## Other
61 | *.moved-aside
62 | *.xccheckout
63 | *.xcscmblueprint
64 |
65 | ## Obj-C/Swift specific
66 | *.hmap
67 | *.ipa
68 | *.dSYM.zip
69 | *.dSYM
70 |
71 | ## Playgrounds
72 | timeline.xctimeline
73 | playground.xcworkspace
74 |
75 | # Swift Package Manager
76 | #
77 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
78 | # Packages/
79 | # Package.pins
80 | .build/
81 |
82 |
83 | # fastlane
84 | #
85 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
86 | # screenshots whenever they are needed.
87 | # For more information about the recommended setup visit:
88 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
89 |
90 | fastlane/report.xml
91 | fastlane/Preview.html
92 | fastlane/screenshots
93 | fastlane/test_output
94 |
95 | ### Windows ###
96 | # Windows thumbnail cache files
97 | Thumbs.db
98 | ehthumbs.db
99 | ehthumbs_vista.db
100 |
101 | # Folder config file
102 | Desktop.ini
103 |
104 | # Recycle Bin used on file shares
105 | $RECYCLE.BIN/
106 |
107 | # Windows Installer files
108 | *.cab
109 | *.msi
110 | *.msm
111 | *.msp
112 |
113 | # Windows shortcuts
114 | *.lnk
115 |
116 | ### Xcode ###
117 | # Xcode
118 | #
119 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
120 |
121 | ## Build generated
122 |
123 | ## Various settings
124 |
125 | ## Other
126 |
127 | ### Xcode Patch ###
128 | *.xcodeproj/*
129 | !*.xcodeproj/project.pbxproj
130 | !*.xcodeproj/xcshareddata/
131 | !*.xcworkspace/contents.xcworkspacedata
132 | /*.gcno
133 |
134 | # End of https://www.gitignore.io/api/osx,swift,xcode,windows
--------------------------------------------------------------------------------
/VirtualCoin/VirtualCoinApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VirtualCoinApp.swift
3 | // VirtualCoin
4 | //
5 | // Created by Marcin Czachurski on 17/04/2021.
6 | // Copyright © 2021 Marcin Czachurski. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import VirtualCoinKit
11 | import BackgroundTasks
12 | import URLImage
13 | import URLImageStore
14 |
15 | @main
16 | struct VirtualCoinApp: App {
17 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
18 |
19 | let urlImageService = URLImageService(inMemoryStore: URLImageInMemoryStore())
20 |
21 | var body: some Scene {
22 | WindowGroup {
23 | AppView()
24 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
25 | PricesService.shared.stop()
26 | appDelegate.submitBackgroundTasks()
27 | }
28 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
29 | PricesService.shared.start()
30 | }
31 | .environmentObject(ApplicationStateService.shared)
32 | .environmentObject(PricesService.shared)
33 | .environmentObject(CoinsService.shared)
34 | .environment(\.managedObjectContext, CoreDataHandler.shared.container.viewContext)
35 | .environment(\.urlImageService, urlImageService)
36 | }
37 | }
38 | }
39 |
40 | class AppDelegate: NSObject, UIApplicationDelegate {
41 | let backgroundTaskId = "dev.mczachurski.vcoin.fetch"
42 |
43 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
44 | self.registerBackgroundFetch()
45 | return true
46 | }
47 |
48 | private func registerBackgroundFetch() {
49 | BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskId, using: nil) { task in
50 |
51 | // Task expired.
52 | task.expirationHandler = {
53 | task.setTaskCompleted(success: false)
54 | }
55 |
56 | let notifications = Notifications()
57 | notifications.sendNotification { result in
58 | switch result {
59 | case .success:
60 | task.setTaskCompleted(success: true)
61 | case .failure(_):
62 | task.setTaskCompleted(success: false)
63 | }
64 | }
65 | }
66 | }
67 |
68 | func submitBackgroundTasks() {
69 | do {
70 | let request = BGAppRefreshTaskRequest(identifier: backgroundTaskId)
71 | request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // Refresh after 1 hour.
72 | try BGTaskScheduler.shared.submit(request)
73 | } catch {
74 | print("Could not schedule app refresh task \(error.localizedDescription)")
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/ExchangeDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 | import NumericText
10 |
11 | struct ExchangeDetailView: View {
12 | @EnvironmentObject private var applicationStateService: ApplicationStateService
13 |
14 | @Binding private var amount: NSNumber?
15 | @Binding private var selectedCurrency: Currency
16 | @Binding private var selectedCoin: CoinViewModel
17 |
18 | init(amount: Binding, currency: Binding, coin: Binding) {
19 | _amount = amount
20 | _selectedCurrency = currency
21 | _selectedCoin = coin
22 | }
23 |
24 | var body: some View {
25 | List {
26 | Section(header: Text("VALUES")) {
27 | HStack {
28 | Text("Amount")
29 | NumericTextField("Amount",
30 | number: $amount,
31 | isDecimalAllowed: true,
32 | numberFormatter: NumberFormatter.amountFormatter)
33 | .multilineTextAlignment(.trailing)
34 | }
35 |
36 | Picker(selection: $selectedCoin, label: Text("Coin")) {
37 | ForEach(applicationStateService.coins, id: \.self) { coin in
38 | HStack {
39 | Text(coin.name)
40 | .font(.body)
41 | Text("(\(coin.symbol))")
42 | .font(.footnote)
43 | .foregroundColor(.accentColor)
44 | }.tag(coin)
45 | }
46 | }
47 |
48 | Picker(selection: $selectedCurrency, label: Text("Currency")) {
49 | ForEach(Currencies.allCurrenciesList, id: \.self) { currency in
50 | HStack {
51 | Text(currency.name)
52 | .font(.body)
53 | Text("(\(currency.symbol))")
54 | .font(.footnote)
55 | .foregroundColor(.accentColor)
56 | }.tag(currency)
57 | }
58 | }
59 | }
60 | }
61 | .listStyle(GroupedListStyle())
62 | }
63 | }
64 |
65 | struct ExchangeDetailView_Previews: PreviewProvider {
66 | @State private static var amount: NSNumber? = 12.32
67 | @State private static var currency = PreviewData.getCurrency()
68 | @State private static var coin = PreviewData.getCoinViewModel()
69 |
70 | static var previews: some View {
71 | ExchangeDetailView(amount: $amount, currency: $currency, coin: $coin)
72 | .environmentObject(ApplicationStateService.preview)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Views/LargeWidgetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import WidgetKit
8 | import SwiftUI
9 | import LightChart
10 | import VirtualCoinKit
11 |
12 | struct LargeWidgetView: View {
13 | var viewModels: [WidgetViewModel]
14 |
15 | @Setting(\.currency) private var currencySymbol: String
16 |
17 | var body: some View {
18 | VStack {
19 | ForEach(viewModels.prefix(6).indices, id: \.self) { index in
20 | VStack {
21 | HStack {
22 | VStack(alignment: .leading) {
23 | Text("\(viewModels[index].name)")
24 | .font(.caption2)
25 | Text("\(viewModels[index].symbol)")
26 | .foregroundColor(Color.gray)
27 | .font(.caption2)
28 | }
29 |
30 | Spacer()
31 |
32 | LightChartView(data: viewModels[index].chart,
33 | type: .curved,
34 | visualType: viewModels[index].changePercent24Hr > 0 ? .green : .red,
35 | currentValueLineType: viewModels[index].changePercent24Hr > 0 ? .green : .red)
36 | .frame(maxWidth: 80, maxHeight: .infinity)
37 | .padding(.top, 2)
38 | .padding(.bottom, 2)
39 |
40 | HStack {
41 | Spacer()
42 | VStack(alignment: .trailing){
43 | Text("\(viewModels[index].price.toFormattedPrice(currency: currencySymbol))")
44 | .font(.caption2)
45 | Text("\(viewModels[index].changePercent24Hr.toFormattedPercent())")
46 | .foregroundColor(viewModels[index].changePercent24Hr > 0 ?.greenPastel : .redPastel)
47 | .font(.caption2)
48 | }
49 | }
50 | .frame(minWidth: 100, maxWidth: 100)
51 | }
52 |
53 | if index + 1 < (viewModels.capacity > 6 ? 6 : viewModels.capacity) {
54 | Divider()
55 | }
56 | }
57 | }
58 | }
59 | .padding()
60 | }
61 | }
62 |
63 | struct LargeWidgetView_Previews: PreviewProvider {
64 | static var previews: some View {
65 | Group {
66 | VirtualCoinWidgetEntryView(entry: WidgetEntry(date: Date(), viewModels: PreviewData.getWidgetViewModels()))
67 | .previewContext(WidgetPreviewContext(family: .systemLarge))
68 | .environment(\.colorScheme, .light)
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/VirtualCoin/ViewModels/CoinViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import VirtualCoinKit
9 |
10 | public class CoinViewModel: Identifiable, ObservableObject {
11 | public let id: String
12 | public let rank: Int
13 | public let symbol: String
14 | public let name: String
15 | public let imageUrl: String
16 | public let orginalPriceUsd: Double
17 |
18 | @Published public var price: Double = 0.0
19 | @Published public var isFavourite = false
20 | @Published public var priceUsd: Double
21 | @Published public var changePercent24Hr: Double
22 |
23 | init(coin: Coin) {
24 | self.id = coin.id
25 | self.symbol = coin.symbol
26 | self.name = coin.name
27 |
28 | if let rank = Int(coin.rank) {
29 | self.rank = rank
30 | } else {
31 | self.rank = 0
32 | }
33 |
34 | var internalPriceUsd = 0.0
35 | if let priceUsd = coin.priceUsd, let price = Double(priceUsd) {
36 | internalPriceUsd = price
37 | }
38 |
39 | var internalChangePercent24Hr = 0.0
40 | if let changePercent24Hr = coin.changePercent24Hr, let price = Double(changePercent24Hr) {
41 | internalChangePercent24Hr = price
42 | }
43 |
44 | self.priceUsd = internalPriceUsd
45 | self.changePercent24Hr = internalChangePercent24Hr
46 | self.orginalPriceUsd = internalPriceUsd / ((internalChangePercent24Hr / 100) + 1)
47 | self.imageUrl = "https://static.coincap.io/assets/icons/\(symbol.lowercased())@2x.png"
48 | }
49 |
50 | init(id: String = "bitcoin", rank: Int = 1, symbol: String = "BTC", name: String = "Bitcoin", priceUsd: Double = 0.0, changePercent24Hr: Double = 0.0) {
51 | self.id = id
52 | self.rank = rank
53 | self.symbol = symbol
54 | self.name = name
55 | self.priceUsd = priceUsd
56 | self.orginalPriceUsd = priceUsd
57 | self.price = priceUsd
58 | self.changePercent24Hr = changePercent24Hr
59 | self.imageUrl = "https://static.coincap.io/assets/icons/\(symbol.lowercased())@2x.png"
60 | }
61 | }
62 |
63 | extension CoinViewModel {
64 | public func refresh(priceUsd: String?, changePercent24Hr: String?, withRateUsd rateUsd: Double) {
65 | if let priceUsd = priceUsd, let price = Double(priceUsd) {
66 | self.priceUsd = price
67 | self.price = price / rateUsd
68 | }
69 |
70 | if let changePercent24Hr = changePercent24Hr, let price = Double(changePercent24Hr) {
71 | self.changePercent24Hr = price
72 | }
73 | }
74 | }
75 |
76 | extension CoinViewModel: Hashable {
77 | public static func == (lhs: CoinViewModel, rhs: CoinViewModel) -> Bool {
78 | return lhs.id == rhs.id
79 | }
80 |
81 | public func hash(into hasher: inout Hasher) {
82 | hasher.combine(self.id)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/Views/MediumWidgetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import WidgetKit
8 | import SwiftUI
9 | import LightChart
10 | import VirtualCoinKit
11 |
12 | struct MediumWidgetView: View {
13 | var viewModels: [WidgetViewModel]
14 |
15 | @Setting(\.currency) private var currencySymbol: String
16 |
17 | var body: some View {
18 | VStack {
19 | ForEach(viewModels.prefix(3).indices, id: \.self) { index in
20 | VStack {
21 | HStack {
22 | VStack(alignment: .leading) {
23 | Text("\(viewModels[index].name)")
24 | .font(.caption2)
25 | Text("\(viewModels[index].symbol)")
26 | .foregroundColor(Color.gray)
27 | .font(.caption2)
28 | }
29 |
30 | Spacer()
31 |
32 | LightChartView(data: viewModels[index].chart,
33 | type: .curved,
34 | visualType: viewModels[index].changePercent24Hr > 0 ? .green : .red,
35 | currentValueLineType: viewModels[index].changePercent24Hr > 0 ? .green : .red)
36 | .frame(maxWidth: 80, maxHeight: .infinity)
37 | .padding(.top, 2)
38 | .padding(.bottom, 2)
39 |
40 | HStack {
41 | Spacer()
42 | VStack(alignment: .trailing){
43 | Text("\(viewModels[index].price.toFormattedPrice(currency: currencySymbol))")
44 | .font(.caption2)
45 | Text("\(viewModels[index].changePercent24Hr.toFormattedPercent())")
46 | .foregroundColor(viewModels[index].changePercent24Hr > 0 ?.greenPastel : .redPastel)
47 | .font(.caption2)
48 | }
49 | }
50 | .frame(minWidth: 100, maxWidth: 100)
51 | }
52 |
53 | if index + 1 < (viewModels.capacity > 3 ? 3 : viewModels.capacity) {
54 | Divider()
55 | }
56 | }
57 | }
58 | }
59 | .padding()
60 | }
61 | }
62 |
63 | struct MediumWidgetView_Previews: PreviewProvider {
64 | static var previews: some View {
65 | Group {
66 | VirtualCoinWidgetEntryView(entry: WidgetEntry(date: Date(), viewModels: PreviewData.getWidgetViewModels()))
67 | .previewContext(WidgetPreviewContext(family: .systemMedium))
68 | .environment(\.colorScheme, .light)
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/PreviewData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | struct PreviewData {
10 |
11 | static func getWidgetViewModels() -> [WidgetViewModel] {
12 | return [
13 | WidgetViewModel(id: "bitcoin",
14 | order: 1,
15 | rank: 1,
16 | symbol: "BTC",
17 | name: "Bitcoin",
18 | priceUsd: 36135.11,
19 | changePercent24Hr: -4.72,
20 | price: 36135.11,
21 | chart: getChartData()),
22 |
23 | WidgetViewModel(id: "ethereum",
24 | order: 2,
25 | rank: 2,
26 | symbol: "ETH",
27 | name: "Ethereum",
28 | priceUsd: 2662.72,
29 | changePercent24Hr: -4.43,
30 | price: 2662.72,
31 | chart: getChartData()),
32 |
33 | WidgetViewModel(id: "dogecoin",
34 | order: 3,
35 | rank: 3,
36 | symbol: "DOGE",
37 | name: "Dogecoin",
38 | priceUsd: 0.37526429,
39 | changePercent24Hr: -5.18,
40 | price: 0.37526429,
41 | chart: getChartData()),
42 |
43 | WidgetViewModel(id: "cardano",
44 | order: 4,
45 | rank: 4,
46 | symbol: "USDT",
47 | name: "Cardano",
48 | priceUsd: 1.69,
49 | changePercent24Hr: -5.18,
50 | price: 1.69,
51 | chart: getChartData()),
52 |
53 | WidgetViewModel(id: "tether",
54 | order: 5,
55 | rank: 5,
56 | symbol: "USDT",
57 | name: "Tether",
58 | priceUsd: 1.00,
59 | changePercent24Hr: 0.03,
60 | price: 1.00,
61 | chart: getChartData()),
62 |
63 | WidgetViewModel(id: "xrp",
64 | order: 6,
65 | rank: 6,
66 | symbol: "XRP",
67 | name: "XRP",
68 | priceUsd: 0.94153174,
69 | changePercent24Hr: -5.35,
70 | price: 0.94153174,
71 | chart: getChartData())
72 | ]
73 | }
74 |
75 | private static func getChartData() -> [Double] {
76 | return [34, 34, 23, 34, 45, 65, 34, 32, 23, 33, 65, 57, 65, 34, 65, 67]
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/AlertDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 | import NumericText
10 |
11 | struct AlertDetailView: View {
12 | @EnvironmentObject private var applicationStateService: ApplicationStateService
13 |
14 | @Binding private var price: NSNumber?
15 | @Binding private var isPriceLower: Bool
16 | @Binding private var selectedCurrency: Currency
17 | @Binding private var selectedCoin: CoinViewModel
18 |
19 | init(price: Binding, isPriceLower: Binding, currency: Binding, coin: Binding) {
20 | _price = price
21 | _isPriceLower = isPriceLower
22 | _selectedCurrency = currency
23 | _selectedCoin = coin
24 | }
25 |
26 | var body: some View {
27 | List {
28 | Section(header: Text("VALUES")) {
29 | HStack {
30 | Text("Notify when")
31 | Picker("", selection: $isPriceLower) {
32 | Text("lower than").tag(true)
33 | Text("higher than").tag(false)
34 | }
35 | .pickerStyle(SegmentedPickerStyle())
36 | }
37 |
38 | HStack {
39 | Text("Price")
40 | NumericTextField("Price", number: $price, isDecimalAllowed: true)
41 | .multilineTextAlignment(.trailing)
42 | }
43 |
44 | Picker(selection: $selectedCurrency, label: Text("Currency")) {
45 | ForEach(Currencies.allCurrenciesList, id: \.self) { currency in
46 | HStack {
47 | Text(currency.name)
48 | .font(.body)
49 | Text("(\(currency.symbol))")
50 | .font(.footnote)
51 | .foregroundColor(.accentColor)
52 | }.tag(currency)
53 | }
54 | }
55 |
56 | Picker(selection: $selectedCoin, label: Text("Coin")) {
57 | ForEach(applicationStateService.coins, id: \.self) { coin in
58 | HStack {
59 | Text(coin.name)
60 | .font(.body)
61 | Text("(\(coin.symbol))")
62 | .font(.footnote)
63 | .foregroundColor(.accentColor)
64 | }.tag(coin)
65 | }
66 | }
67 | }
68 | }
69 | .listStyle(GroupedListStyle())
70 | }
71 | }
72 |
73 | struct AlertDetailView_Previews: PreviewProvider {
74 | @State private static var price: NSNumber? = 21.2
75 | @State private static var isPriceLower: Bool = true
76 | @State private static var currency = PreviewData.getCurrency()
77 | @State private static var coin = PreviewData.getCoinViewModel()
78 |
79 | static var previews: some View {
80 | AlertDetailView(price: $price, isPriceLower: $isPriceLower, currency: $currency, coin: $coin)
81 | .environmentObject(ApplicationStateService.preview)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/EditExchangeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 | import NumericText
10 |
11 | struct EditExchangeView: View {
12 | @EnvironmentObject private var applicationStateService: ApplicationStateService
13 | @Environment(\.presentationMode) var presentationMode
14 |
15 | @State private var amount: NSNumber?
16 | @State private var selectedCurrency: Currency
17 | @State private var selectedCoin: CoinViewModel
18 |
19 | @ObservedObject private var exchangeViewModel: ExchangeViewModel
20 |
21 | init(exchangeViewModel: ExchangeViewModel) {
22 | self.exchangeViewModel = exchangeViewModel
23 |
24 | self._amount = State(initialValue: NSNumber(value: exchangeViewModel.exchangeItem.amount))
25 | self._selectedCurrency = State(initialValue: Currency(symbol: exchangeViewModel.exchangeItem.currency))
26 | self._selectedCoin = State(initialValue: CoinViewModel(id: exchangeViewModel.exchangeItem.coinId))
27 | }
28 |
29 | var body: some View {
30 | NavigationView {
31 | ExchangeDetailView(amount: self.$amount, currency: self.$selectedCurrency, coin: self.$selectedCoin)
32 | .navigationBarTitle(Text("Exchange"), displayMode: .inline)
33 | .navigationBarItems(
34 | leading: Button(action: {
35 | presentationMode.wrappedValue.dismiss()
36 | }) {
37 | Text("Cancel").bold()
38 | },
39 | trailing: Button(action: {
40 | self.saveSettings()
41 | presentationMode.wrappedValue.dismiss()
42 | }) {
43 | Text("Done").bold()
44 | })
45 | }
46 | }
47 |
48 | private func saveSettings() {
49 | self.exchangeViewModel.exchangeItem.currency = self.selectedCurrency.symbol
50 | self.exchangeViewModel.exchangeItem.coinId = self.selectedCoin.id
51 | self.exchangeViewModel.exchangeItem.coinSymbol = self.selectedCoin.symbol
52 | self.exchangeViewModel.exchangeItem.amount = self.amount?.doubleValue ?? 0
53 |
54 | CoreDataHandler.shared.save()
55 |
56 | if let coinViewModel = applicationStateService.coins.first(where: { coinViewModel in
57 | coinViewModel.id == self.exchangeViewModel.exchangeItem.coinId
58 | }) {
59 | self.exchangeViewModel.setCoinViewModel(coinViewModel)
60 | }
61 |
62 | if let currency = Currencies.allCurrenciesDictionary[self.exchangeViewModel.exchangeItem.currency] {
63 | self.exchangeViewModel.setCurrency(currency)
64 | }
65 | }
66 | }
67 |
68 | struct EditExchangeView_Previews: PreviewProvider {
69 | static var previews: some View {
70 | Group {
71 | EditExchangeView(exchangeViewModel: PreviewData.getExchangeViewModel())
72 | .environmentObject(ApplicationStateService.preview)
73 | .preferredColorScheme(.dark)
74 |
75 | EditExchangeView(exchangeViewModel: PreviewData.getExchangeViewModel())
76 | .environmentObject(ApplicationStateService.preview)
77 | .preferredColorScheme(.light)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/EditAlertView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 | import NumericText
10 |
11 | struct EditAlertView: View {
12 | @EnvironmentObject private var applicationStateService: ApplicationStateService
13 | @Environment(\.presentationMode) var presentationMode
14 |
15 | @State private var price: NSNumber?
16 | @State private var isPriceLower: Bool = true
17 | @State private var selectedCurrency: Currency
18 | @State private var selectedCoin: CoinViewModel
19 |
20 | @ObservedObject private var alertViewModel: AlertViewModel
21 |
22 | init(alertViewModel: AlertViewModel) {
23 | self.alertViewModel = alertViewModel
24 |
25 | self._price = State(initialValue: NSNumber(value: alertViewModel.alert.price))
26 | self._isPriceLower = State(initialValue: alertViewModel.alert.isPriceLower)
27 | self._selectedCurrency = State(initialValue: Currency(symbol: alertViewModel.alert.currency))
28 | self._selectedCoin = State(initialValue: CoinViewModel(id: alertViewModel.alert.coinId))
29 | }
30 |
31 | var body: some View {
32 | NavigationView {
33 | AlertDetailView(price: self.$price, isPriceLower: $isPriceLower, currency: self.$selectedCurrency, coin: self.$selectedCoin)
34 | .navigationBarTitle(Text("Alert"), displayMode: .inline)
35 | .navigationBarItems(
36 | leading: Button(action: {
37 | presentationMode.wrappedValue.dismiss()
38 | }) {
39 | Text("Cancel").bold()
40 | },
41 | trailing: Button(action: {
42 | self.saveSettings()
43 | presentationMode.wrappedValue.dismiss()
44 | }) {
45 | Text("Done").bold()
46 | })
47 | }
48 | }
49 |
50 | private func saveSettings() {
51 | self.alertViewModel.alert.currency = self.selectedCurrency.symbol
52 | self.alertViewModel.alert.coinId = self.selectedCoin.id
53 | self.alertViewModel.alert.coinSymbol = self.selectedCoin.symbol
54 | self.alertViewModel.alert.price = self.price?.doubleValue ?? 0
55 | self.alertViewModel.alert.isPriceLower = self.isPriceLower
56 |
57 | CoreDataHandler.shared.save()
58 |
59 | if let coinViewModel = applicationStateService.coins.first(where: { coinViewModel in
60 | coinViewModel.id == self.alertViewModel.alert.coinId
61 | }) {
62 | self.alertViewModel.setCoinViewModel(coinViewModel)
63 | }
64 |
65 | if let currency = Currencies.allCurrenciesDictionary[self.alertViewModel.alert.currency] {
66 | self.alertViewModel.setCurrency(currency)
67 | }
68 | }
69 | }
70 |
71 | struct EEditAlertView_Previews: PreviewProvider {
72 | static var previews: some View {
73 | Group {
74 | EditAlertView(alertViewModel: PreviewData.getAlertViewModel())
75 | .environmentObject(ApplicationStateService.preview)
76 | .preferredColorScheme(.dark)
77 |
78 | EditAlertView(alertViewModel: PreviewData.getAlertViewModel())
79 | .environmentObject(ApplicationStateService.preview)
80 | .preferredColorScheme(.light)
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/VirtualCoin.xcodeproj/xcuserdata/mczachurski.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
21 |
22 |
36 |
37 |
51 |
52 |
53 |
54 |
55 |
57 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/VirtualCoin/Services/PricesService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 |
9 | public class PricesService: ObservableObject {
10 | public static let shared = PricesService()
11 |
12 | var urlSession: URLSession?
13 | var webSocketTask: URLSessionWebSocketTask?
14 | var pricesServiceConnection = PricesServiceConnection()
15 |
16 | deinit {
17 | self.stop()
18 | }
19 |
20 | func start() {
21 | guard self.pricesServiceConnection.isConnected == false else {
22 | return
23 | }
24 |
25 | let favouritesHandler = FavouritesHandler()
26 | let favourites = favouritesHandler.getFavourites()
27 |
28 | guard favourites.isEmpty == false else {
29 | return
30 | }
31 |
32 | let queryParam = favourites.map { favourite in favourite.coinId }.joined(separator: ",")
33 |
34 | self.urlSession = URLSession(configuration: .default, delegate: self.pricesServiceConnection, delegateQueue: OperationQueue())
35 | self.webSocketTask = self.urlSession?.webSocketTask(with: URL(string: "wss://ws.coincap.io/prices?assets=\(queryParam)")!)
36 |
37 | self.webSocketTask?.resume()
38 | self.receiveMessages()
39 | }
40 |
41 | func stop() {
42 | self.webSocketTask?.cancel(with: .goingAway, reason: nil)
43 | self.webSocketTask = nil
44 | self.urlSession = nil
45 | }
46 |
47 | private func receiveMessages() {
48 | webSocketTask?.receive { result in
49 | switch result {
50 | case .failure(let error):
51 | print("Error in receiving message: \(error)")
52 | case .success(let message):
53 | switch message {
54 | case .string(let text):
55 | self.updatePrice(message: text)
56 | case .data(_):
57 | print("Received unsupported message")
58 | @unknown default:
59 | print("Received unknown")
60 | }
61 |
62 | self.receiveMessages()
63 | }
64 | }
65 | }
66 |
67 | private func updatePrice(message: String) {
68 | guard let data = message.data(using: .utf8) else {
69 | return
70 | }
71 |
72 | guard let prices = try? JSONDecoder().decode(([String: String]).self, from: data) else {
73 | return
74 | }
75 |
76 | for price in prices {
77 | guard let coin = ApplicationStateService.shared.coins.first(where: { coinViewModel in coinViewModel.id == price.key }) else {
78 | continue
79 | }
80 |
81 | guard let priceUsd = Double(price.value) else {
82 | continue
83 | }
84 |
85 | let difference = priceUsd - coin.orginalPriceUsd
86 | let changePercent24Hr = (difference / coin.orginalPriceUsd) * 100
87 |
88 | DispatchQueue.runOnMain {
89 | coin.changePercent24Hr = changePercent24Hr
90 | coin.price = priceUsd / ApplicationStateService.shared.currencyRateUsd
91 | coin.priceUsd = priceUsd
92 | }
93 | }
94 | }
95 | }
96 |
97 | class PricesServiceConnection: NSObject, URLSessionWebSocketDelegate {
98 | var isConnected = false
99 | var connectionError = false
100 |
101 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
102 | self.isConnected = true
103 | }
104 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
105 | self.isConnected = false
106 | }
107 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError: Error?) {
108 | self.connectionError = true
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ComponentViews/ChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 | import LightChart
10 |
11 | struct ChartView: View {
12 | @EnvironmentObject private var applicationStateService: ApplicationStateService
13 | @EnvironmentObject private var coinsService: CoinsService
14 |
15 | @State private var state: ViewState = .iddle
16 | @State private var chartData: [Double] = []
17 |
18 | public var chartTimeRange: ChartTimeRange
19 | public var coin: CoinViewModel
20 |
21 | @Setting(\.currency) private var currencySymbol: String
22 |
23 | var body: some View {
24 | switch state {
25 | case .iddle:
26 | Text("").onAppear {
27 | self.load()
28 | }
29 | case .loading:
30 | LoadingView()
31 | case .loaded:
32 | ZStack(alignment: .topTrailing) {
33 | LightChartView(data: self.chartData,
34 | type: .curved,
35 | visualType: .customFilled(color: .main,
36 | lineWidth: 2,
37 | fillGradient: LinearGradient(
38 | gradient: .init(colors: [.main(opacity: 0.5), .main(opacity: 0.1)]),
39 | startPoint: .init(x: 0.5, y: 1),
40 | endPoint: .init(x: 0.5, y: 0)
41 | )),
42 | currentValueLineType: .dash(color: .main(opacity: 0.3), lineWidth: 1, dash: [5]))
43 | VStack {
44 | self.getLabelView(value: self.getMaxValue())
45 | .offset(x: -4, y: 4)
46 | Spacer()
47 | self.getLabelView(value: self.getMinValue())
48 | .offset(x: -4, y: -4)
49 | }
50 | }
51 | case .error(let error):
52 | ErrorView(error: error)
53 | .padding()
54 | }
55 | }
56 |
57 | private func getLabelView(value: Double) -> some View {
58 | Text(value.toFormattedPrice(currency: currencySymbol))
59 | .font(.footnote)
60 | .padding(2)
61 | .background(Color.backgroundLabel(opacity: 0.4))
62 | .foregroundColor(.main)
63 | .cornerRadius(5)
64 | }
65 |
66 | private func getMaxValue() -> Double {
67 | return (self.chartData.max() ?? 0) / self.applicationStateService.currencyRateUsd
68 | }
69 |
70 | private func getMinValue() -> Double {
71 | return (self.chartData.min() ?? 0) / self.applicationStateService.currencyRateUsd
72 | }
73 |
74 | private func load() {
75 | state = .loading
76 |
77 | coinsService.getChartData(coin: coin, chartTimeRange: chartTimeRange) { result in
78 | switch result {
79 | case .success(let chartData):
80 | self.chartData = chartData
81 | self.state = .loaded
82 | break;
83 | case .failure(let error):
84 | self.state = .error(error)
85 | break;
86 | }
87 | }
88 | }
89 | }
90 |
91 | struct ChartView_Previews: PreviewProvider {
92 | static var previews: some View {
93 | Group {
94 | ChartView(chartTimeRange: .hour, coin: PreviewData.getCoinViewModel())
95 | .environmentObject(CoinsService.preview)
96 | .preferredColorScheme(.dark)
97 |
98 | ChartView(chartTimeRange: .hour, coin: PreviewData.getCoinViewModel())
99 | .environmentObject(CoinsService.preview)
100 | .preferredColorScheme(.light)
101 | }
102 | .previewLayout(.fixed(width: 360, height: 360))
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/TabsViews/CoinsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import CoreData
9 | import VirtualCoinKit
10 |
11 | struct CoinsView: View {
12 |
13 | @EnvironmentObject private var coinsService: CoinsService
14 | @EnvironmentObject private var applicationStateService: ApplicationStateService
15 |
16 | @ObservedObject private var searchBar: SearchBar = SearchBar()
17 | @State private var showingSettingsView = false
18 | @State private var state: ViewState = .iddle
19 |
20 | var body: some View {
21 | self.mainBody()
22 | .navigationTitle("All currencies")
23 | .toolbar {
24 | ToolbarItem(placement: .navigationBarLeading) {
25 | Button(action: {
26 | showingSettingsView.toggle()
27 | }) {
28 | Image(systemName: "switch.2")
29 | }
30 | }
31 | }
32 | .sheet(isPresented: $showingSettingsView) {
33 | SettingsView()
34 | }
35 | }
36 |
37 | @ViewBuilder
38 | private func mainBody() -> some View {
39 | switch state {
40 | case .iddle:
41 | self.skeletonView()
42 | .onAppear {
43 | self.load()
44 | }
45 | case .loading:
46 | self.skeletonView()
47 | case .loaded:
48 | if applicationStateService.coins.count > 0 {
49 | List {
50 | ForEach(applicationStateService.coins.filter {
51 | searchBar.text.isEmpty || $0.name.localizedStandardContains(searchBar.text)
52 | }) { coin in
53 | NavigationLink(destination: CoinView(coin: coin)) {
54 | CoinRowView(coin: coin)
55 | }
56 | }
57 | }
58 | .add(self.searchBar)
59 | .listStyle(PlainListStyle())
60 | } else {
61 | NoDataView(title: "No Coins", subtitle: "Coins has not been downloaded")
62 | }
63 | case .error(let error):
64 | ErrorView(error: error) {
65 | self.load()
66 | }
67 | .padding()
68 | }
69 | }
70 |
71 | private func skeletonView() -> some View {
72 | List(PreviewData.getCoinsViewModel(), id: \.id) { coin in
73 | NavigationLink(destination: CoinView(coin: coin)) {
74 | CoinRowView(coin: coin)
75 | }
76 | }
77 | .listStyle(PlainListStyle())
78 | .redacted(reason: .placeholder)
79 | }
80 |
81 | private func load() {
82 | if applicationStateService.coins.isEmpty == false {
83 | self.state = .loaded
84 | return
85 | }
86 |
87 | state = .loading
88 |
89 | coinsService.loadCoins(into: applicationStateService) { result in
90 | DispatchQueue.runOnMain {
91 | switch result {
92 | case .success:
93 | self.state = .loaded
94 | break;
95 | case .failure(let error):
96 | self.state = .error(error)
97 | break;
98 | }
99 | }
100 | }
101 | }
102 | }
103 |
104 | struct CoinsView_Previews: PreviewProvider {
105 | static var previews: some View {
106 | Group {
107 | NavigationView {
108 | CoinsView()
109 | .environmentObject(ApplicationStateService.preview)
110 | .environmentObject(CoinsService.preview)
111 | }
112 | .preferredColorScheme(.dark)
113 |
114 | NavigationView {
115 | CoinsView()
116 | .environmentObject(ApplicationStateService.preview)
117 | .environmentObject(CoinsService.preview)
118 | }
119 | .preferredColorScheme(.light)
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/SettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct SettingsView: View {
11 | @EnvironmentObject private var coinsService: CoinsService
12 | @EnvironmentObject private var applicationStateService: ApplicationStateService
13 | @Environment(\.presentationMode) var presentationMode
14 |
15 | @State var matchSystem: Bool = true
16 | @State var isDarkMode: Bool = true
17 | @State private var selectedCurrency = Currencies.getDefaultCurrency()
18 |
19 | var body: some View {
20 | NavigationView {
21 | List {
22 |
23 | Section(header: Text("MAIN")) {
24 |
25 | Picker(selection: $selectedCurrency, label: Text("Currency")) {
26 | ForEach(Currencies.allCurrenciesList, id: \.self) { currency in
27 | HStack {
28 | Text(currency.name)
29 | .font(.body)
30 | Text("(\(currency.symbol))")
31 | .font(.footnote)
32 | .foregroundColor(.accentColor)
33 | }.tag(currency)
34 | }
35 | }
36 | }
37 |
38 | Section(header: Text("OTHER")) {
39 | NavigationLink(destination: ThirdPartyView()) {
40 | Text("Third party")
41 | }
42 |
43 | HStack {
44 | Text("Source code")
45 | Spacer()
46 | Link("GitHub",
47 | destination: URL(string: "https://github.com/mczachurski/vcoin")!)
48 | }
49 |
50 | HStack {
51 | Text("Report a bug")
52 | Spacer()
53 | Link("Issues on Github",
54 | destination: URL(string: "https://github.com/mczachurski/vcoin/issues")!)
55 | }
56 |
57 | HStack {
58 | Text("Follow me on Twitter")
59 | Spacer()
60 | Link("@mczachurski",
61 | destination: URL(string: "https://twitter.com/@mczachurski")!)
62 | }
63 |
64 | }
65 |
66 | Section {
67 | HStack {
68 | Text("Version")
69 | Spacer()
70 | Text("2.0.0")
71 | }
72 | }
73 | }
74 | .listStyle(GroupedListStyle())
75 | .navigationBarTitle(Text("Settings"), displayMode: .inline)
76 | .navigationBarItems(trailing: Button(action: {
77 | self.saveSettings()
78 | presentationMode.wrappedValue.dismiss()
79 | }) {
80 | Text("Done").bold()
81 | })
82 | }.onAppear {
83 | self.loadDefaultCurrency()
84 | }
85 | }
86 |
87 | private func loadDefaultCurrency() {
88 | let settingsHandler = SettingsHandler()
89 | let defaultSettings = settingsHandler.getDefaultSettings()
90 |
91 | if defaultSettings.currency != "" {
92 | self.selectedCurrency = Currency(symbol: defaultSettings.currency)
93 | }
94 | }
95 |
96 | private func saveSettings() {
97 | let settingsHandler = SettingsHandler()
98 | let defaultSettings = settingsHandler.getDefaultSettings()
99 | defaultSettings.currency = self.selectedCurrency.symbol
100 |
101 | CoreDataHandler.shared.save()
102 | coinsService.loadCoins(into: applicationStateService) { _ in }
103 | }
104 | }
105 |
106 | struct SettingsView_Previews: PreviewProvider {
107 | static var previews: some View {
108 | Group {
109 | SettingsView()
110 | .preferredColorScheme(.dark)
111 |
112 | SettingsView()
113 | .preferredColorScheme(.light)
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/CoreData/CoreDataHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import CoreData
8 | import Foundation
9 | import UIKit
10 |
11 | public class CoreDataHandler {
12 | public static let shared = CoreDataHandler()
13 |
14 | public let container: NSPersistentContainer
15 |
16 | init(inMemory: Bool = false) {
17 | container = NSPersistentContainer(name: "vcoin")
18 |
19 | if inMemory {
20 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
21 | } else {
22 | guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.dev.mczachurski.vcoin") else {
23 | fatalError("Container URL for application cannot be retrieved")
24 | }
25 |
26 | let dbUrl = url.appendingPathComponent("Data.sqlite")
27 | container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: dbUrl)]
28 | }
29 |
30 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in
31 | if let error = error as NSError? {
32 | // Replace this implementation with code to handle the error appropriately.
33 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
34 |
35 | /*
36 | Typical reasons for an error here include:
37 | * The parent directory does not exist, cannot be created, or disallows writing.
38 | * The persistent store is not accessible, due to permissions or data protection when the device is locked.
39 | * The device is out of space.
40 | * The store could not be migrated to the current model version.
41 | Check the error message to determine what the actual problem was.
42 | */
43 | fatalError("Unresolved error \(error), \(error.userInfo)")
44 | }
45 | })
46 | }
47 |
48 | public func save() {
49 | let context = self.container.viewContext
50 | if context.hasChanges {
51 | do {
52 | try context.save()
53 | } catch {
54 | // Replace this implementation with code to handle the error appropriately.
55 | // fatalError() causes the application to generate a crash log and terminate.
56 | // You should not use this function in a shipping application, although it may be useful during development.
57 |
58 | let nserror = error as NSError
59 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
60 | }
61 | }
62 | }
63 | }
64 |
65 | extension CoreDataHandler {
66 | public static var preview: CoreDataHandler = {
67 | let result = CoreDataHandler(inMemory: true)
68 | let viewContext = result.container.viewContext
69 |
70 | let favouriteItem1 = Favourite(context: viewContext)
71 | favouriteItem1.coinId = "ethereum"
72 |
73 | let favouriteItem2 = Favourite(context: viewContext)
74 | favouriteItem2.coinId = "bitcoin"
75 |
76 | let favouriteItem3 = Favourite(context: viewContext)
77 | favouriteItem3.coinId = "dogecoin"
78 |
79 | let exchangeItem1 = ExchangeItem(context: viewContext)
80 | exchangeItem1.coinId = "bitcoin"
81 | exchangeItem1.amount = 2
82 | exchangeItem1.currency = "USD"
83 |
84 | let alertItem1 = Alert(context: viewContext)
85 | alertItem1.coinId = "bitcoin"
86 | alertItem1.currency = "USD"
87 | alertItem1.isEnabled = true
88 | alertItem1.isPriceLower = true
89 | alertItem1.price = 63.33
90 |
91 | do {
92 | try viewContext.save()
93 | } catch {
94 | // Replace this implementation with code to handle the error appropriately.
95 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
96 | let nsError = error as NSError
97 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
98 | }
99 |
100 | return result
101 | }()
102 | }
103 |
--------------------------------------------------------------------------------
/VirtualCoin.xcodeproj/xcshareddata/xcschemes/VirtualCoin.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
53 |
59 |
60 |
61 |
62 |
63 |
73 |
75 |
81 |
82 |
83 |
84 |
90 |
92 |
98 |
99 |
100 |
101 |
103 |
104 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/TabsViews/ExchangesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct ExchangesView: View {
11 | @EnvironmentObject private var applicationStateService: ApplicationStateService
12 |
13 | @State private var showingSettingsView = false
14 | @State private var showingExchangeDetailsView = false
15 |
16 | @FetchRequest(
17 | sortDescriptors: [NSSortDescriptor(keyPath: \ExchangeItem.coinId, ascending: true)],
18 | animation: .default
19 | )
20 | private var exchanges: FetchedResults
21 |
22 | var body: some View {
23 | self.mainBody()
24 | .navigationTitle("Exchanges")
25 | .toolbar {
26 | ToolbarItem(placement: .navigationBarLeading) {
27 | Button(action: {
28 | showingSettingsView.toggle()
29 | }) {
30 | Image(systemName: "switch.2")
31 | }
32 | }
33 | ToolbarItem(placement: .navigationBarTrailing) {
34 | Button(action: {
35 | applicationStateService.selectedExchangeViewModel = nil
36 | self.showingExchangeDetailsView = true
37 | }) {
38 | Image(systemName: "plus")
39 | }
40 | }
41 | }
42 | .sheet(isPresented: $showingSettingsView) {
43 | SettingsView()
44 | }
45 | .sheet(isPresented: $showingExchangeDetailsView) {
46 | if let selectedExchangeViewModel = applicationStateService.selectedExchangeViewModel {
47 | EditExchangeView(exchangeViewModel: selectedExchangeViewModel)
48 | } else {
49 | AddExchangeView()
50 | }
51 | }
52 | }
53 |
54 | @ViewBuilder
55 | private func mainBody() -> some View {
56 | if exchanges.count > 0 {
57 | List {
58 | ForEach(exchanges, id: \.self) { exchange in
59 | let coinViewModelFromApi = applicationStateService.coins.first(where: { coinViewModel in
60 | coinViewModel.id == exchange.coinId
61 | })
62 |
63 | let coinViewModel = coinViewModelFromApi ?? CoinViewModel()
64 |
65 | let currency = Currencies.allCurrenciesDictionary[exchange.currency] ?? Currency()
66 |
67 | let exchangeViewModel = ExchangeViewModel(coinViewModel: coinViewModel,
68 | exchangeItem: exchange,
69 | currency: currency)
70 |
71 | ExchangeRowView(exchangeViewModel: exchangeViewModel) {
72 | applicationStateService.selectedExchangeViewModel = exchangeViewModel
73 | self.showingExchangeDetailsView = true
74 | }
75 | }.onDelete(perform: self.deleteItem)
76 | }
77 | .listStyle(PlainListStyle())
78 | } else {
79 | NoDataView(title: "No Exchanges", subtitle: "Add exchange") {
80 | applicationStateService.selectedExchangeViewModel = nil
81 | self.showingExchangeDetailsView = true
82 | }
83 | }
84 | }
85 |
86 | private func deleteItem(at indexSet: IndexSet) {
87 | let exchangeItemsHandler = ExchangeItemsHandler()
88 |
89 | for index in indexSet {
90 | let exchangeItem = self.exchanges[index]
91 | exchangeItemsHandler.deleteExchangeItemEntity(exchangeItem: exchangeItem)
92 | }
93 |
94 | CoreDataHandler.shared.save()
95 | }
96 | }
97 |
98 | struct ExchangesView_Previews: PreviewProvider {
99 | static var previews: some View {
100 | Group {
101 | NavigationView {
102 | ExchangesView()
103 | .environmentObject(ApplicationStateService.preview)
104 | .environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext)
105 | }
106 | .preferredColorScheme(.dark)
107 |
108 | NavigationView {
109 | ExchangesView()
110 | .environmentObject(ApplicationStateService.preview)
111 | .environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext)
112 | }
113 | .preferredColorScheme(.light)
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/VirtualCoin/Previews/PreviewData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | public struct PreviewData {
11 |
12 | static func getCoinsViewModel() -> [CoinViewModel] {
13 | return [
14 | CoinViewModel(id: "bitcoin", rank: 1, symbol: "BTC", name: "Bitcoin", priceUsd: 36135.11, changePercent24Hr: -4.72),
15 | CoinViewModel(id: "ethereum", rank: 2, symbol: "ETH", name: "Ethereum", priceUsd: 2662.72, changePercent24Hr: -4.43),
16 | CoinViewModel(id: "dogecoin", rank: 3, symbol: "DOGE", name: "Dogecoin", priceUsd: 0.37526429, changePercent24Hr: -5.18),
17 | CoinViewModel(id: "cardano", rank: 4, symbol: "USDT", name: "Cardano", priceUsd: 1.69, changePercent24Hr: -5.18),
18 | CoinViewModel(id: "tether", rank: 5, symbol: "USDT", name: "Tether", priceUsd: 1.00, changePercent24Hr: 0.03),
19 | CoinViewModel(id: "xrp", rank: 6, symbol: "XRP", name: "XRP", priceUsd: 0.94153174, changePercent24Hr: -5.35),
20 | CoinViewModel(id: "uniswap", rank: 7, symbol: "UNI", name: "Uniswap", priceUsd: 25.98, changePercent24Hr: -5.28),
21 | CoinViewModel(id: "litecoin", rank: 8, symbol: "LTC", name: "Litecoin", priceUsd: 175.35, changePercent24Hr: -3.78),
22 | CoinViewModel(id: "chainlink", rank: 9, symbol: "LINK", name: "Chainlink", priceUsd: 27.65, changePercent24Hr: -7.53),
23 | CoinViewModel(id: "binance-usd", rank: 10, symbol: "BUSD", name: "Binance USD", priceUsd: 1.00, changePercent24Hr: 0.04),
24 | CoinViewModel(id: "stellar", rank: 11, symbol: "XLM", name: "Stellar", priceUsd: 0.38132001, changePercent24Hr: -4.78),
25 | CoinViewModel(id: "vechain", rank: 12, symbol: "VET", name: "VeChain", priceUsd: 0.12897915, changePercent24Hr: -5.77),
26 | CoinViewModel(id: "matic-network", rank: 13, symbol: "MATIC", name: "Matic Network", priceUsd: 1.57, changePercent24Hr: -8.23),
27 | CoinViewModel(id: "ethereum-classic", rank: 14, symbol: "MATIC", name: "Ethereum Classic", priceUsd: 63.85, changePercent24Hr: -4.17)
28 | ]
29 | }
30 |
31 | static func getCoinViewModel() -> CoinViewModel {
32 | CoinViewModel(id: "bitcoin",
33 | rank: 1,
34 | symbol: "BTC",
35 | name: "Bitcoin",
36 | priceUsd: 6929.821775,
37 | changePercent24Hr: -0.81014)
38 | }
39 |
40 | static func getMarketsViewModel() -> [MarketViewModel] {
41 | return [
42 | MarketViewModel(id: "Kraken",
43 | baseSymbol: "BTC",
44 | quoteSymbol: "EUR",
45 | priceUsd: 67211.23,
46 | price: 12321.33),
47 | MarketViewModel(id: "BitShop",
48 | baseSymbol: "BTC",
49 | quoteSymbol: "EUR",
50 | priceUsd: 65211.23,
51 | price: 12341.22),
52 | ]
53 | }
54 |
55 | static func getMarketViewModel() -> MarketViewModel {
56 | MarketViewModel(id: "Kraken",
57 | baseSymbol: "BTC",
58 | quoteSymbol: "EUR",
59 | priceUsd: 67211.23,
60 | price: 12321.33)
61 | }
62 |
63 | static func getAlert() -> Alert {
64 | let alert = Alert(context: CoreDataHandler.preview.container.viewContext)
65 | alert.coinId = "bitcoin"
66 | alert.currency = "USD"
67 | alert.isEnabled = true
68 | alert.isPriceLower = true
69 | alert.price = 3212.21
70 |
71 | return alert
72 | }
73 |
74 | static func getExchangeItem() -> ExchangeItem {
75 | let exchangeItem = ExchangeItem(context: CoreDataHandler.preview.container.viewContext)
76 | exchangeItem.coinId = "bitcoin"
77 | exchangeItem.amount = 2
78 | exchangeItem.currency = "USD"
79 |
80 | return exchangeItem
81 | }
82 |
83 | static func getCurrency() -> Currency {
84 | Currency(id: "united-states-dollar", symbol: "USD", locale: "en-US", name: "US Dollar")
85 | }
86 |
87 | static func getExchangeViewModel() -> ExchangeViewModel {
88 | ExchangeViewModel(coinViewModel: getCoinViewModel(),
89 | exchangeItem: getExchangeItem(),
90 | currency: getCurrency())
91 | }
92 |
93 | static func getAlertViewModel() -> AlertViewModel {
94 | AlertViewModel(coinViewModel: getCoinViewModel(),
95 | alert: getAlert(),
96 | currency: getCurrency())
97 | }
98 |
99 | static func getChartData() -> [Double] {
100 | return [34, 34, 23, 34, 45, 65, 34, 32, 23, 33, 65, 57, 65, 34, 65, 67]
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/TabsViews/AlertsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import UserNotifications
9 | import VirtualCoinKit
10 |
11 | struct AlertsView: View {
12 | @EnvironmentObject private var applicationStateService: ApplicationStateService
13 |
14 | @State private var showingSettingsView = false
15 | @State private var showingAlertView = false
16 |
17 | @FetchRequest(
18 | sortDescriptors: [NSSortDescriptor(keyPath: \Alert.coinId, ascending: true)],
19 | animation: .default
20 | )
21 | private var alerts: FetchedResults
22 |
23 | var body: some View {
24 | self.mainBody()
25 | .navigationTitle("Alerts")
26 | .toolbar {
27 | ToolbarItem(placement: .navigationBarLeading) {
28 | Button(action: {
29 | showingSettingsView.toggle()
30 | }) {
31 | Image(systemName: "switch.2")
32 | }
33 | }
34 | ToolbarItem(placement: .navigationBarTrailing) {
35 | Button(action: {
36 | applicationStateService.selectedAlertViewModel = nil
37 | self.showingAlertView = true
38 | }) {
39 | Image(systemName: "plus")
40 | }
41 | }
42 | }
43 | .sheet(isPresented: $showingSettingsView) {
44 | SettingsView()
45 | }
46 | .sheet(isPresented: $showingAlertView) {
47 | if let selectedAlertViewModel = applicationStateService.selectedAlertViewModel {
48 | EditAlertView(alertViewModel: selectedAlertViewModel).onDisappear {
49 | self.grantNotificationPermission()
50 | }
51 | } else {
52 | AddAlertView()
53 | }
54 | }
55 | }
56 |
57 | @ViewBuilder
58 | private func mainBody() -> some View {
59 | if alerts.count > 0 {
60 | List {
61 | ForEach(alerts, id: \.self) { alert in
62 | let coinViewModelFromApi = applicationStateService.coins.first(where: { coinViewModel in
63 | coinViewModel.id == alert.coinId
64 | })
65 |
66 | let coinViewModel = coinViewModelFromApi ?? CoinViewModel(id: "", rank: 1, symbol: "", name: "", priceUsd: 0, changePercent24Hr: 0)
67 |
68 | let currency = Currencies.allCurrenciesDictionary[alert.currency] ?? Currency()
69 |
70 | let alertViewModel = AlertViewModel(coinViewModel: coinViewModel,
71 | alert: alert,
72 | currency: currency)
73 |
74 | AlertRowView(alertViewModel: alertViewModel) {
75 | applicationStateService.selectedAlertViewModel = alertViewModel
76 | self.showingAlertView = true
77 | }
78 | }.onDelete(perform: self.deleteItem)
79 | }
80 | .listStyle(PlainListStyle())
81 | } else {
82 | NoDataView(title: "No Alerts", subtitle: "Add alert") {
83 | applicationStateService.selectedAlertViewModel = nil
84 | self.showingAlertView = true
85 | }
86 | }
87 | }
88 |
89 | private func deleteItem(at indexSet: IndexSet) {
90 | let alertsHandler = AlertsHandler()
91 |
92 | for index in indexSet {
93 | let alert = self.alerts[index]
94 | alertsHandler.deleteAlertEntity(alert: alert)
95 | }
96 |
97 | CoreDataHandler.shared.save()
98 | }
99 |
100 | private func grantNotificationPermission() {
101 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
102 | if success {
103 | print("Authorization was granted: \(success)")
104 | } else if let error = error {
105 | print(error.localizedDescription)
106 | }
107 | }
108 | }
109 | }
110 |
111 | struct AlertsView_Previews: PreviewProvider {
112 | static var previews: some View {
113 | Group {
114 | NavigationView {
115 | AlertsView()
116 | .environmentObject(ApplicationStateService.preview)
117 | .environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext)
118 | }
119 | .preferredColorScheme(.dark)
120 |
121 | NavigationView {
122 | AlertsView()
123 | .environmentObject(ApplicationStateService.preview)
124 | .environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext)
125 | }
126 | .preferredColorScheme(.light)
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/TabsViews/FavouritesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct FavouritesView: View {
10 | @EnvironmentObject private var applicationStateService: ApplicationStateService
11 | @EnvironmentObject private var coinsService: CoinsService
12 |
13 | @State private var showingSettingsView = false
14 | @State private var state: ViewState = .iddle
15 |
16 | var body: some View {
17 | self.mainBody()
18 | .navigationTitle("Favourites")
19 | .toolbar {
20 | ToolbarItem(placement: .navigationBarLeading) {
21 | Button(action: {
22 | showingSettingsView.toggle()
23 | }) {
24 | Image(systemName: "switch.2")
25 | }
26 | }
27 | }
28 | .sheet(isPresented: $showingSettingsView) {
29 | SettingsView()
30 | }
31 | }
32 |
33 | @ViewBuilder
34 | private func mainBody() -> some View {
35 | switch state {
36 | case .iddle:
37 | self.skeletonView()
38 | .onAppear {
39 | self.load()
40 | }
41 |
42 | case .loading:
43 | self.skeletonView()
44 | case .loaded:
45 | if applicationStateService.favourites.count > 0 {
46 | List {
47 | ForEach(applicationStateService.favourites) { coin in
48 | NavigationLink(destination: CoinView(coin: coin)) {
49 | CoinRowView(coin: coin)
50 | }
51 | }.onMove(perform: self.move)
52 | }
53 | .toolbar {
54 | EditButton()
55 | }
56 | .listStyle(PlainListStyle())
57 | } else {
58 | NoDataView(title: "No Favourites", subtitle: "Select your favourite coins")
59 | }
60 | case .error(let error):
61 | ErrorView(error: error) {
62 | self.load()
63 | }
64 | .padding()
65 | }
66 | }
67 |
68 | private func move(from source: IndexSet, to destination: Int) {
69 | // Get favourites form core data.
70 | let favouritesHandler = FavouritesHandler()
71 | var favourites: [Favourite] = favouritesHandler.getFavourites()
72 |
73 | // Change order in core data array.
74 | favourites.move(fromOffsets: source, toOffset: destination)
75 |
76 | // Update the userOrder attribute in revisedItems to persist the new order.
77 | // This is done in reverse order to minimize changes to the indices.
78 | for reverseIndex in stride(from: favourites.count - 1, through: 0, by: -1 ) {
79 | favourites[reverseIndex].order = Int32(reverseIndex)
80 | }
81 |
82 | // Save new order.
83 | CoreDataHandler.shared.save()
84 |
85 | // Change also order in array stored in application state.
86 | self.applicationStateService.favourites.move(fromOffsets: source, toOffset: destination)
87 | }
88 |
89 | private func skeletonView() -> some View {
90 | List(PreviewData.getCoinsViewModel(), id: \.id) { coin in
91 | NavigationLink(destination: CoinView(coin: coin)) {
92 | CoinRowView(coin: coin)
93 | }
94 | }
95 | .listStyle(PlainListStyle())
96 | .redacted(reason: .placeholder)
97 | }
98 |
99 | private func load() {
100 | if applicationStateService.coins.isEmpty == false {
101 | self.state = .loaded
102 | return
103 | }
104 |
105 | state = .loading
106 |
107 | coinsService.loadCoins(into: applicationStateService) { result in
108 | DispatchQueue.runOnMain {
109 | switch result {
110 | case .success:
111 | self.state = .loaded
112 | break;
113 | case .failure(let error):
114 | self.state = .error(error)
115 | break;
116 | }
117 | }
118 | }
119 | }
120 | }
121 |
122 | struct FavouritesView_Previews: PreviewProvider {
123 | static var previews: some View {
124 | Group {
125 | NavigationView {
126 | FavouritesView()
127 | .environmentObject(ApplicationStateService.preview)
128 | .environmentObject(CoinsService.preview)
129 | .environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext)
130 | }
131 | .preferredColorScheme(.dark)
132 |
133 | NavigationView {
134 | FavouritesView()
135 | .environmentObject(ApplicationStateService.preview)
136 | .environmentObject(CoinsService.preview)
137 | .environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext)
138 | }
139 | .preferredColorScheme(.light)
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/VirtualCoin/Notifications/Notifications.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import UserNotifications
9 | import VirtualCoinKit
10 |
11 | class Notifications {
12 | private let alertsHandler = AlertsHandler()
13 | private let coinCapClient = CoinCapClient()
14 | private var priceAlerts: [String: PriceAlert] = [:]
15 |
16 | func sendNotification(completionHandler: @escaping (Result) -> Void) {
17 | let alerts = self.alertsHandler.getActiveAlerts()
18 |
19 | for alert in alerts {
20 | let alertKey = self.getKey(alert: alert)
21 | if priceAlerts[alertKey] == nil {
22 | priceAlerts[alertKey] = PriceAlert(currency: alert.currency,
23 | coinId: alert.coinId)
24 | }
25 | }
26 |
27 | let notificationsGroup = DispatchGroup()
28 |
29 | for priceAlert in priceAlerts {
30 | notificationsGroup.enter()
31 | priceAlert.value.processing = Processing.processing
32 |
33 | guard let currency = Currencies.allCurrenciesDictionary[priceAlert.value.currency] else {
34 | completionHandler(Result.failure(NotificationsError.notRecognizedCurrencySymbol))
35 | return
36 | }
37 |
38 | self.coinCapClient.getCoinPriceAsync(for: priceAlert.value.coinId, currencyId: currency.id) { result in
39 | switch result {
40 | case .success(let price):
41 | priceAlert.value.price = price
42 | priceAlert.value.processing = Processing.finished
43 | case .failure(let error):
44 | print(error.localizedDescription)
45 | }
46 |
47 | notificationsGroup.leave()
48 | }
49 | }
50 |
51 | notificationsGroup.notify(queue: .main) {
52 | self.processAlerts(alerts: alerts)
53 | completionHandler(Result.success(()))
54 | }
55 | }
56 |
57 | private func processAlerts(alerts: [Alert]) {
58 | for alert in alerts {
59 | let alertKey = self.getKey(alert: alert)
60 | if let priceAlert = priceAlerts[alertKey] {
61 | self.processAlert(alert: alert, price: priceAlert.price)
62 | }
63 | }
64 | }
65 |
66 | private func processAlert(alert: Alert, price: Double?) {
67 | guard let price = price else {
68 | return
69 | }
70 |
71 | if alert.isPriceLower && price < alert.price {
72 | let center = UNUserNotificationCenter.current()
73 | center.getNotificationSettings { settings in
74 | self.notifyAboutLowerPrices(settings: settings, center: center, alert: alert, price: price)
75 | }
76 | } else if !alert.isPriceLower && price > alert.price {
77 | let center = UNUserNotificationCenter.current()
78 | center.getNotificationSettings { settings in
79 | self.notifyAboutHigherPrices(settings: settings, center: center, alert: alert, price: price)
80 | }
81 | }
82 | }
83 |
84 | private func notifyAboutLowerPrices(settings: UNNotificationSettings, center: UNUserNotificationCenter, alert: Alert, price: Double) {
85 | if settings.authorizationStatus == .authorized {
86 | let body = "Currency price is \(price.toFormattedPrice(currency: alert.currency)) lower than: \(alert.price.toFormattedPrice(currency: alert.currency))"
87 |
88 | self.sendNotification(center: center,
89 | title: alert.coinSymbol,
90 | body: body)
91 |
92 | alert.alertSentDate = Date()
93 | CoreDataHandler.shared.save()
94 | }
95 | }
96 |
97 | private func notifyAboutHigherPrices(settings: UNNotificationSettings, center: UNUserNotificationCenter, alert: Alert, price: Double) {
98 | if settings.authorizationStatus == .authorized {
99 | let body = "Currency price is \(price.toFormattedPrice(currency: alert.currency)) higher than: \(alert.price.toFormattedPrice(currency: alert.currency))"
100 |
101 | self.sendNotification(center: center,
102 | title: alert.coinSymbol,
103 | body: body)
104 |
105 | alert.alertSentDate = Date()
106 | CoreDataHandler.shared.save()
107 | }
108 | }
109 |
110 | private func sendNotification(center: UNUserNotificationCenter, title: String, body: String) {
111 | let content = UNMutableNotificationContent()
112 | content.title = title
113 | content.body = body
114 | content.sound = UNNotificationSound.default
115 |
116 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
117 |
118 | let identifier = "VCoinLocalNotification"
119 | let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
120 |
121 | center.add(request, withCompletionHandler: { error in
122 | if let error = error {
123 | print("Error during sending notification \(error)")
124 | }
125 | })
126 | }
127 |
128 | private func getKey(alert: Alert) -> String {
129 | return self.getKey(currency: alert.currency, coinId: alert.coinId)
130 | }
131 |
132 | private func getKey(currency: String, coinId: String) -> String {
133 | return "\(currency)|\(coinId)"
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/VirtualCoinWidget/DataFetcher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import Foundation
8 | import VirtualCoinKit
9 |
10 | class DataFetcher {
11 | func getCoins(completionHandler: @escaping (Result<[WidgetViewModel], RestClientError>) -> Void) {
12 | let settingsHandler = SettingsHandler()
13 | let defaultSettings = settingsHandler.getDefaultSettings()
14 |
15 | let coinCapClient = CoinCapClient()
16 | guard let currency = Currencies.allCurrenciesDictionary[defaultSettings.currency] else {
17 | completionHandler(.success([]))
18 | return
19 | }
20 |
21 | coinCapClient.getCurrencyRate(for: currency.id) { currencyResult in
22 | switch currencyResult {
23 | case .success(let currencyRate):
24 | let currencyRateUsd = Double(currencyRate.rateUsd) ?? 1.0
25 | self.downloadCoins(currencyRateUsd: currencyRateUsd, completionHandler: completionHandler)
26 | break
27 | case .failure(let error):
28 | completionHandler(.failure(error))
29 | break
30 | }
31 | }
32 |
33 | }
34 |
35 | private func downloadCoins(currencyRateUsd: Double, completionHandler: @escaping (Result<[WidgetViewModel], RestClientError>) -> Void) {
36 | var results: [WidgetViewModel] = []
37 | let coinOrders = self.getCoinOrders()
38 | let coinCapClient = CoinCapClient()
39 | let downloadCoinsGroup = DispatchGroup()
40 |
41 | for coinOrder in coinOrders {
42 | downloadCoinsGroup.enter()
43 |
44 | coinCapClient.getCoinAsync(for: coinOrder.coinId) { coinResult in
45 | switch coinResult {
46 | case .success(let coin):
47 |
48 | let priceUsd = Double(coin.priceUsd ?? "") ?? 0.0
49 | let changePercent24Hr = Double(coin.changePercent24Hr ?? "") ?? 0.0
50 |
51 | results.append(WidgetViewModel(id: coinOrder.coinId,
52 | order: coinOrder.order,
53 | rank: Int(coin.rank) ?? 0,
54 | symbol: coin.symbol,
55 | name: coin.name,
56 | priceUsd: priceUsd,
57 | changePercent24Hr: changePercent24Hr,
58 | price: priceUsd / currencyRateUsd,
59 | chart: []))
60 |
61 | break
62 | case .failure(let error):
63 | completionHandler(.failure(error))
64 | break
65 | }
66 |
67 | downloadCoinsGroup.leave()
68 | }
69 | }
70 |
71 | downloadCoinsGroup.notify(queue: .main) {
72 | self.downloadCharts(results: results, completionHandler: completionHandler)
73 | }
74 | }
75 |
76 | private func downloadCharts(results: [WidgetViewModel], completionHandler: @escaping (Result<[WidgetViewModel], RestClientError>) -> Void) {
77 | var resultsMutating = results
78 | let downloadChartsGroup = DispatchGroup()
79 | let coinCapClient = CoinCapClient()
80 |
81 | for resultIndex in resultsMutating.indices {
82 |
83 | downloadChartsGroup.enter()
84 | coinCapClient.getChartValuesAsync(for: resultsMutating[resultIndex].id, withRange: .hour) { chartResult in
85 | switch chartResult {
86 | case .success(let chartValues):
87 |
88 | var dataResult: [Double] = []
89 | for chartValue in chartValues {
90 | if let value = Double(chartValue.priceUsd) {
91 | dataResult.append(value)
92 | }
93 | }
94 |
95 | resultsMutating[resultIndex].chart = dataResult
96 |
97 | break
98 | case .failure(let error):
99 | completionHandler(.failure(error))
100 | break
101 | }
102 |
103 | downloadChartsGroup.leave()
104 | }
105 | }
106 |
107 | downloadChartsGroup.notify(queue: .main) {
108 | let resultOrdered = resultsMutating.sorted(by: { lhs, rhs in
109 | lhs.order < rhs.order
110 | })
111 |
112 | completionHandler(.success(resultOrdered))
113 | }
114 | }
115 |
116 | private func getCoinOrders() -> [CoinOrder] {
117 | let favouritesHandler = FavouritesHandler()
118 | let favourites = favouritesHandler.getFavourites()
119 |
120 | // Get coins from favourites.
121 | var coinOrders = favourites.map { favourite in
122 | CoinOrder(coinId: favourite.coinId, order: favourite.order)
123 | }
124 |
125 | var maxOrder = coinOrders.map { coinOrder in coinOrder.order }.max() ?? 0
126 |
127 | // Add other higher rank coins for nice widget.
128 | for widgetViewModel in PreviewData.getWidgetViewModels() {
129 | if coinOrders.contains(where: { coinOrder in coinOrder.coinId == widgetViewModel.id }) {
130 | continue
131 | }
132 |
133 | coinOrders.append(CoinOrder(coinId: widgetViewModel.id, order: maxOrder))
134 | maxOrder = maxOrder + 1
135 | }
136 |
137 | return coinOrders
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/VirtualCoin/Views/ScreenViews/CoinView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // https://mczachurski.dev
3 | // Copyright © 2021 Marcin Czachurski and the repository contributors.
4 | // Licensed under the MIT License.
5 | //
6 |
7 | import SwiftUI
8 | import VirtualCoinKit
9 |
10 | struct CoinView: View {
11 | @EnvironmentObject var applicationStateService: ApplicationStateService
12 | @EnvironmentObject var coinsService: CoinsService
13 |
14 | @ObservedObject public var coin: CoinViewModel
15 |
16 | @State private var state: ViewState = .iddle
17 | @State private var selectedTab: ChartTimeRange = .hour
18 | @State private var isShowingMarketsView = false
19 |
20 | @Setting(\.currency) private var currencySymbol: String
21 |
22 | var body: some View {
23 | VStack {
24 | HStack {
25 | CoinImageView(coin: coin)
26 | Text(coin.name)
27 | .font(.largeTitle)
28 | .fontWeight(.thin)
29 | }.padding(.trailing, 32)
30 |
31 | Text(coin.symbol)
32 | .font(.title2)
33 | .fontWeight(.light)
34 | .foregroundColor(.gray)
35 |
36 | Text(coin.price.toFormattedPrice(currency: currencySymbol))
37 | .fontWeight(.light)
38 | .font(.title)
39 |
40 | Text(coin.changePercent24Hr.toFormattedPercent())
41 | .font(.body)
42 | .foregroundColor(coin.changePercent24Hr > 0 ? .greenPastel : .redPastel)
43 |
44 | VStack {
45 | Picker("", selection: $selectedTab) {
46 | Text(ChartTimeRange.hour.rawValue).tag(ChartTimeRange.hour)
47 | Text(ChartTimeRange.day.rawValue).tag(ChartTimeRange.day)
48 | Text(ChartTimeRange.week.rawValue).tag(ChartTimeRange.week)
49 | Text(ChartTimeRange.month.rawValue).tag(ChartTimeRange.month)
50 | Text(ChartTimeRange.year.rawValue).tag(ChartTimeRange.year)
51 | }
52 | .pickerStyle(SegmentedPickerStyle())
53 | .padding()
54 |
55 | switch(selectedTab) {
56 | case .hour:
57 | ChartView(chartTimeRange: .hour, coin: coin)
58 | .frame(maxWidth: .infinity, maxHeight: .infinity)
59 | case .day:
60 | ChartView(chartTimeRange: .day, coin: coin)
61 | .frame(maxWidth: .infinity, maxHeight: .infinity)
62 | case .week:
63 | ChartView(chartTimeRange: .week, coin: coin)
64 | .frame(maxWidth: .infinity, maxHeight: .infinity)
65 | case .month:
66 | ChartView(chartTimeRange: .month, coin: coin)
67 | .frame(maxWidth: .infinity, maxHeight: .infinity)
68 | case .year:
69 | ChartView(chartTimeRange: .year, coin: coin)
70 | .frame(maxWidth: .infinity, maxHeight: .infinity)
71 | }
72 | }
73 | }
74 | .toolbar {
75 | ToolbarItemGroup(placement: .navigationBarTrailing) {
76 | Button(action: {
77 | isShowingMarketsView.toggle()
78 | }) {
79 | Image(systemName: "globe")
80 | }
81 | .disabled(self.state != .loaded)
82 |
83 | Button(action: {
84 | self.toggleFavourite();
85 | }) {
86 | Image(systemName: coin.isFavourite ? "star.fill" : "star")
87 | }
88 | }
89 | }
90 | .sheet(isPresented: $isShowingMarketsView) {
91 | MarketsView(markets: self.applicationStateService.markets)
92 | }
93 | .onAppear {
94 | self.load()
95 | }
96 | }
97 |
98 | private func load() {
99 | state = .loading
100 |
101 | coinsService.loadMarketValues(into: applicationStateService,
102 | coin: coin) { result in
103 | DispatchQueue.runOnMain {
104 | switch result {
105 | case .success:
106 | self.state = .loaded
107 | break;
108 | case .failure(let error):
109 | self.state = .error(error)
110 | break;
111 | }
112 | }
113 | }
114 | }
115 |
116 | private func toggleFavourite() {
117 | let favouritesHandler = FavouritesHandler()
118 |
119 | if favouritesHandler.isFavourite(coinId: coin.id) {
120 | self.coin.isFavourite = false
121 | self.applicationStateService.removeFromFavourites(coinViewModel: coin)
122 |
123 | favouritesHandler.deleteFavouriteEntity(coinId: coin.id)
124 | } else {
125 | self.coin.isFavourite = true
126 | self.applicationStateService.addToFavourites(coinViewModel: coin)
127 |
128 | let favouriteEntity = favouritesHandler.createFavouriteEntity()
129 | favouriteEntity.coinId = coin.id
130 | }
131 |
132 | CoreDataHandler.shared.save()
133 | }
134 | }
135 |
136 | struct CoinView_Previews: PreviewProvider {
137 | static var previews: some View {
138 | Group {
139 | NavigationView {
140 | CoinView(coin: PreviewData.getCoinViewModel())
141 | .environmentObject(ApplicationStateService.preview)
142 | .environmentObject(CoinsService.preview)
143 | }
144 | .preferredColorScheme(.dark)
145 |
146 | NavigationView {
147 | CoinView(coin: PreviewData.getCoinViewModel())
148 | .environmentObject(ApplicationStateService.preview)
149 | .environmentObject(CoinsService.preview)
150 | }
151 | .preferredColorScheme(.light)
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------