From 0a9176b5b48aa36cea52f228188440680d656c44 Mon Sep 17 00:00:00 2001 From: hryeong66 Date: Sun, 21 Jul 2024 10:07:42 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20pull=20to=20refresh=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/MyPagePregressView.swift | 20 +++++++++++++++++++ .../Features/MyPage/Sources/MyPageView.swift | 6 ++++++ .../MyPage/Sources/MyPageViewModel.swift | 19 ++++++++++++++---- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 Projects/Features/MyPage/Sources/Components/MyPagePregressView.swift diff --git a/Projects/Features/MyPage/Sources/Components/MyPagePregressView.swift b/Projects/Features/MyPage/Sources/Components/MyPagePregressView.swift new file mode 100644 index 0000000..01a00dc --- /dev/null +++ b/Projects/Features/MyPage/Sources/Components/MyPagePregressView.swift @@ -0,0 +1,20 @@ +// +// MyPagePregressView.swift +// MyPage +// +// Created by 장혜령 on 2024/07/21. +// + +import SwiftUI + +struct MyPagePregressView: View { + @Binding var isRefreshCompleted: Bool + var body: some View { + if !isRefreshCompleted { + VStack { + ProgressView() + .padding(.top, 80) + } + } + } +} diff --git a/Projects/Features/MyPage/Sources/MyPageView.swift b/Projects/Features/MyPage/Sources/MyPageView.swift index 4c3c9f9..1e133ac 100644 --- a/Projects/Features/MyPage/Sources/MyPageView.swift +++ b/Projects/Features/MyPage/Sources/MyPageView.swift @@ -19,6 +19,7 @@ public struct MyPageView: View { public var body: some View { ScrollView { + MyPagePregressView(isRefreshCompleted: $viewModel.state.isRefreshCompleted) levelView divider RecentlyMemeListView( @@ -35,6 +36,9 @@ public struct MyPageView: View { .onAppear { viewModel.dispatch(type: .onAppearMyPageView) } + .refreshable { + viewModel.dispatch(type: .pullToRefresh) + } .background { gradientBackgroundView } @@ -88,6 +92,8 @@ public struct MyPageView: View { } + + //#Preview { // let mockImageList = ["https://plus.unsplash.com/premium_photo-1661892088256-0a17130b3d0d?q=80&w=3560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", // "https://plus.unsplash.com/premium_photo-1676955432796-226f504a560b?q=80&w=3333&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", diff --git a/Projects/Features/MyPage/Sources/MyPageViewModel.swift b/Projects/Features/MyPage/Sources/MyPageViewModel.swift index 0ab43e0..3e521e1 100644 --- a/Projects/Features/MyPage/Sources/MyPageViewModel.swift +++ b/Projects/Features/MyPage/Sources/MyPageViewModel.swift @@ -20,6 +20,7 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { public enum Action { case onAppearMyPageView + case pullToRefresh } public struct Handler { @@ -33,7 +34,7 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { var userDetail: UserDetail var lastSeenMemeList: [MemeDetail] var savedMemeList: [MemeDetail] - + var isRefreshCompleted: Bool var memeLevel: MemeLevelType { return MemeLevelType(rawValue: userDetail.level) ?? .level1 } @@ -73,7 +74,7 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { copyImageUseCase: CopyImageUseCase ) { self.router = router - self.state = State(userDetail: userDetail, lastSeenMemeList: [], savedMemeList: []) + self.state = State(userDetail: userDetail, lastSeenMemeList: [], savedMemeList: [], isRefreshCompleted: true) self.userDetail = userDetail self.getUserDetailUseCase = getUserDetailUseCase self.getLastSeenMemeUseCase = getLastSeenMemeUseCase @@ -89,6 +90,8 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { switch type { case .onAppearMyPageView: await self.fetchUserMemes() + case .pullToRefresh: + await self.refreshUserMemes() } } } @@ -96,16 +99,24 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { @MainActor private func fetchUserMemes() async { do { + let userDetail = try await self.getUserDetailUseCase.execute() let lastSeenMemeList = try await self.getLastSeenMemeUseCase.execute() let savedMemeList = try await self.getSavedMemeUseCase.execute() - self.state = State(userDetail: state.userDetail, + self.state = State(userDetail: userDetail, lastSeenMemeList: lastSeenMemeList, - savedMemeList: savedMemeList) + savedMemeList: savedMemeList, + isRefreshCompleted: true) } catch(let error) { print("fetchUserMemes error = \(error)") } } + @MainActor + private func refreshUserMemes() async { + self.state.isRefreshCompleted = false + await fetchUserMemes() + } + private func initHandler() { let memeClickHandler: ((MemeDetail) -> ()) = { [weak self] memeDetail in guard let self else { return } From b9a8a4e74a7f933908a7bb33fd919b7684113273 Mon Sep 17 00:00:00 2001 From: hryeong66 Date: Sun, 21 Jul 2024 16:55:18 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=A0=80=EC=9E=A5=ED=95=9C=20?= =?UTF-8?q?=EB=B0=88=20pagination=20=EA=B5=AC=ED=98=84=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/Meme/MemeListView.swift | 21 ++++- .../Sources/DTO/MemeResponseDTO.swift | 19 ++++ .../Sources/Endpoint/UserEndpoint.swift | 8 +- .../Repository/UserRepositoryImpl.swift | 8 +- .../Sources/Repository/UserRepository.swift | 2 +- .../UseCase/CheckUserInfoUseCase.swift | 2 +- .../Sources/UseCase/GetSavedMemeUseCase.swift | 6 +- .../Sources/Meme/MemeListWithPagination.swift | 57 ++++++++++++ .../Components/LevelProgressView.swift | 8 +- .../Components/SavedMemeListView.swift | 4 +- .../Features/MyPage/Sources/MyPageView.swift | 3 +- .../MyPage/Sources/MyPageViewModel.swift | 91 ++++++++++++++----- 12 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 Projects/Core/PPACModels/Sources/Meme/MemeListWithPagination.swift diff --git a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift index f5ced22..b295a7d 100644 --- a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift +++ b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift @@ -15,15 +15,22 @@ public struct MemeListView: View { alignment: .center),count: 2) private let memeClickHandler: ((MemeDetail) -> ())? private let memeCopyHandler: ((MemeDetail) -> ())? + private let onAppearLastMemeHandler: (() -> ())? + + private var lastMemeId: String { + return memeDetailList.last?.id ?? "" + } public init( memeDetailList: [MemeDetail], memeClickHandler: ((MemeDetail) -> ())? = nil, - memeCopyHandler: ((MemeDetail) -> ())? = nil + memeCopyHandler: ((MemeDetail) -> ())? = nil, + onAppearLastMemeHandler: (() -> ())? = nil ) { self.memeDetailList = memeDetailList self.memeClickHandler = memeClickHandler self.memeCopyHandler = memeCopyHandler + self.onAppearLastMemeHandler = onAppearLastMemeHandler } var oddIndexedItems: [MemeDetail] { @@ -41,7 +48,7 @@ public struct MemeListView: View { public var body: some View { ScrollView { - HStack { + HStack(alignment: .top) { LazyVStack { ForEach(oddIndexedItems) { memeDetail in MemeItemView( @@ -49,6 +56,11 @@ public struct MemeListView: View { memeClickHandler: memeClickHandler, memeCopyHandler: memeCopyHandler ) + .onAppear { + if memeDetail.id == lastMemeId { + onAppearLastMemeHandler?() + } + } } } @@ -59,6 +71,11 @@ public struct MemeListView: View { memeClickHandler: memeClickHandler, memeCopyHandler: memeCopyHandler ) + .onAppear { + if memeDetail.id == lastMemeId { + onAppearLastMemeHandler?() + } + } } } } diff --git a/Projects/Core/PPACData/Sources/DTO/MemeResponseDTO.swift b/Projects/Core/PPACData/Sources/DTO/MemeResponseDTO.swift index 70affb7..96ba5a6 100644 --- a/Projects/Core/PPACData/Sources/DTO/MemeResponseDTO.swift +++ b/Projects/Core/PPACData/Sources/DTO/MemeResponseDTO.swift @@ -23,6 +23,25 @@ struct MemeWithPaginationResponseDTO: Decodable { } } +extension MemeWithPaginationResponseDTO { + func toModel() -> MemeListWithPagination { + let pagination = MemeListWithPagination + .Pagination( + totalPages: self.pagination.totalPages, + totalMemes: self.pagination.total, + perPageOfMemes: self.pagination.perPage, + currentPage: self.pagination.currentPage + ) + let memeList = memeList.map { $0.toModel() } + + return MemeListWithPagination( + pagination: pagination, + memeList: memeList + ) + } +} + + struct MemeResponseDTO: Decodable { let _id: String let title: String diff --git a/Projects/Core/PPACData/Sources/Endpoint/UserEndpoint.swift b/Projects/Core/PPACData/Sources/Endpoint/UserEndpoint.swift index 44e6ec6..fdd03ce 100644 --- a/Projects/Core/PPACData/Sources/Endpoint/UserEndpoint.swift +++ b/Projects/Core/PPACData/Sources/Endpoint/UserEndpoint.swift @@ -13,7 +13,7 @@ public enum UserEndpoint: Requestable { case create(deviceId: String) case userDetail - case savedMeme + case savedMeme(page: Int, size: Int) case lastSeenMeme public var url: String { @@ -55,6 +55,12 @@ public enum UserEndpoint: Requestable { case .create(let deviceId): let createDeviceRequest = CreateUserRequestDTO(deviceId: deviceId) return .body(createDeviceRequest) + case .savedMeme(let page, let size): + let parameters: [String: String] = [ + "page" : "\(page)", + "size" : "\(size)" + ] + return .query(parameters) default: return nil } diff --git a/Projects/Core/PPACData/Sources/Repository/UserRepositoryImpl.swift b/Projects/Core/PPACData/Sources/Repository/UserRepositoryImpl.swift index 1eb3272..0235b4d 100644 --- a/Projects/Core/PPACData/Sources/Repository/UserRepositoryImpl.swift +++ b/Projects/Core/PPACData/Sources/Repository/UserRepositoryImpl.swift @@ -54,17 +54,17 @@ public final class UserRepositoryImpl: UserRepository { } } - public func getSavedMeme() async throws -> [MemeDetail] { + public func getSavedMeme(page: Int, size: Int) async throws -> MemeListWithPagination { let result = await networkservice .request( - UserEndpoint.savedMeme, + UserEndpoint.savedMeme(page: page, size: size), dataType: BaseDTO.self ) switch result { case .success(let data): - guard let memeResponseDTOList = data.data?.memeList else { throw NetworkError.dataDecodingError } - return memeResponseDTOList.map { $0.toModel() } + guard let memeWithPaginationResponseDTO = data.data else { throw NetworkError.dataDecodingError } + return memeWithPaginationResponseDTO.toModel() case .failure(let error): throw error } diff --git a/Projects/Core/PPACDomain/Sources/Repository/UserRepository.swift b/Projects/Core/PPACDomain/Sources/Repository/UserRepository.swift index 2b94ce7..c849b8c 100644 --- a/Projects/Core/PPACDomain/Sources/Repository/UserRepository.swift +++ b/Projects/Core/PPACDomain/Sources/Repository/UserRepository.swift @@ -12,6 +12,6 @@ import PPACModels public protocol UserRepository { func create(deviceId: String) async throws -> UserDetail func getUserDetail() async throws -> UserDetail - func getSavedMeme() async throws -> [MemeDetail] + func getSavedMeme(page: Int, size: Int) async throws -> MemeListWithPagination func getLastSeenMeme() async throws -> [MemeDetail] } diff --git a/Projects/Core/PPACDomain/Sources/UseCase/CheckUserInfoUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/CheckUserInfoUseCase.swift index 783b7be..08489c5 100644 --- a/Projects/Core/PPACDomain/Sources/UseCase/CheckUserInfoUseCase.swift +++ b/Projects/Core/PPACDomain/Sources/UseCase/CheckUserInfoUseCase.swift @@ -56,7 +56,7 @@ public class MockCheckUserInfoUseCase: CheckUserInfoUseCase { class MockUserRepository: UserRepository { func create(deviceId: String) async throws -> UserDetail { return UserDetail.mock } func getUserDetail() async throws -> UserDetail { return UserDetail.mock } - func getSavedMeme() async throws -> [MemeDetail] { return [] } + func getSavedMeme(page: Int, size: Int) async throws -> MemeListWithPagination { return MemeListWithPagination.mock } func getLastSeenMeme() async throws -> [MemeDetail] { return [] } } } diff --git a/Projects/Core/PPACDomain/Sources/UseCase/GetSavedMemeUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/GetSavedMemeUseCase.swift index d85b263..6787249 100644 --- a/Projects/Core/PPACDomain/Sources/UseCase/GetSavedMemeUseCase.swift +++ b/Projects/Core/PPACDomain/Sources/UseCase/GetSavedMemeUseCase.swift @@ -9,7 +9,7 @@ import Foundation import PPACModels public protocol GetSavedMemeUseCase { - func execute() async throws -> [MemeDetail] + func execute(page: Int, size: Int) async throws -> MemeListWithPagination } public class GetSavedMemeUseCaseImpl: GetSavedMemeUseCase { @@ -19,7 +19,7 @@ public class GetSavedMemeUseCaseImpl: GetSavedMemeUseCase { self.userRepository = userRepository } - public func execute() async throws -> [MemeDetail] { - return try await self.userRepository.getSavedMeme() + public func execute(page: Int, size: Int) async throws -> MemeListWithPagination { + return try await self.userRepository.getSavedMeme(page: page, size: size) } } diff --git a/Projects/Core/PPACModels/Sources/Meme/MemeListWithPagination.swift b/Projects/Core/PPACModels/Sources/Meme/MemeListWithPagination.swift new file mode 100644 index 0000000..d3b4609 --- /dev/null +++ b/Projects/Core/PPACModels/Sources/Meme/MemeListWithPagination.swift @@ -0,0 +1,57 @@ +// +// MemeListWithPagination.swift +// PPACModels +// +// Created by 장혜령 on 2024/07/21. +// + +import Foundation + +public struct MemeListWithPagination { + public let pagination: Pagination + public let memeList: [MemeDetail] + + public init( + pagination: Pagination, + memeList: [MemeDetail] + ) { + self.pagination = pagination + self.memeList = memeList + } + + public struct Pagination { + /// 전체 페이지 개수 + public let totalPages: Int + /// 전체 밈 개수 + public let totalMemes: Int + /// page 당 밈 개수 + public let perPageOfMemes: Int + /// 현재 page + public let currentPage: Int + + public init( + totalPages: Int, + totalMemes: Int, + perPageOfMemes: Int, + currentPage: Int + ) { + self.totalPages = totalPages + self.totalMemes = totalMemes + self.perPageOfMemes = perPageOfMemes + self.currentPage = currentPage + } + + static public let none = Pagination(totalPages: 0, totalMemes: 0, perPageOfMemes: 0, currentPage: 0) + } +} + +public extension MemeListWithPagination { + static let mock = MemeListWithPagination( + pagination: Pagination( + totalPages: 1, + totalMemes: 5, + perPageOfMemes: 5, + currentPage: 1 + ), + memeList: []) +} diff --git a/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift b/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift index 0d8c1c4..a652cbb 100644 --- a/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift +++ b/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift @@ -59,13 +59,15 @@ struct LevelProgressView: View { .stroke(Color.Border.secondary, lineWidth: 1, fill: Color.Background.assistive) } - private func getCurrentLevelWidth(_ wiewWidth: CGFloat) -> CGFloat { - let width = wiewWidth / 20.0 * CGFloat(conditionCount) + private func getCurrentLevelWidth(_ viewWidth: CGFloat) -> CGFloat { + let width = viewWidth / 20.0 * CGFloat(conditionCount) + print("level width = \(width)") + print("viewWidth = \(viewWidth)") return width < minimumWidth ? minimumWidth : width } } #Preview { - LevelProgressView(level: .level1, conditionCount: 20) + LevelProgressView(level: .level1, conditionCount: 1) } diff --git a/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift b/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift index 2909ff8..df1978a 100644 --- a/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift +++ b/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift @@ -15,6 +15,7 @@ struct SavedMemeListView: View { @Binding var memeDetailList: [MemeDetail] var memeClickHandler: ((MemeDetail) -> ())? var memeCopyHandler: ((MemeDetail) -> ())? + var onAppearLastMemeHandler: (() -> ())? var body: some View { VStack { @@ -33,7 +34,8 @@ struct SavedMemeListView: View { MemeListView( memeDetailList: memeDetailList, memeClickHandler: memeClickHandler, - memeCopyHandler: memeCopyHandler + memeCopyHandler: memeCopyHandler, + onAppearLastMemeHandler: onAppearLastMemeHandler ) .padding(.horizontal, 20) } diff --git a/Projects/Features/MyPage/Sources/MyPageView.swift b/Projects/Features/MyPage/Sources/MyPageView.swift index 1e133ac..00d75d3 100644 --- a/Projects/Features/MyPage/Sources/MyPageView.swift +++ b/Projects/Features/MyPage/Sources/MyPageView.swift @@ -29,7 +29,8 @@ public struct MyPageView: View { SavedMemeListView( memeDetailList: $viewModel.state.savedMemeList, memeClickHandler: viewModel.handler.memeClickHandler, - memeCopyHandler: viewModel.handler.memeCopyHandler + memeCopyHandler: viewModel.handler.memeCopyHandler, + onAppearLastMemeHandler: viewModel.handler.onAppearLastMemeHandler ) Spacer(minLength: 70) } diff --git a/Projects/Features/MyPage/Sources/MyPageViewModel.swift b/Projects/Features/MyPage/Sources/MyPageViewModel.swift index 3e521e1..00ee06a 100644 --- a/Projects/Features/MyPage/Sources/MyPageViewModel.swift +++ b/Projects/Features/MyPage/Sources/MyPageViewModel.swift @@ -26,7 +26,7 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { public struct Handler { var memeClickHandler: ((MemeDetail) -> ())? var memeCopyHandler: ((MemeDetail) -> ())? - + var onAppearLastMemeHandler: (() -> ())? static let none = Handler() } @@ -34,7 +34,9 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { var userDetail: UserDetail var lastSeenMemeList: [MemeDetail] var savedMemeList: [MemeDetail] + var savedMemePagination: MemeListWithPagination.Pagination var isRefreshCompleted: Bool + var memeLevel: MemeLevelType { return MemeLevelType(rawValue: userDetail.level) ?? .level1 } @@ -51,6 +53,10 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { userDetail.save } } + + var hasNextPageOfSavedMeme: Bool { + return savedMemePagination.currentPage < savedMemePagination.totalPages + } } // MARK: - Properties @@ -63,6 +69,9 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { private let copyImageUseCase: CopyImageUseCase public var handler: Handler = .none + private var currentPage: Int = 1 + private let savedMemeCountPerPage: Int = 2 + // MARK: - Initializers public init( @@ -74,7 +83,11 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { copyImageUseCase: CopyImageUseCase ) { self.router = router - self.state = State(userDetail: userDetail, lastSeenMemeList: [], savedMemeList: [], isRefreshCompleted: true) + self.state = State(userDetail: userDetail, + lastSeenMemeList: [], + savedMemeList: [], + savedMemePagination: .none, + isRefreshCompleted: true) self.userDetail = userDetail self.getUserDetailUseCase = getUserDetailUseCase self.getLastSeenMemeUseCase = getLastSeenMemeUseCase @@ -96,15 +109,51 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { } } + private func initHandler() { + let memeClickHandler: ((MemeDetail) -> ()) = { [weak self] memeDetail in + guard let self else { return } + Task { + await self.router?.showMemeDetail(memeDetail: memeDetail) + } + } + + let memeCopyHandler: ((MemeDetail) -> ()) = { [weak self] memeDetail in + guard let self else { return } + Task { + print("memeCopyHandler \(memeDetail.title)") + do { + try await self.copyImageUseCase.execute(url: memeDetail.imageUrlString) + } catch { + print("복사 실패") + } + } + } + + let onAppearLastMemeHandler: (() -> ()) = { [weak self] in + guard let self else { return } + Task { + await self.fetchNextPageSavedMeme() + } + } + + self.handler = Handler( + memeClickHandler: memeClickHandler, + memeCopyHandler: memeCopyHandler, + onAppearLastMemeHandler: onAppearLastMemeHandler + ) + } + @MainActor private func fetchUserMemes() async { do { let userDetail = try await self.getUserDetailUseCase.execute() let lastSeenMemeList = try await self.getLastSeenMemeUseCase.execute() - let savedMemeList = try await self.getSavedMemeUseCase.execute() + let savedMemeListWithPagination = try await self.getSavedMemeUseCase.execute(page: 1, + size: self.savedMemeCountPerPage) self.state = State(userDetail: userDetail, lastSeenMemeList: lastSeenMemeList, - savedMemeList: savedMemeList, + savedMemeList: savedMemeListWithPagination.memeList, + savedMemePagination: savedMemeListWithPagination.pagination, isRefreshCompleted: true) } catch(let error) { print("fetchUserMemes error = \(error)") @@ -117,26 +166,22 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { await fetchUserMemes() } - private func initHandler() { - let memeClickHandler: ((MemeDetail) -> ()) = { [weak self] memeDetail in - guard let self else { return } - Task { - await self.router?.showMemeDetail(memeDetail: memeDetail) - } - } + @MainActor + private func fetchNextPageSavedMeme() async { + guard state.hasNextPageOfSavedMeme else { return } - let memeCopyHandler: ((MemeDetail) -> ()) = { [weak self] memeDetail in - guard let self else { return } - Task { - print("memeCopyHandler \(memeDetail.title)") - do { - try await self.copyImageUseCase.execute(url: memeDetail.imageUrlString) - } catch { - print("복사 실패") - } - } + do { + let savedMemeListWithPagination = try await self.getSavedMemeUseCase + .execute( + page: state.savedMemePagination.currentPage + 1, + size: self.savedMemeCountPerPage + ) + + self.state.savedMemeList += savedMemeListWithPagination.memeList + self.state.savedMemePagination = savedMemeListWithPagination.pagination + } catch(let error) { + print("fetchNextPageSavedMeme error = \(error)") } - - self.handler = Handler(memeClickHandler: memeClickHandler, memeCopyHandler: memeCopyHandler) } + } From 3f3029351ab9e7f31debaf88d45af083bdf04921 Mon Sep 17 00:00:00 2001 From: hryeong66 Date: Sun, 21 Jul 2024 21:14:10 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20pull=20to=20refresh=20=ED=9B=84=20?= =?UTF-8?q?=EB=82=98=EC=9D=98=20=ED=8C=8C=EB=B0=88=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=9E=84=EC=8B=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/DesignSystem/Sources/View/Meme/MemeListView.swift | 4 ++++ .../MyPage/Sources/Components/LevelProgressView.swift | 2 -- .../MyPage/Sources/Components/SavedMemeListView.swift | 2 +- Projects/Features/MyPage/Sources/MyPageView.swift | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift index b295a7d..3860059 100644 --- a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift +++ b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift @@ -79,6 +79,10 @@ public struct MemeListView: View { } } } + // FIXME: pull to refresh 했을 때 onAppear가 호출되지 않아서 onChange로 임시 호출, 수정필요 + .onChange(of: memeDetailList) { + onAppearLastMemeHandler?() + } .frame(maxWidth: .infinity) } .scrollTargetBehavior(.viewAligned) diff --git a/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift b/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift index a652cbb..9273ce7 100644 --- a/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift +++ b/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift @@ -61,8 +61,6 @@ struct LevelProgressView: View { private func getCurrentLevelWidth(_ viewWidth: CGFloat) -> CGFloat { let width = viewWidth / 20.0 * CGFloat(conditionCount) - print("level width = \(width)") - print("viewWidth = \(viewWidth)") return width < minimumWidth ? minimumWidth : width } } diff --git a/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift b/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift index df1978a..7e46557 100644 --- a/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift +++ b/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift @@ -37,7 +37,7 @@ struct SavedMemeListView: View { memeCopyHandler: memeCopyHandler, onAppearLastMemeHandler: onAppearLastMemeHandler ) - .padding(.horizontal, 20) + .padding(.horizontal, 20) } } diff --git a/Projects/Features/MyPage/Sources/MyPageView.swift b/Projects/Features/MyPage/Sources/MyPageView.swift index 00d75d3..e0a80dd 100644 --- a/Projects/Features/MyPage/Sources/MyPageView.swift +++ b/Projects/Features/MyPage/Sources/MyPageView.swift @@ -32,7 +32,7 @@ public struct MyPageView: View { memeCopyHandler: viewModel.handler.memeCopyHandler, onAppearLastMemeHandler: viewModel.handler.onAppearLastMemeHandler ) - Spacer(minLength: 70) + Spacer(minLength: 80) } .onAppear { viewModel.dispatch(type: .onAppearMyPageView) From bab7508d388ab2dc963c912085d3a606f4f52220 Mon Sep 17 00:00:00 2001 From: hryeong66 Date: Sun, 21 Jul 2024 22:02:04 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20setting=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Project.swift | 2 ++ Projects/Core/PPACDomain/Project.swift | 1 + Projects/Features/Setting/Project.swift | 29 ++++++++++++++++++ .../Features/Setting/Resources/dummy.swift | 1 + .../Setting/Sources/SettingView.swift | 8 +++++ .../Setting/Sources/SettingViewModel.swift | 8 +++++ .../Farmeme_Logo.imageset/Contents.json | 23 ++++++++++++++ .../Farmeme_Logo.imageset/Farmeme_Logo.png | Bin 0 -> 2621 bytes .../Farmeme_Logo.imageset/Farmeme_Logo@2x.png | Bin 0 -> 5034 bytes .../Farmeme_Logo.imageset/Farmeme_Logo@3x.png | Bin 0 -> 7632 bytes .../TargetDependency+.swift | 1 + 11 files changed, 73 insertions(+) create mode 100644 Projects/Features/Setting/Project.swift create mode 100644 Projects/Features/Setting/Resources/dummy.swift create mode 100644 Projects/Features/Setting/Sources/SettingView.swift create mode 100644 Projects/Features/Setting/Sources/SettingViewModel.swift create mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Contents.json create mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo.png create mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@2x.png create mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@3x.png diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index c4ebfe5..d39d1e0 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -27,6 +27,7 @@ let project = Project.configure( .Feature.Recommend, .Feature.Search, .Feature.MyPage, + .Feature.Setting, .ResourceKit, .Core.DesignSystem, .Core.PPACNetwork, @@ -58,6 +59,7 @@ let project = Project.configure( .Feature.Recommend, .Feature.Search, .Feature.MyPage, + .Feature.Setting, .ResourceKit, .Core.DesignSystem, .Core.PPACNetwork, diff --git a/Projects/Core/PPACDomain/Project.swift b/Projects/Core/PPACDomain/Project.swift index 0e8455e..01f0d30 100644 --- a/Projects/Core/PPACDomain/Project.swift +++ b/Projects/Core/PPACDomain/Project.swift @@ -19,6 +19,7 @@ let project = Project( resources: "Resources/**", dependencies: [ .Core.PPACModels, + .Core.PPACNetwork ] ) ] diff --git a/Projects/Features/Setting/Project.swift b/Projects/Features/Setting/Project.swift new file mode 100644 index 0000000..b22ff5e --- /dev/null +++ b/Projects/Features/Setting/Project.swift @@ -0,0 +1,29 @@ +// +// Project.swift +// Setting +// +// Created by hyeryeong on 7/21/24 +// + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project( + name: "Setting", + targets: [ + .configure( + name: "Setting", + product: .framework, + infoPlist: .default, + sources: "Sources/**", + resources: "Resources/**", + dependencies: [ + .ThirdParty.Dependency, + .ResourceKit, + .Core.DesignSystem, + .Core.PPACUtil + ] + ) + ] +) + diff --git a/Projects/Features/Setting/Resources/dummy.swift b/Projects/Features/Setting/Resources/dummy.swift new file mode 100644 index 0000000..2ad55c7 --- /dev/null +++ b/Projects/Features/Setting/Resources/dummy.swift @@ -0,0 +1 @@ +더미임미다 \ No newline at end of file diff --git a/Projects/Features/Setting/Sources/SettingView.swift b/Projects/Features/Setting/Sources/SettingView.swift new file mode 100644 index 0000000..f831e19 --- /dev/null +++ b/Projects/Features/Setting/Sources/SettingView.swift @@ -0,0 +1,8 @@ +// +// SettingView.swift +// Setting +// +// Created by 장혜령 on 2024/07/21. +// + +import Foundation diff --git a/Projects/Features/Setting/Sources/SettingViewModel.swift b/Projects/Features/Setting/Sources/SettingViewModel.swift new file mode 100644 index 0000000..913705e --- /dev/null +++ b/Projects/Features/Setting/Sources/SettingViewModel.swift @@ -0,0 +1,8 @@ +// +// SettingViewModel.swift +// Setting +// +// Created by 장혜령 on 2024/07/21. +// + +import Foundation diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Contents.json b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Contents.json new file mode 100644 index 0000000..c54126f --- /dev/null +++ b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Farmeme_Logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Farmeme_Logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Farmeme_Logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo.png b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..844fca77edd80d8f67b33ac145ab97c7c3b0ee6a GIT binary patch literal 2621 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 Date: Sun, 21 Jul 2024 22:53:35 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=97=B0=EA=B2=B0=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/Meme/MemeListView.swift | 6 +- Projects/Features/MyPage/Project.swift | 1 + .../MyPage/Sources/MyPageRouter.swift | 7 +- .../Features/MyPage/Sources/MyPageView.swift | 5 +- .../MyPage/Sources/MyPageViewModel.swift | 5 +- .../Setting/Sources/SettingRouter.swift | 30 ++++++ .../Setting/Sources/SettingView.swift | 83 ++++++++++++++++- .../Setting/Sources/SettingViewModel.swift | 91 +++++++++++++++++++ 8 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 Projects/Features/Setting/Sources/SettingRouter.swift diff --git a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift index 3860059..320de83 100644 --- a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift +++ b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift @@ -80,9 +80,9 @@ public struct MemeListView: View { } } // FIXME: pull to refresh 했을 때 onAppear가 호출되지 않아서 onChange로 임시 호출, 수정필요 - .onChange(of: memeDetailList) { - onAppearLastMemeHandler?() - } +// .onChange(of: memeDetailList) { +// onAppearLastMemeHandler?() +// } .frame(maxWidth: .infinity) } .scrollTargetBehavior(.viewAligned) diff --git a/Projects/Features/MyPage/Project.swift b/Projects/Features/MyPage/Project.swift index c394ead..2c820c9 100644 --- a/Projects/Features/MyPage/Project.swift +++ b/Projects/Features/MyPage/Project.swift @@ -23,6 +23,7 @@ let project = Project( .Core.DesignSystem, .Core.PPACModels, .Feature.MemeDetail, + .Feature.Setting ] ) ] diff --git a/Projects/Features/MyPage/Sources/MyPageRouter.swift b/Projects/Features/MyPage/Sources/MyPageRouter.swift index 7c73eb9..2f1eb7f 100644 --- a/Projects/Features/MyPage/Sources/MyPageRouter.swift +++ b/Projects/Features/MyPage/Sources/MyPageRouter.swift @@ -16,6 +16,7 @@ import PPACData import MemeDetail import DesignSystem +import Setting public final class MyPageRouter: Router, MyPageRouting { @@ -61,7 +62,11 @@ public final class MyPageRouter: Router, MyPageRouting { router.start() } - public func showSettingView() { } + public func showSettingView() { + let router = SettingRouter(navigationController: self.navigationController) + self.childRouters.append(router) + router.start() + } } diff --git a/Projects/Features/MyPage/Sources/MyPageView.swift b/Projects/Features/MyPage/Sources/MyPageView.swift index e0a80dd..c673053 100644 --- a/Projects/Features/MyPage/Sources/MyPageView.swift +++ b/Projects/Features/MyPage/Sources/MyPageView.swift @@ -74,8 +74,11 @@ public struct MyPageView: View { .frame(width: 20, height: 20, alignment: .center) .padding(.vertical, 15) .padding(.trailing, 20) + .onTapGesture { + viewModel.dispatch(type: .settingButtonTapped) + } } - .padding(.top, 30) + .padding(.top, 40) } var levelTitleTextView: some View { diff --git a/Projects/Features/MyPage/Sources/MyPageViewModel.swift b/Projects/Features/MyPage/Sources/MyPageViewModel.swift index 00ee06a..ad99a70 100644 --- a/Projects/Features/MyPage/Sources/MyPageViewModel.swift +++ b/Projects/Features/MyPage/Sources/MyPageViewModel.swift @@ -18,9 +18,10 @@ public protocol MyPageRouting: AnyObject { final public class MyPageViewModel: ViewModelType, ObservableObject { - public enum Action { + public enum Action { case onAppearMyPageView case pullToRefresh + case settingButtonTapped } public struct Handler { @@ -105,6 +106,8 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { await self.fetchUserMemes() case .pullToRefresh: await self.refreshUserMemes() + case .settingButtonTapped: + router?.showSettingView() } } } diff --git a/Projects/Features/Setting/Sources/SettingRouter.swift b/Projects/Features/Setting/Sources/SettingRouter.swift new file mode 100644 index 0000000..9ad9b2c --- /dev/null +++ b/Projects/Features/Setting/Sources/SettingRouter.swift @@ -0,0 +1,30 @@ +// +// SettingRouter.swift +// Setting +// +// Created by 장혜령 on 2024/07/21. +// +import UIKit + +import PPACUtil + +public final class SettingRouter: Router, SettingRouting { + + // MARK: - Properties + public var delegate: (any RouterDelegate)? + public var navigationController: UINavigationController + public var childRouters: [any Router] = [] + + + // MARK: - Initializers + public init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + public func start() { + let settingView = SettingView( + viewModel: SettingViewModel(router: self) + ) + self.pushView(settingView) + } +} diff --git a/Projects/Features/Setting/Sources/SettingView.swift b/Projects/Features/Setting/Sources/SettingView.swift index f831e19..e2c5be6 100644 --- a/Projects/Features/Setting/Sources/SettingView.swift +++ b/Projects/Features/Setting/Sources/SettingView.swift @@ -5,4 +5,85 @@ // Created by 장혜령 on 2024/07/21. // -import Foundation +import SwiftUI +import ResourceKit +import DesignSystem + +public struct SettingView: View { + @ObservedObject private var viewModel: SettingViewModel + + public init(viewModel: SettingViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + VStack{ + Divider() + .padding(.top, 50) + memeLogoImage + appNameAndVersion + .padding(.bottom, 50) + Divider() + .padding(.horizontal, 10) + .padding(.bottom, 20) + settingListView + Spacer() + } + .plainNavigationBar( + backHandler: { viewModel.dispatch(type: .naviBackButtonTapped) }, + rightActionHandler: nil, + hasConfigureButton: false, + title: "설정" + ) + } + + var memeLogoImage: some View { + ResourceKitAsset.Icon.farmemeLogo.swiftUIImage + .resizable() + .frame(width: 70, height: 70, alignment: .center) + .padding(.top, 50) + } + + var appNameAndVersion: some View { + VStack { + Text("파밈") + .font(Font.Heading.Medium.semiBold) + .foregroundStyle(Color.Text.primary) + .padding(.bottom, 2) + Text(viewModel.state.currnetAppVersion) + .font(Font.Body.Small.medium) + .foregroundStyle(Color.Text.tertiary) + } + } + + var settingListView: some View { + ForEach(viewModel.state.settingList) { settingType in + SettingListItemView(title: settingType.title) + } + } +} + +struct SettingListItemView: View { + let title: String + + var body: some View { + HStack { + Text(title) + .font(Font.Body.Xlarge.semiBold) + .padding(.vertical, 20) + .padding(.leading, 20) + Spacer() + ResourceKitAsset.Icon.arrowRight.swiftUIImage + .resizable() + .renderingMode(.template) + .frame(width: 16, height: 16, alignment: .center) + .foregroundStyle(Color.Icon.assistive) + .padding(.trailing, 20) + } + } +} + + +#Preview { + SettingView(viewModel: SettingViewModel(router: nil)) +} diff --git a/Projects/Features/Setting/Sources/SettingViewModel.swift b/Projects/Features/Setting/Sources/SettingViewModel.swift index 913705e..b788f51 100644 --- a/Projects/Features/Setting/Sources/SettingViewModel.swift +++ b/Projects/Features/Setting/Sources/SettingViewModel.swift @@ -6,3 +6,94 @@ // import Foundation +import PPACUtil + +@MainActor +public protocol SettingRouting: AnyObject { + func popView() +} + +final public class SettingViewModel: ViewModelType, ObservableObject { + + public enum Action { + case naviBackButtonTapped + } + + public struct State { + var currnetAppVersion: String = "" + var needUpdate: Bool = false + var settingList: [SettingType] = [] + } + + struct SettingType: Identifiable { + let id = UUID() + let url: String + let title: String + + init( + url: String, + title: String + ) { + self.url = url + self.title = title + } + } + + // MARK: - Properties + weak var router: SettingRouting? + @Published public var state: State + + // MARK: - Initializers + public init(router: SettingRouting? = nil) { + self.router = router + self.state = State() + self.state.needUpdate = self.checkNeedUpdate() + self.state.currnetAppVersion = self.getCurrentAppVersion() + self.initSettingList() + } + + private func initSettingList() { + var settingList = [SettingType]() + settingList.append( + SettingType(url: "https://www.google.com", title: "개인정보 처리 방침") + ) + self.state.settingList = settingList + } + + // MARK: - Methods + @MainActor + public func dispatch(type: Action) { + switch type { + case .naviBackButtonTapped: + router?.popView() + } + } + + private func getCurrentAppVersion() -> String { + let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + return currentVersion ?? "" + } + + private func checkNeedUpdate() -> Bool { + guard let appStoreVersion = self.getLatestVersion() else { return false } + let currentVersion = getCurrentAppVersion() + print("Setting | currentVersion = \(currentVersion)") + print("Setting | appStoreVersion = \(appStoreVersion)") + + return currentVersion != appStoreVersion + } + + private func getLatestVersion() -> String? { +// guard +// let bundleIdentifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String, +// let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(bundleIdentifier)"), +// let data = try? Data(contentsOf: url), +// let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], +// let results = json["results"] as? [[String: Any]], !results.isEmpty, +// let appStoreVersion = results[0]["version"] as? String else { +// return nil +// } +// return appStoreVersion + return nil + } +}