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/DesignSystem/Sources/View/Meme/MemeListView.swift b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift index f5ced22..320de83 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,9 +71,18 @@ public struct MemeListView: View { memeClickHandler: memeClickHandler, memeCopyHandler: memeCopyHandler ) + .onAppear { + if memeDetail.id == lastMemeId { + onAppearLastMemeHandler?() + } + } } } } + // FIXME: pull to refresh 했을 때 onAppear가 호출되지 않아서 onChange로 임시 호출, 수정필요 +// .onChange(of: memeDetailList) { +// onAppearLastMemeHandler?() +// } .frame(maxWidth: .infinity) } .scrollTargetBehavior(.viewAligned) diff --git a/Projects/Core/PPACData/Sources/DTO/MemeResponseDTO.swift b/Projects/Core/PPACData/Sources/DTO/MemeResponseDTO.swift index c1f83d9..f74e8cc 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/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/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/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/Components/LevelProgressView.swift b/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift index 0d8c1c4..9273ce7 100644 --- a/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift +++ b/Projects/Features/MyPage/Sources/Components/LevelProgressView.swift @@ -59,13 +59,13 @@ 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) return width < minimumWidth ? minimumWidth : width } } #Preview { - LevelProgressView(level: .level1, conditionCount: 20) + LevelProgressView(level: .level1, conditionCount: 1) } 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/Components/SavedMemeListView.swift b/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift index 2909ff8..7e46557 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,9 +34,10 @@ struct SavedMemeListView: View { MemeListView( memeDetailList: memeDetailList, memeClickHandler: memeClickHandler, - memeCopyHandler: memeCopyHandler + memeCopyHandler: memeCopyHandler, + onAppearLastMemeHandler: onAppearLastMemeHandler ) - .padding(.horizontal, 20) + .padding(.horizontal, 20) } } 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 4c3c9f9..c673053 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( @@ -28,13 +29,17 @@ 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) + Spacer(minLength: 80) } .onAppear { viewModel.dispatch(type: .onAppearMyPageView) } + .refreshable { + viewModel.dispatch(type: .pullToRefresh) + } .background { gradientBackgroundView } @@ -69,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 { @@ -88,6 +96,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..ad99a70 100644 --- a/Projects/Features/MyPage/Sources/MyPageViewModel.swift +++ b/Projects/Features/MyPage/Sources/MyPageViewModel.swift @@ -18,14 +18,16 @@ 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 { var memeClickHandler: ((MemeDetail) -> ())? var memeCopyHandler: ((MemeDetail) -> ())? - + var onAppearLastMemeHandler: (() -> ())? static let none = Handler() } @@ -33,6 +35,8 @@ 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 @@ -50,6 +54,10 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { userDetail.save } } + + var hasNextPageOfSavedMeme: Bool { + return savedMemePagination.currentPage < savedMemePagination.totalPages + } } // MARK: - Properties @@ -62,6 +70,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( @@ -73,7 +84,11 @@ 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: [], + savedMemePagination: .none, + isRefreshCompleted: true) self.userDetail = userDetail self.getUserDetailUseCase = getUserDetailUseCase self.getLastSeenMemeUseCase = getLastSeenMemeUseCase @@ -89,23 +104,14 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { switch type { case .onAppearMyPageView: await self.fetchUserMemes() + case .pullToRefresh: + await self.refreshUserMemes() + case .settingButtonTapped: + router?.showSettingView() } } } - @MainActor - private func fetchUserMemes() async { - do { - let lastSeenMemeList = try await self.getLastSeenMemeUseCase.execute() - let savedMemeList = try await self.getSavedMemeUseCase.execute() - self.state = State(userDetail: state.userDetail, - lastSeenMemeList: lastSeenMemeList, - savedMemeList: savedMemeList) - } catch(let error) { - print("fetchUserMemes error = \(error)") - } - } - private func initHandler() { let memeClickHandler: ((MemeDetail) -> ()) = { [weak self] memeDetail in guard let self else { return } @@ -126,6 +132,59 @@ final public class MyPageViewModel: ViewModelType, ObservableObject { } } - self.handler = Handler(memeClickHandler: memeClickHandler, memeCopyHandler: memeCopyHandler) + 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 savedMemeListWithPagination = try await self.getSavedMemeUseCase.execute(page: 1, + size: self.savedMemeCountPerPage) + self.state = State(userDetail: userDetail, + lastSeenMemeList: lastSeenMemeList, + savedMemeList: savedMemeListWithPagination.memeList, + savedMemePagination: savedMemeListWithPagination.pagination, + isRefreshCompleted: true) + } catch(let error) { + print("fetchUserMemes error = \(error)") + } + } + + @MainActor + private func refreshUserMemes() async { + self.state.isRefreshCompleted = false + await fetchUserMemes() + } + + @MainActor + private func fetchNextPageSavedMeme() async { + guard state.hasNextPageOfSavedMeme else { return } + + 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)") + } } + } 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/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 new file mode 100644 index 0000000..e2c5be6 --- /dev/null +++ b/Projects/Features/Setting/Sources/SettingView.swift @@ -0,0 +1,89 @@ +// +// SettingView.swift +// Setting +// +// Created by 장혜령 on 2024/07/21. +// + +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 new file mode 100644 index 0000000..b788f51 --- /dev/null +++ b/Projects/Features/Setting/Sources/SettingViewModel.swift @@ -0,0 +1,99 @@ +// +// SettingViewModel.swift +// Setting +// +// Created by 장혜령 on 2024/07/21. +// + +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 + } +} 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 0000000..844fca7 Binary files /dev/null and b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo.png differ diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@2x.png b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@2x.png new file mode 100644 index 0000000..e728e1e Binary files /dev/null and b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@2x.png differ diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@3x.png b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@3x.png new file mode 100644 index 0000000..94bf53a Binary files /dev/null and b/Projects/ResourceKit/Resources/Icon.xcassets/Farmeme_Logo.imageset/Farmeme_Logo@3x.png differ diff --git a/Tuist/ProjectDescriptionHelpers/TargetDependency+.swift b/Tuist/ProjectDescriptionHelpers/TargetDependency+.swift index 8a14101..0a7189e 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetDependency+.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetDependency+.swift @@ -16,6 +16,7 @@ extension TargetDependency { public static let Search = project(moduleName: "Search") public static let MyPage = project(moduleName: "MyPage") public static let MemeDetail = project(moduleName: "MemeDetail") + public static let Setting = project(moduleName: "Setting") } public struct Core {