diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 8575065a..3c1d42fd 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -62,31 +62,23 @@ public struct GoalDetailView: View { } public var body: some View { - VStack(spacing: 0) { - navigationBar - .zIndex(1) - - if store.item != nil { - cardView - .padding(.horizontal, 27) - .padding(.top, isSEDevice ? 47 : 103) - - if store.isCompleted { - completedBottomContent - } else if store.currentCompletedGoal?.status != .completed { - bottomButton - .padding(.top, 105) - .overlay(alignment: .bottomLeading) { - pokeImage - .offset(x: 79, y: -45) - } + GeometryReader { _ in + ZStack { + mainContent + + if store.isEditing && store.isCommentFocused { + dimmedView + .ignoresSafeArea() + } + + if shouldShowCommentOverlay { + floatingCommentOverlay } } - - Spacer() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } .ignoresSafeArea(.keyboard) - .background(dimmedView) + .background(Color.Common.white) .toolbar(.hidden, for: .navigationBar) .observeKeyboardFrame($keyboardFrame) .onAppear { @@ -121,6 +113,32 @@ public struct GoalDetailView: View { // MARK: - SubViews private extension GoalDetailView { + var mainContent: some View { + VStack(spacing: 0) { + navigationBar + .zIndex(1) + + if store.item != nil { + cardView + .padding(.horizontal, 27) + .padding(.top, isSEDevice ? 47 : 103) + + if store.isCompleted { + completedBottomContent + } else if store.currentCompletedGoal?.status != .completed { + bottomButton + .padding(.top, 105) + .overlay(alignment: .bottomLeading) { + pokeImage + .offset(x: 79, y: -45) + } + } + } + + Spacer() + } + } + var navigationBar: some View { TXNavigationBar( style: .subContent( @@ -135,7 +153,6 @@ private extension GoalDetailView { store.send(.view(.navigationBarTapped(action))) } ) - .overlay(dimmedView) } var cardView: some View { @@ -149,6 +166,7 @@ private extension GoalDetailView { .gesture( DragGesture() .onChanged { value in + guard !store.isEditing else { return } let translation = value.translation let width = resistedDragWidth( for: translation.width, @@ -169,6 +187,7 @@ private extension GoalDetailView { isCrossingDuringDrag = shouldCrossCards(for: width) } .onEnded { _ in + guard !store.isEditing else { return } withAnimation(.spring(response: 0.2, dampingFraction: 0.94)) { resetDragState() store.send(.view(.cardSwiped)) @@ -184,7 +203,6 @@ private extension GoalDetailView { isCompleted: store.myCardIsCompleted, imageData: store.pendingEditedImageData, imageURL: store.myCard?.imageUrl, - comment: store.myCard?.comment ?? "", showsMyEmoji: effectiveIsFrontMyCard && store.selectedReactionEmoji != nil ) .offset(x: cardOffset * (effectiveIsFrontMyCard ? 1 : -1)) @@ -197,7 +215,6 @@ private extension GoalDetailView { isCompleted: store.partnerCardIsCompleted, imageData: nil, imageURL: store.partnerCard?.imageUrl, - comment: store.partnerCard?.comment ?? "", showsMyEmoji: false ) .offset(x: cardOffset * (effectiveIsFrontMyCard ? -1 : 1)) @@ -250,7 +267,6 @@ private extension GoalDetailView { lineWidth: 1.6 ) .frame(width: 336, height: 336) - .overlay(dimmedView) .clipShape(shape) } @@ -260,7 +276,6 @@ private extension GoalDetailView { isCompleted: Bool, imageData: Data?, imageURL: String?, - comment: String, showsMyEmoji: Bool ) -> some View { ZStack { @@ -271,7 +286,6 @@ private extension GoalDetailView { isCompleted: isCompleted, imageData: imageData, imageURL: imageURL, - comment: comment, showsMyEmoji: showsMyEmoji ) .opacity(isFront ? 1 : 0) @@ -283,14 +297,12 @@ private extension GoalDetailView { isCompleted: Bool, imageData: Data?, imageURL: String?, - comment: String, showsMyEmoji: Bool ) -> some View { if isCompleted { completedImageCard( imageData: imageData, imageURL: imageURL, - comment: comment, showsMyEmoji: showsMyEmoji ) } else { @@ -318,26 +330,28 @@ private extension GoalDetailView { shape: shape, lineWidth: 1.6 ) - .overlay(dimmedView) } @ViewBuilder func completedImageCard( imageData: Data?, imageURL: String?, - comment: String, showsMyEmoji: Bool ) -> some View { if let imageData, let editedImage = UIImage(data: imageData) { - completedImageCardContainer(comment: comment, showsMyEmoji: showsMyEmoji) { + completedImageCardContainer( + showsMyEmoji: showsMyEmoji + ) { Image(uiImage: editedImage) .resizable() .scaledToFill() } } else if let imageURL, let url = URL(string: imageURL) { - completedImageCardContainer(comment: comment, showsMyEmoji: showsMyEmoji) { + completedImageCardContainer( + showsMyEmoji: showsMyEmoji + ) { KFImage(url) .resizable() .scaledToFill() @@ -373,22 +387,46 @@ private extension GoalDetailView { ) .perfControl(slug: "goal-detail", element: "primary-cta") } - + @ViewBuilder func commentCircle(comment: String) -> some View { - let keyboardInset = max(0, rectFrame.maxY - keyboardFrame.minY) TXCommentCircle( commentText: store.isEditing ? $store.commentText : .constant(comment), isEditable: store.isEditing, - keyboardInset: keyboardInset, isFocused: $store.isCommentFocused, onFocused: { isFocused in store.send(.view(.focusChanged(isFocused))) } ) - .animation(.easeOut(duration: 0.25), value: keyboardInset) } - + + var shouldShowCommentOverlay: Bool { + guard store.isCompleted, rectFrame != .zero else { return false } + return store.isEditing || !currentFrontComment.isEmpty + } + + var currentFrontComment: String { + if effectiveIsFrontMyCard { + return store.myCard?.comment ?? "" + } else { + return store.partnerCard?.comment ?? "" + } + } + + var floatingCommentOverlay: some View { + GeometryReader { rootGeo in + let rootFrame = rootGeo.frame(in: .global) + let posX = rectFrame.minX - rootFrame.minX + let posY = rectFrame.minY - rootFrame.minY + + commentCircle(comment: currentFrontComment) + .padding(.bottom, 26) + .frame(width: rectFrame.width, height: rectFrame.height, alignment: .bottom) + .offset(x: posX, y: posY - keyboardInset) + .animation(.easeOut(duration: 0.25), value: keyboardInset) + } + } + var dimmedView: some View { Color.Dimmed.dimmed70 .opacity(store.isEditing && store.isCommentFocused ? 1 : 0) @@ -402,7 +440,6 @@ private extension GoalDetailView { } func completedImageCardContainer( - comment: String, showsMyEmoji: Bool, @ViewBuilder content: @escaping () -> Content ) -> some View { @@ -416,14 +453,7 @@ private extension GoalDetailView { .frame(maxWidth: .infinity, maxHeight: .infinity) .clipped() } - .overlay(dimmedView) .clipShape(shape) - .overlay(alignment: .bottom) { - if !comment.isEmpty { - commentCircle(comment: comment) - .padding(.bottom, 26) - } - } .insideBorder( Color.Gray.gray500, shape: shape, @@ -500,6 +530,10 @@ private extension GoalDetailView { isCrossingDuringDrag ? !store.isFrontMyCard : store.isFrontMyCard } + var keyboardInset: CGFloat { + max(0, rectFrame.maxY - keyboardFrame.minY) + } + func repeatedCardOffset(for width: CGFloat) -> CGFloat { let maxOffset = Constants.maxCardOffset let direction: CGFloat = width >= 0 ? 1 : -1 diff --git a/Projects/Feature/MainTab/Sources/View/MainTabView.swift b/Projects/Feature/MainTab/Sources/View/MainTabView.swift index 7a0dffcc..cd0c8616 100644 --- a/Projects/Feature/MainTab/Sources/View/MainTabView.swift +++ b/Projects/Feature/MainTab/Sources/View/MainTabView.swift @@ -64,7 +64,7 @@ public struct MainTabView: View { } .txToast( item: $store.home.home.presentation.toast, - customPadding: Constants.tabBarHeight + customPadding: TXTabBarLayout.height ) .txLoading(isPresented: isTabLoading) } @@ -94,13 +94,7 @@ private extension MainTabView { ) .shadow(color: .black.opacity(0.16), radius: 20, x: 2, y: 1) .padding(.trailing, 16) - .padding(.bottom, 12 + Constants.tabBarHeight) - } -} - -private extension MainTabView { - enum Constants { - static let tabBarHeight: CGFloat = 58 + .padding(.bottom, 12 + TXTabBarLayout.height) } } diff --git a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift index fd20682d..147d9888 100644 --- a/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift +++ b/Projects/Feature/MakeGoal/Sources/MakeGoalView.swift @@ -122,7 +122,7 @@ private extension MakeGoalView { backgroundColor: Color.Common.white ) ), - onTap: { } + onTap: { store.send(.view(.emojiButtonTapped)) } ) .insideBorder( Color.Gray.gray500, @@ -177,7 +177,6 @@ private extension MakeGoalView { Spacer() if store.showPeriodCount { - valueText(store.periodCountText) dropDownButton { store.send(.view(.periodSelected)) } } } @@ -191,7 +190,6 @@ private extension MakeGoalView { Spacer() - valueText(store.startDateText) dropDownButton { store.send(.view(.startDateTapped)) } } .frame(height: 32) @@ -216,7 +214,6 @@ private extension MakeGoalView { Spacer() - valueText(store.endDateText) dropDownButton { store.send(.view(.endDateTapped)) } } .padding(.vertical, 21.5) @@ -240,11 +237,15 @@ private extension MakeGoalView { } func dropDownButton(_ action: @escaping () -> Void) -> some View { - Button { - action() - } label: { + HStack(spacing: 0) { + Text(store.startDateText) + .typography(.b2_14r) + .foregroundStyle(Color.Gray.gray500) Image.Icon.Symbol.arrow2Down } + .onTapGesture { + action() + } } func sectionTitleText(_ text: String) -> some View { diff --git a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift index 19d70e8b..25066860 100644 --- a/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift +++ b/Projects/Feature/ProofPhoto/Sources/ProofPhoto/ProofPhotoView.swift @@ -347,7 +347,7 @@ private extension ProofPhotoView { } .padding(.bottom, 28) .frame(width: rectFrame.width, height: rectFrame.height, alignment: .bottom) - .offset(x: posX, y: posY) + .offset(x: posX, y: posY - keyboardInset) .animation(.easeOut(duration: 0.25), value: keyboardInset) } } @@ -362,7 +362,6 @@ private extension ProofPhotoView { TXCommentCircle( commentText: $store.commentText, isEditable: true, - keyboardInset: keyboardInset, isFocused: $store.isCommentFocused, onFocused: { isFocused in store.send(.view(.focusChanged(isFocused))) diff --git a/Projects/Feature/Stats/Sources/Stats/StatsView.swift b/Projects/Feature/Stats/Sources/Stats/StatsView.swift index b325190f..b5fa40fb 100644 --- a/Projects/Feature/Stats/Sources/Stats/StatsView.swift +++ b/Projects/Feature/Stats/Sources/Stats/StatsView.swift @@ -19,11 +19,6 @@ struct StatsView: View { VStack(spacing: 0) { navigationBar topTabBar - if store.isOngoing { - monthNavigation - .padding(.top, 16) - .background(Color.Gray.gray50) - } if let items = store.items, !items.isEmpty { cardList @@ -88,6 +83,12 @@ private extension StatsView { private var scrollCardList: some View { ScrollView { + if store.isOngoing { + monthNavigation + .padding(.top, 16) + .background(Color.Gray.gray50) + } + LazyVStack(spacing: 16) { ForEach(store.items ?? [], id: \.self.goalId) { item in StatsCardView( @@ -101,7 +102,8 @@ private extension StatsView { } } .padding(.top, store.isOngoing ? 12 : 20) - .padding([.horizontal, .bottom], 20) + .padding(.horizontal, 20) + .padding(.bottom, 85 + TXTabBarLayout.height) .perfFeed("stats") } .background(Color.Gray.gray50) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Bar/TabBar/TXTabBar.swift b/Projects/Shared/DesignSystem/Sources/Components/Bar/TabBar/TXTabBar.swift index cf35de20..2fb422d9 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Bar/TabBar/TXTabBar.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Bar/TabBar/TXTabBar.swift @@ -23,7 +23,7 @@ struct TXTabBar: View { tabItemView(item: item) } } - .frame(height: Constants.tabBarHeight) + .frame(height: TXTabBarLayout.height) .background(Constants.backgroundColor) .insideRectEdgeBorder( width: Constants.borderWidth, @@ -60,7 +60,6 @@ private extension TXTabBar { // MARK: - Constants private extension TXTabBar { enum Constants { - static let tabBarHeight: CGFloat = 58 static let iconSize: CGFloat = 24 static let iconLabelSpacing: CGFloat = 4 static let topPadding: CGFloat = 12 diff --git a/Projects/Shared/DesignSystem/Sources/Components/Bar/TabBar/TXTabBarLayout.swift b/Projects/Shared/DesignSystem/Sources/Components/Bar/TabBar/TXTabBarLayout.swift new file mode 100644 index 00000000..d2f29d64 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Bar/TabBar/TXTabBarLayout.swift @@ -0,0 +1,13 @@ +// +// TXTabBarLayout.swift +// SharedDesignSystem +// +// Created by 정지훈 on 6/1/26. +// + +import Foundation + +/// 하단 탭바와 주변 레이아웃에서 공유하는 치수입니다. +public enum TXTabBarLayout { + public static let height: CGFloat = 58 +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift index cbcebbb3..ea895461 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/Round/TXRoundButton.swift @@ -14,9 +14,7 @@ struct TXRoundButton: View { public var body: some View { if case let .round(style, size, state) = shape { Button { - if state != .disabled { - onTap() - } + onTap() } label: { ZStack { Capsule() diff --git a/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift b/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift index 7eddf617..077a57dd 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Tab/TopBar/TXTopTabBar.swift @@ -43,7 +43,6 @@ struct TXTopTabBar: View { } label: { tabItem(item: item, isSelected: selectedItem == item) } - .buttonStyle(.plain) .frame(maxWidth: .infinity) } } diff --git a/Projects/Shared/DesignSystem/Sources/Components/TextField/TXCommentCircle.swift b/Projects/Shared/DesignSystem/Sources/Components/TextField/TXCommentCircle.swift index c0f633e3..ab83ae44 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/TextField/TXCommentCircle.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/TextField/TXCommentCircle.swift @@ -12,7 +12,6 @@ public struct TXCommentCircle: View { @Binding private var commentText: String @FocusState private var isFocused: Bool private let isEditable: Bool - private let keyboardInset: CGFloat private let externalFocus: Binding? public var onFocused: ((Bool) -> Void)? @@ -28,13 +27,11 @@ public struct TXCommentCircle: View { public init( commentText: Binding, isEditable: Bool, - keyboardInset: CGFloat, isFocused: Binding? = nil, onFocused: ((Bool) -> Void)? = nil ) { self._commentText = commentText self.isEditable = isEditable - self.keyboardInset = keyboardInset self.externalFocus = isFocused self.onFocused = onFocused } @@ -55,12 +52,6 @@ public struct TXCommentCircle: View { commentText = String(commentText.prefix(Constants.maxCount)) } } - .safeAreaInset(edge: .bottom) { - if isFocused { - Color.clear - .frame(height: keyboardInset) - } - } .onChange(of: isFocused) { onFocused?(isFocused) externalFocus?.wrappedValue = isFocused @@ -189,7 +180,6 @@ private struct PositionedCircleShape: Shape { @Previewable @State var text: String = "" TXCommentCircle( commentText: $text, - isEditable: true, - keyboardInset: .zero + isEditable: true ) }