From f7d9f8a64f4a25fcee65893a1dfb56735514615f Mon Sep 17 00:00:00 2001 From: noeyiz Date: Thu, 22 Feb 2024 13:53:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20ProfileViewController=20MVVM?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Presenter/MyPage/MyPageView.swift | 87 ++++++++++++++++++ .../MyPage/MyPageViewController.swift | 92 +++++-------------- .../Presenter/MyPage/Theme/ThemeView.swift | 67 ++++++++++++++ .../MyPage/Theme/ThemeViewController.swift | 38 +------- .../ViewModel/MyPageCellViewModel.swift | 26 ++++++ .../ViewModel/MyPageSectionViewModel.swift | 19 ++++ iBox/Sources/ViewModel/MyPageViewModel.swift | 27 ++++++ iBox/Sources/ViewModel/ThemeViewModel.swift | 23 +++++ 8 files changed, 276 insertions(+), 103 deletions(-) create mode 100644 iBox/Sources/ViewModel/MyPageCellViewModel.swift create mode 100644 iBox/Sources/ViewModel/MyPageSectionViewModel.swift create mode 100644 iBox/Sources/ViewModel/MyPageViewModel.swift create mode 100644 iBox/Sources/ViewModel/ThemeViewModel.swift diff --git a/iBox/Sources/Presenter/MyPage/MyPageView.swift b/iBox/Sources/Presenter/MyPage/MyPageView.swift index 976188a..ee07b29 100644 --- a/iBox/Sources/Presenter/MyPage/MyPageView.swift +++ b/iBox/Sources/Presenter/MyPage/MyPageView.swift @@ -9,6 +9,11 @@ import UIKit class MyPageView: BaseView { + // MARK: - Properties + + var delegate: MyPageViewDelegate? + private var viewModel: MyPageViewModel? + // MARK: - UI let profileView = UIView().then { @@ -38,8 +43,27 @@ class MyPageView: BaseView { $0.sectionHeaderTopPadding = 0 } + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + setupProperties() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - BaseViewProtocol + private func setupProperties() { + tableView.delegate = self + tableView.dataSource = self + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileViewTapped)) + profileView.addGestureRecognizer(tapGesture) + } + override func configureUI() { addSubview(profileView) profileView.addSubview(profileImageView) @@ -75,4 +99,67 @@ class MyPageView: BaseView { } } + // MARK: - Bind ViewModel + + func bindViewModel(_ viewModel: MyPageViewModel) { + self.viewModel = viewModel + } + + // MARK: - functions + + @objc func profileViewTapped() { + delegate?.pushViewController(ProfileViewController()) + } + +} + +extension MyPageView: UITableViewDelegate, UITableViewDataSource { + + // 테이블 뷰의 섹션 개수 설정 + func numberOfSections(in tableView: UITableView) -> Int { + guard let viewModel = viewModel else { return 0 } + return viewModel.myPageSectionViewModels.count + } + + // 테이블 뷰의 행 개수 설정 + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let viewModel = viewModel else { return 0 } + return viewModel.myPageSectionViewModels[section].model.items.count + } + + // 테이블 뷰 셀 구성 + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let viewModel = viewModel, + let cell = tableView.dequeueReusableCell(withIdentifier: "MyPageItemCell") + as? MyPageItemCell else { return UITableViewCell() } + let item = viewModel.myPageSectionViewModels[indexPath.section].model.items[indexPath.row] + cell.titleLabel.text = item.title + cell.descriptionLabel.text = item.description + return cell + } + + // 셀의 높이 설정 + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 65 + } + + // 섹션 헤더의 View 설정 + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = .systemGroupedBackground + return headerView + } + + // 섹션 헤더의 높이 설정 + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 10 + } + + // 테이블 뷰 셀이 선택되었을 때 실행되는 메서드 + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let viewModel = viewModel else { return } + let item = viewModel.myPageSectionViewModels[indexPath.section].model.items[indexPath.row] + delegate?.pushViewController(indexPath) + } + } diff --git a/iBox/Sources/Presenter/MyPage/MyPageViewController.swift b/iBox/Sources/Presenter/MyPage/MyPageViewController.swift index 0858dc7..886a2f6 100644 --- a/iBox/Sources/Presenter/MyPage/MyPageViewController.swift +++ b/iBox/Sources/Presenter/MyPage/MyPageViewController.swift @@ -7,32 +7,24 @@ import UIKit +protocol MyPageViewDelegate { + func pushViewController(_ indexPath: IndexPath) + func pushViewController(_ viewController: UIViewController) +} + class MyPageViewController: BaseNavigationBarViewController { - // MARK: - properties + // MARK: - Properties - var myPageSections: [MyPageSection] = [ - .init(title: "settings", items: [ - MyPageItem(title: "테마", viewController: ThemeViewController()) - ]), - .init(title: "help", items: [ - MyPageItem(title: "이용 가이드"), - MyPageItem(title: "앱 피드백"), - MyPageItem(title: "개발자 정보", description: "지쿠 😆✌🏻") - ]) - ] + private let viewModel = MyPageViewModel() // MARK: - life cycle override func viewDidLoad() { super.viewDidLoad() guard let contentView = contentView as? MyPageView else { return } - - contentView.tableView.delegate = self - contentView.tableView.dataSource = self - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileViewTapped)) - contentView.profileView.addGestureRecognizer(tapGesture) + contentView.delegate = self + contentView.bindViewModel(viewModel) } // MARK: - BaseNavigationBarViewControllerProtocol @@ -41,58 +33,24 @@ class MyPageViewController: BaseNavigationBarViewController { setNavigationBarTitleLabelText("My Page") } - // MARK: - functions - - @objc func profileViewTapped(_ gesture: UITapGestureRecognizer) { - let viewController = ProfileViewController() - navigationController?.pushViewController(viewController, animated: true) - } - } -extension MyPageViewController: UITableViewDelegate, UITableViewDataSource { - - // 테이블 뷰의 섹션 개수 설정 - func numberOfSections(in tableView: UITableView) -> Int { - return myPageSections.count - } - - // 테이블 뷰의 행 개수 설정 - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return myPageSections[section].items.count - } - - // 테이블 뷰 셀 구성 - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyPageItemCell") - as? MyPageItemCell else { return UITableViewCell() } - let item = myPageSections[indexPath.section].items[indexPath.row] - cell.titleLabel.text = item.title - cell.descriptionLabel.text = item.description - return cell - } - - // 셀의 높이 설정 - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 65 - } - - // 섹션 헤더의 View 설정 - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let headerView = UIView() - headerView.backgroundColor = .systemGroupedBackground - return headerView - } - - // 섹션 헤더의 높이 설정 - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 10 - } - - // 테이블 뷰 셀이 선택되었을 때 실행되는 메서드 - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let item = myPageSections[indexPath.section].items[indexPath.row] - guard let viewController = item.viewController else { return } +extension MyPageViewController: MyPageViewDelegate { + + func pushViewController(_ indexPath: IndexPath) { + if indexPath.section == 0 { + navigationController?.pushViewController(ThemeViewController(), animated: true) + } else { + switch indexPath.row { + case 0: print("이용 가이드 탭 !") + case 1: print("앱 피드백 탭 !") + case 2: print("개발자 정보 탭 !") + default: break; + } + } + } + + func pushViewController(_ viewController: UIViewController) { navigationController?.pushViewController(viewController, animated: true) } diff --git a/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift b/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift index 9d005e0..bd15e7f 100644 --- a/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift +++ b/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift @@ -5,12 +5,18 @@ // Created by jiyeon on 1/3/24. // +import Combine import UIKit import SnapKit class ThemeView: BaseView { + // MARK: - Properties + + private var viewModel: ThemeViewModel? + private var cancellables = Set() + // MARK: - UI let tableView = UITableView().then { @@ -19,8 +25,24 @@ class ThemeView: BaseView { $0.sectionHeaderTopPadding = 0 } + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + setupProperties() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - BaseViewProtocol + private func setupProperties() { + tableView.delegate = self + tableView.dataSource = self + } + override func configureUI() { addSubview(tableView) @@ -29,4 +51,49 @@ class ThemeView: BaseView { } } + // MARK: - Bind ViewModel + + func bineViewModel(_ viewModel: ThemeViewModel) { + self.viewModel = viewModel + viewModel.$selectedIndex + .receive(on: RunLoop.main) + .sink { [weak self] selectedIndex in + guard let window = self?.window else { return } + UserDefaultsManager.theme.value = Theme.allCases[selectedIndex] + window.overrideUserInterfaceStyle = UserDefaultsManager.theme.value.toUserInterfaceStyle() + self?.tableView.reloadData() + }.store(in: &cancellables) + } + +} + +extension ThemeView: UITableViewDelegate, UITableViewDataSource { + + // 테이블 뷰의 행 개수 설정 + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return Theme.allCases.count + } + + // 테이블 뷰 셀 구성 + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let viewModel = viewModel, + let cell = tableView.dequeueReusableCell(withIdentifier: "ThemeCell") + as? ThemeCell else { return UITableViewCell() } + let theme = Theme.allCases[indexPath.row] + cell.bind(theme) + cell.setupSelectButton(viewModel.selectedIndex == indexPath.row) + return cell + } + + // 셀의 높이 설정 + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 55 + } + + // 테이블 뷰 셀이 선택되었을 때 실행되는 메서드 + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let viewModel = viewModel else { return } + viewModel.selectedIndex = indexPath.row + } + } diff --git a/iBox/Sources/Presenter/MyPage/Theme/ThemeViewController.swift b/iBox/Sources/Presenter/MyPage/Theme/ThemeViewController.swift index 5bacd40..ca443fc 100644 --- a/iBox/Sources/Presenter/MyPage/Theme/ThemeViewController.swift +++ b/iBox/Sources/Presenter/MyPage/Theme/ThemeViewController.swift @@ -11,7 +11,7 @@ class ThemeViewController: BaseNavigationBarViewController { // MARK: - properties - var selected = UserDefaultsManager.theme.value + private let viewModel = ThemeViewModel() // MARK: - life cycle @@ -20,8 +20,7 @@ class ThemeViewController: BaseNavigationBarViewController { setupNavigationBar() guard let contentView = contentView as? ThemeView else { return } - contentView.tableView.delegate = self - contentView.tableView.dataSource = self + contentView.bineViewModel(viewModel) } // MARK: - BaseNavigationBarViewControllerProtocol @@ -33,36 +32,3 @@ class ThemeViewController: BaseNavigationBarViewController { } } - -extension ThemeViewController: UITableViewDelegate, UITableViewDataSource { - - // 테이블 뷰의 행 개수 설정 - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return Theme.allCases.count - } - - // 테이블 뷰 셀 구성 - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "ThemeCell") - as? ThemeCell else { return UITableViewCell() } - let theme = Theme.allCases[indexPath.row] - cell.bind(theme) - cell.setupSelectButton(theme == selected) - return cell - } - - // 셀의 높이 설정 - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 55 - } - - // 테이블 뷰 셀이 선택되었을 때 실행되는 메서드 - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - selected = Theme.allCases[indexPath.row] - guard let window = self.view.window else { return } - window.overrideUserInterfaceStyle = selected.toUserInterfaceStyle() - tableView.reloadData() // 다시 그리기 - UserDefaultsManager.theme.value = selected - } - -} diff --git a/iBox/Sources/ViewModel/MyPageCellViewModel.swift b/iBox/Sources/ViewModel/MyPageCellViewModel.swift new file mode 100644 index 0000000..f5c4c81 --- /dev/null +++ b/iBox/Sources/ViewModel/MyPageCellViewModel.swift @@ -0,0 +1,26 @@ +// +// MyPageCellViewModel.swift +// iBox +// +// Created by jiyeon on 2/22/24. +// + +import Foundation + +class MyPageCellViewModel { + + let model: MyPageItem + + init(_ model: MyPageItem) { + self.model = model + } + + var title: String { + model.title + } + + var description: String? { + model.description + } + +} diff --git a/iBox/Sources/ViewModel/MyPageSectionViewModel.swift b/iBox/Sources/ViewModel/MyPageSectionViewModel.swift new file mode 100644 index 0000000..550d976 --- /dev/null +++ b/iBox/Sources/ViewModel/MyPageSectionViewModel.swift @@ -0,0 +1,19 @@ +// +// MyPageSectionViewModel.swift +// iBox +// +// Created by jiyeon on 2/22/24. +// + +import Foundation + +class MyPageSectionViewModel { + + + let model: MyPageSection + + init(_ model: MyPageSection) { + self.model = model + } + +} diff --git a/iBox/Sources/ViewModel/MyPageViewModel.swift b/iBox/Sources/ViewModel/MyPageViewModel.swift new file mode 100644 index 0000000..75ce095 --- /dev/null +++ b/iBox/Sources/ViewModel/MyPageViewModel.swift @@ -0,0 +1,27 @@ +// +// MyPageViewModel.swift +// iBox +// +// Created by jiyeon on 2/22/24. +// + +import Foundation + +class MyPageViewModel { + + let myPageSectionViewModels: [MyPageSectionViewModel] = [ + MyPageSectionViewModel(MyPageSection( + title: "settings", + items: [MyPageItem(title: "테마")] + )), + MyPageSectionViewModel(MyPageSection( + title: "help", + items: [ + MyPageItem(title: "이용 가이드"), + MyPageItem(title: "앱 피드백"), + MyPageItem(title: "개발자 정보") + ] + )) + ] + +} diff --git a/iBox/Sources/ViewModel/ThemeViewModel.swift b/iBox/Sources/ViewModel/ThemeViewModel.swift new file mode 100644 index 0000000..a89e6ea --- /dev/null +++ b/iBox/Sources/ViewModel/ThemeViewModel.swift @@ -0,0 +1,23 @@ +// +// ThemeViewModel.swift +// iBox +// +// Created by jiyeon on 2/22/24. +// + +import Combine +import Foundation + +class ThemeViewModel { + + @Published var selectedIndex: Int + + init() { + switch UserDefaultsManager.theme.value { + case .light: selectedIndex = 0 + case .dark: selectedIndex = 1 + case .system: selectedIndex = 2 + } + } + +} From f19907d77c989a8d05a37a0cb9eeb7be43b12074 Mon Sep 17 00:00:00 2001 From: noeyiz Date: Thu, 22 Feb 2024 15:13:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20Model=EC=97=90=EC=84=9C=20UI=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=A7=80=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iBox/Sources/Model/MyPageItem.swift | 3 +-- iBox/Sources/Model/Theme.swift | 8 ++++---- iBox/Sources/Presenter/MyPage/Theme/ThemeCell.swift | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/iBox/Sources/Model/MyPageItem.swift b/iBox/Sources/Model/MyPageItem.swift index 881cc26..9c695c6 100644 --- a/iBox/Sources/Model/MyPageItem.swift +++ b/iBox/Sources/Model/MyPageItem.swift @@ -5,7 +5,7 @@ // Created by jiyeon on 1/3/24. // -import UIKit +import Foundation struct MyPageSection { var title: String @@ -15,5 +15,4 @@ struct MyPageSection { struct MyPageItem { var title: String var description: String? - var viewController: UIViewController? } diff --git a/iBox/Sources/Model/Theme.swift b/iBox/Sources/Model/Theme.swift index 413e934..b3eb176 100644 --- a/iBox/Sources/Model/Theme.swift +++ b/iBox/Sources/Model/Theme.swift @@ -20,11 +20,11 @@ enum Theme: Codable, CaseIterable { } } - func toImage() -> UIImage? { + func toImageString() -> String { switch self { - case .light: UIImage(systemName: "circle") - case .dark: UIImage(systemName: "circle.fill") - case .system: UIImage(systemName: "circle.righthalf.filled") + case .light: "circle" + case .dark: "circle.fill" + case .system: "circle.righthalf.filled" } } diff --git a/iBox/Sources/Presenter/MyPage/Theme/ThemeCell.swift b/iBox/Sources/Presenter/MyPage/Theme/ThemeCell.swift index 8a2cbdb..8d1997a 100644 --- a/iBox/Sources/Presenter/MyPage/Theme/ThemeCell.swift +++ b/iBox/Sources/Presenter/MyPage/Theme/ThemeCell.swift @@ -64,7 +64,7 @@ class ThemeCell: UITableViewCell, BaseViewProtocol { func bind(_ theme: Theme) { titleLabel.text = theme.toString() - themeImageView.image = theme.toImage() + themeImageView.image = UIImage(systemName: theme.toImageString()) } func setupSelectButton(_ selected: Bool) { From 216fa69cfae7d817e717362d98b3eeb090b4554c Mon Sep 17 00:00:00 2001 From: noeyiz Date: Thu, 22 Feb 2024 15:26:14 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20Model=EC=97=90=EC=84=9C=20UI=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=A7=80=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iBox/Sources/Extension/UIView+Extension.swift | 12 ++++++++++++ iBox/Sources/Model/Theme.swift | 8 -------- iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift | 2 +- iBox/Sources/SceneDelegate.swift | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/iBox/Sources/Extension/UIView+Extension.swift b/iBox/Sources/Extension/UIView+Extension.swift index 5841271..77e401b 100644 --- a/iBox/Sources/Extension/UIView+Extension.swift +++ b/iBox/Sources/Extension/UIView+Extension.swift @@ -19,3 +19,15 @@ extension Then where Self: AnyObject { } extension UIView: Then {} + +extension UIView { + + func toUserInterfaceStyle(_ theme: Theme) -> UIUserInterfaceStyle { + switch theme { + case .light: return UIUserInterfaceStyle.light + case .dark: return UIUserInterfaceStyle.dark + case .system: return UIUserInterfaceStyle.unspecified + } + } + +} diff --git a/iBox/Sources/Model/Theme.swift b/iBox/Sources/Model/Theme.swift index b3eb176..aceb433 100644 --- a/iBox/Sources/Model/Theme.swift +++ b/iBox/Sources/Model/Theme.swift @@ -27,12 +27,4 @@ enum Theme: Codable, CaseIterable { case .system: "circle.righthalf.filled" } } - - func toUserInterfaceStyle() -> UIUserInterfaceStyle { - switch self { - case .light: UIUserInterfaceStyle.light - case .dark: UIUserInterfaceStyle.dark - case .system: UIUserInterfaceStyle.unspecified - } - } } diff --git a/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift b/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift index bd15e7f..0d42e03 100644 --- a/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift +++ b/iBox/Sources/Presenter/MyPage/Theme/ThemeView.swift @@ -60,7 +60,7 @@ class ThemeView: BaseView { .sink { [weak self] selectedIndex in guard let window = self?.window else { return } UserDefaultsManager.theme.value = Theme.allCases[selectedIndex] - window.overrideUserInterfaceStyle = UserDefaultsManager.theme.value.toUserInterfaceStyle() + window.overrideUserInterfaceStyle = self?.toUserInterfaceStyle(UserDefaultsManager.theme.value) ?? .unspecified self?.tableView.reloadData() }.store(in: &cancellables) } diff --git a/iBox/Sources/SceneDelegate.swift b/iBox/Sources/SceneDelegate.swift index 1b368ff..94e635e 100644 --- a/iBox/Sources/SceneDelegate.swift +++ b/iBox/Sources/SceneDelegate.swift @@ -18,7 +18,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.windowScene = windowScene // 앱 테마 정보 - window?.overrideUserInterfaceStyle = UserDefaultsManager.theme.value.toUserInterfaceStyle() + window?.overrideUserInterfaceStyle = window?.toUserInterfaceStyle(UserDefaultsManager.theme.value) ?? .unspecified // 나중에 userDefaults에 저장해두고 꺼내와서 preload하기 let urlsToPreload = [