diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 3c1d42fd..e329fdb3 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -121,7 +121,7 @@ private extension GoalDetailView { if store.item != nil { cardView .padding(.horizontal, 27) - .padding(.top, isSEDevice ? 47 : 103) + .padding(.top, Constants.cardTopPadding) if store.isCompleted { completedBottomContent @@ -157,6 +157,8 @@ private extension GoalDetailView { var cardView: some View { ZStack { + cardFrameReader + myCard .zIndex(effectiveIsFrontMyCard ? 1 : 0) @@ -195,6 +197,12 @@ private extension GoalDetailView { } ) } + + var cardFrameReader: some View { + Color.clear + .frame(width: Constants.cardSize, height: Constants.cardSize) + .readSize { rectFrame = $0 } + } @ViewBuilder var myCard: some View { @@ -235,7 +243,7 @@ private extension GoalDetailView { if store.isShowReactionBar { reactionBar - .padding(.top, isSEDevice ? 23 : 73) + .padding(.top, Constants.emojiTopPadding) .padding(.horizontal, 20) } } @@ -266,7 +274,7 @@ private extension GoalDetailView { shape: shape, lineWidth: 1.6 ) - .frame(width: 336, height: 336) + .frame(width: Constants.cardSize, height: Constants.cardSize) .clipShape(shape) } @@ -401,7 +409,7 @@ private extension GoalDetailView { } var shouldShowCommentOverlay: Bool { - guard store.isCompleted, rectFrame != .zero else { return false } + guard effectiveFrontCardIsCompleted, rectFrame != .zero else { return false } return store.isEditing || !currentFrontComment.isEmpty } @@ -422,6 +430,7 @@ private extension GoalDetailView { commentCircle(comment: currentFrontComment) .padding(.bottom, 26) .frame(width: rectFrame.width, height: rectFrame.height, alignment: .bottom) + .rotationEffect(frontCardRotation) .offset(x: posX, y: posY - keyboardInset) .animation(.easeOut(duration: 0.25), value: keyboardInset) } @@ -446,8 +455,7 @@ private extension GoalDetailView { let shape = RoundedRectangle(cornerRadius: 20) return Color.clear - .frame(width: 336, height: 336) - .readSize { rectFrame = $0 } + .frame(width: Constants.cardSize, height: Constants.cardSize) .overlay { content() .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -534,6 +542,14 @@ private extension GoalDetailView { max(0, rectFrame.maxY - keyboardFrame.minY) } + var frontCardRotation: Angle { + effectiveIsFrontMyCard ? .degrees(0) : .degrees(-8) + } + + var effectiveFrontCardIsCompleted: Bool { + effectiveIsFrontMyCard ? store.myCardIsCompleted : store.partnerCardIsCompleted + } + func repeatedCardOffset(for width: CGFloat) -> CGFloat { let maxOffset = Constants.maxCardOffset let direction: CGFloat = width >= 0 ? 1 : -1 @@ -565,19 +581,21 @@ private extension GoalDetailView { cardOffset = .zero isCrossingDuringDrag = false } - - // 다른곳에서도 쓸 때 Util로 빼기 - private var isSEDevice: Bool { - UIScreen.main.bounds.height <= 667 - } } // MARK: - Constants private extension GoalDetailView { enum Constants { + static var isSEDevice: Bool { + UIScreen.main.bounds.height <= 667 + } + static let maxCardOffset: CGFloat = 100 static let dragVelocityThreshold: CGFloat = 1200 static let minimumDragResistance: CGFloat = 0.35 + static var cardTopPadding: CGFloat { isSEDevice ? 34 : 89 } + static var cardSize: CGFloat { isSEDevice ? 321 : 336 } + static var emojiTopPadding: CGFloat { isSEDevice ? 19 : 69 } } } diff --git a/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift b/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift index c03921a2..a54af3db 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift @@ -27,8 +27,8 @@ struct ReactionBarView: View { var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - shadowView(proxy: proxy) - .offset(y: 10) + shadowView + .offset(y: 9) reactionBar(proxy: proxy) } @@ -46,46 +46,97 @@ struct ReactionBarView: View { // MARK: - SubViews private extension ReactionBarView { - func shadowView(proxy: GeometryProxy) -> some View { + var shadowView: some View { Color.Gray.gray200 - .frame(width: proxy.size.width, height: 67) + .frame(maxWidth: 368) + .frame(height: 68) .clipShape(.capsule) } func reactionBar(proxy: GeometryProxy) -> some View { HStack(spacing: 0) { ForEach(ReactionEmoji.allCases, id: \.self) { emoji in - Button { - onSelect(emoji) - flyingReactionEmitter.emit( - emoji: emoji, - config: .reactionBar(width: proxy.size.width) - ) - } label: { - emoji.image - .padding(.horizontal, 8) + Group { + if case .happy = emoji { + firstButton(proxy: proxy, emoji: emoji) + } else if case .fuck = emoji { + lastButton(proxy: proxy, emoji: emoji) + } else { + rectButton(proxy: proxy, emoji: emoji) + } } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear) .perfControl(slug: "goal-detail", element: "reaction-\(emoji.rawValue)") - - if emoji != ReactionEmoji.allCases.last { - Rectangle() - .frame(width: 1) - } } } .background(Color.Gray.gray100) - .frame(width: proxy.size.width, height: 68) + .frame(maxWidth: 368) + .frame(height: 68) .clipShape(.capsule) .overlay( Capsule() .stroke(Color.Gray.gray500, lineWidth: 1) ) } + + func firstButton(proxy: GeometryProxy, emoji: ReactionEmoji) -> some View { + Button { + onSelect(emoji) + flyingReactionEmitter.emit( + emoji: emoji, + config: .reactionBar(width: proxy.size.width) + ) + } label: { + emoji.image + .padding(.leading, 10) + .padding(.trailing, 6) + .padding(.bottom, 2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(minWidth: 70, maxWidth: 84, maxHeight: .infinity) + .background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear) + .clipShape(UnevenRoundedRectangle(topLeadingRadius: 999, bottomLeadingRadius: 999)) + } + + func lastButton(proxy: GeometryProxy, emoji: ReactionEmoji) -> some View { + Button { + onSelect(emoji) + flyingReactionEmitter.emit( + emoji: emoji, + config: .reactionBar(width: proxy.size.width) + ) + } label: { + emoji.image + .padding(.leading, 3) + .padding(.trailing, 10) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(minWidth: 70, maxWidth: 84, maxHeight: .infinity) + .background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear) + .clipShape(UnevenRoundedRectangle(bottomTrailingRadius: 999, topTrailingRadius: 999)) + } + + func rectButton(proxy: GeometryProxy, emoji: ReactionEmoji) -> some View { + Button { + onSelect(emoji) + flyingReactionEmitter.emit( + emoji: emoji, + config: .reactionBar(width: proxy.size.width) + ) + } label: { + emoji.image + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(minWidth: 70, maxWidth: 80, maxHeight: .infinity) + .background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear) + .overlay( + Rectangle() + .stroke(Color.Gray.gray500, lineWidth: 1) + ) + } } private extension ReactionBarView { + static func reactionBarConfig(width: CGFloat) -> FlyingReactionConfig { let minX: CGFloat = 8 let maxXInset: CGFloat = 32