From 1f1b1395e7c55fff31912a10e294a012c663f237 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 1 Jun 2026 15:57:10 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EC=88=98=EC=A0=95=EC=A4=91=EC=9D=BC?= =?UTF-8?q?=20=EB=95=8C=20=EC=B9=B4=EB=93=9C=20=EC=9D=B8=ED=84=B0=EB=9E=99?= =?UTF-8?q?=EC=85=98=20=EB=A7=89=EC=9D=8C=20-=20#330?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 8575065a..87779177 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -149,6 +149,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 +170,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)) From ae5f3b56ccacaeaf8b5b1284d09a9db6fa10384e Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 1 Jun 2026 16:30:51 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EB=82=98,=20=EC=83=81=EB=8C=80?= =?UTF-8?q?=EB=B0=A9=20=EC=BD=94=EB=A9=98=ED=8A=B8=20=EB=91=98=20=EB=8B=A4?= =?UTF-8?q?=20=EC=9E=88=EC=9D=84=20=EB=95=8C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Detail/GoalDetailView.swift | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 87779177..85662fba 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -274,7 +274,8 @@ private extension GoalDetailView { imageData: imageData, imageURL: imageURL, comment: comment, - showsMyEmoji: showsMyEmoji + showsMyEmoji: showsMyEmoji, + isFront: isFront ) .opacity(isFront ? 1 : 0) } @@ -286,10 +287,12 @@ private extension GoalDetailView { imageData: Data?, imageURL: String?, comment: String, - showsMyEmoji: Bool + showsMyEmoji: Bool, + isFront: Bool ) -> some View { if isCompleted { completedImageCard( + isFront: isFront, imageData: imageData, imageURL: imageURL, comment: comment, @@ -325,6 +328,7 @@ private extension GoalDetailView { @ViewBuilder func completedImageCard( + isFront: Bool, imageData: Data?, imageURL: String?, comment: String, @@ -332,14 +336,22 @@ private extension GoalDetailView { ) -> some View { if let imageData, let editedImage = UIImage(data: imageData) { - completedImageCardContainer(comment: comment, showsMyEmoji: showsMyEmoji) { + completedImageCardContainer( + comment: comment, + showsMyEmoji: showsMyEmoji, + isFront: isFront + ) { Image(uiImage: editedImage) .resizable() .scaledToFill() } } else if let imageURL, let url = URL(string: imageURL) { - completedImageCardContainer(comment: comment, showsMyEmoji: showsMyEmoji) { + completedImageCardContainer( + comment: comment, + showsMyEmoji: showsMyEmoji, + isFront: isFront + ) { KFImage(url) .resizable() .scaledToFill() @@ -406,6 +418,7 @@ private extension GoalDetailView { func completedImageCardContainer( comment: String, showsMyEmoji: Bool, + isFront: Bool, @ViewBuilder content: @escaping () -> Content ) -> some View { let shape = RoundedRectangle(cornerRadius: 20) @@ -421,7 +434,7 @@ private extension GoalDetailView { .overlay(dimmedView) .clipShape(shape) .overlay(alignment: .bottom) { - if !comment.isEmpty { + if isFront, !comment.isEmpty { commentCircle(comment: comment) .padding(.bottom, 26) } From 8a5d088897f550ef1c43cd528afc1ae5a7302e43 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 1 Jun 2026 19:49:07 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20TXCommentCircle=20safeAreaPadding=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20#330?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/ProofPhoto/ProofPhotoView.swift | 3 +-- .../Components/TextField/TXCommentCircle.swift | 12 +----------- 2 files changed, 2 insertions(+), 13 deletions(-) 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/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 ) } From f7fe5c3a34eceff96e4614d6bcc01325b4409534 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 1 Jun 2026 19:49:40 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=ED=82=A4=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=98=AC=EB=9D=BC=EC=98=AC=20=EB=95=8C=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?View=20=EB=B0=80=EB=A6=AC=EB=8A=94=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20#330?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Detail/GoalDetailView.swift | 133 ++++++++++-------- 1 file changed, 76 insertions(+), 57 deletions(-) diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 85662fba..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 { @@ -186,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)) @@ -199,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)) @@ -252,7 +267,6 @@ private extension GoalDetailView { lineWidth: 1.6 ) .frame(width: 336, height: 336) - .overlay(dimmedView) .clipShape(shape) } @@ -262,7 +276,6 @@ private extension GoalDetailView { isCompleted: Bool, imageData: Data?, imageURL: String?, - comment: String, showsMyEmoji: Bool ) -> some View { ZStack { @@ -273,9 +286,7 @@ private extension GoalDetailView { isCompleted: isCompleted, imageData: imageData, imageURL: imageURL, - comment: comment, - showsMyEmoji: showsMyEmoji, - isFront: isFront + showsMyEmoji: showsMyEmoji ) .opacity(isFront ? 1 : 0) } @@ -286,16 +297,12 @@ private extension GoalDetailView { isCompleted: Bool, imageData: Data?, imageURL: String?, - comment: String, - showsMyEmoji: Bool, - isFront: Bool + showsMyEmoji: Bool ) -> some View { if isCompleted { completedImageCard( - isFront: isFront, imageData: imageData, imageURL: imageURL, - comment: comment, showsMyEmoji: showsMyEmoji ) } else { @@ -323,23 +330,18 @@ private extension GoalDetailView { shape: shape, lineWidth: 1.6 ) - .overlay(dimmedView) } @ViewBuilder func completedImageCard( - isFront: Bool, imageData: Data?, imageURL: String?, - comment: String, showsMyEmoji: Bool ) -> some View { if let imageData, let editedImage = UIImage(data: imageData) { completedImageCardContainer( - comment: comment, - showsMyEmoji: showsMyEmoji, - isFront: isFront + showsMyEmoji: showsMyEmoji ) { Image(uiImage: editedImage) .resizable() @@ -348,9 +350,7 @@ private extension GoalDetailView { } else if let imageURL, let url = URL(string: imageURL) { completedImageCardContainer( - comment: comment, - showsMyEmoji: showsMyEmoji, - isFront: isFront + showsMyEmoji: showsMyEmoji ) { KFImage(url) .resizable() @@ -387,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) @@ -416,9 +440,7 @@ private extension GoalDetailView { } func completedImageCardContainer( - comment: String, showsMyEmoji: Bool, - isFront: Bool, @ViewBuilder content: @escaping () -> Content ) -> some View { let shape = RoundedRectangle(cornerRadius: 20) @@ -431,14 +453,7 @@ private extension GoalDetailView { .frame(maxWidth: .infinity, maxHeight: .infinity) .clipped() } - .overlay(dimmedView) .clipShape(shape) - .overlay(alignment: .bottom) { - if isFront, !comment.isEmpty { - commentCircle(comment: comment) - .padding(.bottom, 26) - } - } .insideBorder( Color.Gray.gray500, shape: shape, @@ -515,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 From bbc04f2dac9b578f3beece2783a759d9056c3db0 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 1 Jun 2026 20:33:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EC=B0=8C=EB=A5=B4=EA=B8=B0=20disabl?= =?UTF-8?q?e=EC=83=81=ED=83=9C=EC=9D=BC=20=EB=95=8C=20=EB=88=8C=EB=9F=AC?= =?UTF-8?q?=EB=8F=84=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=82=98=EC=98=A4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20-=20#331?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/Button/Round/TXRoundButton.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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()