diff --git a/.gitignore b/.gitignore index 0ea58a5..90e4c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,5 @@ Derived/ *.p12 master.key *.package.resolved + +GoogleService-Info.plist \ No newline at end of file diff --git a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift index 07275d7..79e4aa4 100644 --- a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift +++ b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Taget+Extensions/TargetDependency+Module.swift @@ -119,7 +119,8 @@ public extension [TargetDependency] { ] case .Folio: return [ - .external(name: "GoogleMobileAds") + .external(name: "GoogleMobileAds"), + .external(name: "FirebaseAnalytics") ] default: return [] } diff --git a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift index 16eaf13..3858a8f 100644 --- a/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift +++ b/Plugins/ModulePlugin/ProjectDescriptionHelpers/Target+Module.swift @@ -309,7 +309,7 @@ public extension Target { deploymentTarget: .shared(product), infoPlist: .shared(product), sources: .path(type: .implement), - resources: nil, + resources: .path(type: .implement), copyFiles: nil, headers: nil, entitlements: nil, @@ -341,7 +341,7 @@ public extension Target { deploymentTarget: .shared(product, module: module), infoPlist: .shared(product, module: module), sources: .path(type: type), - resources: nil, + resources: .path(type: .implement), copyFiles: nil, headers: nil, entitlements: nil, diff --git a/Projects/Dying/.package.resolved b/Projects/Dying/.package.resolved deleted file mode 100644 index 03774b2..0000000 --- a/Projects/Dying/.package.resolved +++ /dev/null @@ -1,104 +0,0 @@ -{ - "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" - } - }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture", - "state" : { - "revision" : "195284b94b799b326729640453f547f08892293a", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "ea631ce892687f5432a833312292b80db238186a", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-identified-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections", - "state" : { - "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" - } - }, - { - "identity" : "swiftui-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation", - "state" : { - "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34", - "version" : "1.0.0" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7", - "version" : "1.0.0" - } - } - ], - "version" : 2 -} diff --git a/Projects/Folio/.package.resolved b/Projects/Folio/.package.resolved deleted file mode 100644 index 27d02ec..0000000 --- a/Projects/Folio/.package.resolved +++ /dev/null @@ -1,158 +0,0 @@ -{ - "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" - } - }, - { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "03b9beee1a61f62d32c521e172e192a1663a5e8b", - "version" : "10.13.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "c38ce365d77b04a9a300c31061c5227589e5597b", - "version" : "7.11.5" - } - }, - { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", - "version" : "2.30909.0" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", - "version" : "2.3.1" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" - } - }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture", - "state" : { - "revision" : "a7c1f799b55ecb418f85094b142565834f7ee7c7", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "ea631ce892687f5432a833312292b80db238186a", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-identified-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections", - "state" : { - "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-package-manager-google-mobile-ads", - "kind" : "remoteSourceControl", - "location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads", - "state" : { - "revision" : "1a6faf6b9b82ddf8780f678745381b8628711077", - "version" : "10.9.0" - } - }, - { - "identity" : "swift-package-manager-google-user-messaging-platform", - "kind" : "remoteSourceControl", - "location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git", - "state" : { - "revision" : "129fa838520cd02174f890ae0cfe0242e60714ae", - "version" : "2.1.0" - } - }, - { - "identity" : "swiftui-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation", - "state" : { - "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34", - "version" : "1.0.0" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version" : "1.0.2" - } - } - ], - "version" : 2 -} diff --git a/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift b/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift index 3bbeee9..d7f9c63 100644 --- a/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift +++ b/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift @@ -8,7 +8,61 @@ import SwiftUI public extension Color { - static func blackOrWhite(_ isSelected: Bool = false) -> Self { - return isSelected ? Color(uiColor: .label) : Color(uiColor: .systemBackground) + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + self.init( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } + + static var foreground: Self { + return Color(uiColor: .label) + } + + static var background: Self { + return Color(uiColor: .systemBackground) + } + +// static func blackOrWhite(_ isSelected: Bool = false) -> Self { +// return isSelected ? Color(uiColor: .label) : Color(uiColor: .systemBackground) +// } + + func toHex() -> String { + let uic = UIColor(self) + guard let components = uic.cgColor.components, components.count >= 3 else { + return "" + } + let r = Float(components[0]) + let g = Float(components[1]) + let b = Float(components[2]) + var a = Float(1.0) + + if components.count >= 4 { + a = Float(components[3]) + } + + if a != Float(1.0) { + return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) + } else { + return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) + } } } diff --git a/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift b/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift index 57471eb..af11440 100644 --- a/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift +++ b/Projects/Folio/Shared/DesignSystem/Sources/MinimalButton.swift @@ -40,7 +40,7 @@ public struct MinimalButton: View { } .padding(.vertical, 10) }) - .background(.black) + .background(isActive ? Color.foreground : Color.foreground) //TODO: active 현재 미사용 .clipShape( RoundedRectangle( cornerRadius: 8, diff --git a/Projects/Toolinder/.package.resolved b/Projects/Toolinder/.package.resolved deleted file mode 100644 index 6963374..0000000 --- a/Projects/Toolinder/.package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "realm-core", - "kind" : "remoteSourceControl", - "location" : "https://github.com/realm/realm-core", - "state" : { - "revision" : "c04f5e401a1ec682e6b08b1ee157e19a0f834a5f", - "version" : "13.17.1" - } - }, - { - "identity" : "realm-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/realm/realm-swift", - "state" : { - "revision" : "330a239712af77a3b0926b9ffa9582302a0b9923", - "version" : "10.42.1" - } - } - ], - "version" : 2 -} diff --git a/Projects/Toolinder/App/Resources/Localizable.xcstrings b/Projects/Toolinder/App/Resources/Localizable.xcstrings new file mode 100644 index 0000000..62ecc38 --- /dev/null +++ b/Projects/Toolinder/App/Resources/Localizable.xcstrings @@ -0,0 +1,142 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Buy" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "売り" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "매수" + } + } + } + }, + "Edit" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "編集" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "편집" + } + } + } + }, + "History" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "記録" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "기록" + } + } + } + }, + "Save" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장" + } + } + } + }, + "Sell" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "買い" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "매도" + } + } + } + }, + "Summary" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "概略" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "요약" + } + } + } + }, + "Ticker" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "銘柄" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "종목" + } + } + } + }, + "Trade" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "商売" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "거래" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Projects/Toolinder/App/Sources/AppDelegate.swift b/Projects/Toolinder/App/Sources/AppDelegate.swift index 7614bed..508b59d 100644 --- a/Projects/Toolinder/App/Sources/AppDelegate.swift +++ b/Projects/Toolinder/App/Sources/AppDelegate.swift @@ -7,6 +7,8 @@ // import UIKit +import SwiftUI +import FirebaseCore import GoogleMobileAds @@ -22,7 +24,8 @@ class AppDelegate: NSObject, UIApplicationDelegate { GADMobileAds.sharedInstance().start(completionHandler: nil) } } - + + FirebaseApp.configure() return true } diff --git a/Projects/Toolinder/App/Sources/RootApp.swift b/Projects/Toolinder/App/Sources/RootApp.swift index 169eb73..ef2b685 100644 --- a/Projects/Toolinder/App/Sources/RootApp.swift +++ b/Projects/Toolinder/App/Sources/RootApp.swift @@ -15,15 +15,8 @@ import ToolinderDomain @main struct RootApp: App { - let modelContainer: ModelContainer + @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate - init() { - do { - modelContainer = try ModelContainer(for: Ticker.self, Trade.self) - } catch { - fatalError("Could not initialize ModelContainer \(error)") - } - } var body: some Scene { WindowGroup { RootView( @@ -32,8 +25,12 @@ struct RootApp: App { ._printChanges() } ) + .modelContainer(for: [ + Ticker.self, + Trade.self, + Tag.self + ]) .onAppear(perform: UIApplication.shared.hideKeyboard) } - .modelContainer(modelContainer) } } diff --git a/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist b/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist index 16f5986..e4e5ae3 100644 --- a/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist +++ b/Projects/Toolinder/App/Support/ToolinderAppIOS-Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Toolinder + Toff CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/Projects/Toolinder/App/ToolinderIOS.entitlements b/Projects/Toolinder/App/ToolinderIOS.entitlements index 90bda52..9c1f471 100644 --- a/Projects/Toolinder/App/ToolinderIOS.entitlements +++ b/Projects/Toolinder/App/ToolinderIOS.entitlements @@ -2,17 +2,17 @@ - com.apple.security.app-sandbox - aps-environment development com.apple.developer.icloud-container-identifiers - iCloud.toolinder + iCloud.com.tamsadan.toolinder com.apple.developer.icloud-services CloudKit + com.apple.security.app-sandbox + diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TagDTO.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TagDTO.swift new file mode 100644 index 0000000..de18175 --- /dev/null +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TagDTO.swift @@ -0,0 +1,34 @@ +// +// TagDTO.swift +// ToolinderDomainTradeInterface +// +// Created by 송영모 on 2023/10/01. +// + +import Foundation +import SwiftData +import SwiftUI + +public class TagDTO { + public var id: UUID = UUID() + public var hex: String = "" + public var name: String = "" + + public init( + id: UUID = .init(), + hex: String, + name: String + ) { + self.id = id + self.hex = hex + self.name = name + } + + func toDomain() -> Tag { + return Tag( + id: id, + hex: hex, + name: name + ) + } +} diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift index 96a995b..1f7b2fd 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TickerDTO.swift @@ -9,25 +9,33 @@ import Foundation import SwiftData public class TickerDTO { + public let id: UUID public var type: TickerType public var currency: Currency public var name: String + public var tags: [Tag] public init( + id: UUID = .init(), type: TickerType, currency: Currency, - name: String + name: String, + tags: [Tag] ) { + self.id = id self.type = type self.currency = currency self.name = name + self.tags = tags } func toDomain() -> Ticker { return Ticker( + id: id, type: type, currency: currency, - name: name + name: name, + tags: tags ) } } diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift index 8ea8e65..d4b9f9e 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/DTO/TradeDTO.swift @@ -9,9 +9,10 @@ import Foundation import SwiftData public struct TradeDTO { + public let id: UUID public var side: TradeSide public var price: Double - public var volume: Double + public var quantity: Double public var fee: Double public var images: [Data] public var note: String @@ -20,19 +21,21 @@ public struct TradeDTO { public var ticker: Ticker? public init( + id: UUID = .init(), side: TradeSide = .buy, price: Double = 0, - volume: Double = 0, + quantity: Double = 0, fee: Double = 0, images: [Data] = [], note: String = "", date: Date = .now, ticker: Ticker ) { + self.id = id self.side = side self.images = images self.price = price - self.volume = volume + self.quantity = quantity self.fee = fee self.note = note self.date = date @@ -41,9 +44,10 @@ public struct TradeDTO { func toDomain() -> Trade { return Trade( + id: id, side: side, price: price, - volume: volume, + quantity: quantity, fee: fee, images: images, note: note, diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift index 9cc31ed..f9343d4 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerSummaryDataEntity.swift @@ -31,12 +31,12 @@ public extension Ticker { for trade in trades { switch trade.side { case .buy: - totalAmount += trade.price * trade.volume - buyVolume += trade.volume + totalAmount += trade.price * trade.quantity + buyVolume += trade.quantity case .sell: - totalAmount -= avgPrice * trade.volume - sellVoume += trade.volume + totalAmount -= avgPrice * trade.quantity + sellVoume += trade.quantity } totalVolume = buyVolume - sellVoume @@ -47,7 +47,7 @@ public extension Ticker { // 수익 계산 if trade.side == .sell { - profit += (trade.price - avgPrice) * trade.volume + profit += (trade.price - avgPrice) * trade.quantity } } // 수익률 계산 diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift index f45e408..60aaba1 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Entity/TickerTypeChartDataEntity.swift @@ -19,7 +19,7 @@ public extension [Trade] { return TickerType.allCases.map { type in let trades = self.filter({ $0.ticker?.type == type }) let hold = trades.map { trade in - let sum: Double = trade.price * trade.volume + let sum: Double = trade.price * trade.quantity let sign: Double = trade.side == .buy ? 1.0 : -1.0 return sum * sign diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift index d159c4c..dd4a1e7 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Model/TradeSide.swift @@ -8,6 +8,6 @@ import Foundation public enum TradeSide: String, Codable, CaseIterable { - case buy - case sell + case buy = "Buy" + case sell = "Sell" } diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Tag.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Tag.swift new file mode 100644 index 0000000..eb98edb --- /dev/null +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Tag.swift @@ -0,0 +1,29 @@ +// +// HashTag.swift +// ToolinderDomainTradeInterface +// +// Created by 송영모 on 2023/09/30. +// + +import Foundation +import SwiftData +import SwiftUI + +@Model +public class Tag { + public let id: UUID = UUID() + public var hex: String = "" + public var name: String = "" + + @Relationship public var tickers: [Ticker]? = [] + + public init( + id: UUID = .init(), + hex: String, + name: String + ) { + self.id = id + self.hex = hex + self.name = name + } +} diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift index 5475a9b..23f2897 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Ticker.swift @@ -10,19 +10,25 @@ import SwiftData @Model public class Ticker { + public let id: UUID = UUID() public var type: TickerType = TickerType.stock public var currency: Currency = Currency.dollar public var name: String = "" @Relationship(deleteRule: .cascade, inverse: \Trade.ticker) public var trades: [Trade]? = [] + @Relationship(inverse: \Tag.tickers) public var tags: [Tag]? = [] public init( + id: UUID = .init(), type: TickerType, currency: Currency, - name: String + name: String, + tags: [Tag] ) { + self.id = id self.type = type self.currency = currency self.name = name + self.tags = tags } } diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift index e91b9aa..8257fc1 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/PersistentModel/Trade.swift @@ -10,9 +10,10 @@ import SwiftData @Model public class Trade { + public let id: UUID = UUID() public var side: TradeSide = TradeSide.buy public var price: Double = 0 - public var volume: Double = 0 + public var quantity: Double = 0 public var fee: Double = 0 public var images: [Data] = [] public var note: String = "" @@ -21,19 +22,21 @@ public class Trade { @Relationship public var ticker: Ticker? public init( + id: UUID = .init(), side: TradeSide, price: Double, - volume: Double, + quantity: Double, fee: Double, images: [Data], note: String, date: Date, ticker: Ticker? ) { + self.id = id self.side = side self.images = images self.price = price - self.volume = volume + self.quantity = quantity self.fee = fee self.note = note self.date = date diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift index a2706db..812ff4e 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/StorageContainer.swift @@ -16,8 +16,7 @@ public class StorageContainer { private init() { do { - container = try ModelContainer(for: Ticker.self, Trade.self) - + container = try ModelContainer(for: Ticker.self, Trade.self, Tag.self) if let container { context = ModelContext(container) } diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TagRepository.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TagRepository.swift new file mode 100644 index 0000000..5530a0d --- /dev/null +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TagRepository.swift @@ -0,0 +1,76 @@ +// +// TagRepository.swift +// ToolinderDomainTradeInterface +// +// Created by 송영모 on 2023/10/01. +// + +import Foundation +import SwiftData + +public protocol TagRepositoryInterface { + func fetchTags(descriptor: FetchDescriptor) -> Result<[Tag], TagError> + func saveTag(_ tag: TagDTO) -> Result + func updateTag(_ tag: Tag, new newTag: TagDTO) -> Result + func deleteTag(_ tag: Tag) -> Result + + func isValidatedSaveTag(_ tag: TagDTO) -> Bool + func isValidatedUpdateTag(_ tag: Tag, new newTag: TagDTO) -> Bool + func isValidatedDeleteTag(_ tag: Tag) -> Bool +} + +public class TagRepository: TagRepositoryInterface { + private var context: ModelContext? = StorageContainer.shared.context + + public init() { } + + public func fetchTags(descriptor: FetchDescriptor) -> Result<[Tag], TagError> { + if let tags = try? context?.fetch(descriptor) { + return .success(tags) + } else { + return .failure(.unknown) + } + } + + public func saveTag(_ tag: TagDTO) -> Result { + if isValidatedSaveTag(tag) { + let tag = tag.toDomain() + context?.insert(tag) + return .success(tag) + } else { + return .failure(.unknown) + } + } + + public func updateTag(_ tag: Tag, new newTag: TagDTO) -> Result { + if isValidatedUpdateTag(tag, new: newTag) { + let tag = tag + tag.hex = newTag.hex + tag.name = newTag.name + return .success(tag) + } else { + return .failure(.unknown) + } + } + + public func deleteTag(_ tag: Tag) -> Result { + if isValidatedDeleteTag(tag) { + context?.delete(tag) + return .success(tag) + } else { + return .failure(.unknown) + } + } + + public func isValidatedSaveTag(_ tag: TagDTO) -> Bool { + return true + } + + public func isValidatedUpdateTag(_ tag: Tag, new newTag: TagDTO) -> Bool { + return true + } + + public func isValidatedDeleteTag(_ tag: Tag) -> Bool { + return true + } +} diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift index 3471bdf..d29209a 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TickerRepository.swift @@ -46,6 +46,7 @@ public class TickerRepository: TickerRepositoryInterface { ticker.type = newTicker.type ticker.currency = newTicker.currency ticker.name = newTicker.name + ticker.tags = newTicker.tags return .success(ticker) } else { return .failure(.unknown) diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift index 002d086..b34b012 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/Repository/TradeRepository.swift @@ -10,13 +10,13 @@ import SwiftData public protocol TradeRepositoryInterface { func fetchTrades(descriptor: FetchDescriptor) -> Result<[Trade], TradeError> - func saveTrade(dto: TradeDTO) -> Result - func updateTrade(model: Trade, dto: TradeDTO) -> Result - func deleteTrade(trade: Trade) -> Result + func saveTrade(_ trade: TradeDTO) -> Result + func updateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Result + func deleteTrade(_ trade: Trade) -> Result - func isValidatedSaveTrade(dto: TradeDTO) -> Bool - func isValidatedUpdateTrade(origin: Trade, new: TradeDTO) -> Bool - func isValidatedDeleteTrade(origin: Trade) -> Bool + func isValidatedSaveTrade(_ trade: TradeDTO) -> Bool + func isValidatedUpdateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Bool + func isValidatedDeleteTrade(_ trade: Trade) -> Bool } public class TradeRepository: TradeRepositoryInterface { @@ -24,44 +24,6 @@ public class TradeRepository: TradeRepositoryInterface { public init() { } - public func fetchTickers(descriptor: FetchDescriptor) -> Result<[Ticker], TradeError> { - if let tickers = try? context?.fetch(descriptor) { - return .success(tickers) - } else { - return .failure(.unknown) - } - } - - public func saveTicker(ticker: Ticker) -> Result { - if isValidatedSaveTicker(new: ticker) { - context?.insert(ticker) - return .success(ticker) - } else { - return .failure(.unknown) - } - } - - public func updateTicker(model: Ticker, dto: TickerDTO) -> Result { - if isValidatedUpdateTicker(origin: model, new: dto) { - let ticker = model - ticker.type = dto.type - ticker.currency = dto.currency - ticker.name = dto.name - return .success(ticker) - } else { - return .failure(.unknown) - } - } - - public func deleteTicker(ticker: Ticker) -> Result { - if isValidatedDeleteTicker(origin: ticker) { - context?.delete(ticker) - return .success(ticker) - } else { - return .failure(.unknown) - } - } - public func fetchTrades(descriptor: FetchDescriptor) -> Result<[Trade], TradeError> { if let trades = try? context?.fetch(descriptor) { return .success(trades) @@ -70,9 +32,9 @@ public class TradeRepository: TradeRepositoryInterface { } } - public func saveTrade(dto: TradeDTO) -> Result { - if isValidatedSaveTrade(dto: dto) { - let trade = dto.toDomain() + public func saveTrade(_ trade: TradeDTO) -> Result { + if isValidatedSaveTrade(trade) { + let trade = trade.toDomain() context?.insert(trade) return .success(trade) } else { @@ -80,24 +42,24 @@ public class TradeRepository: TradeRepositoryInterface { } } - public func updateTrade(model: Trade, dto: TradeDTO) -> Result { - if isValidatedUpdateTrade(origin: model, new: dto) { - let trade = model - trade.ticker = dto.ticker - trade.date = dto.date - trade.side = dto.side - trade.price = dto.price - trade.volume = dto.volume - trade.images = dto.images - trade.note = dto.note + public func updateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Result { + if isValidatedUpdateTrade(trade, new: newTrade) { + let trade = trade + trade.ticker = newTrade.ticker + trade.date = newTrade.date + trade.side = newTrade.side + trade.price = newTrade.price + trade.quantity = newTrade.quantity + trade.images = newTrade.images + trade.note = newTrade.note return .success(trade) } else { return .failure(.unknown) } } - public func deleteTrade(trade: Trade) -> Result { - if isValidatedDeleteTrade(origin: trade) { + public func deleteTrade(_ trade: Trade) -> Result { + if isValidatedDeleteTrade(trade) { context?.delete(trade) return .success(trade) } else { @@ -105,79 +67,60 @@ public class TradeRepository: TradeRepositoryInterface { } } - public func isValidatedSaveTicker(new: Ticker) -> Bool { - return true - } - - public func isValidatedUpdateTicker(origin: Ticker, new: TickerDTO) -> Bool { - if new.type != nil && new.currency != nil && new.name.isEmpty == false { - return true - } - return false - } - - public func isValidatedDeleteTicker(origin: Ticker) -> Bool { - return true - } - - public func isValidatedSaveTrade(dto: TradeDTO) -> Bool { - if dto.side == .buy { return true } - - guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == dto.ticker }) + public func isValidatedSaveTrade(_ trade: TradeDTO) -> Bool { + if trade.side == .buy { return true } + guard let trades = try? fetchTrades(descriptor: .init()).get().filter({ $0.ticker == trade.ticker }).sorted(by: { $0.date < $1.date }) else { return false } let currentVolume = trades.reduce(0) { (result, trade) in if trade.side == .buy { - return result + (trade.volume ?? 0) + return result + trade.quantity } else { - return result - (trade.volume ?? 0) + return result - trade.quantity } } - return currentVolume - (dto.volume ?? 0) > 0 + return currentVolume - trade.quantity >= 0 } - public func isValidatedUpdateTrade(origin: Trade, new: TradeDTO) -> Bool { - if origin.side == .sell && new.side == .buy { return true } - - guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == origin.ticker }) + public func isValidatedUpdateTrade(_ trade: Trade, new newTrade: TradeDTO) -> Bool { + if trade.side == .sell && newTrade.side == .buy { return true } + guard let trades = try? fetchTrades(descriptor: .init()).get().filter({ $0.ticker == trade.ticker }).sorted(by: { $0.date < $1.date }) else { return false } var currentVolume = 0.0 - for trade in trades { + for tmpTrade in trades { if trade.side == .buy { - currentVolume += trade == origin ? (new.volume ?? 0) : (trade.volume ?? 0) + currentVolume += tmpTrade == trade ? newTrade.quantity : tmpTrade.quantity } else { - currentVolume -= trade == origin ? (new.volume ?? 0) : (trade.volume ?? 0) + currentVolume -= tmpTrade == trade ? newTrade.quantity : tmpTrade.quantity } if currentVolume < 0 { return false } } - return true } - public func isValidatedDeleteTrade(origin: Trade) -> Bool { - if origin.side == .sell { return true } + public func isValidatedDeleteTrade(_ trade: Trade) -> Bool { + if trade.side == .sell { return true } - guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == origin.ticker }) + guard let trades = try? fetchTrades(descriptor: .init(sortBy: [.init(\.date)])).get().filter({ $0.ticker == trade.ticker }) else { return false } var currentVolume = 0.0 - for trade in trades { + for tmpTrade in trades { if trade.side == .buy { - currentVolume += trade == origin ? 0 : (trade.volume ?? 0) + currentVolume += tmpTrade == trade ? 0 : tmpTrade.quantity } else { - currentVolume -= trade == origin ? 0 : (trade.volume ?? 0) + currentVolume -= tmpTrade == trade ? 0 : tmpTrade.quantity } if currentVolume < 0 { return false } } - return true } } diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/TagClient.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/TagClient.swift new file mode 100644 index 0000000..d2d7e03 --- /dev/null +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/TagClient.swift @@ -0,0 +1,92 @@ +// +// TagClient.swift +// ToolinderDomainTradeInterface +// +// Created by 송영모 on 2023/10/01. +// + +import Foundation +import SwiftData + +import ComposableArchitecture + +public enum TagError: Error { + case unknown +} + +public struct TagClient { + public static let tagRepository: TagRepositoryInterface = TagRepository() + + public var fetchTags: () -> Result<[Tag], TagError> + public var saveTag: (TagDTO) -> Result + public var updateTag: (Tag, TagDTO) -> Result + public var deleteTag: (Tag) -> Result + + public var isValidatedSaveTag: (TagDTO) -> Bool + public var isValidatedUpdateTag: (Tag, TagDTO) -> Bool + public var isValidatedDeleteTag: (Tag) -> Bool + + public init( + fetchTags: @escaping () -> Result<[Tag], TagError>, + saveTag: @escaping (TagDTO) -> Result, + updateTag: @escaping (Tag, TagDTO) -> Result, + deleteTag: @escaping (Tag) -> Result, + + isValidatedSaveTag: @escaping (TagDTO) -> Bool, + isValidatedUpdateTag: @escaping (Tag, TagDTO) -> Bool, + isValidatedDeleteTag: @escaping (Tag) -> Bool + ) { + self.fetchTags = fetchTags + self.saveTag = saveTag + self.updateTag = updateTag + self.deleteTag = deleteTag + + self.isValidatedSaveTag = isValidatedSaveTag + self.isValidatedUpdateTag = isValidatedUpdateTag + self.isValidatedDeleteTag = isValidatedDeleteTag + } +} + +extension TagClient: TestDependencyKey { + public static var previewValue: TagClient = Self( + fetchTags: { return .failure(.unknown) }, + saveTag: { _ in return .failure(.unknown) }, + updateTag: { _, _ in return .failure(.unknown) }, + deleteTag: { _ in return .failure(.unknown) }, + + isValidatedSaveTag: { _ in return true }, + isValidatedUpdateTag: { _, _ in return true }, + isValidatedDeleteTag: { _ in return true } + ) + + public static var testValue = Self( + fetchTags: unimplemented("\(Self.self).fetchTags"), + saveTag: unimplemented("\(Self.self).saveTag"), + updateTag: unimplemented("\(Self.self).updateTag"), + deleteTag: unimplemented("\(Self.self).deleteTag"), + + isValidatedSaveTag: unimplemented("\(Self.self).isValidatedSaveTag"), + isValidatedUpdateTag: unimplemented("\(Self.self).isValidatedUpdateTag"), + isValidatedDeleteTag: unimplemented("\(Self.self).isValidatedDeleteTag") + ) +} + +public extension DependencyValues { + var tagClient: TagClient { + get { self[TagClient.self] } + set { self[TagClient.self] = newValue } + } +} + +extension TagClient: DependencyKey { + public static var liveValue = TagClient( + fetchTags: { tagRepository.fetchTags(descriptor: .init()) }, + saveTag: { tagRepository.saveTag($0) }, + updateTag: { tagRepository.updateTag($0, new: $1) }, + deleteTag: { tagRepository.deleteTag($0) }, + + isValidatedSaveTag: { tagRepository.isValidatedSaveTag($0) }, + isValidatedUpdateTag: { tagRepository.isValidatedUpdateTag($0, new: $1) }, + isValidatedDeleteTag: { tagRepository.isValidatedDeleteTag($0) } + ) +} diff --git a/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift b/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift index deb9074..7defe95 100644 --- a/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift +++ b/Projects/Toolinder/Domain/Trade/Interface/Sources/TradeClient.swift @@ -61,8 +61,8 @@ public extension DependencyValues { extension TradeClient: DependencyKey { public static var liveValue = TradeClient( fetchTrades: { tradeRepository.fetchTrades(descriptor: .init()) }, - saveTrade: { tradeRepository.saveTrade(dto: $0) }, - updateTrade: { tradeRepository.updateTrade(model:$0, dto: $1) }, - deleteTrade: { tradeRepository.deleteTrade(trade: $0) } + saveTrade: { tradeRepository.saveTrade($0) }, + updateTrade: { tradeRepository.updateTrade($0, new: $1) }, + deleteTrade: { tradeRepository.deleteTrade($0) } ) } diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift index 30ebace..ca8c466 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift @@ -43,8 +43,8 @@ public struct CalendarStore: Reducer { public var calendarItem: IdentifiedArrayOf = [] public var tradeItem: IdentifiedArrayOf = [] - @PresentationState var tickerEdit: TickerEditStore.State? - @PresentationState var tradeEdit: TradeEditStore.State? + @PresentationState var selectTicker: SelectTickerStore.State? + @PresentationState var editTrade: EditTradeStore.State? public init( id: UUID = .init(), @@ -83,8 +83,8 @@ public struct CalendarStore: Reducer { case calendarItem(id: CalendarItemCellStore.State.ID, action: CalendarItemCellStore.Action) case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action) - case tickerEdit(PresentationAction) - case tradeEdit(PresentationAction) + case selectTicker(PresentationAction) + case editTrade(PresentationAction) case delegate(Delegate) @@ -104,7 +104,7 @@ public struct CalendarStore: Reducer { return .none case .newButtonTapped: - state.tickerEdit = .init() + state.selectTicker = .init() return .none case let .tradeItemTapped(trade): @@ -142,31 +142,27 @@ public struct CalendarStore: Reducer { return .none } - case .tickerEdit(.presented(.delegate(.cancel))): - state.tickerEdit = nil + case let .selectTicker(.presented(.delegate(.select(ticker)))): + state.selectTicker = nil + state.editTrade = .init(selectedTicker: ticker, selectedDate: state.selectedDate) return .none - case let .tickerEdit(.presented(.delegate(.next(ticker)))): - state.tickerEdit = nil - state.tradeEdit = .init(selectedTicker: ticker, selectedDate: state.selectedDate) + case .selectTicker(.dismiss): + state.selectTicker = nil return .none - case .tickerEdit(.dismiss): - state.tickerEdit = nil + case .editTrade(.presented(.delegate(.save))): + state.selectTicker = nil + state.editTrade = nil return .none - case .tradeEdit(.presented(.delegate(.save))): - state.tickerEdit = nil - state.tradeEdit = nil + case let .editTrade(.presented(.delegate(.cancel(ticker)))): + state.selectTicker = .init(selectedTicker: ticker) + state.editTrade = nil return .none - case let .tradeEdit(.presented(.delegate(.cancel(ticker)))): - state.tickerEdit = .init(selectedTicker: ticker) - state.tradeEdit = nil - return .none - - case .tradeEdit(.dismiss): - state.tradeEdit = nil + case .editTrade(.dismiss): + state.editTrade = nil return .none default: @@ -179,11 +175,11 @@ public struct CalendarStore: Reducer { .forEach(\.tradeItem, action: /Action.tradeItem(id:action:)) { TradeItemCellStore() } - .ifLet(\.$tickerEdit, action: /Action.tickerEdit) { - TickerEditStore() + .ifLet(\.$selectTicker, action: /Action.selectTicker) { + SelectTickerStore() } - .ifLet(\.$tradeEdit, action: /Action.tradeEdit) { - TradeEditStore() + .ifLet(\.$editTrade, action: /Action.editTrade) { + EditTradeStore() } } } diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift index 666d93f..8707c54 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift @@ -48,20 +48,20 @@ public struct CalendarView: View { } .sheet( store: self.store.scope( - state: \.$tickerEdit, - action: { .tickerEdit($0) } + state: \.$selectTicker, + action: { .selectTicker($0) } ) ) { - TickerEditView(store: $0) + SelectTickerView(store: $0) .presentationDetents([.medium]) } .sheet( store: self.store.scope( - state: \.$tradeEdit, - action: { .tradeEdit($0) } + state: \.$editTrade, + action: { .editTrade($0) } ) ) { - TradeEditView(store: $0) + EditTradeView(store: $0) .presentationDetents([.medium]) } .tag(viewStore.offset) diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift index 6652866..24a7aa6 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift @@ -29,7 +29,7 @@ public struct CalendarItemCellView: View { Text("\(viewStore.state.date.day)") .font(.subheadline) .fontWeight(.semibold) - .foregroundStyle(Color.blackOrWhite(!viewStore.state.isSelected)) + .foregroundStyle(viewStore.state.isSelected ? Color.background : Color.foreground) Spacer() } @@ -41,7 +41,7 @@ public struct CalendarItemCellView: View { Spacer() } - .background(Color.blackOrWhite(viewStore.state.isSelected)) + .background(viewStore.state.isSelected ? Color.foreground : Color.background) .clipShape( RoundedRectangle( cornerRadius: 8, diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift index 5c83817..010a35d 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/CalendarMain/CalendarMainStore.swift @@ -123,10 +123,10 @@ public struct CalendarMainStore: Reducer { case .delegate(.refresh): return .send(.fetch) - case .tradeEdit(.presented(.delegate(.save))): + case .editTrade(.presented(.delegate(.save))): return .send(.fetch) - case .tradeEdit(.dismiss): + case .editTrade(.dismiss): return .send(.fetch) case let .delegate(.detail(trade)): diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyView.swift deleted file mode 100644 index d851b3e..0000000 --- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyView.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ExistingUserPolicyView.swift -// ToolinderFeatureMyPageDemo -// -// Created by 송영모 on 2023/09/14. -// - -import SwiftUI - -import ComposableArchitecture - -public struct ExistingUserPolicyView: View { - let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - ScrollView { - HStack { - VStack(alignment: .leading) { - Text("1 버전을 사용해주신 고마운 사용자분들께") - .font(.body) - .padding(.bottom, 5) - - Text("여러분께 아주 죄송한 마음 뿐입니다.") - .font(.headline) - - Text("우선 지금부터 하는 얘기는 기존 사용자들의 데이터를 살리지 못하는 결정을 내리게된 이유를 설명드리려고 합니다. 앱을 업데이트 했는데, 데이터가 모두 사라지는 현상이 있었습니다. 그래서 이를 해결하고자 하였지만, 아주 오래전 작성된 코드(물론 제가 작성했었음)는 문제가 있었고 현재 모두 새로운 것으로 고쳤습니다. 그래서 현재 버전부터는 모든 문제가 해결되었지만, 기존에 발생하고 있던 문제는 심지어 앱을 강제로 꺼지는 버그도 존재했습니다. 이를 해결하는 원인 파악이 어렵고 꽤 많은 곳에서 버그들이 발생하기 때문에 아예 새로 다시 만들자는 판단을 하였습니다.") - .font(.caption) - .padding(.bottom, 5) - - Text("더 나은 사용성을 위하여 피드백을 적극적으로 수용하겠습니다.") - .font(.headline) - - Text("앞으로는 유저 여러분의 소중한 피드백을 하나하나씩 반영해 나가겠습니다! 긴 글을 읽어주셔서 감사합니다. 다시 한번 죄송합니다. 마이페이지의 설문을 간단하게 달아놓았습니다. 언제든지 피드백을 주시면 곧바로 반영하겠습니다.") - .font(.caption) - .padding(.bottom, 5) - } - - Spacer() - } - .padding() - } - .navigationTitle("Existing User Policy") - } - } -} diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift index 5fe8f74..e25e583 100644 --- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift +++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainStore.swift @@ -20,10 +20,10 @@ public struct MyPageMainStore: Reducer { case onAppear case delegate(Delegate) - case existingUserPolicyTapped + case whatIsNew public enum Delegate: Equatable { - case existingUserPolicy + case whatIsNew } } @@ -33,8 +33,8 @@ public struct MyPageMainStore: Reducer { case .onAppear: return .none - case .existingUserPolicyTapped: - return .send(.delegate(.existingUserPolicy)) + case .whatIsNew: + return .send(.delegate(.whatIsNew)) default: return .none diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift index 102b59c..76726ae 100644 --- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift +++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/Main/MyPageMainView.swift @@ -46,7 +46,7 @@ public struct MyPageMainView: View { HStack { Label( title: { - Text("Existing User Policy") + Text("What's New \(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "")") }, icon: { Image(systemName: "info.circle.fill") .foregroundStyle(.blue) @@ -55,7 +55,7 @@ public struct MyPageMainView: View { Spacer() Button( action: { - viewStore.send(.existingUserPolicyTapped) + viewStore.send(.whatIsNew) }, label: { Image(systemName: "chevron.right") diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift index 6260252..c730365 100644 --- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift +++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackStore.swift @@ -29,16 +29,16 @@ public struct MyPageNavigationStackStore: Reducer { public struct Path: Reducer { public enum State: Equatable { - case existingUserPolicy(ExistingUserPolicyStore.State) + case whatIsNew(WhatIsNewStore.State) } public enum Action: Equatable { - case existingUserPolicy(ExistingUserPolicyStore.Action) + case whatIsNew(WhatIsNewStore.Action) } public var body: some Reducer { - Scope(state: /State.existingUserPolicy, action: /Action.existingUserPolicy) { - ExistingUserPolicyStore() + Scope(state: /State.whatIsNew, action: /Action.whatIsNew) { + WhatIsNewStore() } } } @@ -51,8 +51,8 @@ public struct MyPageNavigationStackStore: Reducer { case .onAppear: return .none - case .main(.delegate(.existingUserPolicy)): - state.path.append(.existingUserPolicy(.init())) + case .main(.delegate(.whatIsNew)): + state.path.append(.whatIsNew(.init())) return .none default: diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift index 723eca3..fc48edd 100644 --- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift +++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/NavigationStack/MyPageNavigationStackView.swift @@ -33,11 +33,11 @@ public struct MyPageNavigationStackView: View { } } destination: { switch $0 { - case .existingUserPolicy: + case .whatIsNew: CaseLet( - /MyPageNavigationStackStore.Path.State.existingUserPolicy, - action: MyPageNavigationStackStore.Path.Action.existingUserPolicy, - then: ExistingUserPolicyView.init(store:)) + /MyPageNavigationStackStore.Path.State.whatIsNew, + action: MyPageNavigationStackStore.Path.Action.whatIsNew, + then: WhatIsNewView.init(store:)) } } } diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyStore.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewStore.swift similarity index 91% rename from Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyStore.swift rename to Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewStore.swift index 164d162..3bef097 100644 --- a/Projects/Toolinder/Feature/MyPage/Interface/Sources/ExistingUserPolicy/ExistingUserPolicyStore.swift +++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewStore.swift @@ -9,7 +9,7 @@ import Foundation import ComposableArchitecture -public struct ExistingUserPolicyStore: Reducer { +public struct WhatIsNewStore: Reducer { public init() {} public struct State: Equatable { diff --git a/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewView.swift b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewView.swift new file mode 100644 index 0000000..06ae633 --- /dev/null +++ b/Projects/Toolinder/Feature/MyPage/Interface/Sources/WhatIsNew/WhatIsNewView.swift @@ -0,0 +1,51 @@ +// +// ExistingUserPolicyView.swift +// ToolinderFeatureMyPageDemo +// +// Created by 송영모 on 2023/09/14. +// + +import SwiftUI + +import ComposableArchitecture + +public struct WhatIsNewView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + ScrollView { + HStack { + VStack(alignment: .leading) { + Text("투린더를 사용해주신 고마운 사용자분들께") + .font(.body) + .padding(.bottom, 5) + + Text( +""" +유저가 애초에 많은 서비스는 아니었지만, 꾸준히 사용해주시는 10명 정도의 사용자분들께 작성하는 편지라고 생각하시면 감사할 것 같습니다. +우선 먼저 감사하다는 말씀을 드립니다. 댓글도 별로 안달리고 평점도 낮은 앱이지만 작은 관심은 항상 큰 도움이 되었습니다. 그리고 업데이트를 결정한 이유도 모두 꾸준히 사용해주시는 분들이 계셔서라고 할 수 있습니다. + +하지만 이 다음부터 하는 이야기는 모두 죄송하다는 말 뿐이어서 저도 속상한 마음으로 작성 중입니다. 기술적으로 투린더의 데이터를 살리지 못하는 버그가 존재했고, 모든 것을 살리기에는 시간적으로 많은 시간이 걸리는 것으로 파악을 하였습니다. 저의 첫번째 앱이기도 하고 그때 당시에 실력이 좋지 못해서 큰 그림을 그리며 설계를 하지 못해서 이렇게 되었습니다. 살리려고 노력을 많이 해봤지만, 아예 구조자체를 바꾸려고 하는 상황이어서 너무 많은 시간과 노력이 필요하다는 결론을 지었습니다. 그곳에 사용하는 시간을 앞으로의 새로운 앱에 집중하고자 하였습니다. + +이런 상황은 다시 발생해서는 안된다고 생각합니다. 앞으로는 더욱더 나은 서비스를 만들도록 노력하겠습니다. + +긴 글 읽어주셔서 감사합니다. +""" + ) + .font(.caption) + .padding(.bottom, 5) + } + + Spacer() + } + .padding() + } + .navigationTitle("What's New \(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "")") + } + } +} diff --git a/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift b/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift index fb5afa0..6c0631a 100644 --- a/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift +++ b/Projects/Toolinder/Feature/Portfolio/Interface/Sources/Main/PortfolioMainView.swift @@ -11,6 +11,7 @@ import Charts import ComposableArchitecture import ToolinderFeatureTradeInterface +import ToolinderShared public struct PortfolioMainView: View { public let store: StoreOf diff --git a/Projects/Toolinder/Feature/Sources/MainTabView.swift b/Projects/Toolinder/Feature/Sources/MainTabView.swift index 23e4705..a0fb1bb 100644 --- a/Projects/Toolinder/Feature/Sources/MainTabView.swift +++ b/Projects/Toolinder/Feature/Sources/MainTabView.swift @@ -47,7 +47,7 @@ public struct MainTabView: View { .onAppear { viewStore.send(.onAppear) } - .accentColor(Color.blackOrWhite(true)) + .accentColor(Color.foreground) } } } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellStore.swift new file mode 100644 index 0000000..f232bd7 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellStore.swift @@ -0,0 +1,68 @@ +// +// TagItemCellStore.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/01. +// + +import Foundation + +import ComposableArchitecture + +import ToolinderDomain + +public struct TagItemCellStore: Reducer { + public init() {} + + public struct State: Equatable, Identifiable { + public let mode: EditMode + public let id: UUID + + public let tag: Tag + public var isSelected: Bool + + public init( + mode: EditMode = .edit, + id: UUID = .init(), + tag: Tag, + isSelected: Bool = false + ) { + self.mode = mode + self.id = id + self.tag = tag + self.isSelected = isSelected + } + } + + public enum Action: Equatable { + case onAppear + + case tapped + case editButtonTapped + + case delegate(Delegate) + + public enum Delegate: Equatable { + case tapped + case editButtonTapped + } + } + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onAppear: + return .none + + case .tapped: + return .send(.delegate(.tapped)) + + case .editButtonTapped: + return .send(.delegate(.editButtonTapped)) + + default: + return .none + } + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellView.swift new file mode 100644 index 0000000..e9a9206 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TagItemCellView.swift @@ -0,0 +1,55 @@ +// +// TagItemCellView.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/01. +// + +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain +import ToolinderShared + +public struct TagItemCellView: View { + private let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + HStack(spacing: 10) { + Circle() + .fill(Color(hex: viewStore.state.tag.hex)) + .frame(width: 15, height: 15) + + Text(viewStore.state.tag.name) + .font(.caption2) + .foregroundStyle(.foreground) + + if viewStore.state.mode == .edit { + Button(action: { + viewStore.send(.editButtonTapped) + }, label: { + Image(systemName: "pencil.circle.fill") + }) + .foregroundStyle(.foreground) + } + } + .padding(10) + .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) + .clipShape( + RoundedRectangle( + cornerRadius: 8, + style: .continuous + ) + ) + .onTapGesture { + viewStore.send(.tapped) + } + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift index c800732..4ed1f95 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TickerItemCellView.swift @@ -21,68 +21,83 @@ public struct TickerItemCellView: View { public var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - HStack { - tradeView(viewStore: viewStore) - } - } - } - - private func tradeView(viewStore: ViewStoreOf) -> some View { - HStack(spacing: 10) { - VStack { - viewStore.state.ticker.type.image - .font(.title3) + HStack(spacing: 10) { + symbolView(viewStore: viewStore) + + nameView(viewStore: viewStore) if viewStore.mode == .item { - Text("\(viewStore.state.ticker.type.rawValue)") - .font(.caption2) - .frame(width: 40) + Spacer() + + tradeSummaryView(viewStore: viewStore) } } + .onTapGesture { + viewStore.send(.tapped) + } + .frame(height: 35) + .padding(10) + .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) + .clipShape( + RoundedRectangle( + cornerRadius: 8 + ) + ) + } + } + + private func symbolView(viewStore: ViewStoreOf) -> some View { + VStack { + viewStore.state.ticker.type.image + .font(.title3) + if viewStore.mode == .item { + Text("\(viewStore.state.ticker.type.rawValue)") + .font(.caption2) + .frame(width: 40) + } + } + } + + private func nameView(viewStore: ViewStoreOf) -> some View { + HStack { Text("\(viewStore.state.ticker.name) \(viewStore.state.ticker.trades?.count ?? 0)" ) .font(.body) .fontWeight(.semibold) - if viewStore.mode == .item { + HStack(spacing: 1) { + ForEach(viewStore.state.ticker.tags ?? [], id: \.self) { tag in + Circle() + .fill(Color(hex: tag.hex)) + .frame(width: 5, height: 5) + } + } + } + } + + private func tradeSummaryView(viewStore: ViewStoreOf) -> some View { + VStack { + HStack { + Spacer() + Text("\(Int(viewStore.tickerSummaryDataEntity.profit)) \(viewStore.state.ticker.currency.rawValue) (\(Int(viewStore.tickerSummaryDataEntity.yield))%)") + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(Int(viewStore.tickerSummaryDataEntity.yield) > 0 ? .pink : .mint) + } + + HStack(spacing: .zero) { Spacer() - VStack { - HStack { - Spacer() - - Text("\(Int(viewStore.tickerSummaryDataEntity.profit)) \(viewStore.state.ticker.currency.rawValue) (\(Int(viewStore.tickerSummaryDataEntity.yield))%)") - .font(.caption) - .fontWeight(.semibold) - .foregroundStyle(Int(viewStore.tickerSummaryDataEntity.yield) > 0 ? .pink : .mint) - } - - HStack(spacing: .zero) { - Spacer() - - Text("\(Int(viewStore.tickerSummaryDataEntity.avgPrice)) \(viewStore.state.ticker.currency.rawValue)") - .font(.caption2) - } - - HStack(spacing: .zero) { - Spacer() + Text("\(Int(viewStore.tickerSummaryDataEntity.avgPrice)) \(viewStore.state.ticker.currency.rawValue)") + .font(.caption2) + } + + HStack(spacing: .zero) { + Spacer() - Text("\(Int(viewStore.tickerSummaryDataEntity.currentVolume)) vol") - .font(.caption2) - } - } + Text("\(Int(viewStore.tickerSummaryDataEntity.currentVolume)) vol") + .font(.caption2) } } - .frame(height: 35) - .padding(10) - .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) - .clipShape( - RoundedRectangle( - cornerRadius: 8 - ) - ) - .onTapGesture { - viewStore.send(.tapped) - } } } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift index f20f91f..0e311b1 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradeItemCellView.swift @@ -58,7 +58,6 @@ public struct TradeItemCellView: View { .font(.caption2) } } - .frame(width: 70) viewStore.state.trade.ticker?.type.image .font(.title3) @@ -74,7 +73,7 @@ public struct TradeItemCellView: View { Text("\(Int(viewStore.state.trade.price)) \(viewStore.state.trade.ticker?.currency.rawValue ?? "")") .font(.caption) - Text("\(Int(viewStore.state.trade.volume)) vol") + Text("\(Int(viewStore.state.trade.quantity)) vol") .font(.caption) } } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift index 1ed4e76..991ee2e 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/Cell/TradePreviewItemCellView.swift @@ -29,7 +29,7 @@ public struct TradePreviewItemCellView: View { Text(viewStore.state.trade.ticker?.name ?? "") .font(.caption2) .fontWeight(.light) - .foregroundStyle(Color.blackOrWhite(!viewStore.state.isSelected)) + .foregroundStyle(viewStore.state.isSelected ? Color.background : Color.foreground) Spacer() } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/View/EditHeaderView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/View/EditHeaderView.swift new file mode 100644 index 0000000..9e9d4c2 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/Components/View/EditHeaderView.swift @@ -0,0 +1,87 @@ +// +// EditHeaderView.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/02. +// + +import Foundation +import SwiftUI + +public enum EditMode { + case add + case edit + case select + + public enum Action { + case dismiss + case new + case delete + } +} + +public struct EditHeaderView: View { + public let mode: EditMode + public let title: LocalizedStringKey + public let isShowDismissButton: Bool + public let isShowNewButton: Bool + public let isShowDeleteButton: Bool + + public var action: (EditMode.Action) -> () + + public init( + mode: EditMode, + title: LocalizedStringKey, + isShowDismissButton: Bool = false, + isShowNewButton: Bool = false, + isShowDeleteButton: Bool = false, + action: @escaping (EditMode.Action) -> Void + ) { + self.mode = mode + self.title = title + self.isShowDismissButton = isShowDismissButton + self.isShowNewButton = isShowNewButton + self.isShowDeleteButton = isShowDeleteButton + + self.action = action + } + + public var body: some View { + HStack { + if isShowDismissButton { + Button(action: { + action(.dismiss) + }, label: { + Image(systemName: "chevron.left") + .font(.title) + .foregroundStyle(.foreground) + }) + } + + Text(title) + .font(.title) + + Spacer() + + if isShowDeleteButton { + Button(action: { + action(.delete) + }, label: { + Image(systemName: "trash.circle.fill") + .foregroundStyle(.foreground) + .font(.title) + }) + } + + if isShowNewButton { + Button(action: { + action(.new) + }, label: { + Image(systemName: "plus.circle.fill") + .foregroundStyle(.foreground) + .font(.title) + }) + } + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagStore.swift new file mode 100644 index 0000000..24c9b5a --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagStore.swift @@ -0,0 +1,96 @@ +// +// EditTagStore.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/02. +// + +import Foundation +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain + +public struct EditTagStore: Reducer { + public init() {} + + public struct State: Equatable { + public var mode: EditMode + public var tag: Tag? + + public var title: LocalizedStringKey = "" + + public var tagName: String = "" + public var tagColor: Color = .foreground + + public init( + mode: EditMode, + tag: Tag? = nil + ) { + self.mode = mode + self.tag = tag + + if mode == .edit { + self.tagName = tag?.name ?? "" + self.tagColor = Color(hex: tag?.hex ?? "") + } + } + } + + public enum Action: Equatable { + case onAppear + + case setTagName(String) + case setTagColor(Color) + case dismissButtonTapped + case deleteButtonTapped + case saveButtonTapped + + case delegate(Delegate) + + public enum Delegate: Equatable { + case cancle + case save(Tag) + case delete(Tag) + } + } + + @Dependency(\.tagClient) var tagClient + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onAppear: + return .none + + case let .setTagName(name): + state.tagName = name + return .none + + case let .setTagColor(color): + state.tagColor = color + return .none + + case .dismissButtonTapped: + return .send(.delegate(.cancle)) + + case .deleteButtonTapped: + if let tag = state.tag, let deletedTag = try? tagClient.deleteTag(tag).get() { + return .send(.delegate(.delete(deletedTag))) + } + return .none + + case .saveButtonTapped: + guard state.tagName != "" else { return .none } + if let tag = try? tagClient.saveTag(.init(hex: state.tagColor.toHex(), name: state.tagName)).get() { + return .send(.delegate(.save(tag))) + } + return .none + + default: + return .none + } + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagView.swift new file mode 100644 index 0000000..69093e2 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTag/EditTagView.swift @@ -0,0 +1,91 @@ +// +// EditTagView.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/02. +// + +import Foundation +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain +import ToolinderShared + +public struct EditTagView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(alignment: .leading, spacing: 20) { + headerView(viewStore: viewStore) + + nameView(viewStore: viewStore) + + colorView(viewStore: viewStore) + + Spacer() + + MinimalButton(title: "Save") { + viewStore.send(.saveButtonTapped) + } + } + .padding() + } + } + + @ViewBuilder + private func headerView(viewStore: ViewStoreOf) -> some View { + switch viewStore.state.mode { + case .add: + EditHeaderView( + mode: viewStore.state.mode, + title: "Tag", + isShowDismissButton: true + ) { action in + switch action { + case .dismiss: + viewStore.send(.dismissButtonTapped) + default: break + } + } + case .edit: + EditHeaderView( + mode: viewStore.state.mode, + title: "Tag", + isShowDeleteButton: true + ) { action in + switch action { + case .dismiss: + viewStore.send(.dismissButtonTapped) + case .delete: + viewStore.send(.deleteButtonTapped) + default: break + } + } + + default: EmptyView() + } + } + + private func nameView(viewStore: ViewStoreOf) -> some View { + TextField( + text: viewStore.binding(get: \.tagName, send: EditTagStore.Action.setTagName), + label: { + Label("Name", systemImage: "highlighter") + } + ) + .foregroundStyle(.foreground) + } + + private func colorView(viewStore: ViewStoreOf) -> some View { + ColorPicker(selection: viewStore.binding(get: \.tagColor, send: EditTagStore.Action.setTagColor), label: { + Label("Color", systemImage: "paintpalette.fill") + }) + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerStore.swift similarity index 54% rename from Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditStore.swift rename to Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerStore.swift index d1cef26..47519bd 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditStore.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerStore.swift @@ -11,38 +11,49 @@ import ComposableArchitecture import ToolinderDomainTradeInterface -public struct TickerEditStore: Reducer { +public struct EditTickerStore: Reducer { public init() {} - public enum Mode { - case add - case edit - } - public struct State: Equatable { - public var mode: Mode - public var name: String = "" - public var tickerType: TickerType? - public var currency: Currency? + public var mode: EditMode + public var ticker: Ticker? - public var selectedTicker: Ticker? + public var name: String = "" + public var selectedTickerType: TickerType? + public var selectedCurrency: Currency? + public var selectedTags: [Tag] = [] { + didSet { + tagItem = .init( + uniqueElements: selectedTags.map { tag in + return .init(tag: tag) + } + ) + } + } - public var tickerItem: IdentifiedArrayOf = [] + public var tagItem: IdentifiedArrayOf = [] @PresentationState var selectTickerType: SelectTickerTypeStore.State? @PresentationState var selectCurrency: SelectCurrencyStore.State? + @PresentationState var selectTag: SelectTagStore.State? @PresentationState var alert: AlertState? public init( - mode: Mode = .add, - selectedTicker: Ticker? = nil + mode: EditMode = .add, + ticker: Ticker? = nil ) { self.mode = mode - self.selectedTicker = selectedTicker + self.ticker = ticker if mode == .edit { - self.name = selectedTicker?.name ?? "" - self.tickerType = selectedTicker?.type ?? .stock - self.currency = selectedTicker?.currency ?? .dollar + self.name = ticker?.name ?? "" + self.selectedTickerType = ticker?.type ?? .stock + self.selectedCurrency = ticker?.currency ?? .dollar + self.selectedTags = ticker?.tags ?? [] + self.tagItem = .init( + uniqueElements: ticker?.tags?.map { tag in + return .init(mode: .select, tag: tag) + } ?? [] + ) } } } @@ -51,18 +62,17 @@ public struct TickerEditStore: Reducer { case onAppear case setName(String) - case tickerTapped(Ticker) - case tickerTypeViewTapped - case currencyViewTapped + case tickerTypeButtonTapped + case currencyButtonTapped + case tagButtonTapped + case dismissButtonTapped case deleteButtonTapped - case nextButtonTapped + case saveButtonTapped - case fetchTickersRequest - case fetchTickersResponse([Ticker]) - - case tickerItem(id: TickerItemCellStore.State.ID, action: TickerItemCellStore.Action) + case tagItem(id: TagItemCellStore.State.ID, action: TagItemCellStore.Action) case selectTickerType(PresentationAction) case selectCurrency(PresentationAction) + case selectTag(PresentationAction) case alert(PresentationAction) case delegate(Delegate) @@ -72,10 +82,9 @@ public struct TickerEditStore: Reducer { } public enum Delegate: Equatable { - case cancel - case next(Ticker) + case cancle case save(Ticker) - case delete(Ticker) + case delete } } @@ -85,34 +94,30 @@ public struct TickerEditStore: Reducer { Reduce { state, action in switch action { case .onAppear: - return .concatenate([ - .send(.fetchTickersRequest) - ]) + return .none case let .setName(name): state.name = name return .none - case let .tickerTapped(ticker): - if ticker == state.selectedTicker { - state.selectedTicker = nil - } else { - state.selectedTicker = ticker - } - - return .none - - case .tickerTypeViewTapped: + case .tickerTypeButtonTapped: state.selectTickerType = .init() return .none - case .currencyViewTapped: + case .currencyButtonTapped: state.selectCurrency = .init() return .none + case .tagButtonTapped: + state.selectTag = .init(selectedTags: state.selectedTags) + return .none + + case .dismissButtonTapped: + return .send(.delegate(.cancle)) + case .deleteButtonTapped: state.alert = AlertState { - TextState("\(state.selectedTicker?.trades?.count ?? 0) records are also deleted.") + TextState("\(state.ticker?.trades?.count ?? 0) records are also deleted.") } actions: { ButtonState(role: .destructive, action: .confirmDeletion) { TextState("Delete") @@ -120,66 +125,53 @@ public struct TickerEditStore: Reducer { } return .none - case .nextButtonTapped: - if let ticker = state.selectedTicker, state.mode == .add { - return .send(.delegate(.next(ticker))) - } - + case .saveButtonTapped: return validateAndSaveTickerEffect( mode: state.mode, - ticker: state.selectedTicker, - tickerType: state.tickerType, - currency: state.currency, - name: state.name + ticker: state.ticker, + tickerType: state.selectedTickerType, + currency: state.selectedCurrency, + name: state.name, + tags: state.selectedTags ) - case .fetchTickersRequest: - let tickers = (try? tickerClient.fetchTickers().get()) ?? [] - return .send(.fetchTickersResponse(tickers)) - - case let .fetchTickersResponse(tickers): - state.tickerItem = .init(uniqueElements: tickers.map { .init(mode: .preview, ticker: $0) }) - return .none - - case let .tickerItem(id: id, action: .delegate(.tapped)): - let isSelected = state.tickerItem[id: id]?.isSelected ?? false - - for id in state.tickerItem.ids { - state.tickerItem[id: id]?.isSelected = false - } - - state.tickerItem[id: id]?.isSelected = !isSelected - - if !isSelected { - state.selectedTicker = state.tickerItem[id: id]?.ticker - } else { - state.selectedTicker = nil - } - - return .none - case let .selectTickerType(.presented(.delegate(.select(tickerType)))): state.selectTickerType = nil - state.tickerType = tickerType + state.selectedTickerType = tickerType + return .none + + case let .selectCurrency(.presented(.delegate(.select(currency)))): + state.selectCurrency = nil + state.selectedCurrency = currency return .none case .selectTickerType(.dismiss): state.selectTickerType = nil return .none - case let .selectCurrency(.presented(.delegate(.select(currency)))): + case .selectCurrency(.dismiss): state.selectCurrency = nil - state.currency = currency return .none - case .selectCurrency(.dismiss): - state.selectCurrency = nil + case .selectTag(.dismiss): + state.selectTag = nil + return .none + + case let .selectTag(.presented(.delegate(.select(tags)))): + state.selectTag = nil + state.selectedTags = tags + return .none + + case let .selectTag(.presented(.delegate(.deleted(tag)))): + if let index = state.selectedTags.firstIndex(of: tag) { + state.selectedTags.remove(at: index) + } return .none case .alert(.presented(.confirmDeletion)): - if let ticker = state.selectedTicker { + if let ticker = state.ticker { let _ = tickerClient.deleteTicker(ticker) - return .send(.delegate(.delete(ticker))) + return .send(.delegate(.delete)) } return .none @@ -187,38 +179,41 @@ public struct TickerEditStore: Reducer { return .none } } - .forEach(\.tickerItem, action: /Action.tickerItem(id:action:)) { - TickerItemCellStore() + .ifLet(\.$selectTag, action: /Action.selectTag) { + SelectTagStore() } + .ifLet(\.$alert, action: /Action.alert) } private func validateAndSaveTickerEffect( - mode: Mode, + mode: EditMode, ticker: Ticker?, tickerType: TickerType?, currency: Currency?, - name: String - ) -> Effect { + name: String, + tags: [Tag] + ) -> Effect { guard let tickerType = tickerType else { return .none } guard let currency = currency else { return .none } guard name.isEmpty == false else { return .none } switch mode { case .add: - if let ticker = try? tickerClient.saveTicker(.init(type: tickerType, currency: currency, name: name)).get() { - return .send(.delegate(.next(ticker))) + if let ticker = try? tickerClient.saveTicker(.init(type: tickerType, currency: currency, name: name, tags: tags)).get() { + return .send(.delegate(.save(ticker))) } else { return .none } case .edit: guard let ticker = ticker else { return .none } - if let ticker = try? tickerClient.updateTicker(ticker, .init(type: tickerType, currency: currency, name: name)).get() { + if let ticker = try? tickerClient.updateTicker(ticker, .init(type: tickerType, currency: currency, name: name, tags: tags)).get() { return .send(.delegate(.save(ticker))) } else { return .none } + default: return .none } } } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerView.swift new file mode 100644 index 0000000..892ec38 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/EditTickerView.swift @@ -0,0 +1,158 @@ +// +// AddTickerView.swift +// ToolinderFeatureCalendarDemo +// +// Created by 송영모 on 2023/09/07. +// + +import SwiftUI +import SwiftData + +import ComposableArchitecture + +import ToolinderDomainTradeInterface +import ToolinderShared + +public struct EditTickerView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(alignment: .leading, spacing: 20) { + headerView(viewStore: viewStore) + + nameView(viewStore: viewStore) + + tickerTypeView(viewStore: viewStore) + + currencyView(viewStore: viewStore) + + tagView(viewStore: viewStore) + + Spacer() + + MinimalButton(title: "Save") { + viewStore.send(.saveButtonTapped) + } + } + .onAppear { + viewStore.send(.onAppear) + } + .sheet( + store: self.store.scope( + state: \.$selectCurrency, + action: { .selectCurrency($0) } + ) + ) { + SelectCurrencyView(store: $0) + .presentationDetents([.medium]) + } + .sheet( + store: self.store.scope( + state: \.$selectTickerType, + action: { .selectTickerType($0) } + ) + ) { + SelectTickerTypeView(store: $0) + .presentationDetents([.medium]) + } + .sheet( + store: self.store.scope( + state: \.$selectTag, + action: { .selectTag($0) } + ) + ) { + SelectTagView(store: $0) + .presentationDetents([.medium]) + } + .alert( + store: self.store.scope( + state: \.$alert, + action: { .alert($0) } + ) + ) + .padding() + } + } + + @ViewBuilder + private func headerView(viewStore: ViewStoreOf) -> some View { + switch viewStore.state.mode { + case .add: + EditHeaderView( + mode: viewStore.state.mode, + title: LocalizedStringKey(viewStore.state.ticker?.name ?? "Ticker"), + isShowDismissButton: true, + isShowDeleteButton: false + ) { mode in + switch mode { + case .dismiss: + viewStore.send(.dismissButtonTapped) + default: break + } + } + + case .edit: + EditHeaderView( + mode: viewStore.state.mode, + title: LocalizedStringKey(viewStore.state.ticker?.name ?? "Ticker"), + isShowDismissButton: true, + isShowDeleteButton: true + ) { mode in + switch mode { + case .dismiss: + viewStore.send(.dismissButtonTapped) + case .delete: + viewStore.send(.deleteButtonTapped) + default: break + } + } + default: EmptyView() + } + } + + private func nameView(viewStore: ViewStoreOf) -> some View { + TextField("Name", text: viewStore.binding(get: \.name.localizedUppercase, send: EditTickerStore.Action.setName)) + .foregroundStyle(.foreground) + } + + private func tickerTypeView(viewStore: ViewStoreOf) -> some View { + Button(action: { + viewStore.send(.tickerTypeButtonTapped) + }, label: { + Label(viewStore.selectedTickerType?.rawValue ?? "Ticker Type", systemImage: viewStore.selectedTickerType?.systemImageName ?? "questionmark.circle.fill") + .foregroundStyle(.foreground) + }) + } + + private func currencyView(viewStore: ViewStoreOf) -> some View { + Button(action: { + viewStore.send(.currencyButtonTapped) + }, label: { + Label(viewStore.selectedCurrency?.rawValue ?? "Currency", systemImage: viewStore.selectedCurrency?.systemImageName ?? "questionmark.circle.fill") + .foregroundStyle(.foreground) + }) + } + + private func tagView(viewStore: ViewStoreOf) -> some View { + ScrollView(.horizontal) { + HStack(spacing: .zero) { + Button(action: { + viewStore.send(.tagButtonTapped) + }, label: { + Label(viewStore.state.selectedTags.isEmpty ? "Tag" : "", systemImage: "tag.circle.fill") + .foregroundStyle(.foreground) + }) + + ForEachStore(self.store.scope(state: \.tagItem, action: EditTickerStore.Action.tagItem(id:action:))) { + TagItemCellView(store: $0) + .padding(.trailing) + } + } + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditView.swift deleted file mode 100644 index 161c5dd..0000000 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerEditView.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// AddTickerView.swift -// ToolinderFeatureCalendarDemo -// -// Created by 송영모 on 2023/09/07. -// - -import SwiftUI -import SwiftData - -import ComposableArchitecture - -import ToolinderDomainTradeInterface -import ToolinderShared - -public struct TickerEditView: View { - let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - VStack(alignment: .leading, spacing: 20) { - headerView(viewStore: viewStore) - .padding([.top, .horizontal]) - - if viewStore.state.mode == .add { - tickersView(viewStore: viewStore) - } - - Divider() - .padding(.horizontal) - - inputView(viewStore: viewStore) - .padding(.horizontal) - - Spacer() - - MinimalButton(title: "Next") { - viewStore.send(.nextButtonTapped) - } - .padding(.horizontal) - } - .onAppear { - viewStore.send(.onAppear) - } - .sheet( - store: self.store.scope( - state: \.$selectCurrency, - action: { .selectCurrency($0) } - ) - ) { store in - SelectCurrencyView(store: store) - .presentationDetents([.medium]) - } - .sheet( - store: self.store.scope( - state: \.$selectTickerType, - action: { .selectTickerType($0) } - ) - ) { store in - SelectTickerTypeView(store: store) - .presentationDetents([.medium]) - } - .alert( - store: self.store.scope( - state: \.$alert, - action: { .alert($0) } - ) - ) - } - } - - private func headerView(viewStore: ViewStoreOf) -> some View { - HStack { - if viewStore.state.mode == .add { - Text("Ticker") - .font(.title) - } else { - Text(viewStore.state.selectedTicker?.name ?? "") - .font(.title) - } - - Spacer() - - if viewStore.state.mode == .edit { - Button(action: { - viewStore.send(.deleteButtonTapped) - }, label: { - Image(systemName: "trash.circle.fill") - .foregroundStyle(.foreground) - .font(.title) - }) - } - } - } - - private func tickersView(viewStore: ViewStoreOf) -> some View { - ScrollView(.horizontal) { - HStack { - ForEachStore(self.store.scope(state: \.tickerItem, action: TickerEditStore.Action.tickerItem(id:action:))) { - TickerItemCellView(store: $0) - } - } - .padding(.horizontal) - } - } - - private func inputView(viewStore: ViewStoreOf) -> some View { - VStack(alignment: .leading, spacing: 20) { - TextField("Name", text: viewStore.binding(get: \.name, send: TickerEditStore.Action.setName)) - - Button(action: { - viewStore.send(.tickerTypeViewTapped) - }, label: { - Label(viewStore.tickerType?.rawValue ?? "Ticker Type", systemImage: viewStore.tickerType?.systemImageName ?? "questionmark.circle.fill") - }) - - Button(action: { - viewStore.send(.currencyViewTapped) - }, label: { - Label(viewStore.currency?.rawValue ?? "Currency", systemImage: viewStore.currency?.systemImageName ?? "questionmark.circle.fill") - }) - } - .foregroundStyle(.foreground) - } -} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerItem.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerItem.swift deleted file mode 100644 index 6e214b8..0000000 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTicker/TickerItem.swift +++ /dev/null @@ -1,78 +0,0 @@ -//// -//// TickerItem.swift -//// ToolinderFeatureCalendarInterface -//// -//// Created by 송영모 on 2023/09/11. -//// -// -//import SwiftUI -// -//import ToolinderDomain -// -//public struct TickerItem: View { -// private let ticker: Ticker -// private let isSelected: Bool -// -// private var action: () -> Void -// -// public init( -// ticker: Ticker, -// isSelected: Bool, -// action: @escaping () -> Void = {} -// ) { -// self.ticker = ticker -// self.isSelected = isSelected -// self.action = action -// } -// -// public var body: some View { -// VStack(spacing: .zero) { -// HStack { -// Image(systemName: ticker.type.systemImageName) -// .font(.body) -// -// Text("\(ticker.name) \(ticker.trades?.count ?? 0)") -// .font(.body) -// .fontWeight(.semibold) -// .padding(.trailing) -// -// Spacer() -// -// Image(systemName: "checkmark.circle") -// .font(.caption) -// } -// .padding(.bottom, 10) -// -// HStack(spacing: .zero) { -// Spacer() -// -// Text("++76 ") -// .font(.caption2) -// .foregroundStyle(.pink) -// Text("--59") -// .font(.caption2) -// .foregroundStyle(.mint) -// Text(" 12 vol") -// .font(.caption2) -// } -// -// HStack(spacing: .zero) { -// Spacer() -// -// Text("(avg) 12,000 \(ticker.currency.rawValue)") -// .font(.caption2) -// } -// } -// .padding(10) -// .background(isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) -// .clipShape( -// RoundedRectangle( -// cornerRadius: 8, -// style: .continuous -// ) -// ) -// .onTapGesture { -// self.action() -// } -// } -//} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeStore.swift similarity index 84% rename from Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditStore.swift rename to Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeStore.swift index b7fd8bc..98e926c 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditStore.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeStore.swift @@ -12,7 +12,7 @@ import ComposableArchitecture import ToolinderDomain -public struct TradeEditStore: Reducer { +public struct EditTradeStore: Reducer { public init() {} public enum Mode { @@ -27,7 +27,7 @@ public struct TradeEditStore: Reducer { public var selectedTrade: Trade? public var price: Double - public var volume: Double + public var quantity: Double public var fee: Double public var selectedDate: Date = .now public var selectedTradeSide: TradeSide = .buy @@ -48,7 +48,7 @@ public struct TradeEditStore: Reducer { self.selectedTicker = selectedTicker self.selectedTrade = selectedTrade self.price = selectedTrade?.price ?? 0 - self.volume = selectedTrade?.volume ?? 0 + self.quantity = selectedTrade?.quantity ?? 0 self.fee = selectedTrade?.fee ?? 0 self.selectedDate = selectedTrade?.date ?? selectedDate self.note = selectedTrade?.note ?? "" @@ -60,7 +60,7 @@ public struct TradeEditStore: Reducer { case onAppear case setPrice(Double) - case setVolume(Double) + case setQuantity(Double) case setFee(Double) case selectDate(Date) case selectTradeSide(TradeSide) @@ -98,8 +98,8 @@ public struct TradeEditStore: Reducer { state.price = price return .none - case let .setVolume(volume): - state.volume = volume + case let .setQuantity(volume): + state.quantity = volume return .none case let .setFee(fee): @@ -135,7 +135,7 @@ public struct TradeEditStore: Reducer { trade: state.selectedTrade, side: state.selectedTradeSide, price: state.price, - volume: state.volume, + quantity: state.quantity, fee: state.fee, images: state.images, note: state.note, @@ -172,41 +172,34 @@ public struct TradeEditStore: Reducer { trade: Trade? = nil, side: TradeSide, price: Double, - volume: Double, + quantity: Double, fee: Double, images: [Data], note: String, date: Date, ticker: Ticker - ) -> Effect { + ) -> Effect { guard !price.isZero else { return .none } - guard !volume.isZero else { return .none } + guard !quantity.isZero else { return .none } guard fee < 100 else { return .none } + let tradeDTO = TradeDTO( + side: side, + price: price, + quantity: quantity, + fee: fee, + images: images, + note: note, + date: date, + ticker: ticker + ) + if let unSavedTrade = trade { - if let trade = try? tradeClient.updateTrade(unSavedTrade, .init( - side: side, - price: price, - volume: volume, - fee: fee, - images: images, - note: note, - date: date, - ticker: ticker - )).get() { + if let trade = try? tradeClient.updateTrade(unSavedTrade, tradeDTO).get() { return .send(.delegate(.save(trade))) } } else { - if let trade = try? tradeClient.saveTrade(.init( - side: side, - price: price, - volume: volume, - fee: fee, - images: images, - note: note, - date: date, - ticker: ticker - )).get() { + if let trade = try? tradeClient.saveTrade(tradeDTO).get() { return .send(.delegate(.save(trade))) } } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeView.swift similarity index 83% rename from Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditView.swift rename to Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeView.swift index c621e53..3b244e4 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/TradeEditView.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/EditTrade/EditTradeView.swift @@ -14,10 +14,10 @@ import ComposableArchitecture import ToolinderDomain import ToolinderShared -public struct TradeEditView: View { - let store: StoreOf +public struct EditTradeView: View { + let store: StoreOf - public init(store: StoreOf) { + public init(store: StoreOf) { self.store = store } @@ -46,7 +46,7 @@ public struct TradeEditView: View { } } - private func headerView(viewStore: ViewStoreOf) -> some View { + private func headerView(viewStore: ViewStoreOf) -> some View { HStack { if viewStore.state.mode == .add { Button(action: { @@ -75,9 +75,9 @@ public struct TradeEditView: View { } } - private func pickerView(viewStore: ViewStoreOf) -> some View { + private func pickerView(viewStore: ViewStoreOf) -> some View { HStack { - Picker("", selection: viewStore.binding(get: \.selectedTradeSide, send: TradeEditStore.Action.selectTradeSide)) { + Picker("", selection: viewStore.binding(get: \.selectedTradeSide, send: EditTradeStore.Action.selectTradeSide)) { ForEach(TradeSide.allCases, id: \.self) { tradeSide in Text(tradeSide.rawValue) .tag(tradeSide) @@ -85,26 +85,26 @@ public struct TradeEditView: View { } .pickerStyle(.segmented) - DatePicker("", selection: viewStore.binding(get: \.selectedDate, send: TradeEditStore.Action.selectDate)) + DatePicker("", selection: viewStore.binding(get: \.selectedDate, send: EditTradeStore.Action.selectDate)) } } - private func inputView(viewStore: ViewStoreOf) -> some View { + private func inputView(viewStore: ViewStoreOf) -> some View { VStack(spacing: 20) { HStack { viewStore.state.selectedTicker.currency.image - TextField("Price", value: viewStore.binding(get: \.price, send: TradeEditStore.Action.setPrice), format: .number) + TextField("Price", value: viewStore.binding(get: \.price, send: EditTradeStore.Action.setPrice), format: .number) .keyboardType(.decimalPad) Image(systemName: "plusminus.circle.fill") - TextField("Volume", value: viewStore.binding(get: \.volume, send: TradeEditStore.Action.setVolume), format: .number) + TextField("Volume", value: viewStore.binding(get: \.quantity, send: EditTradeStore.Action.setQuantity), format: .number) .keyboardType(.decimalPad) Image(systemName: "building.columns.circle.fill") - TextField("Fee %", value: viewStore.binding(get: \.fee, send: TradeEditStore.Action.setFee), format: .number) + TextField("Fee %", value: viewStore.binding(get: \.fee, send: EditTradeStore.Action.setFee), format: .number) .keyboardType(.decimalPad) Spacer() @@ -113,7 +113,7 @@ public struct TradeEditView: View { VStack(alignment: .leading) { Image(systemName: "note.text") - TextEditor(text: viewStore.binding(get: \.note, send: TradeEditStore.Action.setNote)) + TextEditor(text: viewStore.binding(get: \.note, send: EditTradeStore.Action.setNote)) } ScrollView(.horizontal) { @@ -124,7 +124,7 @@ public struct TradeEditView: View { ImageItem(imageData: imageData) } - PhotosPicker(selection: viewStore.binding(get: \.selectedPhotosPickerItems, send: TradeEditStore.Action.setPhotoPickerItems), + PhotosPicker(selection: viewStore.binding(get: \.selectedPhotosPickerItems, send: EditTradeStore.Action.setPhotoPickerItems), matching: .images) { ImageNewItem() } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagStore.swift new file mode 100644 index 0000000..bd744ed --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagStore.swift @@ -0,0 +1,129 @@ +// +// SelectTagStore.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/01. +// + +import Foundation +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain + +public struct SelectTagStore: Reducer { + public init() {} + + public struct State: Equatable { + public var selectedTags: [Tag] + + public var tagItem: IdentifiedArrayOf = [] + @PresentationState var editTag: EditTagStore.State? + + public init(selectedTags: [Tag]) { + self.selectedTags = selectedTags + } + } + + public enum Action: Equatable { + case onAppear + + case addButtonTapped + case confirmButtonTapped + + case fetchTagsRequest + case fetchTagsResponse([Tag]) + + case tagItem(id: TagItemCellStore.State.ID, action: TagItemCellStore.Action) + case editTag(PresentationAction) + + case delegate(Delegate) + + public enum Delegate: Equatable { + case select([Tag]) + case deleted(Tag) + } + } + + @Dependency(\.tagClient) var tagClient + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onAppear: + return .concatenate([ + .send(.fetchTagsRequest) + ]) + + case .addButtonTapped: + state.editTag = .init(mode: .add) + return .none + + case .confirmButtonTapped: + return .send(.delegate(.select(state.selectedTags))) + + case .fetchTagsRequest: + let tags = (try? tagClient.fetchTags().get()) ?? [] + return .send(.fetchTagsResponse(tags)) + + case let .fetchTagsResponse(tags): + state.tagItem = .init( + uniqueElements: tags.map { tag in + return .init( + mode: .edit, + tag: tag, + isSelected: state.selectedTags.contains(where: { $0 == tag }) + ) + } + ) + + return .none + + case let .tagItem(id: id, action: .delegate(.tapped)): + guard let tag = state.tagItem[id: id]?.tag else { return .none } + + state.tagItem[id: id]?.isSelected.toggle() + if let index = state.selectedTags.firstIndex(of: tag) { + state.selectedTags.remove(at: index) + } else { + state.selectedTags.append(tag) + } + return .none + + case let .tagItem(id: id, action: .delegate(.editButtonTapped)): + state.editTag = .init(mode: .edit, tag: state.tagItem[id: id]?.tag) + return .none + + case .editTag(.presented(.delegate(.save))): + state.editTag = nil + return .send(.fetchTagsRequest) + + case let .editTag(.presented(.delegate(.delete(tag)))): + if let index = state.selectedTags.firstIndex(of: tag) { + state.selectedTags.remove(at: index) + } + state.editTag = nil + return .concatenate([ + .send(.fetchTagsRequest), + .send(.delegate(.deleted(tag))) + ]) + + case .editTag(.dismiss): + state.editTag = nil + return .none + + default: + return .none + } + } + + .ifLet(\.$editTag, action: /Action.editTag) { + EditTagStore() + } + + .forEach(\.tagItem, action: /Action.tagItem(id:action:)) { + TagItemCellStore() + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagView.swift new file mode 100644 index 0000000..5ea05d8 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTag/SelectTagView.swift @@ -0,0 +1,77 @@ +// +// SelectTagView.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/01. +// + +import Foundation +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain +import ToolinderShared + +public struct SelectTagView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + GeometryReader { proxy in + ScrollView { + VStack(alignment: .leading) { + headerView(viewStore: viewStore) + .padding() + + tagItemListView() + + Spacer() + + MinimalButton(title: "Confirm") { + viewStore.send(.confirmButtonTapped) + } + .padding() + } + .frame(minHeight: proxy.size.height) + } + } + .onAppear { + viewStore.send(.onAppear) + } + .sheet( + store: self.store.scope( + state: \.$editTag, + action: { .editTag($0) } + ) + ) { + EditTagView(store: $0) + .presentationDetents([.medium]) + } + } + } + + @ViewBuilder + private func headerView(viewStore: ViewStoreOf) -> some View { + EditHeaderView(mode: .select, title: "Tag", isShowNewButton: true) { mode in + switch mode { + case .new: + viewStore.send(.addButtonTapped) + default: break + } + } + } + + private func tagItemListView() -> some View { + LazyVGrid(columns: .init(repeating: .init(.flexible(minimum: 10, maximum: 500)), count: 3), alignment: .leading, spacing: 10) { + ForEachStore(self.store.scope(state: \.tagItem, action: SelectTagStore.Action.tagItem(id:action:))) { + TagItemCellView(store: $0) + } + } + .padding(.horizontal) + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerStore.swift new file mode 100644 index 0000000..dcec6e2 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerStore.swift @@ -0,0 +1,102 @@ +// +// SelectTickerStore.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/02. +// + +import Foundation +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain + +public struct SelectTickerStore: Reducer { + public init() {} + + public struct State: Equatable { + public var tickerItem: IdentifiedArrayOf = [] + + public var selectedTicker: Ticker? + + @PresentationState var editTicker: EditTickerStore.State? + + public init(selectedTicker: Ticker? = nil) { + self.selectedTicker = selectedTicker + } + } + + public enum Action: Equatable { + case onAppear + + case addButtonTapped + + case fetchTickersRequest + case fetchTickersResponse([Ticker]) + + case tickerItem(id: TickerItemCellStore.State.ID, action: TickerItemCellStore.Action) + case editTicker(PresentationAction) + + case delegate(Delegate) + + public enum Delegate: Equatable { + case select(Ticker) + } + } + + @Dependency(\.tickerClient) var tickerClient + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onAppear: + return .send(.fetchTickersRequest) + + case .addButtonTapped: + state.editTicker = .init() + return .none + + case .fetchTickersRequest: + let tags = (try? tickerClient.fetchTickers().get()) ?? [] + return .send(.fetchTickersResponse(tags)) + + case let .fetchTickersResponse(tickers): + state.tickerItem = .init( + uniqueElements: tickers.map { ticker in + .init( + mode: .preview, + ticker: ticker, + isSelected: state.selectedTicker == ticker + ) + } + ) + return .none + + case let .tickerItem(id: id, action: .delegate(.tapped)): + if let ticker = state.tickerItem[id: id]?.ticker { + return .send(.delegate(.select(ticker))) + } else { + return .none + } + + case .editTicker(.presented(.delegate(.save))): + state.editTicker = nil + return .send(.fetchTickersRequest) + + case .editTicker(.dismiss), .editTicker(.presented(.delegate(.cancle))): + state.editTicker = nil + return .none + + default: + return .none + } + } + .ifLet(\.$editTicker, action: /Action.editTicker) { + EditTickerStore() + } + .forEach(\.tickerItem, action: /Action.tickerItem(id:action:)) { + TickerItemCellStore() + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerView.swift new file mode 100644 index 0000000..18e8439 --- /dev/null +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/SelectTicker/SelectTickerView.swift @@ -0,0 +1,67 @@ +// +// SelectTicker.swift +// ToolinderFeatureTradeInterface +// +// Created by 송영모 on 2023/10/02. +// + +import SwiftUI + +import ComposableArchitecture + +import ToolinderShared + +public struct SelectTickerView: View { + public let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + ScrollView { + headerView(viewStore: viewStore) + .padding() + + tickerItemListView() + .padding() + } + .onAppear { + viewStore.send(.onAppear) + } + .sheet( + store: self.store.scope( + state: \.$editTicker, + action: { .editTicker($0) } + ) + ) { + EditTickerView(store: $0) + .presentationDetents([.medium]) + } + } + } + + @ViewBuilder + private func headerView(viewStore: ViewStoreOf) -> some View { + EditHeaderView( + mode: .select, + title: "Ticker", + isShowNewButton: true + ) { mode in + switch mode { + case .new: + viewStore.send(.addButtonTapped) + default: break + } + } + } + + private func tickerItemListView() -> some View { + LazyVGrid(columns: .init(repeating: .init(.flexible(minimum: 10, maximum: 500)), count: 2), alignment: .leading, spacing: 10) { + ForEachStore(self.store.scope(state: \.tickerItem, action: SelectTickerStore.Action.tickerItem(id:action:))) { + TickerItemCellView(store: $0) + } + } + } +} diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift index 8b6550a..2201ba1 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailStore.swift @@ -15,11 +15,11 @@ public struct TickerDetailStore: Reducer { public init() {} public struct State: Equatable { - public let ticker: Ticker + public var ticker: Ticker public var tradeDateChartDataEntity: TradeDateChartDataEntity = .init() - @PresentationState var tickerEdit: TickerEditStore.State? + @PresentationState var editTicker: EditTickerStore.State? public var tradeItem: IdentifiedArrayOf = [] public init( @@ -37,7 +37,7 @@ public struct TickerDetailStore: Reducer { case tickerTypeChartDataEntityRequest case tickerTypeChartDataEntityResponse(TradeDateChartDataEntity) - case tickerEdit(PresentationAction) + case editTicker(PresentationAction) case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action) case delegate(Delegate) @@ -61,7 +61,7 @@ public struct TickerDetailStore: Reducer { ]) case .editButtonTapped: - state.tickerEdit = .init(mode: .edit, selectedTicker: state.ticker) + state.editTicker = .init(mode: .edit, ticker: state.ticker) return .none case .tickerTypeChartDataEntityRequest: @@ -78,12 +78,17 @@ public struct TickerDetailStore: Reducer { state.tradeDateChartDataEntity = entity return .none - case .tickerEdit(.presented(.delegate(.delete))): - state.tickerEdit = nil + case let .editTicker(.presented(.delegate(.save(ticker)))): + state.editTicker = nil + state.ticker = ticker + return .none + + case .editTicker(.presented(.delegate(.delete))): + state.editTicker = nil return .send(.delegate(.deleted)) - case .tickerEdit(.dismiss): - state.tickerEdit = nil + case .editTicker(.dismiss): + state.editTicker = nil return .none default: @@ -91,8 +96,8 @@ public struct TickerDetailStore: Reducer { } } - .ifLet(\.$tickerEdit, action: /Action.tickerEdit) { - TickerEditStore() + .ifLet(\.$editTicker, action: /Action.editTicker) { + EditTickerStore() } } } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift index b6a148d..6a99b57 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TickerDetail/TickerDetailView.swift @@ -37,11 +37,11 @@ public struct TickerDetailView: View { } .sheet( store: self.store.scope( - state: \.$tickerEdit, - action: { .tickerEdit($0) } + state: \.$editTicker, + action: { .editTicker($0) } ) ) { - TickerEditView(store: $0) + EditTickerView(store: $0) .presentationDetents([.medium]) } .toolbar { diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift index a5060ae..5d08f10 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailStore.swift @@ -17,7 +17,7 @@ public struct TradeDetailStore: Reducer { public struct State: Equatable { public var trade: Trade - @PresentationState var tradeEdit: TradeEditStore.State? + @PresentationState var editTrade: EditTradeStore.State? public var tradeItem: IdentifiedArrayOf = [] public init(trade: Trade) { @@ -31,7 +31,7 @@ public struct TradeDetailStore: Reducer { case editButtonTapped case newButtonTapped - case tradeEdit(PresentationAction) + case editTrade(PresentationAction) case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action) case delegate(Delegate) @@ -56,27 +56,27 @@ public struct TradeDetailStore: Reducer { case .editButtonTapped: if let ticker = state.trade.ticker { - state.tradeEdit = .init(mode: .edit, selectedTicker: ticker, selectedTrade: state.trade) + state.editTrade = .init(mode: .edit, selectedTicker: ticker, selectedTrade: state.trade) } return .none case .newButtonTapped: if let ticker = state.trade.ticker { - state.tradeEdit = .init(mode: .bypassAdd, selectedTicker: ticker) + state.editTrade = .init(mode: .bypassAdd, selectedTicker: ticker) } return .none - case let .tradeEdit(.presented(.delegate(.save(trade)))): + case let .editTrade(.presented(.delegate(.save(trade)))): state.trade = trade - state.tradeEdit = nil + state.editTrade = nil return .none - case let .tradeEdit(.presented(.delegate(.delete(trade)))): - state.tradeEdit = nil + case let .editTrade(.presented(.delegate(.delete(trade)))): + state.editTrade = nil return .send(.delegate(.delete(trade))) - case .tradeEdit(.dismiss), .tradeEdit(.presented(.delegate(.cancel))): - state.tradeEdit = nil + case .editTrade(.dismiss), .editTrade(.presented(.delegate(.cancel))): + state.editTrade = nil return .none default: @@ -84,8 +84,8 @@ public struct TradeDetailStore: Reducer { } } - .ifLet(\.$tradeEdit, action: /Action.tradeEdit) { - TradeEditStore() + .ifLet(\.$editTrade, action: /Action.editTrade) { + EditTradeStore() } } } diff --git a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift index 873a43f..d38eaf5 100644 --- a/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift +++ b/Projects/Toolinder/Feature/Trade/Interface/Sources/TradeDetail/TradeDetailView.swift @@ -45,11 +45,11 @@ public struct TradeDetailView: View { } .sheet( store: self.store.scope( - state: \.$tradeEdit, - action: { .tradeEdit($0) } + state: \.$editTrade, + action: { .editTrade($0) } ) ) { - TradeEditView(store: $0) + EditTradeView(store: $0) .presentationDetents([.medium]) } .navigationTitle(viewStore.state.trade.ticker?.name ?? "") @@ -80,7 +80,7 @@ public struct TradeDetailView: View { HStack(alignment: .bottom, spacing: .zero) { Spacer() - Text(scaledString(valueOrNil: viewStore.state.trade.volume)) + Text(scaledString(valueOrNil: viewStore.state.trade.quantity)) .font(.title) .fontWeight(.semibold) diff --git a/Projects/Toolinder/Shared/Util/Resources/Localizable.xcstrings b/Projects/Toolinder/Shared/Util/Resources/Localizable.xcstrings new file mode 100644 index 0000000..e940975 --- /dev/null +++ b/Projects/Toolinder/Shared/Util/Resources/Localizable.xcstrings @@ -0,0 +1,210 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Buy" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "売り" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "매수" + } + } + } + }, + "Currency" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通貨" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "화폐" + } + } + } + }, + "Edit" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "編集" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "편집" + } + } + } + }, + "History" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "記録" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "기록" + } + } + } + }, + "Name" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이름" + } + } + } + }, + "Next" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "次の" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음" + } + } + } + }, + "Save" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장" + } + } + } + }, + "Sell" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "買い" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "매도" + } + } + } + }, + "Summary" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "概略" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "요약" + } + } + } + }, + "Ticker" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "銘柄" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "종목" + } + } + } + }, + "Ticker Type" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "銘柄 種類" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "종목 종류" + } + } + } + }, + "Trade" : { + "extractionState" : "manual", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "商売" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "거래" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index a7eb0cf..9216c6a 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -4,7 +4,9 @@ let dependencies = Dependencies( swiftPackageManager: [ .remote(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads", requirement: .upToNextMajor(from: "10.9.0")), .remote(url: "https://github.com/pointfreeco/swift-composable-architecture", requirement: .upToNextMajor(from: "1.2.0")), - .remote(url: "https://github.com/realm/realm-swift", requirement: .upToNextMajor(from: "10.42.2")) + .remote(url: "https://github.com/realm/realm-swift", requirement: .upToNextMajor(from: "10.42.2")), + .remote(url: "https://github.com/firebase/firebase-ios-sdk", requirement: .upToNextMajor(from: "10.15.0")) + ], platforms: [.iOS] )