diff --git a/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/Black.colorset/Contents.json b/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/Black.colorset/Contents.json new file mode 100644 index 0000000..fa62c02 --- /dev/null +++ b/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/Black.colorset/Contents.json @@ -0,0 +1,33 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "labelColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/Contents.json b/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/White.colorset/Contents.json b/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/White.colorset/Contents.json new file mode 100644 index 0000000..951b907 --- /dev/null +++ b/Projects/Folio/Shared/DesignSystem/Resources/Color.xcassets/White.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift b/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift new file mode 100644 index 0000000..3bbeee9 --- /dev/null +++ b/Projects/Folio/Shared/DesignSystem/Sources/Color+Extension.swift @@ -0,0 +1,14 @@ +// +// Color+Extension.swift +// FolioSharedDesignSystem +// +// Created by 송영모 on 2023/09/18. +// + +import SwiftUI + +public extension Color { + static func blackOrWhite(_ isSelected: Bool = false) -> Self { + return isSelected ? Color(uiColor: .label) : Color(uiColor: .systemBackground) + } +} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/AddTrade/AddTradeView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/AddTrade/AddTradeView.swift index b0b33f6..71f0694 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/AddTrade/AddTradeView.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/AddTrade/AddTradeView.swift @@ -23,19 +23,22 @@ public struct AddTradeView: View { public var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - ScrollView { - VStack(spacing: 20) { - headerView(viewStore: viewStore) - .padding(.top) - - pickerView(viewStore: viewStore) - .padding(.bottom) - - inputView(viewStore: viewStore) - - Spacer() - - saveButtonView(viewStore: viewStore) + GeometryReader { proxy in + ScrollView { + VStack(spacing: 20) { + headerView(viewStore: viewStore) + + pickerView(viewStore: viewStore) + .padding(.bottom) + + inputView(viewStore: viewStore) + + Spacer() + + saveButtonView(viewStore: viewStore) + } + .frame(height: proxy.size.height) + .padding() } } } @@ -112,6 +115,8 @@ public struct AddTradeView: View { ScrollView(.horizontal) { HStack { + Image(systemName: "photo") + ForEach(viewStore.state.images, id: \.self) { imageData in ImageItem(imageData: imageData) } @@ -122,12 +127,6 @@ public struct AddTradeView: View { } } } - - VStack(alignment: .leading) { - Image(systemName: "note.text") - - TextEditor(text: viewStore.binding(get: \.note, send: AddTradeStore.Action.setNote)) - } } } diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradeItemCellStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradeItemCellStore.swift new file mode 100644 index 0000000..5cf7244 --- /dev/null +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradeItemCellStore.swift @@ -0,0 +1,79 @@ +// +// TradeItemCellStore.swift +// ToolinderFeatureCalendarDemo +// +// Created by 송영모 on 2023/09/18. +// + +import Foundation + +import ComposableArchitecture + +import ToolinderDomain + +public struct TradeItemCellStore: Reducer { + public init() {} + + public enum ViewType { + case `default` + case edit + } + + public struct State: Equatable, Identifiable { + public let id: UUID + public let trade: Trade + + public let viewType: ViewType + public let dateStyle: DateFormatter.Style + public let timeStyle: DateFormatter.Style + + public var isSelected: Bool + public init( + id: UUID = .init(), + trade: Trade, + viewType: ViewType = .default, + dateStyle: DateFormatter.Style, + timeStyle: DateFormatter.Style, + isSelected: Bool = false + ) { + self.id = id + self.trade = trade + self.viewType = viewType + self.dateStyle = dateStyle + self.timeStyle = timeStyle + 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/Calendar/Interface/Sources/Asset/Cell/TradeItemCellView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradeItemCellView.swift new file mode 100644 index 0000000..829a0b4 --- /dev/null +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradeItemCellView.swift @@ -0,0 +1,86 @@ +// +// TradeItemCellView.swift +// ToolinderFeatureCalendarDemo +// +// Created by 송영모 on 2023/09/18. +// + +import SwiftUI + +import ComposableArchitecture + +import ToolinderShared +import ToolinderDomain + +public struct TradeItemCellView: 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 { + tradeView(viewStore: viewStore) + + switch viewStore.state.viewType { + case .default: + EmptyView() + case .edit: + editButtonView(viewStore: viewStore) + } + } + } + } + + private func tradeView(viewStore: ViewStoreOf) -> some View { + HStack(spacing: 10) { + Text( + viewStore.trade.date.localizedString( + dateStyle: viewStore.state.dateStyle, + timeStyle: viewStore.state.timeStyle + ) + ) + .font(.headline) + .fontWeight(.semibold) + + viewStore.state.trade.ticker?.type?.image + .font(.title3) + .foregroundStyle(viewStore.state.trade.side == .buy ? .pink : .mint) + + Text(viewStore.state.trade.ticker?.name ?? "") + .font(.body) + .fontWeight(.semibold) + + Spacer() + } + .frame(height: 35) + .padding(10) + .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) + .clipShape( + RoundedRectangle( + cornerRadius: 8 + ) + ) + .onTapGesture { + viewStore.send(.tapped) + } + } + + private func editButtonView(viewStore: ViewStoreOf) -> some View { + Button(action: { + viewStore.send(.editButtonTapped) + }, label: { + Image(systemName: "square.and.pencil") + }) + .frame(height: 35) + .padding(10) + .background(viewStore.state.isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) + .clipShape( + RoundedRectangle( + cornerRadius: 8 + ) + ) + } +} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradePreviewItemCellStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradePreviewItemCellStore.swift new file mode 100644 index 0000000..bb042b4 --- /dev/null +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradePreviewItemCellStore.swift @@ -0,0 +1,60 @@ +// +// TradePreviewItemCellStore.swift +// ToolinderFeatureCalendarDemo +// +// Created by 송영모 on 2023/09/18. +// + +import Foundation + +import ComposableArchitecture + +import ToolinderDomain + +public struct TradePreviewItemCellStore: Reducer { + public init() {} + + public struct State: Equatable, Identifiable { + public let id: UUID + + public let trade: Trade + public var isSelected: Bool + + public init( + id: UUID = .init(), + trade: Trade, + isSelected: Bool = false + ) { + self.id = id + self.trade = trade + self.isSelected = isSelected + } + } + + public enum Action: Equatable { + case onAppear + + case tapped + + case delegate(Delegate) + + public enum Delegate: Equatable { + case tapped + } + } + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onAppear: + return .none + + case .tapped: + return .send(.delegate(.tapped)) + + default: + return .none + } + } + } +} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradePreviewItemCellView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradePreviewItemCellView.swift new file mode 100644 index 0000000..0b074e0 --- /dev/null +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Cell/TradePreviewItemCellView.swift @@ -0,0 +1,36 @@ +// +// TradePreviewItemCellView.swift +// ToolinderFeatureCalendarDemo +// +// Created by 송영모 on 2023/09/18. +// + +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain +import ToolinderShared + +public struct TradePreviewItemCellView: 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: 2) { + RoundedRectangle(cornerRadius: 3) + .fill(viewStore.state.trade.side == .buy ? .pink : .mint) + .frame(width: 2.5, height: 11) + + Text(viewStore.state.trade.ticker?.name ?? "") + .font(.caption2) + .fontWeight(.light) + .foregroundStyle(Color.blackOrWhite(!viewStore.state.isSelected)) + } + } + } +} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradeNewItem.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Component/TradeNewItem.swift similarity index 100% rename from Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradeNewItem.swift rename to Projects/Toolinder/Feature/Calendar/Interface/Sources/Asset/Component/TradeNewItem.swift diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift index 027d5e7..bc92b57 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarStore.swift @@ -22,6 +22,10 @@ public struct CalendarStore: Reducer { public var selectedDate: Date public var selectedCalendar: CalendarEntity? + public var selectedCalendarItemID: CalendarItemCellStore.State.ID? + + public var calendarItem: IdentifiedArrayOf = [] + public var tradeItem: IdentifiedArrayOf = [] @PresentationState var addTicker: AddTickerStore.State? @PresentationState var addTrade: AddTradeStore.State? @@ -35,16 +39,32 @@ public struct CalendarStore: Reducer { self.offset = offset self.calendars = calendars self.selectedDate = selectedDate + self.calendarItem = .init(uniqueElements: calendars.map { calendar in + let id = UUID() + let isSelected = calendar.date.isEqual(date: selectedDate) + if isSelected { + self.selectedCalendarItemID = id + } + return .init( + id: id, + trades: calendar.trades, + date: calendar.date, + isSelected: isSelected + ) + }) } } public enum Action: Equatable { case onAppear - case selectDate(Date) case newButtonTapped case tradeItemTapped(Trade) + case refreshTradeItem([Trade]) + + case calendarItem(id: CalendarItemCellStore.State.ID, action: CalendarItemCellStore.Action) + case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action) case addTicker(PresentationAction) case addTrade(PresentationAction) @@ -59,11 +79,6 @@ public struct CalendarStore: Reducer { Reduce { state, action in switch action { case .onAppear: - return .send(.selectDate(state.selectedDate)) - - case let .selectDate(date): - state.selectedDate = date - state.selectedCalendar = state.calendars.first(where: { $0.date.isEqual(date: date) }) return .none case .newButtonTapped: @@ -73,6 +88,35 @@ public struct CalendarStore: Reducer { case let .tradeItemTapped(trade): return .send(.delegate(.detail(trade))) + case let .refreshTradeItem(trades): + state.tradeItem = .init( + uniqueElements: trades.map { trade in + return .init( + trade: trade, + dateStyle: .short, + timeStyle: .none + ) + } + ) + return .none + + case let .calendarItem(id, action: .delegate(.tapped)): + if let prevSelectedCalendarItemID = state.selectedCalendarItemID { + state.calendarItem[id: prevSelectedCalendarItemID]?.isSelected = false + } + state.selectedCalendarItemID = id + state.calendarItem[id: id]?.isSelected = true + state.selectedDate = state.calendarItem[id: id]?.date ?? .now + let selectedTrades: [Trade] = state.calendarItem[id: id]?.trades ?? [] + return .send(.refreshTradeItem(selectedTrades)) + + case let .tradeItem(id, action: .delegate(.tapped)): + if let trade = state.tradeItem[id: id]?.trade { + return .send(.delegate(.detail(trade))) + } else { + return .none + } + case .addTicker(.presented(.delegate(.cancel))): state.addTicker = nil return .none @@ -104,6 +148,12 @@ public struct CalendarStore: Reducer { return .none } } + .forEach(\.calendarItem, action: /Action.calendarItem(id:action:)) { + CalendarItemCellStore() + } + .forEach(\.tradeItem, action: /Action.tradeItem(id:action:)) { + TradeItemCellStore() + } .ifLet(\.$addTicker, action: /Action.addTicker) { AddTickerStore() } diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift index ea6c4ed..f8d0b3a 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/CalendarView.swift @@ -25,7 +25,7 @@ public struct CalendarView: View { ScrollView(showsIndicators: false) { VStack(spacing: .zero) { - calender(viewStore: viewStore, proxy: proxy) + calenderItemListView(viewStore: viewStore, proxy: proxy) .padding(.horizontal, 10) .padding(.bottom, 10) @@ -60,7 +60,7 @@ public struct CalendarView: View { ) ) { AddTradeView(store: $0) - .presentationDetents([.medium, .large]) + .presentationDetents([.large]) .interactiveDismissDisabled() } .tag(viewStore.offset) @@ -76,26 +76,18 @@ public struct CalendarView: View { Spacer() } .padding(.horizontal, 10) - .background(.white.opacity(0.7)) + .background(Color(uiColor: .systemBackground).opacity(0.7)) Spacer() } } - private func calender(viewStore: ViewStoreOf, proxy: GeometryProxy) -> some View { + private func calenderItemListView(viewStore: ViewStoreOf, proxy: GeometryProxy) -> some View { LazyVGrid(columns: Array(repeating: .init(.flexible(), spacing: .zero), count: 7), spacing: .zero) { - ForEach(viewStore.state.calendars) { calendar in - CalendarItem( - date: calendar.date, - trades: calendar.trades, - isSelected: calendar.date.isEqual(date: viewStore.selectedDate) - ) - .frame(height: proxy.size.height * 0.12) - .onTapGesture { - viewStore.send(.selectDate(calendar.date)) - } + ForEachStore(self.store.scope(state: \.calendarItem, action: CalendarStore.Action.calendarItem(id:action:))) { + CalendarItemCellView(store: $0) + .frame(height: proxy.size.height * 0.12) } - Spacer() } } @@ -106,14 +98,8 @@ public struct CalendarView: View { .font(.title3) .fontWeight(.bold) - ForEach(viewStore.state.selectedCalendar?.trades ?? []) { trade in - TradeItem( - trade: trade, - isShowEdit: false, - action: { - viewStore.send(.tradeItemTapped(trade)) - } - ) + ForEachStore(self.store.scope(state: \.tradeItem, action: CalendarStore.Action.tradeItem(id:action:))) { + TradeItemCellView(store: $0) } TradeNewItem() diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarCell.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarCell.swift deleted file mode 100644 index 7337626..0000000 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarCell.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// CalendarCell.swift -// ToolinderFeatureCalendarInterface -// -// Created by 송영모 on 2023/09/02. -// - -import SwiftUI - -import ComposableArchitecture - -import ToolinderDomain -import ToolinderShared - -public struct CalendarItem: View { - let date: Date - let trades: [Trade] - let isSelected: Bool - - public init(date: Date, trades: [Trade], isSelected: Bool) { - self.date = date - self.trades = trades - self.isSelected = isSelected - } - - public var body: some View { - VStack(spacing: 2) { - HStack { - Spacer() - - Text("\(date.day)") - .font(.subheadline) - .fontWeight(.semibold) - .foregroundStyle(isSelected ? .white : .black) - - Spacer() - } - .padding(.top, 2) - - ForEach(trades) { trade in - TradePreviewItem(trade: trade) - } - - Spacer() - } - .background(isSelected ? .black : .white) - .clipShape( - RoundedRectangle( - cornerRadius: 8, - style: .continuous - ) - ) - } -} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellStore.swift new file mode 100644 index 0000000..73f27eb --- /dev/null +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellStore.swift @@ -0,0 +1,82 @@ +// +// CalendarItemCellStore.swift +// ToolinderFeatureCalendarDemo +// +// Created by 송영모 on 2023/09/18. +// + +import Foundation + +import ComposableArchitecture + +import ToolinderDomain + +public struct CalendarItemCellStore: Reducer { + static let PREVIEW_CELL_MAX_LENGTH = 3 + public init() {} + + public struct State: Equatable, Identifiable { + public let id: UUID + + public let trades: [Trade] + public let date: Date + public var isSelected: Bool { + didSet { + self.tradePreviewItem.ids.forEach { id in + self.tradePreviewItem[id: id]?.isSelected = isSelected + } + } + } + + public var tradePreviewItem: IdentifiedArrayOf = [] + + public init( + id: UUID = .init(), + trades: [Trade] = [], + date: Date = .now, + isSelected: Bool = false + ) { + self.id = id + self.trades = trades + self.date = date + self.isSelected = isSelected + + self.tradePreviewItem = .init( + uniqueElements: + self.trades.map { + .init(trade: $0, isSelected: isSelected) + } + .suffix(PREVIEW_CELL_MAX_LENGTH) + ) + } + } + + public enum Action: Equatable { + case onAppear + + case tapped + + case tradePreviewItem(id: TradePreviewItemCellStore.State.ID, action: TradePreviewItemCellStore.Action) + + case delegate(Delegate) + + public enum Delegate: Equatable { + case tapped + } + } + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onAppear: + return .none + + case .tapped: + return .send(.delegate(.tapped)) + + default: + return .none + } + } + } +} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift new file mode 100644 index 0000000..f59773d --- /dev/null +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/CalendarItemCellView.swift @@ -0,0 +1,56 @@ +// +// CalendarItemCellView.swift +// ToolinderFeatureCalendarDemo +// +// Created by 송영모 on 2023/09/18. +// + +import SwiftUI + +import ComposableArchitecture + +import ToolinderDomain +import ToolinderShared + +public struct CalendarItemCellView: View { + private let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(spacing: 2) { + HStack { + Spacer() + + Text("\(viewStore.state.date.day)") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(Color.blackOrWhite(!viewStore.state.isSelected)) + + Spacer() + } + .padding(.top, 2) + + ForEachStore(self.store.scope(state: \.tradePreviewItem, action: CalendarItemCellStore.Action.tradePreviewItem(id:action:))) { + TradePreviewItemCellView(store: $0) + } + + Spacer() + } + .background(Color.blackOrWhite(viewStore.state.isSelected)) + .clipShape( + RoundedRectangle( + cornerRadius: 8, + style: .continuous + ) + ) + .onTapGesture { + viewStore.send(.tapped) + } + } + } +} + diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradeItem.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradeItem.swift deleted file mode 100644 index a0509d6..0000000 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradeItem.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// TradeItem.swift -// ToolinderFeatureCalendarInterface -// -// Created by 송영모 on 2023/09/02. -// - -import SwiftUI - -import ToolinderShared -import ToolinderDomain - -public struct TradeItem: View { - private let trade: Trade - private let isSelected: Bool - - private let isShowOnlyTime: Bool - private let isShowEdit: Bool - - public var action: () -> Void - public var trailingAction: () -> Void - - public init( - trade: Trade, - isSelected: Bool = false, - isShowOnlyTime: Bool = true, - isShowEdit: Bool = true, - action: @escaping () -> Void = {}, - trailingAction: @escaping () -> Void = {} - ) { - self.trade = trade - self.isSelected = isSelected - self.isShowOnlyTime = isShowOnlyTime - self.isShowEdit = isShowEdit - - self.action = action - self.trailingAction = trailingAction - } - - public var body: some View { - HStack { - tradeView() - - if isShowEdit { - editButtonView() - } - } - } - - private func tradeView() -> some View { - HStack(spacing: 10) { - if isShowOnlyTime { - Text(trade.date.localizedString(dateStyle: .none, timeStyle: .short)) - .font(.headline) - .fontWeight(.semibold) - } else { - Text(trade.date.localizedString(dateStyle: .short, timeStyle: .short)) - .font(.headline) - .fontWeight(.semibold) - } - - trade.ticker?.type?.image - .font(.title3) - .foregroundStyle(trade.side == .buy ? .pink : .mint) - - Text(trade.ticker?.name ?? "") - .font(.body) - .fontWeight(.semibold) - - Spacer() - } - .frame(height: 35) - .padding(10) - .background(isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) - .clipShape( - RoundedRectangle( - cornerRadius: 8 - ) - ) - .onTapGesture { - self.action() - } - } - - private func editButtonView() -> some View { - Button(action: {}, label: { - Image(systemName: "square.and.pencil") - }) - .frame(height: 35) - .padding(10) - .background(isSelected ? Color(uiColor: .systemGray5) : Color(uiColor: .systemGray6)) - .clipShape( - RoundedRectangle( - cornerRadius: 8 - ) - ) - .onTapGesture { - self.trailingAction() - } - } -} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradePreviewItem.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradePreviewItem.swift deleted file mode 100644 index 75ac136..0000000 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/Calendar/Cell/TradePreviewItem.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// TradePreviewItem.swift -// ToolinderFeatureCalendarInterface -// -// Created by 송영모 on 2023/09/02. -// - -import SwiftUI - -import ToolinderDomain -import ToolinderShared - -public struct TradePreviewItem: View { - private let trade: Trade - - public init(trade: Trade) { - self.trade = trade - } - - public var body: some View { - HStack(spacing: 2) { - RoundedRectangle(cornerRadius: 3) - .fill(trade.side == .buy ? .pink : .mint) - .frame(width: 2.5, height: 11) - - Text(trade.ticker?.name ?? "") - .font(.caption2) - .fontWeight(.light) - } - } -} diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailStore.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailStore.swift index e109ff8..cdb043d 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailStore.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailStore.swift @@ -18,6 +18,7 @@ public struct TradeDetailStore: Reducer { public var trade: Trade @PresentationState var addTrade: AddTradeStore.State? + public var tradeItem: IdentifiedArrayOf = [] public init(trade: Trade) { self.trade = trade @@ -30,6 +31,7 @@ public struct TradeDetailStore: Reducer { case editButtonTapped case addTrade(PresentationAction) + case tradeItem(id: TradeItemCellStore.State.ID, action: TradeItemCellStore.Action) case delegate(Delegate) @@ -44,6 +46,11 @@ public struct TradeDetailStore: Reducer { Reduce { state, action in switch action { case .onAppear: + state.tradeItem = .init( + uniqueElements: state.trade.ticker?.trades?.compactMap { trade in + return .init(trade: trade, dateStyle: .short, timeStyle: .none) + } ?? [] + ) return .none case .editButtonTapped: diff --git a/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailView.swift b/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailView.swift index 1fcc355..269c611 100644 --- a/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailView.swift +++ b/Projects/Toolinder/Feature/Calendar/Interface/Sources/TradeDetail/TradeDetailView.swift @@ -28,24 +28,10 @@ public struct TradeDetailView: View { tradeView(viewStore: viewStore) Divider() - .padding(.horizontal) - HStack { - Label("Note", systemImage: "note.text") - .font(.headline) - - Spacer() - } - .padding(.horizontal) - .padding(.bottom, 5) + noteView(viewStore: viewStore) - HStack { - Text("월터 테비스[1]의 1983년 소설 《The Queen's Gambit》을 원작으로 넷플릭스에서 제작해 2020년 10월 23일에 공개된 7부작 미니시리즈이다. 제목인 퀸즈 갬빗이 체스 용어이듯 체스를 소재로 한 드라마로, 시청 등급은 청소년 관람불가. 스콧 프랭크[2]가 감독을 맡고 안야 테일러조이가 주인공 엘리자베스 하먼을 연기한다.체스판을 묘사한 대다수의 영화나 텔레비전 쇼와는 달리, 체스판이 정확하게 세팅되어 있고 체스 게임과 포지션 역시 꽤나 현실적인 것이 이 시리즈의 특징이다. 내셔널 마스터 브루스 판돌피니와 전직 세계 챔피언이자 역사상 최고의 체스 플레이어 중 하나로 평가받는 그랜드마스터 가리 카스파로프가 이 시리즈의 컨설턴트 역할을 했다.") - .font(.caption) - - Spacer() - } - .padding(.horizontal) + photoView(viewStore: viewStore) HStack { if let ticker = viewStore.state.trade.ticker { @@ -54,19 +40,13 @@ public struct TradeDetailView: View { Spacer() } - .padding(.horizontal) tradeListView(viewStore: viewStore) } + .padding() } - .navigationTitle(viewStore.state.trade.ticker?.name ?? "") - .navigationBarTitleDisplayMode(.large) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Edit") { - viewStore.send(.editButtonTapped) - } - } + .onAppear { + viewStore.send(.onAppear) } .sheet( store: self.store.scope( @@ -75,7 +55,16 @@ public struct TradeDetailView: View { ) ) { AddTradeView(store: $0) - .presentationDetents([.medium, .large]) + .presentationDetents([.large]) + } + .navigationTitle(viewStore.state.trade.ticker?.name ?? "") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Edit") { + viewStore.send(.editButtonTapped) + } + } } } } @@ -92,7 +81,6 @@ public struct TradeDetailView: View { Text("vol") .fontWeight(.semibold) } - .padding(.horizontal) HStack(alignment: .bottom, spacing: .zero) { Spacer() @@ -104,7 +92,6 @@ public struct TradeDetailView: View { Text("\(viewStore.state.trade.ticker?.currency?.rawValue.lowercased() ?? "")") .fontWeight(.semibold) } - .padding(.horizontal) } } @@ -117,13 +104,49 @@ public struct TradeDetailView: View { } .padding(.bottom, 5) - ForEach(viewStore.state.trade.ticker?.trades ?? []) { trade in - TradeItem(trade: trade, isShowOnlyTime: false, isShowEdit: true) + ForEachStore(self.store.scope(state: \.tradeItem, action: TradeDetailStore.Action.tradeItem(id:action:))) { + TradeItemCellView(store: $0) } TradeNewItem() } - .padding(.horizontal) + } + + private func noteView(viewStore: ViewStoreOf) -> some View { + VStack(spacing: 5) { + HStack { + Label("Note", systemImage: "note.text") + .font(.headline) + + Spacer() + } + + HStack { + Text(viewStore.state.trade.note ?? "") + .font(.caption) + + Spacer() + } + } + } + + private func photoView(viewStore: ViewStoreOf) -> some View { + VStack(spacing: 5) { + HStack { + Label("Photo", systemImage: "photo") + .font(.headline) + + Spacer() + } + + HStack { + ForEach(viewStore.state.trade.images, id: \.self) { imageData in + ImageItem(imageData: imageData) + } + + Spacer() + } + } } private func scaledString(valueOrNil: Double?) -> String { @@ -138,4 +161,3 @@ public struct TradeDetailView: View { } } } - diff --git a/Projects/Toolinder/Shared/Util/Interface/Sources/Date+Extension.swift b/Projects/Toolinder/Shared/Util/Interface/Sources/Date+Extension.swift index 65d6635..110e1fe 100644 --- a/Projects/Toolinder/Shared/Util/Interface/Sources/Date+Extension.swift +++ b/Projects/Toolinder/Shared/Util/Interface/Sources/Date+Extension.swift @@ -22,20 +22,37 @@ public extension Date { func allDatesInMonth() -> [Date] { let calendar = Calendar.current - let components = calendar.dateComponents([.year, .month], from: self) - guard let startDate = calendar.date(from: components) else { - return [] + + let startOfMonth = self.startOfMonth + var prevDates: [Date] = [] + var prevDate = calendar.date(byAdding: .day, value: -1, to: startOfMonth) ?? .now + + while calendar.dateComponents([.weekday], from: prevDate).weekday != 7 { + prevDates.append(prevDate) + guard let date = calendar.date(byAdding: .day, value: -1, to: prevDate) else { return [] } + prevDate = date + } + + let endOfMonth: Date = self.endOfMonth + var nextDates: [Date] = [] + var nextDate: Date = calendar.date(byAdding: .day, value: 1, to: endOfMonth) ?? .now + + while calendar.dateComponents([.weekday], from: nextDate).weekday != 1 { + nextDates.append(nextDate) + guard let date = calendar.date(byAdding: .day, value: 1, to: nextDate) else { return [] } + nextDate = date } - var currentDate = startDate - var allDates: [Date] = [] + var currentDates: [Date] = [] + var currentDate: Date = startOfMonth - while calendar.isDate(currentDate, equalTo: startDate, toGranularity: .month) { - allDates.append(currentDate) - currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? Date() + while !currentDate.isEqual(date: endOfMonth.add(byAdding: .day, value: 1)) { + currentDates.append(currentDate) + guard let date = calendar.date(byAdding: .day, value: 1, to: currentDate) else { return [] } + currentDate = date } - return allDates + return prevDates.reversed() + currentDates + nextDates } func add(byAdding: Calendar.Component, value: Int) -> Date { @@ -47,4 +64,29 @@ public extension Date { let calendar = Calendar.current return calendar.isDate(self, inSameDayAs: date) } + + var startOfDay: Date { + return Calendar.current.startOfDay(for: self) + } + + var startOfMonth: Date { + let calendar = Calendar(identifier: .gregorian) + let components = calendar.dateComponents([.year, .month], from: self) + + return calendar.date(from: components)! + } + + var endOfDay: Date { + var components = DateComponents() + components.day = 1 + components.second = -1 + return Calendar.current.date(byAdding: components, to: startOfDay)! + } + + var endOfMonth: Date { + var components = DateComponents() + components.month = 1 + components.second = -1 + return Calendar(identifier: .gregorian).date(byAdding: components, to: startOfMonth)! + } }