diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index e329fdb3..75bd83c3 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -188,8 +188,22 @@ private extension GoalDetailView { cardOffset = repeatedCardOffset(for: width) isCrossingDuringDrag = shouldCrossCards(for: width) } - .onEnded { _ in + .onEnded { value in guard !store.isEditing else { return } + + let translation = value.translation + let width = resistedDragWidth( + for: translation.width, + velocity: value.velocity.width + ) + + guard abs(width) >= abs(translation.height) else { + withAnimation(.spring(response: 0.2, dampingFraction: 0.94)) { + resetDragState() + } + return + } + withAnimation(.spring(response: 0.2, dampingFraction: 0.94)) { resetDragState() store.send(.view(.cardSwiped)) @@ -431,7 +445,7 @@ private extension GoalDetailView { .padding(.bottom, 26) .frame(width: rectFrame.width, height: rectFrame.height, alignment: .bottom) .rotationEffect(frontCardRotation) - .offset(x: posX, y: posY - keyboardInset) + .offset(x: posX + cardOffset, y: posY - keyboardInset) .animation(.easeOut(duration: 0.25), value: keyboardInset) } } diff --git a/Projects/Feature/Home/Sources/Home/HomeEmptyContentSection.swift b/Projects/Feature/Home/Sources/Home/HomeEmptyContentSection.swift index 6212798f..e056ef9b 100644 --- a/Projects/Feature/Home/Sources/Home/HomeEmptyContentSection.swift +++ b/Projects/Feature/Home/Sources/Home/HomeEmptyContentSection.swift @@ -10,42 +10,34 @@ import FeatureHomeInterface import SharedDesignSystem /// `hadFirstGoal`을 읽는 빈 상태 영역입니다. -/// `emptyScrollHeight`는 로컬 `@State`로 관리해 다른 콘텐츠 section 재렌더링에 의해 -/// 초기화되지 않게 합니다. +/// `goalEmptyView`의 center anchor를 기기 화면 기준 y축 중앙에 배치합니다. struct HomeEmptyContentSection: View { let store: StoreOf - @State private var emptyScrollHeight: CGFloat = 0 - var body: some View { VStack(spacing: 0) { - HomeHeaderRow(store: store) - .padding(.horizontal, 20) - .padding(.top, 16) + GeometryReader { geo in + let frame = geo.frame(in: .global) + let deviceHeight = UIScreen.main.bounds.height + let deviceCenterYInSection = max(0, deviceHeight / 2 - frame.minY) - ScrollView { - goalEmptyView - // 실제 가시 영역 기준으로 중앙 정렬되도록 탭바 높이만큼 차감 - .frame(maxWidth: .infinity, minHeight: max(0, emptyScrollHeight - 58)) - .padding(.bottom, 58) - } - .scrollIndicators(.hidden) - .refreshable { - store.send(.view(.refreshPulled)) - } - .overlay(alignment: .bottomTrailing) { - emptyArrow - } - .frame(maxHeight: .infinity) - .background { - GeometryReader { geo in - Color.clear - .onAppear { emptyScrollHeight = geo.size.height } - .onChange(of: geo.size.height) { _, newValue in - emptyScrollHeight = newValue - } + ScrollView { + goalEmptyView + .frame(width: geo.size.width) + .position(x: geo.size.width / 2, y: deviceCenterYInSection) + } + .scrollIndicators(.hidden) + .refreshable { + store.send(.view(.refreshPulled)) } + .overlay(alignment: .bottomTrailing) { + if store.hadFirstGoal == false { + emptyArrow + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) } + .frame(maxHeight: .infinity) } } @@ -79,12 +71,11 @@ struct HomeEmptyContentSection: View { } } } - .frame(maxWidth: .infinity, maxHeight: .infinity) } var emptyArrow: some View { Image.Illustration.arrow - .padding(.bottom, 71 + 58) + .padding(.bottom, 71 + TXTabBarLayout.height) .padding(.trailing, 86) .ignoresSafeArea() } diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index 147d9888..7f94a323 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -177,7 +177,9 @@ private extension MakeGoalView { Spacer() if store.showPeriodCount { - dropDownButton { store.send(.view(.periodSelected)) } + dropDownButton(text: store.periodCountText) { + store.send(.view(.periodSelected)) + } } } } @@ -190,7 +192,9 @@ private extension MakeGoalView { Spacer() - dropDownButton { store.send(.view(.startDateTapped)) } + dropDownButton(text: store.startDateText) { + store.send(.view(.startDateTapped)) + } } .frame(height: 32) .padding(.vertical, 16) @@ -214,7 +218,9 @@ private extension MakeGoalView { Spacer() - dropDownButton { store.send(.view(.endDateTapped)) } + dropDownButton(text: store.endDateText) { + store.send(.view(.endDateTapped)) + } } .padding(.vertical, 21.5) } @@ -236,9 +242,12 @@ private extension MakeGoalView { .padding(.vertical, -1) } - func dropDownButton(_ action: @escaping () -> Void) -> some View { + func dropDownButton( + text: String, + action: @escaping () -> Void + ) -> some View { HStack(spacing: 0) { - Text(store.startDateText) + Text(text) .typography(.b2_14r) .foregroundStyle(Color.Gray.gray500) Image.Icon.Symbol.arrow2Down diff --git a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift index 92f52b5c..0ce3b6df 100644 --- a/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift +++ b/Projects/Feature/Stats/Sources/Detail/StatsDetailView.swift @@ -156,8 +156,7 @@ private extension StatsDetailView { summaryTitle(for: summary.title) summartyContent(content: summary.content, isCompletedCount: summary.isCompletedCount) .layoutPriority(1) - - Spacer() + .frame(maxWidth: .infinity, alignment: .leading) } } } @@ -188,7 +187,6 @@ private extension StatsDetailView { Text(content[0]) .typography(.b4_12b) .foregroundStyle(Color.Gray.gray500) - .lineLimit(1) if isCompletedCount { Text("|") @@ -199,11 +197,9 @@ private extension StatsDetailView { Text(content[1]) .typography(.b4_12b) .foregroundStyle(Color.Gray.gray500) - .lineLimit(1) } } - .lineLimit(1) - .fixedSize(horizontal: true, vertical: false) + .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift index ea895461..b72bdaf1 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift @@ -9,19 +9,27 @@ import SwiftUI struct TXRoundButton: View { let shape: TXButtonShape + let allowsActionWhenDisabled: Bool let onTap: () -> Void public var body: some View { if case let .round(style, size, state) = shape { Button { - onTap() + switch state { + case .disabled: + if allowsActionWhenDisabled { + onTap() + } + case .standard: + onTap() + } } label: { - ZStack { + ZStack(alignment: .top) { Capsule() .fill(style.backgroundColor(state: state)) .frame(maxWidth: size.frameWidth) - .frame(height: size.backgroundHeight(state: state)) - .padding(.top, size.bottomYOffset(state: state)) + .frame(height: size.backgroundHeight) + .padding(.top, size.backgroundYOffset) Text(style.text) .typography(size.typography) @@ -34,8 +42,8 @@ struct TXRoundButton: View { lineWidth: size.borderWidth ) .background(style.foregroundColor(state: state), in: .capsule) + .padding(.top, size.foregroundYOffset(state: state)) } - .padding(.top, size.topYOffset(state: state)) } .buttonStyle(.plain) } else { @@ -112,29 +120,19 @@ private extension TXButtonShape.TXRoundSize { } } - func backgroundHeight(state: TXButtonShape.TXRoundState) -> CGFloat { + var backgroundHeight: CGFloat { switch self { case .l, .m: 70 - case .s: - switch state { - case .standard: 31 - case .disabled: 28 - } + case .s: 28 } } - func bottomYOffset(state: TXButtonShape.TXRoundState) -> CGFloat { - switch self { - case .s, .l, .m: - switch state { - case .standard: 4 - case .disabled: 1 - } - } + var backgroundYOffset: CGFloat { + 4 } - func topYOffset(state: TXButtonShape.TXRoundState) -> CGFloat { + func foregroundYOffset(state: TXButtonShape.TXRoundState) -> CGFloat { switch self { case .s, .l, .m: switch state { diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/TXButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/TXButton.swift index 6af90249..cf8d27e7 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/TXButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/TXButton.swift @@ -24,6 +24,7 @@ import SwiftUI /// ``` public struct TXButton: View { let shape: TXButtonShape + let allowsActionWhenDisabled: Bool let onTap: () -> Void /// 버튼을 생성합니다. @@ -41,9 +42,11 @@ public struct TXButton: View { /// ``` public init( shape: TXButtonShape, + allowsActionWhenDisabled: Bool = false, onTap: @escaping () -> Void ) { self.shape = shape + self.allowsActionWhenDisabled = allowsActionWhenDisabled self.onTap = onTap } @@ -65,6 +68,7 @@ public struct TXButton: View { case .round: TXRoundButton( shape: shape, + allowsActionWhenDisabled: allowsActionWhenDisabled, onTap: onTap ) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift index 524cb235..ae498178 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/Goal/GoalCardView.swift @@ -169,6 +169,7 @@ private extension GoalCardView { size: .s, state: isButtonDisabled ? .disabled : .standard ), + allowsActionWhenDisabled: true, onTap: { buttonAction?() } ) .padding(.bottom, 14) @@ -179,7 +180,7 @@ private extension GoalCardView { .padding(.bottom, 10) } } - .frame(maxHeight: .infinity) + .frame(maxHeight: .infinity, alignment: .center) } func emojiImage(emoji: Image) -> some View {