From 2ba3b8433e7291cc45515c66e42abb7676b495bf Mon Sep 17 00:00:00 2001 From: jongnan Date: Thu, 3 Oct 2024 21:27:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=88=20=EB=94=94=ED=85=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Extension/View+Extension.swift | 24 +++- .../Sources/View/Meme/MemeImageView.swift | 3 +- .../Sources/MemeDetailCardView.swift | 89 ++++++++++--- .../MemeDetail/Sources/MemeDetailView.swift | 126 +++++++++++++----- .../MemeDetail/Sources/View/HashTagView.swift | 11 +- .../Extension/View+Extenstion.swift | 23 ---- 6 files changed, 198 insertions(+), 78 deletions(-) diff --git a/Projects/Core/DesignSystem/Sources/Extension/View+Extension.swift b/Projects/Core/DesignSystem/Sources/Extension/View+Extension.swift index 6ceaa19..e58b886 100644 --- a/Projects/Core/DesignSystem/Sources/Extension/View+Extension.swift +++ b/Projects/Core/DesignSystem/Sources/Extension/View+Extension.swift @@ -41,9 +41,23 @@ public extension View { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) } } -} - -public extension View { + + @ViewBuilder + func onReadSize(_ perform: @escaping (CGSize) -> Void) -> some View { + self.customBackground { + GeometryReader { geometryProxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometryProxy.size) + } + } + .onPreferenceChange(SizePreferenceKey.self, perform: perform) + } + + @ViewBuilder + func customBackground(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View { + self.background(alignment: alignment, content: content) + } + func popup( isActive: Binding, image: SwiftUI.Image?, @@ -55,4 +69,8 @@ public extension View { } } +struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { } +} diff --git a/Projects/Core/DesignSystem/Sources/View/Meme/MemeImageView.swift b/Projects/Core/DesignSystem/Sources/View/Meme/MemeImageView.swift index d559919..b4cf1ec 100644 --- a/Projects/Core/DesignSystem/Sources/View/Meme/MemeImageView.swift +++ b/Projects/Core/DesignSystem/Sources/View/Meme/MemeImageView.swift @@ -39,8 +39,7 @@ public struct MemeImageView: View { .cacheMemoryOnly() .fade(duration: 0.25) .frame(maxWidth: .infinity) - .aspectRatio(0.9375, contentMode: .fit) - .cornerRadius(10) + .aspectRatio(contentMode: .fit) } var skeletonView: some View { diff --git a/Projects/Features/MemeDetail/Sources/MemeDetailCardView.swift b/Projects/Features/MemeDetail/Sources/MemeDetailCardView.swift index 796738d..8a237eb 100644 --- a/Projects/Features/MemeDetail/Sources/MemeDetailCardView.swift +++ b/Projects/Features/MemeDetail/Sources/MemeDetailCardView.swift @@ -22,15 +22,18 @@ struct MemeDetailCardView: View { @State var playbackMode: LottiePlaybackMode = .paused(at: .progress(100)) private let reactionButtonTapped: (() -> Void)? + private let isShortCard: Bool // MARK: - Initializers init( meme: Binding, + isShortCard: Bool, reactionButtonTapped: (() -> Void)? ) { self._meme = meme self.reactionButtonTapped = reactionButtonTapped + self.isShortCard = isShortCard } // MARK: - UI @@ -38,13 +41,51 @@ struct MemeDetailCardView: View { var body: some View { VStack(alignment: .center, spacing: 0) { - MemeImageView(imageUrlString: meme.imageUrlString) - .padding(.bottom, 25) + Rectangle() + .frame(width: 330, height: 352) + .overlay { + ZStack { + MemeImageView(imageUrlString: meme.imageUrlString) + + if isShortCard { + shortCardGradation + + VStack(spacing: 0) { + Spacer() + + infoView + } + } + } + } + .cornerRadius(10) + .padding(.bottom, isShortCard ? 0 : 25) + if(!isShortCard) { + infoView + } + } + .frame(maxWidth: 330) + .padding(.horizontal, /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/) + .padding(.vertical, 12.5) + .background(Color.Background.white) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .inset(by: 1) + .stroke(.black, lineWidth: 2) + .frame(maxWidth: 350) + ) + } + + // MARK: - Methods + + var infoView: some View { + VStack(spacing: 0) { titleLabel .padding(.bottom, 5) - HashTagView(keywords: meme.keywords) + HashTagView(keywords: meme.keywords, isShortCard: isShortCard) .padding(.bottom, 11) .onTapGesture { PPACAnalytics.shared @@ -71,25 +112,18 @@ struct MemeDetailCardView: View { } .offset(y: -50) }) - .padding(.bottom, 20) + .padding(.bottom, 10) + .padding(.horizontal, 10) } - .padding(10) - .background(Color.Background.white) - .cornerRadius(20) - .overlay( - RoundedRectangle(cornerRadius: 20) - .inset(by: 1) - .stroke(.black, lineWidth: 2) - ) } - // MARK: - Methods - var titleLabel: some View { Text(meme.title) .font(Font.Heading.Large.semiBold) .multilineTextAlignment(.center) - .foregroundColor(Color.Text.primary) + .foregroundColor( + isShortCard ? Color.Text.inverse : Color.Text.primary + ) .frame(maxWidth: .infinity, alignment: .center) } @@ -97,7 +131,24 @@ struct MemeDetailCardView: View { Text("출처: \(self.meme.source)") .font(Font.Body.Xsmall.medium) .lineLimit(1) - .foregroundColor(Color.Icon.assistive) + .foregroundColor( + isShortCard ? Color.Text.assistive : Color.Icon.assistive + ) + } + + var shortCardGradation: some View { + Rectangle() + .opacity(0) + .background( + LinearGradient( + colors: [ + ResourceKitAsset.PrimaryColor.neutral70.swiftUIColor.opacity(0), + ResourceKitAsset.PrimaryColor.neutral70.swiftUIColor + ], + startPoint: .top, + endPoint: .bottom + ) + ) } private func handleReactionTapped() { @@ -110,7 +161,11 @@ struct MemeDetailCardView: View { @State var mock: MemeDetail = .mock return VStack { - MemeDetailCardView(meme: $mock, reactionButtonTapped: nil) + MemeDetailCardView( + meme: $mock, + isShortCard: false, + reactionButtonTapped: nil + ) } .background(.red) } diff --git a/Projects/Features/MemeDetail/Sources/MemeDetailView.swift b/Projects/Features/MemeDetail/Sources/MemeDetailView.swift index a96e1c7..746416e 100644 --- a/Projects/Features/MemeDetail/Sources/MemeDetailView.swift +++ b/Projects/Features/MemeDetail/Sources/MemeDetailView.swift @@ -14,6 +14,9 @@ import DesignSystem import Kingfisher import PPACAnalytics +import PPACDomain +import PPACData +import PPACNetwork public struct MemeDetailView: View { @@ -21,6 +24,14 @@ public struct MemeDetailView: View { @ObservedObject private var viewModel: MemeDetailViewModel + @State private var totalHeight: CGFloat = 0 + @State private var memeCardHeight: CGFloat = 0 + @State private var tabBarHeight: CGFloat = 0 + + private var isShortCard: Bool { + memeCardHeight + tabBarHeight > totalHeight - 30 + } + // MARK: - Initializers public init(viewModel: MemeDetailViewModel) { @@ -30,27 +41,71 @@ public struct MemeDetailView: View { // MARK: - UI public var body: some View { - Spacer() - - MemeDetailCardView(meme: $viewModel.state.meme) { - viewModel.dispatch(type: .likeButtonTapped) + ZStack { + VStack(spacing: 0) { + Spacer() + + MemeDetailCardView( + meme: $viewModel.state.meme, + isShortCard: totalHeight == 0 ? false : isShortCard + ) { + viewModel.dispatch(type: .likeButtonTapped) + } + .padding(.top, 40) + .onReadSize { size in + if(memeCardHeight == 0) { + memeCardHeight = size.height + } + } + + Spacer() + + // 가짜 탭뷰 + Rectangle() + .frame(height: 64) + .foregroundColor(.black.opacity(0)) + .clipShape( + .rect( + topLeadingRadius: 30, + topTrailingRadius: 30 + ) + ) + } + + VStack(spacing: 0) { + Spacer() + + EmptyView() + .memeDetailTabBar( + isFarmemed: $viewModel.state.meme.isFarmemed + ) { tab in + tabBarTap(tab) + } + .frame(maxHeight: 64) + .onReadSize { size in + if(tabBarHeight == 0) { + tabBarHeight = size.height + } + } + } } - .padding(.horizontal, 24) - .memeDetailTabBar(isFarmemed: $viewModel.state.meme.isFarmemed) { tab in - tabBarTap(tab) + .onReadSize { size in + if(totalHeight == 0) { + totalHeight = size.height + } } .background( - KFImage(URL(string: viewModel.state.meme.imageUrlString)) - .resizable() - .loadDiskFileSynchronously() - .cacheMemoryOnly() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .clipped() - .opacity(0.4) // Image Opacity: 40% - .blur(radius: 50) // Layer Blur: 50 - .overlay(Color.white.opacity(0.3)) // White Dim: #fff, Opacity: 30% - .edgesIgnoringSafeArea(.top) + KFImage(URL(string: viewModel.state.meme.imageUrlString)) + .resizable() + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .opacity(0.4) // Image Opacity: 40% + .blur(radius: 50) // Layer Blur: 50 + .overlay(Color.white.opacity(0.3)) // White Dim: #fff, Opacity: 30% + .clipped() + .edgesIgnoringSafeArea(.top) ) .onAppear { viewModel.logMemeDetail(interaction: .view, event: .meme) @@ -59,7 +114,7 @@ public struct MemeDetailView: View { backHandler: { viewModel.dispatch(type: .naviBackButtonTapped) }, rightActionHandler: nil, hasConfigureButton: false, - title: viewModel.state.meme.title + title: "밈 자세히 보기" ) .popup( isActive: $viewModel.state.isCopied, @@ -71,8 +126,6 @@ public struct MemeDetailView: View { image: viewModel.state.meme.isFarmemed ? ResourceKitAsset.Icon.copyFilled.swiftUIImage : nil, text: viewModel.state.meme.isFarmemed ? "파밈 완료!" : "파밈을 취소했어요" ) - - Spacer() } @MainActor @@ -87,13 +140,24 @@ public struct MemeDetailView: View { } } } -// -//#Preview { -// MemeDetailView( -// viewModel: MemeDetailViewModel( -// meme: .mock, -// router: nil, -// postLikeUseCase: MockPostLikeUseCase() -// ) -// ) -//} + +#Preview { + let networkService = NetworkService() + let memeRepository = MemeRepositoryImpl(networkservice: networkService) + + let bookmarkMemeUseCase = BookmarkMemeUseCaseImpl(repository: memeRepository) + let watchMemeUseCase = WatchMemeUseCaseImpl(repository: memeRepository) + let reactToMemeUseCase = ReactToMemeUseCaseImpl(repository: memeRepository) + let shareMemeUseCase = ShareMemeUseCaseImpl(repository: memeRepository) + + return MemeDetailView( + viewModel: MemeDetailViewModel( + meme: .mock, + router: nil, + bookmarkMemeUseCase: bookmarkMemeUseCase, + shareMemeUseCase: shareMemeUseCase, + watchMemeUseCase: watchMemeUseCase, + reactToMemeUseCase:reactToMemeUseCase + ) + ) +} diff --git a/Projects/Features/MemeDetail/Sources/View/HashTagView.swift b/Projects/Features/MemeDetail/Sources/View/HashTagView.swift index 613a7e7..dce1401 100644 --- a/Projects/Features/MemeDetail/Sources/View/HashTagView.swift +++ b/Projects/Features/MemeDetail/Sources/View/HashTagView.swift @@ -14,11 +14,16 @@ public struct HashTagView: View { // MARK: - Properties private let keywords: [String] + private let isShortCard: Bool // MARK: - Initializers - public init(keywords: [String]) { + public init( + keywords: [String], + isShortCard: Bool + ) { self.keywords = keywords + self.isShortCard = isShortCard } // MARK: - UI @@ -36,7 +41,9 @@ public struct HashTagView: View { func hashTag(title: String) -> some View { Text("#\(title)") .font(Font.Body.Large.medium) - .foregroundColor(Color.Text.tertiary) + .foregroundColor( + isShortCard ? Color.Text.disabled : Color.Text.tertiary + ) } } diff --git a/Projects/Features/Recommend/Sources/Presentation/Extension/View+Extenstion.swift b/Projects/Features/Recommend/Sources/Presentation/Extension/View+Extenstion.swift index 3a157df..ca873e5 100644 --- a/Projects/Features/Recommend/Sources/Presentation/Extension/View+Extenstion.swift +++ b/Projects/Features/Recommend/Sources/Presentation/Extension/View+Extenstion.swift @@ -7,29 +7,6 @@ import SwiftUI -extension View { - @ViewBuilder - func onReadSize(_ perform: @escaping (CGSize) -> Void) -> some View { - self.customBackground { - GeometryReader { geometryProxy in - Color.clear - .preference(key: SizePreferenceKey.self, value: geometryProxy.size) - } - } - .onPreferenceChange(SizePreferenceKey.self, perform: perform) - } - - @ViewBuilder - func customBackground(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View { - self.background(alignment: alignment, content: content) - } -} - -struct SizePreferenceKey: PreferenceKey { - static var defaultValue: CGSize = .zero - static func reduce(value: inout CGSize, nextValue: () -> CGSize) { } -} - extension View { func recommendSkeleton( isShow: Bool,