From 3e691e09ca6bd9a5c575bd5295f72bec0112e76a Mon Sep 17 00:00:00 2001 From: chansooo <89574881+chansooo@users.noreply.github.com> Date: Sat, 29 Jun 2024 16:52:36 +0900 Subject: [PATCH] =?UTF-8?q?Feature/=EB=B0=88=EC=83=81=EC=84=B8=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B5=AC=ED=98=84=20#23=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: memeDetail 모듈 추가 * feat: kingfisher 추가 * design: ㅋ, 개웃겨 리소스 추가 * feat: MemeDetail 모델 구현 * feat: MemeDetail UI 구현 * chore: 코드리뷰 봇 무시 * refactor: 재사용 가능한 뷰 분리 * move: 파일 경로 변경 * feat: util 관련 router, viewmodeltype 구현 * fix: farmemedetail에 id 필드 추가 * feat: 상세에서 필요한 usecase 정의 * feat: memedetailviewmodel 구현 --- .../PPACModels/Sources/Meme/MemeDetail.swift | 5 +- Projects/Core/PPACUtil/Sources/Router.swift | 59 ++++++++++++ .../Core/PPACUtil/Sources/ViewModelType.swift | 17 ++++ Projects/Core/PPACUtil/Sources/dummy.swift | 2 +- Projects/Features/MemeDetail/Project.swift | 1 + .../Sources/Domain/CopyImageUseCase.swift | 38 ++++++++ .../Sources/Domain/PostFarmemeUseCase.swift | 39 ++++++++ .../Sources/Domain/PostLikeUseCase.swift | 41 +++++++++ .../Presentation/MemeDetailCardView.swift | 71 +++++++++++++++ .../Presentation/MemeDetailRouter.swift | 45 ++++++++++ .../Sources/Presentation/MemeDetailView.swift | 32 +++++++ .../Presentation/MemeDetailViewModel.swift | 90 +++++++++++++++++++ .../Presentation/View/HashTagView.swift | 42 +++++++++ .../Presentation/View/LikeButton.swift | 23 +++++ .../Presentation/View/MemeImageView.swift | 36 ++++++++ 15 files changed, 539 insertions(+), 2 deletions(-) create mode 100644 Projects/Core/PPACUtil/Sources/Router.swift create mode 100644 Projects/Core/PPACUtil/Sources/ViewModelType.swift create mode 100644 Projects/Features/MemeDetail/Sources/Domain/CopyImageUseCase.swift create mode 100644 Projects/Features/MemeDetail/Sources/Domain/PostFarmemeUseCase.swift create mode 100644 Projects/Features/MemeDetail/Sources/Domain/PostLikeUseCase.swift create mode 100644 Projects/Features/MemeDetail/Sources/Presentation/MemeDetailCardView.swift create mode 100644 Projects/Features/MemeDetail/Sources/Presentation/MemeDetailRouter.swift create mode 100644 Projects/Features/MemeDetail/Sources/Presentation/MemeDetailView.swift create mode 100644 Projects/Features/MemeDetail/Sources/Presentation/MemeDetailViewModel.swift create mode 100644 Projects/Features/MemeDetail/Sources/Presentation/View/HashTagView.swift create mode 100644 Projects/Features/MemeDetail/Sources/Presentation/View/LikeButton.swift create mode 100644 Projects/Features/MemeDetail/Sources/Presentation/View/MemeImageView.swift diff --git a/Projects/Core/PPACModels/Sources/Meme/MemeDetail.swift b/Projects/Core/PPACModels/Sources/Meme/MemeDetail.swift index d33e88b..9ecf349 100644 --- a/Projects/Core/PPACModels/Sources/Meme/MemeDetail.swift +++ b/Projects/Core/PPACModels/Sources/Meme/MemeDetail.swift @@ -10,7 +10,7 @@ import Foundation public struct MemeDetail { // MARK: - Properties - + public let id: String public let title: String public let keywords: [String] public let imageUrlString: String @@ -21,6 +21,7 @@ public struct MemeDetail { // MARK: - Initializers public init( + id: String, title: String, keywords: [String], imageUrlString: String, @@ -28,6 +29,7 @@ public struct MemeDetail { isTodayMeme: Bool, reaction: Int ) { + self.id = id self.title = title self.keywords = keywords self.imageUrlString = imageUrlString @@ -39,6 +41,7 @@ public struct MemeDetail { public extension MemeDetail { static let mock = MemeDetail( + id: "1", title: "나는 공부를 찢어", keywords: ["공부", "학생", "시험기간"], imageUrlString: "https://avatars.githubusercontent.com/u/26344479?s=64&v=4", diff --git a/Projects/Core/PPACUtil/Sources/Router.swift b/Projects/Core/PPACUtil/Sources/Router.swift new file mode 100644 index 0000000..2c58738 --- /dev/null +++ b/Projects/Core/PPACUtil/Sources/Router.swift @@ -0,0 +1,59 @@ +// +// Router.swift +// PPACUtil +// +// Created by kimchansoo on 6/29/24. +// + +import UIKit +import SwiftUI + +public protocol RouterDelegate: AnyObject { + + func didFinish(childRouter: Router) +} + +public protocol Router: AnyObject { + + var delegate: RouterDelegate? { get set } + var navigationController: UINavigationController { get set } + var childRouters: [Router] { get set } + + func start() + func finish() + func popView() + func dismissView() +} + +public extension Router { + + func finish() { + childRouters.removeAll() + delegate?.didFinish(childRouter: self) + } + + func popView() { + self.navigationController.popViewController(animated: true) + } + + func dismissView() { + navigationController.dismiss(animated: true) + } + + func pushView(_ view: V, animated: Bool = true) { + let viewController = UIHostingController(rootView: view) + navigationController.pushViewController(viewController, animated: animated) + } + + func presentModallyView(_ view: V, animated: Bool = true) { + let viewController = UIHostingController(rootView: view) + viewController.modalPresentationStyle = .formSheet + navigationController.present(viewController, animated: animated) + } + + func presentFullscreenView(_ view: V, animated: Bool = true) { + let viewController = UIHostingController(rootView: view) + viewController.modalPresentationStyle = .fullScreen + navigationController.present(viewController, animated: animated) + } +} diff --git a/Projects/Core/PPACUtil/Sources/ViewModelType.swift b/Projects/Core/PPACUtil/Sources/ViewModelType.swift new file mode 100644 index 0000000..3c47d52 --- /dev/null +++ b/Projects/Core/PPACUtil/Sources/ViewModelType.swift @@ -0,0 +1,17 @@ +// +// ViewModelType.swift +// PPACUtil +// +// Created by kimchansoo on 6/29/24. +// + +import Foundation + +public protocol ViewModelType { + associatedtype Action + associatedtype State + + var state: State { get } + + func dispatch(type: Action) +} diff --git a/Projects/Core/PPACUtil/Sources/dummy.swift b/Projects/Core/PPACUtil/Sources/dummy.swift index 2ad55c7..1f7ee25 100644 --- a/Projects/Core/PPACUtil/Sources/dummy.swift +++ b/Projects/Core/PPACUtil/Sources/dummy.swift @@ -1 +1 @@ -더미임미다 \ No newline at end of file +//더미임미다 diff --git a/Projects/Features/MemeDetail/Project.swift b/Projects/Features/MemeDetail/Project.swift index fb6ae79..49132ac 100644 --- a/Projects/Features/MemeDetail/Project.swift +++ b/Projects/Features/MemeDetail/Project.swift @@ -23,6 +23,7 @@ let project = Project( .ResourceKit, .Core.DesignSystem, .Core.PPACModels, + .Core.PPACUtil, ] ) ] diff --git a/Projects/Features/MemeDetail/Sources/Domain/CopyImageUseCase.swift b/Projects/Features/MemeDetail/Sources/Domain/CopyImageUseCase.swift new file mode 100644 index 0000000..c77a7e7 --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Domain/CopyImageUseCase.swift @@ -0,0 +1,38 @@ +// +// CopyImageUseCase.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import Foundation +import UniformTypeIdentifiers +import UIKit +import Dependencies + +public protocol CopyImageUseCase { + func execute(data: Data) +} + +public final class CopyImageUseCaseImpl: CopyImageUseCase { + + + // MARK: - Properties + + // MARK: - Initializers + + // MARK: - Methods + + public func execute(data: Data) { + let pasteboard = [ + [UTType.png.identifier : data], + ] + UIPasteboard.general.setItems(pasteboard) + } +} +// +//public enum CopyImageUseCaseKey: DependencyKey, TestDependencyKey { +// public static let liveValue: any CopyImageUseCase = CopyImageUseCaseImpl() +// public static let testValue: any CopyImageUseCase = CopyImageUseCaseImpl() +// public static let previewValue: any CopyImageUseCase = CopyImageUseCaseImpl() +//} diff --git a/Projects/Features/MemeDetail/Sources/Domain/PostFarmemeUseCase.swift b/Projects/Features/MemeDetail/Sources/Domain/PostFarmemeUseCase.swift new file mode 100644 index 0000000..951b6be --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Domain/PostFarmemeUseCase.swift @@ -0,0 +1,39 @@ +// +// PostFarmemeUseCase.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import Foundation + +import Dependencies + +public protocol PostFarmemeUseCase { + func execute(id: String) async -> Bool +} + +public final class PostFarmemeUseCaseImpl: PostFarmemeUseCase { + + // MARK: - Properties + + // MARK: - Initializers + + // MARK: - Methods + + public func execute(id: String) async -> Bool { + return false + } +} + +public final class MockPostFarmemeUseCase: PostFarmemeUseCase { + public func execute(id: String) async -> Bool { + return false + } +} + +public enum PostFarmemeUseCaseKey: DependencyKey { + public static let liveValue: any PostFarmemeUseCase = MockPostFarmemeUseCase() + public static let testValue: any PostFarmemeUseCase = MockPostFarmemeUseCase() + public static let previewValue: any PostFarmemeUseCase = MockPostFarmemeUseCase() +} diff --git a/Projects/Features/MemeDetail/Sources/Domain/PostLikeUseCase.swift b/Projects/Features/MemeDetail/Sources/Domain/PostLikeUseCase.swift new file mode 100644 index 0000000..a03db25 --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Domain/PostLikeUseCase.swift @@ -0,0 +1,41 @@ +// +// PostLikeUseCase.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import Foundation + +import Dependencies + +public protocol PostLikeUseCase { + + func execute(id: String) async -> Bool +} + +public final class PostLikeUseCaseImpl: PostLikeUseCase { + + // MARK: - Properties + + // MARK: - Initializers + + // MARK: - Methods + + public func execute(id: String) async -> Bool { + // TODO: - api 나오면 연결 + return true + } +} + +public final class MockPostLikeUseCase: PostLikeUseCase { + public func execute(id: String) async -> Bool { + return true + } +} + +public enum PostLikeUseCaseKey: DependencyKey { + public static let liveValue: any PostLikeUseCase = MockPostLikeUseCase() + public static let testValue: any PostLikeUseCase = MockPostLikeUseCase() + public static let previewValue: any PostLikeUseCase = MockPostLikeUseCase() +} diff --git a/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailCardView.swift b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailCardView.swift new file mode 100644 index 0000000..0be0224 --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailCardView.swift @@ -0,0 +1,71 @@ +// +// MemeDetailCardView.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import SwiftUI + +import PPACModels +import Kingfisher +import ResourceKit + +struct MemeDetailCardView: View { + + // MARK: - Properties + + let meme: MemeDetail + + // MARK: - UI + + var body: some View { + VStack(alignment: .center, spacing: 0) { + + MemeImageView(imageUrlString: meme.imageUrlString) + .padding(.bottom, 25) + + titleLabel + .padding(.bottom, 5) + + HashTagView(keywords: meme.keywords) + .padding(.bottom, 11) + + subtitleLabel + .padding(.bottom, 20) + + LikeButton() + .padding(.bottom, 20) + } + .padding(10) + .background(.white) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .inset(by: 1) + .stroke(.black, lineWidth: 2) + ) + .padding(.horizontal, 24) + } + + // MARK: - Methods + + var titleLabel: some View { + Text(meme.title) + .font(Font.Heading.large.weight(.semibold)) + .multilineTextAlignment(.center) + .foregroundColor(Color.Text.primary) + .frame(maxWidth: .infinity, alignment: .center) + } + + var subtitleLabel: some View { + Text("출처: \(self.meme.source)") + .font(Font.Body.xsmall) + .multilineTextAlignment(.center) + .foregroundColor(Color.Icon.assistive) + } +} + +#Preview { + MemeDetailCardView(meme: .mock) +} diff --git a/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailRouter.swift b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailRouter.swift new file mode 100644 index 0000000..273520d --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailRouter.swift @@ -0,0 +1,45 @@ +// +// MemeDetailRouter.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import UIKit +import SwiftUI + +import PPACUtil +import PPACModels + +final class MemeDetailRouter: Router { + + + // MARK: - Properties + + var delegate: (any RouterDelegate)? + + var navigationController: UINavigationController + + var childRouters: [any Router] = [] + + let meme: MemeDetail + + // MARK: - Initializers + + init(_ navigationController: UINavigationController, meme: MemeDetail) { + navigationController.isNavigationBarHidden = true + self.meme = meme + self.navigationController = navigationController + } + + // MARK: - UI + + // MARK: - Override + + // MARK: - Methods + + func start() { + self.pushView(MemeDetailView(meme: meme)) + } + +} diff --git a/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailView.swift b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailView.swift new file mode 100644 index 0000000..ba2a1c3 --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailView.swift @@ -0,0 +1,32 @@ +// +// MemeDetailView.swift +// MemeDetail +// +// Created by kimchansoo on 6/28/24. +// + +import SwiftUI + +import PPACModels + +struct MemeDetailView: View { + + // MARK: - Properties + private let meme: MemeDetail + + // MARK: - Initializers + + init(meme: MemeDetail) { + self.meme = meme + } + + // MARK: - UI + + var body: some View { + MemeDetailCardView(meme: meme) + } +} + +#Preview { + MemeDetailView(meme: .mock) +} diff --git a/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailViewModel.swift b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailViewModel.swift new file mode 100644 index 0000000..6c8d5e7 --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Presentation/MemeDetailViewModel.swift @@ -0,0 +1,90 @@ +// +// MemeDetailViewModel.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import Foundation + +import Dependencies + +import PPACUtil +import PPACModels +import UIKit + +protocol MemeDetailRouting: AnyObject { + func popView() + func showShareView() +} + +final class MemeDetailViewModel: ViewModelType, ObservableObject { + + enum Action { + case likeButtonTapped + case copyButtonTapped + case shreButtonTapped + case farmemeButtonTapped + case naviBackButtonTapped + } + + struct State { + var meme: MemeDetail + } + + // MARK: - Properties + + weak var router: MemeDetailRouting? + @Published var state: State + + private let copyImageUseCase: CopyImageUseCase + private let postLikeUseCase: PostLikeUseCase + + + // MARK: - Initializers + + init( + meme: MemeDetail, + router: MemeDetailRouting?, + copyImageUseCase: CopyImageUseCase, + postLikeUseCase: PostLikeUseCase + ) { + self.router = router + self.state = State(meme: meme) + self.copyImageUseCase = copyImageUseCase + self.postLikeUseCase = postLikeUseCase + } + + // MARK: - Methods + + func dispatch(type: Action) { + switch type { + case .likeButtonTapped: + postLike() + case .copyButtonTapped: + copyImage() + case .shreButtonTapped: + router?.showShareView() + case .farmemeButtonTapped: + postSavedFarmeme() + case .naviBackButtonTapped: + router?.popView() + } + } +} + +private extension MemeDetailViewModel { + func postLike() { + + } + + func copyImage() { + // TODO: - 이미지 viewmodel이 알지 못하도록 수정 + let imageData: Data = try! Data(contentsOf: URL(string: state.meme.imageUrlString)!) + copyImageUseCase.execute(data: imageData) + } + + func postSavedFarmeme() { + + } +} diff --git a/Projects/Features/MemeDetail/Sources/Presentation/View/HashTagView.swift b/Projects/Features/MemeDetail/Sources/Presentation/View/HashTagView.swift new file mode 100644 index 0000000..bcc69b6 --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Presentation/View/HashTagView.swift @@ -0,0 +1,42 @@ +// +// HashTagView.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import SwiftUI + +import ResourceKit + +public struct HashTagView: View { + + // MARK: - Properties + + private let keywords: [String] + + // MARK: - Initializers + + public init(keywords: [String]) { + self.keywords = keywords + } + + // MARK: - UI + + public var body: some View { + HStack(alignment: .center, spacing: 6) { + ForEach(keywords, id: \.self) { title in + hashTag(title: title) + } + } + .frame(maxWidth: .infinity, alignment: .center) + .cornerRadius(40) + } + + func hashTag(title: String) -> some View { + Text("#\(title)") + .font(Font.Body.large) + .foregroundColor(Color.Text.tertiary) + } + +} diff --git a/Projects/Features/MemeDetail/Sources/Presentation/View/LikeButton.swift b/Projects/Features/MemeDetail/Sources/Presentation/View/LikeButton.swift new file mode 100644 index 0000000..6ec7dfc --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Presentation/View/LikeButton.swift @@ -0,0 +1,23 @@ +// +// LikeButton.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import SwiftUI + +import ResourceKit + +public struct LikeButton: View { + public var body: some View { + HStack(alignment: .center, spacing: 6) { + ResourceKitAsset.Icon.ㅋ.swiftUIImage + ResourceKitAsset.Icon.개웃겨.swiftUIImage + } + .frame(maxWidth: .infinity) + .frame(height: 46, alignment: .center) + .background(Color.Skeleton.primary) + .cornerRadius(10) + } +} diff --git a/Projects/Features/MemeDetail/Sources/Presentation/View/MemeImageView.swift b/Projects/Features/MemeDetail/Sources/Presentation/View/MemeImageView.swift new file mode 100644 index 0000000..4538f00 --- /dev/null +++ b/Projects/Features/MemeDetail/Sources/Presentation/View/MemeImageView.swift @@ -0,0 +1,36 @@ +// +// MemeImageView.swift +// MemeDetail +// +// Created by kimchansoo on 6/29/24. +// + +import SwiftUI + +import Kingfisher + +struct MemeImageView: View { + + // MARK: - Properties + + private let imageUrlString: String + + // MARK: - Initializers + + init(imageUrlString: String) { + self.imageUrlString = imageUrlString + } + + // MARK: - UI + + var body: some View { + KFImage(URL(string: imageUrlString)) + .resizable() + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .fade(duration: 0.25) + .frame(maxWidth: .infinity) + .aspectRatio(0.9375, contentMode: .fit) + .cornerRadius(10) + } +}