From d92e60de5dfd3c282f2dfc828b5189e0cd4f28dc Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:37:19 +0900 Subject: [PATCH 01/16] =?UTF-8?q?[Chore]=20#87=20-=20=EB=B0=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Wable-iOS/Global/Literals/ImageLiterals.swift | 1 + .../Toast/toast_ban.imageset/Contents.json | 23 ++++++++++++++++++ .../Toast/toast_ban.imageset/toast.png | Bin 0 -> 7082 bytes .../Toast/toast_ban.imageset/toast@2x.png | Bin 0 -> 15082 bytes .../Toast/toast_ban.imageset/toast@3x.png | Bin 0 -> 25378 bytes 5 files changed, 24 insertions(+) create mode 100644 Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/Contents.json create mode 100644 Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast.png create mode 100644 Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast@2x.png create mode 100644 Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast@3x.png diff --git a/Wable-iOS/Global/Literals/ImageLiterals.swift b/Wable-iOS/Global/Literals/ImageLiterals.swift index ec28be8..6bb03e9 100644 --- a/Wable-iOS/Global/Literals/ImageLiterals.swift +++ b/Wable-iOS/Global/Literals/ImageLiterals.swift @@ -96,6 +96,7 @@ enum ImageLiterals { static var toastSuccess: UIImage { .load(name: "toast_success") } static var toastWarning: UIImage { .load(name: "toast_warning") } static var toastGhost: UIImage { .load(name: "toast_ghost") } + static var toastBan: UIImage { .load(name: "toast_ban") } static var toastReport: UIImage { .load(name: "toast_report") } static var toastAgreementLoading: UIImage { .load(name: "toast_agreement_loading") } } diff --git a/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/Contents.json b/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/Contents.json new file mode 100644 index 0000000..e31b951 --- /dev/null +++ b/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toast.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toast@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toast@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast.png b/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast.png new file mode 100644 index 0000000000000000000000000000000000000000..ad28bd23607888898d0793e7dd5c9b55d4ff92ee GIT binary patch literal 7082 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 literal 0 HcmV?d00001 diff --git a/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast@2x.png b/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ed379b2962b40356afee91044333b4435f0a1b62 GIT binary patch literal 15082 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 XI0^z~Wti|sQxRk&6vZn<4TAn3T<-WU literal 0 HcmV?d00001 diff --git a/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast@3x.png b/Wable-iOS/Global/Resources/Assets.xcassets/Toast/toast_ban.imageset/toast@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ddab0241370b3f76826661f0ceadd0ad3249951c GIT binary patch literal 25378 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 MXc=i%s@sMCAM-M>W&i*H literal 0 HcmV?d00001 From 0b48041820f9ef4470485e471a1de9dbf14f4015 Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:49:04 +0900 Subject: [PATCH 02/16] =?UTF-8?q?[Chore]=20#87=20-=20TabBar=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/TabBar/WableTabBarController.swift | 2 +- Wable-iOS/Presentation/TabBar/WableTabBarItem.swift | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Wable-iOS/Presentation/TabBar/WableTabBarController.swift b/Wable-iOS/Presentation/TabBar/WableTabBarController.swift index a831e3b..521a115 100644 --- a/Wable-iOS/Presentation/TabBar/WableTabBarController.swift +++ b/Wable-iOS/Presentation/TabBar/WableTabBarController.swift @@ -129,7 +129,7 @@ extension WableTabBarController: UITabBarControllerDelegate { case 0: if let navController = viewController as? UINavigationController, - let homeVC = navController.viewControllers.first as? HomeViewController { + let homeVC = navController.viewControllers.first as? MigratedHomeViewController { homeVC.scrollToTop() if previousTabIndex == 3 { homeVC.showLoadView() diff --git a/Wable-iOS/Presentation/TabBar/WableTabBarItem.swift b/Wable-iOS/Presentation/TabBar/WableTabBarItem.swift index a635faf..e0f2db3 100644 --- a/Wable-iOS/Presentation/TabBar/WableTabBarItem.swift +++ b/Wable-iOS/Presentation/TabBar/WableTabBarItem.swift @@ -43,10 +43,14 @@ enum WableTabBarItem: CaseIterable { var targetViewController: UIViewController? { switch self { - case .home: return HomeViewController(viewModel: HomeViewModel(networkProvider: NetworkService()), likeViewModel: LikeViewModel(networkProvider: NetworkService())) - case .info: return InfoViewController() - case .noti: return NotificationViewController() - case .my: return MyPageViewController(viewModel: MyPageViewModel(networkProvider: NetworkService()), likeViewModel: LikeViewModel(networkProvider: NetworkService())) + case .home: + return MigratedHomeViewController(viewModel: MigratedHomeViewModel()) + case .info: + return InfoViewController() + case .noti: + return NotificationViewController() + case .my: + return MyPageViewController(viewModel: MyPageViewModel(networkProvider: NetworkService()), likeViewModel: LikeViewModel(networkProvider: NetworkService())) } } } From b6dee4b01a43342feecd0830a5c805e336f02272 Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:54:12 +0900 Subject: [PATCH 03/16] =?UTF-8?q?[Fix]=20#87=20-=20Popup=EC=9D=84=20VC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95,=20PopupType=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomePopupViewController.swift | 131 ++++++++++++++++++ .../UIComponents/WablePopupView.swift | 67 +++++++++ 2 files changed, 198 insertions(+) create mode 100644 Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift diff --git a/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift b/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift new file mode 100644 index 0000000..df5446e --- /dev/null +++ b/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift @@ -0,0 +1,131 @@ +// +// HomePopupViewController.swift +// Wable-iOS +// +// Created by 박윤빈 on 1/12/25. +// + +import Combine +import CombineCocoa +import UIKit + +import SnapKit + +final class HomePopupViewController: UIViewController { + + // MARK: - Properties + + private let viewModel: PopupViewModel + private let rootView: WablePopupView + private let cancelBag = CancelBag() + + private let deleteButtonTapSubject = PassthroughSubject() + private let reportButtonDidTapSubject = PassthroughSubject() + private let banButtonDidTapSubject = PassthroughSubject() + private let ghostButtonDidTapSubject = PassthroughSubject() + + var deleteButtonDidTapAction: ((Int) -> Void)? + var ghostButtonDidTapAction: ((Int) -> Void)? + var banButtonDidTapAction: ((Int) -> Void)? + var reportButtonDidTapAction: (() -> Void)? + + // MARK: - Initializer + + init(viewModel: PopupViewModel, popupType: PopupViewType) { + self.viewModel = viewModel + self.rootView = WablePopupView(popupType: popupType) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life Cycles + + override func viewDidLoad() { + super.viewDidLoad() + view = rootView + setupBinding() + rootView.delegate = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + +} + +private extension HomePopupViewController { + func setupBinding() { + let input = PopupViewModel.Input( + deleteButtonDidTapSubject: deleteButtonTapSubject.eraseToAnyPublisher(), + reportButtonDidTapSubject: reportButtonDidTapSubject.eraseToAnyPublisher(), + banButtonDidTapSubject: banButtonDidTapSubject.eraseToAnyPublisher(), + ghostButtonDidTapSubject: ghostButtonDidTapSubject.eraseToAnyPublisher() + ) + + let output = viewModel.transform(from: input, cancelBag: cancelBag) + + output.dismissView + .receive(on: RunLoop.main) + .sink { [weak self] data, type in + guard let self else { return } + dismiss(animated: true) + switch type { + case .delete: + deleteButtonDidTap() + case .report: + reportButtonDidTap() + case .ghost: + ghostButtonDidTap() + case .ban: + banButtonDidTap() + } + } + .store(in: cancelBag) + } +} + +extension HomePopupViewController { + + func deleteButtonDidTap() { + deleteButtonDidTapAction?(viewModel.data.contentID ?? -1) + } + + func reportButtonDidTap() { + reportButtonDidTapAction?() + } + + func ghostButtonDidTap() { + ghostButtonDidTapAction?(viewModel.data.memberID) + } + + func banButtonDidTap() { + banButtonDidTapAction?(viewModel.data.memberID) + } +} + +extension HomePopupViewController: WablePopupDelegate { + func cancleButtonTapped() { + self.dismiss(animated: true) + } + + func confirmButtonTapped() { + switch rootView.popupType { + case .delete: + deleteButtonTapSubject.send(()) + case .report: + reportButtonDidTapSubject.send(()) + case .ghost: + ghostButtonDidTapSubject.send(()) + case .ban: + banButtonDidTapSubject.send(()) + } + } + + func singleButtonTapped() { + self.dismiss(animated: true) + } + +} diff --git a/Wable-iOS/Presentation/UIComponents/WablePopupView.swift b/Wable-iOS/Presentation/UIComponents/WablePopupView.swift index 9d993d8..8df7fd2 100644 --- a/Wable-iOS/Presentation/UIComponents/WablePopupView.swift +++ b/Wable-iOS/Presentation/UIComponents/WablePopupView.swift @@ -9,6 +9,13 @@ import UIKit import SnapKit +enum PopupViewType { + case delete + case report + case ghost + case ban +} + protocol WablePopupDelegate: AnyObject { func cancleButtonTapped() func confirmButtonTapped() @@ -20,6 +27,8 @@ final class WablePopupView: UIView { // MARK: - Properties weak var delegate: WablePopupDelegate? + var cancelBag: CancelBag? + var popupType: PopupViewType // MARK: - UI Components @@ -85,6 +94,8 @@ final class WablePopupView: UIView { // MARK: - Life Cycles init(popupTitle: String, popupContent: String, leftButtonTitle: String, rightButtonTitle: String) { + self.popupType = .ban + super.init(frame: .zero) popupTitleLabel.setTextWithLineHeight(text: popupTitle, lineHeight: 28.8.adjusted, alignment: .center) @@ -99,6 +110,8 @@ final class WablePopupView: UIView { } init(popupTitle: String, popupContent: String, singleButtonTitle: String) { + self.popupType = .ban + super.init(frame: .zero) popupTitleLabel.setTextWithLineHeight(text: popupTitle, lineHeight: 28.8.adjusted, alignment: .center) @@ -111,6 +124,17 @@ final class WablePopupView: UIView { setSingleAddTarget() } + init(popupType: PopupViewType) { + self.popupType = popupType + + super.init(frame: .zero) + setInitialPopup(type: popupType) + setUI() + setHierarchy() + setLayout() + setAddTarget() + } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -181,6 +205,49 @@ extension WablePopupView { self.confirmButton.addTarget(self, action: #selector(confirmButtonTapped), for: .touchUpInside) } + func setInitialPopup(type: PopupViewType) { + switch type { + case .delete: + popupTitleLabel.setTextWithLineHeight( + text: StringLiterals.Home.deletePopupTitle, + lineHeight: 28.8.adjusted, + alignment: .center + ) + popupContentLabel.text = StringLiterals.Home.deletePopupContent + cancleButton.setTitle(StringLiterals.Home.deletePopupUndo, for: .normal) + confirmButton.setTitle(StringLiterals.Home.deletePopupDo, for: .normal) + + case .report: + popupTitleLabel.setTextWithLineHeight( + text: StringLiterals.Home.reportPopupTitle, + lineHeight: 28.8.adjusted, + alignment: .center + ) + popupContentLabel.text = StringLiterals.Home.reportPopupContent + cancleButton.setTitle(StringLiterals.Home.reportPopupUndo, for: .normal) + confirmButton.setTitle(StringLiterals.Home.reportPopupDo, for: .normal) + + case .ghost: + popupTitleLabel.setTextWithLineHeight( + text: StringLiterals.Home.ghostPopupTitle, + lineHeight: 28.8.adjusted, + alignment: .center + ) + popupContentLabel.text = "" + cancleButton.setTitle(StringLiterals.Home.ghostPopupUndo, for: .normal) + confirmButton.setTitle(StringLiterals.Home.ghostPopupDo, for: .normal) + case .ban: + popupTitleLabel.setTextWithLineHeight( + text: "밴하기 ㅋㅋ", + lineHeight: 28.8.adjusted, + alignment: .center + ) + popupContentLabel.text = "너이놈밴머거랏!!!" + cancleButton.setTitle("함봐줌", for: .normal) + confirmButton.setTitle("밴ㄱㄱ", for: .normal) + } + } + func setSingleHierarchy() { self.addSubview(container) From 79175e684b4c9198bb3a7f45e97b2630cb33d7e8 Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:54:29 +0900 Subject: [PATCH 04/16] =?UTF-8?q?[Feat]=20#87=20-=20PopUpViewModel=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/ViewModels/PopupViewModel.swift | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift diff --git a/Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift b/Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift new file mode 100644 index 0000000..74cda8f --- /dev/null +++ b/Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift @@ -0,0 +1,119 @@ +// +// PopupViewModel.swift +// Wable-iOS +// +// Created by 박윤빈 on 1/11/25. +// + +import Foundation +import Combine + +final class PopupViewModel { + private let service: HomeAPI + let data: HomeFeedDTO + init(service: HomeAPI = HomeAPI.shared, data: HomeFeedDTO) { + self.service = service + self.data = data + } +} + +extension PopupViewModel: ViewModelType { + struct Input { + let deleteButtonDidTapSubject: AnyPublisher + let reportButtonDidTapSubject: AnyPublisher + let banButtonDidTapSubject: AnyPublisher + let ghostButtonDidTapSubject: AnyPublisher + } + + struct Output { + let dismissView: AnyPublisher<(HomeFeedDTO, PopupViewType), Never> + } + + func transform(from input: Input, cancelBag: CancelBag) -> Output { + let dismissViewSubject = PassthroughSubject<(HomeFeedDTO, PopupViewType), Never>() + input.deleteButtonDidTapSubject + .flatMap { [weak self] _ -> AnyPublisher in + guard let self else { + return Just(EmptyDTO()).eraseToAnyPublisher() + } + + return service.deleteFeed(contentID: data.contentID ?? -1) + .mapWableNetworkError() + .replaceError(with: nil) + .compactMap { $0 } + .eraseToAnyPublisher() + } + .sink { _ in + let popupViewType = PopupViewType.delete + dismissViewSubject.send((self.data, popupViewType)) + } + .store(in: cancelBag) + + input.banButtonDidTapSubject + .flatMap { [weak self] _ -> AnyPublisher in + guard let self else { + return Just(EmptyDTO()).eraseToAnyPublisher() + } + let memberID = data.memberID + let triggerID = data.contentID ?? -1 + return service.postBan( + memberID: memberID, + triggerType: "content", + triggerID: triggerID + ) + .mapWableNetworkError() + .replaceError(with: nil) + .compactMap { $0 } + .eraseToAnyPublisher() + } + .sink { _ in + let popupViewType = PopupViewType.ban + dismissViewSubject.send((self.data, popupViewType)) + } + .store(in: cancelBag) + + input.reportButtonDidTapSubject + .flatMap { [weak self] _ -> AnyPublisher in + guard let self else { + return Just(EmptyDTO()).eraseToAnyPublisher() + } + let nickname = data.memberNickname + let titleText = data.contentTitle ?? "" + return service.postReport(nickname: nickname, relateText: titleText) + .mapWableNetworkError() + .replaceError(with: nil) + .compactMap { $0 } + .eraseToAnyPublisher() + } + .sink { _ in + let popupViewType = PopupViewType.report + dismissViewSubject.send((self.data, popupViewType)) + } + .store(in: cancelBag) + + input.ghostButtonDidTapSubject + .flatMap { [weak self] _ -> AnyPublisher in + guard let self else { + return Just(EmptyDTO()).eraseToAnyPublisher() + } + let memberID = data.memberID + let triggerID = data.contentID ?? -1 + return service.postBeGhost( + triggerType: "contentGhost", + memberID: memberID, + triggerID: triggerID + ) + .mapWableNetworkError() + .replaceError(with: nil) + .compactMap { $0 } + .eraseToAnyPublisher() + } + .sink { _ in + let popupViewType = PopupViewType.ghost + dismissViewSubject.send((self.data, popupViewType)) + } + .store(in: cancelBag) + + return Output(dismissView: dismissViewSubject.eraseToAnyPublisher()) + } +} From 95d6b8146d581ba40b2f8e9e7fc1405913fcb1d8 Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:55:52 +0900 Subject: [PATCH 05/16] =?UTF-8?q?[Fix]=20#87=20-=20DiffableDataSource?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=A8=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20=EB=A7=81=ED=81=AC=20=ED=84=B0=EC=B9=98=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Views/Subviews/FeedContentView.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Wable-iOS/Presentation/Home/Views/Subviews/FeedContentView.swift b/Wable-iOS/Presentation/Home/Views/Subviews/FeedContentView.swift index 1a90ccc..1ea6b76 100644 --- a/Wable-iOS/Presentation/Home/Views/Subviews/FeedContentView.swift +++ b/Wable-iOS/Presentation/Home/Views/Subviews/FeedContentView.swift @@ -229,7 +229,6 @@ extension FeedContentView { } } - // 탭 제스처 처리 함수 @objc func handleTitleLabelTap(_ gesture: UITapGestureRecognizer) { guard let attributedText = titleLabel.attributedText else { return } @@ -251,11 +250,13 @@ extension FeedContentView { } } - // 하이퍼링크가 아닌 부분을 클릭한 경우에만 `didSelectRowAt` 호출 - if !isLinkTapped, let tableView = self.superview(of: UITableView.self), let cell = self.superview(of: UITableViewCell.self), let indexPath = tableView.indexPath(for: cell) { - tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath) + if !isLinkTapped, + let collectionView = self.superview(of: UICollectionView.self), + let cell = self.superview(of: UICollectionViewCell.self), + let indexPath = collectionView.indexPath(for: cell) { + + collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath) } - } @objc func handleContentLabelTap(_ gesture: UITapGestureRecognizer) { @@ -279,9 +280,12 @@ extension FeedContentView { } } - // 하이퍼링크가 아닌 부분을 클릭한 경우에만 `didSelectRowAt` 호출 - if !isLinkTapped, let tableView = self.superview(of: UITableView.self), let cell = self.superview(of: UITableViewCell.self), let indexPath = tableView.indexPath(for: cell) { - tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath) + if !isLinkTapped, + let collectionView = self.superview(of: UICollectionView.self), + let cell = self.superview(of: UICollectionViewCell.self), + let indexPath = collectionView.indexPath(for: cell) { + + collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath) } } } From 8b460d9935d82da5d129396e40ddb887e1fafe84 Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:57:21 +0900 Subject: [PATCH 06/16] =?UTF-8?q?[Feat]=20#87=20-=20HomeView,=20ViewContro?= =?UTF-8?q?ller=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Wable-iOS.xcodeproj/project.pbxproj | 24 + .../MigratedHomeViewController.swift | 512 ++++++++++++++++++ .../Views/Cells/MigratedHomeFeedCell.swift | 257 +++++++++ .../Home/Views/HomeBottomSheetView.swift | 1 + .../Home/Views/MigratedHomeView.swift | 91 ++++ 5 files changed, 885 insertions(+) create mode 100644 Wable-iOS/Presentation/Home/ViewController/MigratedHomeViewController.swift create mode 100644 Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift create mode 100644 Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift diff --git a/Wable-iOS.xcodeproj/project.pbxproj b/Wable-iOS.xcodeproj/project.pbxproj index 0af4916..a392223 100644 --- a/Wable-iOS.xcodeproj/project.pbxproj +++ b/Wable-iOS.xcodeproj/project.pbxproj @@ -103,6 +103,12 @@ 05B4F47D2CF8BE360033FF67 /* Array+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B4F47C2CF8BE360033FF67 /* Array+.swift */; }; 05B4F47F2CF8C2450033FF67 /* BanRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B4F47E2CF8C2450033FF67 /* BanRequestDTO.swift */; }; 05F1FF422D11C95F00982033 /* BanTargetInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F1FF412D11C95F00982033 /* BanTargetInfo.swift */; }; + 05F1FF442D17EA1D00982033 /* MigratedHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F1FF432D17EA1D00982033 /* MigratedHomeViewController.swift */; }; + 05F1FF462D1AAAE300982033 /* MigratedHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F1FF452D1AAAE300982033 /* MigratedHomeViewModel.swift */; }; + 05F1FF482D1AB39000982033 /* MigratedHomeFeedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F1FF472D1AB39000982033 /* MigratedHomeFeedCell.swift */; }; + 05F1FF4A2D1AB42A00982033 /* MigratedHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F1FF492D1AB42A00982033 /* MigratedHomeView.swift */; }; + 05F1FF4C2D32685A00982033 /* PopupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F1FF4B2D32685A00982033 /* PopupViewModel.swift */; }; + 05F1FF502D33DF9600982033 /* HomePopupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F1FF4F2D33DF9600982033 /* HomePopupViewController.swift */; }; 05FBEED22C886A0200E4BF17 /* HomeFeedContentDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05FBEED12C886A0200E4BF17 /* HomeFeedContentDTO.swift */; }; 3C3531822C6F15D30015A8FA /* KeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3531812C6F15D30015A8FA /* KeychainWrapper.swift */; }; 3C3531842C6F16D00015A8FA /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3531832C6F16D00015A8FA /* Config.swift */; }; @@ -317,6 +323,12 @@ 05B4F47C2CF8BE360033FF67 /* Array+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+.swift"; sourceTree = ""; }; 05B4F47E2CF8C2450033FF67 /* BanRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanRequestDTO.swift; sourceTree = ""; }; 05F1FF412D11C95F00982033 /* BanTargetInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanTargetInfo.swift; sourceTree = ""; }; + 05F1FF432D17EA1D00982033 /* MigratedHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedHomeViewController.swift; sourceTree = ""; }; + 05F1FF452D1AAAE300982033 /* MigratedHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedHomeViewModel.swift; sourceTree = ""; }; + 05F1FF472D1AB39000982033 /* MigratedHomeFeedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedHomeFeedCell.swift; sourceTree = ""; }; + 05F1FF492D1AB42A00982033 /* MigratedHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedHomeView.swift; sourceTree = ""; }; + 05F1FF4B2D32685A00982033 /* PopupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupViewModel.swift; sourceTree = ""; }; + 05F1FF4F2D33DF9600982033 /* HomePopupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePopupViewController.swift; sourceTree = ""; }; 05FBEED12C886A0200E4BF17 /* HomeFeedContentDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedContentDTO.swift; sourceTree = ""; }; 3C3531812C6F15D30015A8FA /* KeychainWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainWrapper.swift; sourceTree = ""; }; 3C3531832C6F16D00015A8FA /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; @@ -489,6 +501,7 @@ 050E0CB42C74B85800326EEA /* HomeView.swift */, 050E0CB62C74B87400326EEA /* FeedDetailView.swift */, 3C8B4E522C83FB7C00174943 /* HomeBottomSheetView.swift */, + 05F1FF492D1AB42A00982033 /* MigratedHomeView.swift */, ); path = Views; sourceTree = ""; @@ -499,6 +512,8 @@ 050E0CC82C74B92A00326EEA /* HomeViewModel.swift */, 3C8B4E472C80E1C900174943 /* FeedDetailViewModel.swift */, 3C8B4E4E2C83F01500174943 /* LikeViewModel.swift */, + 05F1FF452D1AAAE300982033 /* MigratedHomeViewModel.swift */, + 05F1FF4B2D32685A00982033 /* PopupViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -516,6 +531,7 @@ children = ( 050E0CC42C74B8F000326EEA /* HomeFeedTableViewCell.swift */, 050E0CC62C74B8FD00326EEA /* FeedDetailTableViewCell.swift */, + 05F1FF472D1AB39000982033 /* MigratedHomeFeedCell.swift */, ); path = Cells; sourceTree = ""; @@ -862,6 +878,8 @@ children = ( 0547F4F42C64C76A001E3039 /* HomeViewController.swift */, 050E0CCA2C74B94800326EEA /* FeedDetailViewController.swift */, + 05F1FF432D17EA1D00982033 /* MigratedHomeViewController.swift */, + 05F1FF4F2D33DF9600982033 /* HomePopupViewController.swift */, ); path = ViewController; sourceTree = ""; @@ -1691,6 +1709,7 @@ 3CF344DE2C74AB3B0038BB53 /* MyPageAccountInfoViewModel.swift in Sources */, 050E0CF12C750F6300326EEA /* NotificationContentView.swift in Sources */, 3C8B4E1B2C78E55200174943 /* TokenManager.swift in Sources */, + 05F1FF4C2D32685A00982033 /* PopupViewModel.swift in Sources */, 3CDE2E1D2C723325004A84CB /* NotificationSegmentedControl.swift in Sources */, 050E0CB52C74B85800326EEA /* HomeView.swift in Sources */, DE9D0E342CF087B30024BB1F /* LCKGameTypeDTO.swift in Sources */, @@ -1702,6 +1721,7 @@ 050E0D162C8252B200326EEA /* NotificationAPI.swift in Sources */, 050E0CDF2C74C8B400326EEA /* MatchSessionTableViewCell.swift in Sources */, 3C8B4E3B2C79119700174943 /* MyPageMemberContentResponseDTO.swift in Sources */, + 05F1FF442D17EA1D00982033 /* MigratedHomeViewController.swift in Sources */, 3C8B4E352C79117C00174943 /* MyPageMemberDeleteDTO.swift in Sources */, 05FBEED22C886A0200E4BF17 /* HomeFeedContentDTO.swift in Sources */, 0547F4C02C62166C001E3039 /* Adjusted+.swift in Sources */, @@ -1744,6 +1764,7 @@ 0547F4FC2C64C797001E3039 /* MyPageViewController.swift in Sources */, 0547F4FA2C64C78C001E3039 /* NotificationViewController.swift in Sources */, 3CF344E72C750DBD0038BB53 /* MyPageSignOutReasonViewModel.swift in Sources */, + 05F1FF502D33DF9600982033 /* HomePopupViewController.swift in Sources */, 3C8B4E1D2C78E55C00174943 /* TokenReissueResponseDTO.swift in Sources */, 3C8B4E372C79118500174943 /* MyPageAccountInfoResponseDTO.swift in Sources */, 050E0CCD2C74B95B00326EEA /* Team.swift in Sources */, @@ -1773,10 +1794,12 @@ 050E0CEA2C74DC4B00326EEA /* MatchProgress.swift in Sources */, 3C8B4E262C78E61B00174943 /* HttpMethod.swift in Sources */, DE8001B32CF323B100D9DAD9 /* InfoNewsViewController.swift in Sources */, + 05F1FF4A2D1AB42A00982033 /* MigratedHomeView.swift in Sources */, 05B4F47D2CF8BE360033FF67 /* Array+.swift in Sources */, 05B4F47F2CF8C2450033FF67 /* BanRequestDTO.swift in Sources */, DE8001B62CF3250800D9DAD9 /* NewsCell.swift in Sources */, DE8001A82CF31ECE00D9DAD9 /* InfoNewsViewModel.swift in Sources */, + 05F1FF462D1AAAE300982033 /* MigratedHomeViewModel.swift in Sources */, 050E0CD12C74B9A700326EEA /* WriteViewController.swift in Sources */, 050E0CAA2C74B7A900326EEA /* FeedDetailReplyDTO.swift in Sources */, 3C35319C2C6F22050015A8FA /* ViewModelType.swift in Sources */, @@ -1789,6 +1812,7 @@ DEAD9CBD2CF2618F00D5CD11 /* SessionCell.swift in Sources */, 050E0CA42C74B35500326EEA /* UIApplication+.swift in Sources */, 050E0CB72C74B87400326EEA /* FeedDetailView.swift in Sources */, + 05F1FF482D1AB39000982033 /* MigratedHomeFeedCell.swift in Sources */, DEFF2F7E2D13052500DC1A16 /* AnyPublisher+.swift in Sources */, 050E0CF92C7516C900326EEA /* InfoNotificationDTO.swift in Sources */, 3C8B4E442C80886800174943 /* WriteContentRequestDTO.swift in Sources */, diff --git a/Wable-iOS/Presentation/Home/ViewController/MigratedHomeViewController.swift b/Wable-iOS/Presentation/Home/ViewController/MigratedHomeViewController.swift new file mode 100644 index 0000000..2efc620 --- /dev/null +++ b/Wable-iOS/Presentation/Home/ViewController/MigratedHomeViewController.swift @@ -0,0 +1,512 @@ +// +// MigratedHomeViewController.swift +// Wable-iOS +// +// Created by 박윤빈 on 12/22/24. +// + +import UIKit +import Combine + +import CombineCocoa + +final class MigratedHomeViewController: UIViewController { + + typealias Item = HomeFeedDTO + typealias DataSource = UICollectionViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + + enum Section: CaseIterable { + case feed + } + + // MARK: - Properties + + private var dataSource: DataSource? + + private let viewModel: MigratedHomeViewModel + + private let viewDidLoadSubject = PassthroughSubject() + private let collectionViewDidRefreshSubject = PassthroughSubject() + private let collectionViewDidSelectedSubject = PassthroughSubject() + private let collectionViewDidEndDragSubject = PassthroughSubject() + private let profileImageTapSubject = PassthroughSubject() + private let menuButtonTapSubject = PassthroughSubject() + private let feedImageTapSubject = PassthroughSubject() + private let heartButtonTapSubject = PassthroughSubject() + private let commentButtonTapSubject = PassthroughSubject() + + private let feedDeleteButtonDidTap = PassthroughSubject() + private let feedGhostButtonDidTap = PassthroughSubject() + private let feedBanButtonDidTap = PassthroughSubject() + + private let cancelBag = CancelBag() + private let rootView = MigratedHomeView() + private var photoDetailView: WablePhotoDetailView? + private let homeBottomsheetView = HomeBottomSheetView() + private let reportToastImageView = UIImageView(image: ImageLiterals.Toast.toastReport) + + // MARK: - Initializer + + init(viewModel: MigratedHomeViewModel) { + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life Cycle + + override func loadView() { + view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupCollectionView() + setupDataSource() + setupAction() + setupBinding() + popupEventBinding() + showLoadView() + + viewDidLoadSubject.send(()) + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.navigationBar.isHidden = true + } +} + +// MARK: - UICollectionViewDelegate + +extension MigratedHomeViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionViewDidSelectedSubject.send(indexPath.item) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + guard scrollView == rootView.collectionView, + (scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height + else { + return + } + + collectionViewDidEndDragSubject.send(()) + } +} + +// MARK: - Private Method + +private extension MigratedHomeViewController { + func setupCollectionView() { + rootView.collectionView.setCollectionViewLayout(collectionViewLayout, animated: false) + + rootView.collectionView.delegate = self + } + + func setupDataSource() { + let homeFeedCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in + cell.bind(data: item) + cell.onMenuButtonTap = { [weak self] in + self?.menuButtonTapSubject.send(indexPath.item) + } + + cell.onProfileImageTap = { [weak self] in + self?.profileImageTapSubject.send(indexPath.item) + } + + cell.onFeedImageTap = { [weak self] in + self?.feedImageTapSubject.send(indexPath.item) + } + + cell.onHeartButtonTap = { [weak self] in + self?.heartButtonTapSubject.send(indexPath.item) + } + + cell.onCommentButtonTap = { [weak self] in + self?.commentButtonTapSubject.send(indexPath.item) + } + + cell.onGhostButtonTap = { [weak self] in + self?.presentPopup(popupType: .ghost, data: item) + } + } + + dataSource = DataSource(collectionView: rootView.collectionView) { collectionView, indexPath, item in + collectionView.dequeueConfiguredReusableCell( + using: homeFeedCellRegistration, + for: indexPath, + item: item + ) + } + } + + func applySnapshot(items: [Item], to section: Section) { + var snapshot = Snapshot() + snapshot.appendSections([.feed]) + snapshot.appendItems(items, toSection: section) + dataSource?.apply(snapshot, animatingDifferences: false) + } + + func setupAction() { + let refreshAction = UIAction { [weak self] _ in + self?.collectionViewDidRefreshSubject.send(()) + } + + rootView.collectionView.refreshControl?.addAction(refreshAction, for: .valueChanged) + + rootView.writeFeedButton.tapPublisher + .sink { [weak self] _ in + let writeViewController = WriteViewController(viewModel: WriteViewModel(networkProvider: NetworkService())) + writeViewController.hidesBottomBarWhenPushed = true + writeViewController.writeViewDidDisappear = { [weak self] in + self?.viewDidLoadSubject.send(()) + } + self?.navigationController?.pushViewController(writeViewController, animated: true) + } + .store(in: cancelBag) + } + + func setupBinding() { + let input = MigratedHomeViewModel.Input( + viewDidLoad: viewDidLoadSubject.eraseToAnyPublisher(), + collectionViewDidRefresh: collectionViewDidRefreshSubject.eraseToAnyPublisher(), + collectionViewDidSelect: collectionViewDidSelectedSubject.eraseToAnyPublisher(), + collectionViewDidEndDrag: collectionViewDidEndDragSubject.eraseToAnyPublisher(), + menuButtonDidTap: menuButtonTapSubject.eraseToAnyPublisher(), + profileImageDidTap: profileImageTapSubject.eraseToAnyPublisher(), + feedImageURL: feedImageTapSubject.eraseToAnyPublisher(), + heartButtonDidTap: heartButtonTapSubject.eraseToAnyPublisher(), + commentButtonDidTap: commentButtonTapSubject.eraseToAnyPublisher() + ) + + let output = viewModel.transform(from: input, cancelBag: cancelBag) + + output.feedData + .receive(on: RunLoop.main) + .handleEvents(receiveOutput: { [weak self] _ in + self?.endRefreshing() + }) + .removeDuplicates() + .sink { [weak self] feed in + self?.applySnapshot(items: feed, to: .feed) + } + .store(in: cancelBag) + + output.profileImageTapped + .receive(on: RunLoop.main) + .sink { [weak self] memberID in + if memberID == loadUserData()?.memberId { + self?.tabBarController?.selectedIndex = 3 + } else { + let viewController = MyPageViewController( + viewModel: MyPageViewModel(networkProvider: NetworkService()), + likeViewModel: LikeViewModel(networkProvider: NetworkService()) + ) + viewController.memberId = memberID + self?.navigationController?.pushViewController(viewController, animated: true) + } + } + .store(in: cancelBag) + + output.feedImageTapped + .receive(on: RunLoop.main) + .sink { [weak self] imageURL in + self?.makePhotoDetailView(imageURL: imageURL) + } + .store(in: cancelBag) + + output.selectedFeed + .receive(on: RunLoop.main) + .sink { [weak self] feed in + self?.pushToDetailView(feed: feed) + } + .store(in: cancelBag) + + output.toggleHeartButton + .receive(on: RunLoop.main) + .sink { [weak self] datas, index in + guard let self = self else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.feed]) + snapshot.appendItems(datas, toSection: .feed) + dataSource?.apply(snapshot, animatingDifferences: false) + } + .store(in: cancelBag) + + output.showBottomSheet + .receive(on: RunLoop.main) + .sink { [weak self] data in + let isMine = loadUserData()?.memberId == data.memberID + let isAdmin = loadUserData()?.isAdmin + self?.setBottomSheetButton(isMine: isMine, isAdmin: isAdmin ?? false, data: data) + } + .store(in: cancelBag) + } + + func popupEventBinding() { + feedDeleteButtonDidTap.sink { [weak self] contentID in + guard let self else { return } + viewModel.deleteFeed(at: contentID) + var snapshot = dataSource?.snapshot() + if let itemToDelete = snapshot?.itemIdentifiers.first(where: { $0.contentID == contentID }) { + snapshot?.deleteItems([itemToDelete]) + dataSource?.apply(snapshot ?? NSDiffableDataSourceSnapshot(), animatingDifferences: true) + } + } + .store(in: cancelBag) + + feedGhostButtonDidTap.sink { [weak self] memberID in + guard let self else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.feed]) + snapshot.appendItems(viewModel.updateGhostState(for: memberID), toSection: .feed) + dataSource?.apply(snapshot, animatingDifferences: true) + + makeToast(toastImage: ImageLiterals.Toast.toastGhost) + } + .store(in: cancelBag) + + feedBanButtonDidTap.sink { [weak self] memberID in + guard let self else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.feed]) + snapshot.appendItems(viewModel.updateBanState(for: memberID), toSection: .feed) + dataSource?.apply(snapshot, animatingDifferences: true) + + makeToast(toastImage: ImageLiterals.Toast.toastBan) + } + .store(in: cancelBag) + } + + func endRefreshing() { + guard let refreshControl = rootView.collectionView.refreshControl, + refreshControl.isRefreshing else { return } + refreshControl.endRefreshing() + } + + func makePhotoDetailView(imageURL: String) { + + self.photoDetailView = WablePhotoDetailView() + + guard let photoDetailView = self.photoDetailView, + let window = UIApplication.shared.keyWindowInConnectedScenes else { return } + + window.addSubview(photoDetailView) + + photoDetailView.removePhotoButton.tapPublisher + .sink { [weak self] in + self?.photoDetailView?.removeFromSuperview() + self?.photoDetailView = nil + } + .store(in: self.cancelBag) + + photoDetailView.photoImageView.loadContentImage(url: imageURL) { [weak self] image in + DispatchQueue.main.async { + self?.photoDetailView?.updateImageViewHeight(with: image) + } + } + + photoDetailView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + func makeToast(toastImage: UIImage) { + let toastImageView = UIImageView(image: toastImage) + toastImageView.contentMode = .scaleAspectFit + + if let window = UIApplication.shared.keyWindowInConnectedScenes { + window.addSubviews(toastImageView) + } + + toastImageView.snp.makeConstraints { + $0.top.equalToSuperview().inset(75.adjusted) + $0.centerX.equalToSuperview() + $0.width.equalTo(343.adjusted) + } + + UIView.animate(withDuration: 1, delay: 1, options: .curveEaseIn) { + toastImageView.alpha = 0 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + toastImageView.removeFromSuperview() + } + } +} + +private extension MigratedHomeViewController { + var collectionViewLayout: UICollectionViewCompositionalLayout { + UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(170.adjustedH)) + + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(170.adjusted)) + + let group = NSCollectionLayoutGroup.vertical( + layoutSize: groupSize, + subitems: [item] + ) + + let section = NSCollectionLayoutSection(group: group) + + let sectionKind = Section.allCases[sectionIndex] + switch sectionKind { + case .feed: + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + } + return section + } + } +} + +extension MigratedHomeViewController { + func scrollToTop() { + self.rootView.collectionView.setContentOffset(CGPoint(x: 0, y: -self.rootView.collectionView.contentInset.top), animated: true) + } + + func showLoadView() { + displayLoadingView() + } + + func pushToDetailView(feed: HomeFeedDTO) { + let detailViewController = FeedDetailViewController( + viewModel: FeedDetailViewModel(networkProvider: NetworkService()), + likeViewModel: LikeViewModel(networkProvider: NetworkService()) + ) + detailViewController.hidesBottomBarWhenPushed = true + detailViewController.getFeedData(data: feed) + detailViewController.memberId = feed.memberID + self.navigationController?.pushViewController(detailViewController, animated: true) + } + + func displayLoadingView() { + tabBarController?.tabBar.isHidden = true + self.rootView.loadingView.alpha = 1.0 + self.rootView.loadingView.isHidden = false + self.rootView.loadingView.loadingLabel.setTextWithLineHeight( + text: self.rootView.loadingView.loadingText.randomElement(), + lineHeight: 32.adjusted, + alignment: .center + ) + self.rootView.loadingView.lottieLoadingView.play( + fromProgress: 0, + toProgress: 0.7, + loopMode: .playOnce + ) { [weak self] _ in + guard let self else { return } + self.fadeLoadingView() + } + } + + func fadeLoadingView() { + UIView.animate(withDuration: 0.3, animations: { + self.tabBarController?.tabBar.isHidden = false + self.rootView.loadingView.alpha = 0.0 + }) + } + + func removeBottomsheetView() { + if UIApplication.shared.keyWindowInConnectedScenes != nil { + UIView.animate( + withDuration: 0.3, + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 1, + options: .curveEaseOut, + animations: { + self.homeBottomsheetView.dimView.alpha = 0 + if let window = UIApplication.shared.keyWindowInConnectedScenes { + self.homeBottomsheetView.bottomsheetView.frame = CGRect( + x: 0, + y: window.frame.height, + width: self.homeBottomsheetView.frame.width, + height: self.homeBottomsheetView.bottomsheetView.frame.height + ) + } + } + ) + homeBottomsheetView.dimView.removeFromSuperview() + homeBottomsheetView.bottomsheetView.removeFromSuperview() + } + } + + func setBottomSheetButton(isMine: Bool, isAdmin: Bool, data: HomeFeedDTO) { + let bottomSheetHeight = isAdmin ? 178.adjusted : 122.adjusted + homeBottomsheetView.bottomsheetView.snp.remakeConstraints { + $0.height.equalTo(bottomSheetHeight) + } + homeBottomsheetView.showSettings() + homeBottomsheetView.deleteButton.isHidden = !isMine + homeBottomsheetView.reportButton.isHidden = isMine + homeBottomsheetView.banButton.isHidden = !isAdmin + + setBottomSheetButtonAction(isMine: isMine, data: data) + } + + func setBottomSheetButtonAction(isMine: Bool, data: HomeFeedDTO) { + + let bottomSheetCancelBag = CancelBag() + homeBottomsheetView.cancelBag = bottomSheetCancelBag + + if isMine { + homeBottomsheetView.deleteButton.tapPublisher + .sink { [weak self] in + self?.presentPopup(popupType: .delete, data: data) + } + .store(in: bottomSheetCancelBag) + } else { + homeBottomsheetView.reportButton.tapPublisher + .sink { [weak self] in + self?.presentPopup(popupType: .report, data: data) + } + .store(in: bottomSheetCancelBag) + } + + if loadUserData()?.isAdmin ?? false { + homeBottomsheetView.banButton.tapPublisher + .sink { [weak self] in + self?.presentPopup(popupType: .ban, data: data) + } + .store(in: bottomSheetCancelBag) + } + } + + private func presentPopup(popupType: PopupViewType, data: HomeFeedDTO) { + removeBottomsheetView() + let popupViewController = HomePopupViewController( + viewModel: PopupViewModel(data: data), + popupType: popupType + ) + popupViewController.deleteButtonDidTapAction = { [weak self] contentID in + self?.feedDeleteButtonDidTap.send(contentID) + } + + popupViewController.reportButtonDidTapAction = { [weak self] in + self?.makeToast(toastImage: ImageLiterals.Toast.toastReport) + } + + popupViewController.ghostButtonDidTapAction = { [weak self] memberID in + self?.feedGhostButtonDidTap.send(memberID) + } + + popupViewController.banButtonDidTapAction = { [weak self] memberID in + self?.feedBanButtonDidTap.send(memberID) + } + + popupViewController.modalPresentationStyle = .overFullScreen + popupViewController.modalTransitionStyle = .crossDissolve + present(popupViewController, animated: true) + } +} diff --git a/Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift b/Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift new file mode 100644 index 0000000..77b316e --- /dev/null +++ b/Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift @@ -0,0 +1,257 @@ +// +// MigratedHomeFeedCell.swift +// Wable-iOS +// +// Created by 박윤빈 on 12/24/24. +// + +import Combine +import UIKit + +import SnapKit +import CombineCocoa + +final class MigratedHomeFeedCell: UICollectionViewCell{ + + // MARK: - Properties + + var cancelBag = CancelBag() + + var onMenuButtonTap: (() -> Void)? + var onProfileImageTap: (() -> Void)? + var onFeedImageTap: (() -> Void)? + var onHeartButtonTap: (() -> Void)? + var onCommentButtonTap: (() -> Void)? + var onGhostButtonTap: (() -> Void)? + + // MARK: - Components + + let grayView: UIView = { + let view = UIView() + view.backgroundColor = .wableWhite + view.alpha = 0 + view.isUserInteractionEnabled = false + return view + }() + + var infoView = FeedInfoView() + var feedContentView = FeedContentView() + var bottomView = FeedBottomView() + var divideLine = UIView().makeDivisionLine() + + var profileImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = ImageLiterals.Image.imgProfileSmall + imageView.isUserInteractionEnabled = true + return imageView + }() + + private var menuButton: UIButton = { + let button = UIButton() + button.setImage(ImageLiterals.Icon.icMeatball, for: .normal) + return button + }() + + var seperateLineView: UIView = { + let view = UIView() + view.backgroundColor = .gray200 + view.isHidden = true + return view + }() + + // MARK: - inits + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + setLayout() + setEventPublisher() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + DispatchQueue.main.async { + self.profileImageView.contentMode = .scaleAspectFill + self.profileImageView.layer.cornerRadius = self.profileImageView.frame.size.width / 2 + self.profileImageView.clipsToBounds = true + } + } + + override func prepareForReuse() { + super.prepareForReuse() + self.profileImageView.image = UIImage() + self.feedContentView.blindImageView.isHidden = true + self.feedContentView.titleLabel.isHidden = false + self.feedContentView.contentLabel.isHidden = false + self.feedContentView.photoImageView.isHidden = false + self.feedContentView.titleLabel.attributedText = nil + self.feedContentView.titleLabel.textColor = .wableBlack + self.feedContentView.contentLabel.attributedText = nil + self.feedContentView.contentLabel.textColor = .gray800 + self.grayView.alpha = 0 + } + + // MARK: - Functions + + private func setupView() { + self.backgroundColor = .wableWhite + self.contentView.addSubviews( + profileImageView, + menuButton, + infoView, + feedContentView, + bottomView, + divideLine, + seperateLineView, + grayView + ) + + self.profileImageView.contentMode = .scaleAspectFill + self.profileImageView.layer.cornerRadius = self.profileImageView.frame.size.width / 2 + self.profileImageView.clipsToBounds = true + } + + private func setLayout() { + grayView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.bottom.equalTo(bottomView.snp.top) + } + + profileImageView.snp.makeConstraints { + $0.height.width.equalTo(36.adjusted) + $0.leading.equalToSuperview().inset(16.adjusted) + $0.centerY.equalTo(infoView) + } + + infoView.snp.makeConstraints { + $0.top.equalToSuperview().inset(18.adjusted) + $0.leading.equalTo(profileImageView.snp.trailing).offset(10.adjusted) + $0.height.equalTo(43.adjusted) + } + + menuButton.snp.makeConstraints { + $0.height.width.equalTo(32.adjusted) + $0.top.equalTo(infoView) + $0.trailing.equalToSuperview().inset(16.adjusted) + } + + feedContentView.snp.makeConstraints { + $0.top.equalTo(infoView.snp.bottom).offset(12.adjusted) + $0.leading.trailing.equalToSuperview().inset(16.adjusted) + } + + bottomView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(16.adjusted) + $0.height.equalTo(31.adjusted) + $0.top.equalTo(feedContentView.snp.bottom).offset(20.adjusted).priority(.low) + $0.bottom.equalToSuperview().inset(20.adjusted) + } + + divideLine.snp.makeConstraints { + $0.height.equalTo(1) + $0.leading.trailing.bottom.equalToSuperview() + } + + seperateLineView.snp.makeConstraints { + $0.height.equalTo(8.adjusted) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + func bind(data: HomeFeedDTO) { + data.memberProfileURL.isEmpty ? + (profileImageView.image = ImageLiterals.Image.imgProfile3) : + profileImageView.load(url: data.memberProfileURL) + + infoView.bind( + nickname: data.memberNickname, + team: Team(rawValue: data.memberFanTeam) ?? .TBD, + ghostPercent: data.memberGhost, + time: data.time + ) + + feedContentView.bind( + title: data.contentTitle ?? "", + content: data.contentText ?? "", + image: data.contentImageURL, + isBlind: data.isBlind + ) + + bottomView.bind( + heart: data.likedNumber, + comment: data.commentNumber ?? Int(), + memberID: data.memberID + ) + + bottomView.isLiked = data.isLiked + + if data.isGhost || data.isBlind ?? false { + bottomView.ghostButton.setImage(ImageLiterals.Button.btnGhostDisabledLarge, for: .normal) + bottomView.ghostButton.isEnabled = false + } else { + bottomView.ghostButton.setImage(ImageLiterals.Button.btnGhostDefaultLarge, for: .normal) + bottomView.ghostButton.isEnabled = true + } + + let memberGhost = adjustGhostValue(data.memberGhost) + + if data.isGhost { + print("\(data.memberNickname)\n\(data.isGhost)") + grayView.alpha = 0.85 + } else { + grayView.alpha = CGFloat(Double(-memberGhost) / 100) + } + } + + func changeButtonState(isLiked: Bool) { + bottomView.isLiked = isLiked + } + + private func setEventPublisher() { + let profileImageTapGesture = UITapGestureRecognizer() + let feedImageTapGesture = UITapGestureRecognizer() + + menuButton.tapPublisher + .sink { [weak self] in + self?.onMenuButtonTap?() + } + .store(in: cancelBag) + + profileImageView.gesturePublisher(profileImageTapGesture) + .sink { [weak self] _ in + self?.onProfileImageTap?() + } + .store(in: cancelBag) + + feedContentView.photoImageView.gesturePublisher(feedImageTapGesture) + .sink { [weak self] _ in + self?.onFeedImageTap?() + } + .store(in: cancelBag) + + bottomView.heartButton.tapPublisher + .sink { [weak self] in + self?.onHeartButtonTap?() + } + .store(in: cancelBag) + + bottomView.commentButton.tapPublisher + .sink { [weak self] in + self?.onCommentButtonTap?() + } + .store(in: cancelBag) + + bottomView.ghostButton.tapPublisher + .sink { [weak self] in + self?.onGhostButtonTap?() + } + .store(in: cancelBag) + } +} diff --git a/Wable-iOS/Presentation/Home/Views/HomeBottomSheetView.swift b/Wable-iOS/Presentation/Home/Views/HomeBottomSheetView.swift index d8a64f1..1fb2ed1 100644 --- a/Wable-iOS/Presentation/Home/Views/HomeBottomSheetView.swift +++ b/Wable-iOS/Presentation/Home/Views/HomeBottomSheetView.swift @@ -15,6 +15,7 @@ final class HomeBottomSheetView: UIView { var initialPosition: CGPoint = CGPoint(x: 0, y: 0) var isUser: Bool = true + var cancelBag: CancelBag? // MARK: - UI Components diff --git a/Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift b/Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift new file mode 100644 index 0000000..a1c994d --- /dev/null +++ b/Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift @@ -0,0 +1,91 @@ +// +// MigratedHomeView.swift +// Wable-iOS +// +// Created by 박윤빈 on 12/24/24. +// + +import UIKit + +import SnapKit + +final class MigratedHomeView: UIView { + + // MARK: - UI Components + + private let homeTabView = HomeTabView() + let loadingView = HomeLoadingView() + let writeFeedButton: UIButton = { + let button = UIButton() + button.setImage(ImageLiterals.Button.btnWrite, for: .normal) + return button + }() + + let collectionView: UICollectionView = { + let collectionView = UICollectionView( + frame: .zero, + collectionViewLayout: UICollectionViewLayout() + ) + + collectionView.showsVerticalScrollIndicator = false + collectionView.showsHorizontalScrollIndicator = false + collectionView.backgroundColor = .wableWhite + collectionView.refreshControl = UIRefreshControl() + return collectionView + }() + + // MARK: - Life Cycles + + override init(frame: CGRect) { + super.init(frame: frame) + + setUI() + setHierarchy() + setLayout() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Extensions + +extension MigratedHomeView { + private func setUI() { + backgroundColor = .wableWhite + loadingView.isHidden = true + } + + private func setHierarchy() { + self.addSubviews(homeTabView, + collectionView, + writeFeedButton, + loadingView) + } + + private func setLayout() { + loadingView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview() + $0.top.equalToSuperview() + $0.height.equalTo(UIScreen.main.bounds.height) + } + + homeTabView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.top.equalTo(safeAreaLayoutGuide) + } + + collectionView.snp.makeConstraints { + $0.top.equalTo(homeTabView.snp.bottom).offset(-2) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(safeAreaLayoutGuide) + } + + writeFeedButton.snp.makeConstraints { + $0.height.width.equalTo(60.adjusted) + $0.bottom.trailing.equalToSuperview().inset(16.adjusted) + } + } +} From d9149a04bde4b5241f5024440ab96e3fae36e33d Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:58:13 +0900 Subject: [PATCH 07/16] =?UTF-8?q?[Feat]=20#87=20-=20HomeFeedDTO=EA=B0=80?= =?UTF-8?q?=20Hashable=EC=9D=84=20=EC=A4=80=EC=88=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Wable-iOS/Network/Home/ResponseDTO/HomeFeedListDTO.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Wable-iOS/Network/Home/ResponseDTO/HomeFeedListDTO.swift b/Wable-iOS/Network/Home/ResponseDTO/HomeFeedListDTO.swift index f125c28..998961d 100644 --- a/Wable-iOS/Network/Home/ResponseDTO/HomeFeedListDTO.swift +++ b/Wable-iOS/Network/Home/ResponseDTO/HomeFeedListDTO.swift @@ -8,7 +8,6 @@ import Foundation struct HomeFeedDTO: Codable { - // 공통 속성 let memberID: Int let memberProfileURL, memberNickname: String let isGhost: Bool @@ -17,8 +16,6 @@ struct HomeFeedDTO: Codable { let time: String let likedNumber: Int let memberFanTeam: String - - // 선택적 속성 let contentID: Int? let contentTitle: String? let contentText: String? @@ -39,3 +36,9 @@ struct HomeFeedDTO: Codable { case isBlind } } + +extension HomeFeedDTO: Hashable { + static func == (lhs: HomeFeedDTO, rhs: HomeFeedDTO) -> Bool { + lhs.contentID == rhs.contentID + } +} From 29a77b863ca8a1cc264417503a4446d0cc4a902c Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:58:53 +0900 Subject: [PATCH 08/16] =?UTF-8?q?[Fix]=20#87=20-=20HomeAPI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Wable-iOS/Network/Home/HomeAPI.swift | 65 +++++++++++++++++++++++++ Wable-iOS/Network/Home/HomeRouter.swift | 39 ++++++++++++--- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/Wable-iOS/Network/Home/HomeAPI.swift b/Wable-iOS/Network/Home/HomeAPI.swift index 0a55a59..6b86728 100644 --- a/Wable-iOS/Network/Home/HomeAPI.swift +++ b/Wable-iOS/Network/Home/HomeAPI.swift @@ -27,6 +27,71 @@ extension HomeAPI { } } + func migratedGetHomeFeed(cursor: Int) -> AnyPublisher<[HomeFeedDTO]?, WableNetworkError> { + homeProvider.requestPublisher(.getContent(param: cursor)) + .tryMap { [weak self] response -> [HomeFeedDTO]? in + return try self?.parseResponse(statusCode: response.statusCode, data: response.data) + } + .mapError { $0 as? WableNetworkError ?? .unknownError($0.localizedDescription) } + .eraseToAnyPublisher() + } + + func postFeedLike(contentID: Int) -> AnyPublisher { + homeProvider.requestPublisher(.postFeedLike(contentID: contentID)) + .tryMap { [weak self] response -> EmptyDTO? in + return try self?.parseResponse(statusCode: response.statusCode, data: response.data) + } + .mapError { $0 as? WableNetworkError ?? .unknownError($0.localizedDescription) } + .eraseToAnyPublisher() + } + + func deleteFeedLike(contentID: Int) -> AnyPublisher { + homeProvider.requestPublisher(.deleteFeedLike(contentID: contentID)) + .tryMap { [weak self] response -> EmptyDTO? in + return try self?.parseResponse(statusCode: response.statusCode, data: response.data) + } + .mapError { $0 as? WableNetworkError ?? .unknownError($0.localizedDescription) } + .eraseToAnyPublisher() + } + + func deleteFeed(contentID: Int) -> AnyPublisher { + homeProvider.requestPublisher(.deleteFeed(contentID: contentID)) + .tryMap { [weak self] response -> EmptyDTO? in + return try self?.parseResponse(statusCode: response.statusCode, data: response.data) + } + .mapError { $0 as? WableNetworkError ?? .unknownError($0.localizedDescription) } + .eraseToAnyPublisher() + } + + func postReport(nickname: String, relateText: String) -> AnyPublisher { + homeProvider.requestPublisher(.postReport(param: ReportRequestDTO( + reportTargetNickname: nickname, + relateText: relateText + ))) + .tryMap { [weak self] response -> EmptyDTO? in + return try self?.parseResponse(statusCode: response.statusCode, data: response.data) + } + .mapError { $0 as? WableNetworkError ?? + .unknownError($0.localizedDescription) } + .eraseToAnyPublisher() + } + + func postBeGhost(triggerType: String, memberID: Int, triggerID: Int) -> AnyPublisher { + let param = PostTransparencyRequestDTO( + alarmTriggerType: triggerType, + targetMemberId: memberID, + alarmTriggerId: triggerID, + ghostReason: "" + ) + return homeProvider.requestPublisher(.postBeGhost(param: param)) + .tryMap { [weak self] response -> EmptyDTO? in + return try self?.parseResponse(statusCode: response.statusCode, data: response.data) + } + .mapError { $0 as? WableNetworkError ?? + .unknownError($0.localizedDescription) } + .eraseToAnyPublisher() + } + func postReply(contentID: Int, requestBody: WriteReplyRequestV3DTO) -> AnyPublisher { homeProvider.requestPublisher(.postReply(param: contentID, requestBody: requestBody)) .tryMap { [weak self] response -> EmptyDTO? in diff --git a/Wable-iOS/Network/Home/HomeRouter.swift b/Wable-iOS/Network/Home/HomeRouter.swift index b0daade..c4d6427 100644 --- a/Wable-iOS/Network/Home/HomeRouter.swift +++ b/Wable-iOS/Network/Home/HomeRouter.swift @@ -14,6 +14,11 @@ enum HomeRouter { case patchFCMToken(param: UserProfileRequestDTO) case postReply(param: Int, requestBody: WriteReplyRequestV3DTO) case postBan(requestBody: BanRequestDTO) + case postFeedLike(contentID: Int) + case deleteFeedLike(contentID: Int) + case deleteFeed(contentID: Int) + case postBeGhost(param: PostTransparencyRequestDTO) + case postReport(param: ReportRequestDTO) } extension HomeRouter: BaseTargetType { @@ -27,6 +32,16 @@ extension HomeRouter: BaseTargetType { return StringLiterals.Endpoint.Home.postReply(contentID: contentID) case .postBan: return StringLiterals.Endpoint.Home.postBan + case .postFeedLike(let contentID): + return StringLiterals.Endpoint.Home.postFeedLike(contentID: contentID) + case .deleteFeedLike(let contentID): + return StringLiterals.Endpoint.Home.deleteFeedLike(contentID: contentID) + case .deleteFeed(let contentID): + return StringLiterals.Endpoint.Home.deleteFeed(contentID: contentID) + case .postBeGhost(let param): + return StringLiterals.Endpoint.Home.postOpacityDown + case .postReport: + return StringLiterals.Endpoint.Home.postReport } } @@ -36,10 +51,10 @@ extension HomeRouter: BaseTargetType { return .get case .patchFCMToken: return .patch - case .postReply: - return .post - case .postBan: + case .postReply, .postBan, .postFeedLike, .postBeGhost, .postReport: return .post + case .deleteFeed, .deleteFeedLike: + return .delete } } @@ -47,8 +62,8 @@ extension HomeRouter: BaseTargetType { switch self { case .getContent(let cursor): return .requestParameters(parameters: ["cursor": cursor], encoding: URLEncoding.queryString) + case .patchFCMToken(let data): - var formData = [MultipartFormData]() // fcmToken 추가 @@ -61,7 +76,6 @@ extension HomeRouter: BaseTargetType { let pushAlarmData = String(describing: data.isPushAlarmAllowed).data(using: .utf8) ?? Data() let pushAlarmPart = MultipartFormData(provider: .data(pushAlarmData), name: "isPushAlarmAllowed") formData.append(pushAlarmPart) - return .uploadMultipart(formData) case .postReply(_, let requestBody): @@ -69,12 +83,25 @@ extension HomeRouter: BaseTargetType { case .postBan(let requestBody): return .requestJSONEncodable(requestBody) + + case .postFeedLike: + let requestBody = ContentLikeRequestDTO(alarmTriggerType: "contentLiked") + return .requestJSONEncodable(requestBody) + + case .deleteFeedLike, .deleteFeed: + return .requestPlain + + case .postBeGhost(let requestBody): + return .requestJSONEncodable(requestBody) + + case .postReport(let requestBody): + return .requestJSONEncodable(requestBody) } } var headers: [String : String]? { switch self { - case .getContent, .postReply, .postBan: + case .getContent, .postReply, .postBan, .postFeedLike, .deleteFeedLike, .postBeGhost, .deleteFeed, .postReport: return APIConstants.hasTokenHeader case .patchFCMToken: return APIConstants.multipartHeader From 112e919f967993012dfe5aff13002886701a701f Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 18:59:39 +0900 Subject: [PATCH 09/16] =?UTF-8?q?[Feat]=20#87=20-=20HomeViewModel=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/MigratedHomeViewModel.swift | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift diff --git a/Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift b/Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift new file mode 100644 index 0000000..e04274c --- /dev/null +++ b/Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift @@ -0,0 +1,268 @@ +// +// MigratedHomeViewModel.swift +// Wable-iOS +// +// Created by 박윤빈 on 12/24/24. +// + +import Foundation +import Combine + +final class MigratedHomeViewModel { + private var cursor: Int = -1 + let feedSubject = CurrentValueSubject<[HomeFeedDTO], Never>([]) + + private let service: HomeAPI + + init(service: HomeAPI = HomeAPI.shared) { + self.service = service + } +} + +extension MigratedHomeViewModel: ViewModelType { + struct Input { + let viewDidLoad: AnyPublisher + let collectionViewDidRefresh: AnyPublisher + let collectionViewDidSelect: AnyPublisher + let collectionViewDidEndDrag: AnyPublisher + let menuButtonDidTap: AnyPublisher + let profileImageDidTap: AnyPublisher + let feedImageURL: AnyPublisher + let heartButtonDidTap: AnyPublisher + let commentButtonDidTap: AnyPublisher + } + + struct Output { + let feedData: AnyPublisher<[HomeFeedDTO], Never> + let selectedFeed: AnyPublisher + let showBottomSheet: AnyPublisher + let profileImageTapped: AnyPublisher + let feedImageTapped: AnyPublisher + let toggleHeartButton: AnyPublisher<([HomeFeedDTO], Int), Never> + } + + func transform(from input: Input, cancelBag: CancelBag) -> Output { + + input.viewDidLoad + .merge(with: input.collectionViewDidRefresh) + .flatMap { [weak self] _ -> AnyPublisher<[HomeFeedDTO], Never> in + guard let self else { + return Just([]).eraseToAnyPublisher() + } + + return resetCursorAndGetHomeFeed() + } + .subscribe(feedSubject) + .store(in: cancelBag) + + let lastContentIDPublisher = input.collectionViewDidEndDrag + .compactMap { + self.feedSubject.value.last?.contentID + } + + let feedPublisher = lastContentIDPublisher + .filter { [weak self] lastContentID in // 페이지네이션 조건 필터링 + (self?.feedSubject.value.count ?? 0) % 20 == 0 && + lastContentID != -1 && + lastContentID != self?.cursor ?? .zero + } + .flatMap { [weak self] lastContentID -> AnyPublisher<[HomeFeedDTO], Never> in + guard let self else { + return Just([]).eraseToAnyPublisher() + } + cursor = lastContentID + return self.getHomeFeed(cursor: lastContentID) + } + .map { feeds in + var previousFeeds = self.feedSubject.value + previousFeeds.append(contentsOf: feeds) + return previousFeeds + } + + + // feedPublisher를 feedSubject에 구독 + feedPublisher + .subscribe(feedSubject) + .store(in: cancelBag) + + let feed = feedSubject + .filter { !$0.isEmpty } + .eraseToAnyPublisher() + + // 인덱스 초과를 방지하기 위해서 filter로 검사해준 뒤, 인덱스에 맞는 값 찾아줌 + let selectedFeed = input.collectionViewDidSelect + .merge(with: input.commentButtonDidTap) + .filter { $0 < self.feedSubject.value.count} + .map { self.feedSubject.value[$0] } + .eraseToAnyPublisher() + + let profileImageDidTap = input.profileImageDidTap + .compactMap { $0 } + .filter { $0 < self.feedSubject.value.count } + .map { self.feedSubject.value[$0].memberID } + .eraseToAnyPublisher() + + let feedImageURL = input.feedImageURL + .compactMap { $0 } + .filter { $0 < self.feedSubject.value.count } + .map { self.feedSubject.value[$0].contentImageURL ?? String() } + .eraseToAnyPublisher() + + let bottomSheetInfo = input.menuButtonDidTap + .compactMap { $0 } + .filter { $0 < self.feedSubject.value.count } + .map { self.feedSubject.value[$0] } + .eraseToAnyPublisher() + + + let heartButtonState = input.heartButtonDidTap + .throttle(for: .milliseconds(500), scheduler: RunLoop.main, latest: false) + .compactMap { $0 } + .filter { $0 < self.feedSubject.value.count } + .map { index -> (Bool?, Int?, Int) in + let item = self.feedSubject.value[index] + return (item.isLiked, item.contentID, index) + } + .flatMap { [weak self] state -> AnyPublisher<(EmptyDTO?, Int), Never> in + guard let self = self else { return Just((nil, state.2)).eraseToAnyPublisher() } + + if state.0 ?? false { + return service.deleteFeedLike(contentID: state.1 ?? Int()) + .replaceError(with: nil) + .map { return ($0, state.2) } + .eraseToAnyPublisher() + } else { + return service.postFeedLike(contentID: state.1 ?? Int()) + .replaceError(with: nil) + .map { ($0, state.2) } + .eraseToAnyPublisher() + } + } + + let toggleHeart = heartButtonState + .map { [weak self] apiResult, index -> ([HomeFeedDTO], Int) in + guard let self else { + return ([], index) + } + self.updateHeartButtonState(at: index) + return (self.feedSubject.value, index) + } + .eraseToAnyPublisher() + + + return Output( + feedData: feed, + selectedFeed: selectedFeed, + showBottomSheet: bottomSheetInfo, + profileImageTapped: profileImageDidTap, + feedImageTapped: feedImageURL, + toggleHeartButton: toggleHeart + ) + } + + func deleteFeed(at contentID: Int) { + feedSubject.value.removeAll { $0.contentID == contentID } + } + + func updateGhostState(for memberID: Int) -> [HomeFeedDTO] { + let updatedDatas = feedSubject.value.map { item in + guard item.memberID == memberID else { return item } + + return HomeFeedDTO( + memberID: item.memberID, + memberProfileURL: item.memberProfileURL, + memberNickname: item.memberNickname, + isGhost: true, + memberGhost: item.memberGhost - 1 , + isLiked: item.isLiked, + time: item.time, + likedNumber: item.likedNumber, + memberFanTeam: item.memberFanTeam, + contentID: item.contentID, + contentTitle: item.contentTitle, + contentText: item.contentText, + commentNumber: item.commentNumber, + isDeleted: item.isDeleted, + commnetNumber: item.commnetNumber, + contentImageURL: item.contentImageURL, + isBlind: item.isBlind + ) + } + feedSubject.send(updatedDatas) + return updatedDatas + } + + func updateBanState(for memberID: Int) -> [HomeFeedDTO] { + let updatedDatas = feedSubject.value.map { item in + guard item.memberID == memberID else { return item } + + return HomeFeedDTO( + memberID: item.memberID, + memberProfileURL: item.memberProfileURL, + memberNickname: item.memberNickname, + isGhost: item.isGhost, + memberGhost: item.memberGhost, + isLiked: item.isLiked, + time: item.time, + likedNumber: item.likedNumber, + memberFanTeam: item.memberFanTeam, + contentID: item.contentID, + contentTitle: item.contentTitle, + contentText: item.contentText, + commentNumber: item.commentNumber, + isDeleted: item.isDeleted, + commnetNumber: item.commnetNumber, + contentImageURL: item.contentImageURL, + isBlind: true + ) + } + feedSubject.send(updatedDatas) + return updatedDatas + } + + func updateHeartButtonState(at index: Int){ + var updatedDatas = feedSubject.value + + guard updatedDatas.indices.contains(index) else { return } + + let item = updatedDatas[index] + let newData = HomeFeedDTO( + memberID: item.memberID, + memberProfileURL: item.memberProfileURL, + memberNickname: item.memberNickname, + isGhost: item.isGhost, + memberGhost: item.memberGhost, + isLiked: !item.isLiked, + time: item.time, + likedNumber: item.isLiked ? item.likedNumber - 1 : item.likedNumber + 1, + memberFanTeam: item.memberFanTeam, + contentID: item.contentID, + contentTitle: item.contentTitle, + contentText: item.contentText, + commentNumber: item.commentNumber, + isDeleted: item.isDeleted, + commnetNumber: item.commnetNumber, + contentImageURL: item.contentImageURL, + isBlind: item.isBlind + ) + + updatedDatas[index] = newData + + feedSubject.send(updatedDatas) + } +} + +private extension MigratedHomeViewModel { + func getHomeFeed(cursor: Int) -> AnyPublisher<[HomeFeedDTO], Never> { + service.migratedGetHomeFeed(cursor: cursor) + .mapWableNetworkError() + .replaceError(with: []) + .compactMap { $0 } + .eraseToAnyPublisher() + } + + func resetCursorAndGetHomeFeed() -> AnyPublisher<[HomeFeedDTO], Never> { + cursor = -1 + return getHomeFeed(cursor: cursor) + } +} From 28e83ef296b1c0ed8159094ac753a540f5d134cb Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 19:00:08 +0900 Subject: [PATCH 10/16] =?UTF-8?q?[Feat]=20#87=20-=20=EB=B0=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B8=EB=94=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Home/Views/Cells/HomeFeedTableViewCell.swift | 4 +++- .../Presentation/Home/Views/Subviews/FeedBottomView.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Wable-iOS/Presentation/Home/Views/Cells/HomeFeedTableViewCell.swift b/Wable-iOS/Presentation/Home/Views/Cells/HomeFeedTableViewCell.swift index 25c43b8..6e309f9 100644 --- a/Wable-iOS/Presentation/Home/Views/Cells/HomeFeedTableViewCell.swift +++ b/Wable-iOS/Presentation/Home/Views/Cells/HomeFeedTableViewCell.swift @@ -202,7 +202,9 @@ final class HomeFeedTableViewCell: UITableViewCell{ isBlind: data.isBlind) bottomView.bind(heart: data.likedNumber, - comment: data.commentNumber ?? Int()) + comment: data.commentNumber ?? Int(), + memberID: data.memberID + ) bottomView.isLiked = data.isLiked diff --git a/Wable-iOS/Presentation/Home/Views/Subviews/FeedBottomView.swift b/Wable-iOS/Presentation/Home/Views/Subviews/FeedBottomView.swift index 1dad694..3599aa4 100644 --- a/Wable-iOS/Presentation/Home/Views/Subviews/FeedBottomView.swift +++ b/Wable-iOS/Presentation/Home/Views/Subviews/FeedBottomView.swift @@ -119,8 +119,10 @@ extension FeedBottomView { commentButtonTapped?() } - func bind(heart: Int, comment: Int) { + func bind(heart: Int, comment: Int, memberID: Int) { heartButton.setTitleWithConfiguration("\(heart)", font: .caption1, textColor: .wableBlack) commentButton.setTitleWithConfiguration("\(comment)", font: .caption1, textColor: .wableBlack) + let isMine = memberID == loadUserData()?.memberId + ghostButton.isHidden = isMine } } From 8eb9b6b5e90cb327c4d18e0623c446bc5ceb7827 Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 19:00:36 +0900 Subject: [PATCH 11/16] =?UTF-8?q?[Chore]=20#87=20-=20HomeAPI=20EndPoint?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Wable-iOS/Global/Literals/StringLiterals.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Wable-iOS/Global/Literals/StringLiterals.swift b/Wable-iOS/Global/Literals/StringLiterals.swift index 9054fd6..bdf47bf 100644 --- a/Wable-iOS/Global/Literals/StringLiterals.swift +++ b/Wable-iOS/Global/Literals/StringLiterals.swift @@ -203,6 +203,17 @@ enum StringLiterals { return "v3/content/\(contentID)/comment" } static let postBan = "v1/report/ban" + static func postFeedLike(contentID: Int) -> String { + return "v1/content/\(contentID)/liked" + } + static func deleteFeedLike(contentID: Int) -> String { + return "v1/content/\(contentID)/unliked" + } + static func deleteFeed(contentID: Int) -> String { + return "v1/content/\(contentID)" + } + static let postOpacityDown = "v1/ghost2" + static let postReport = "v1/report/slack" } enum Info { From 73f3d21c40052a595f6dc8e4b26782eaf2da2648 Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 19:01:06 +0900 Subject: [PATCH 12/16] =?UTF-8?q?[Feat]=20#87=20-=20UIGestureRecognizer=20?= =?UTF-8?q?Combine=20Publisher=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Wable-iOS/Global/Extention/UIView+.swift | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Wable-iOS/Global/Extention/UIView+.swift b/Wable-iOS/Global/Extention/UIView+.swift index 90d5558..3a8d12f 100644 --- a/Wable-iOS/Global/Extention/UIView+.swift +++ b/Wable-iOS/Global/Extention/UIView+.swift @@ -5,6 +5,7 @@ // Created by 박윤빈 on 8/6/24. // +import Combine import UIKit extension UIView { @@ -30,3 +31,61 @@ extension UIView { return superview as? T ?? superview?.superview(of: type) } } + +// MARK: - UIGestureRecognizer Combine Publisher + +extension UIView { + func gesturePublisher(_ gestureRecognizer: T) -> AnyPublisher { + GesturePublisher(view: self, gestureRecognizer: gestureRecognizer).eraseToAnyPublisher() + } +} + +// MARK: - GesturePublisher 정의 + +struct GesturePublisher: Publisher { + typealias Output = T + typealias Failure = Never + + private let view: UIView + private let gestureRecognizer: T + + init(view: UIView, gestureRecognizer: T) { + self.view = view + self.gestureRecognizer = gestureRecognizer + } + + func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { + let subscription = GestureSubscription(subscriber: subscriber, view: view, gestureRecognizer: gestureRecognizer) + subscriber.receive(subscription: subscription) + } +} + +// MARK: - GestureSubscription 정의 + +final class GestureSubscription: Subscription where S.Input == T { + private var subscriber: S? + private let gestureRecognizer: T + private weak var view: UIView? + + init(subscriber: S, view: UIView, gestureRecognizer: T) { + self.subscriber = subscriber + self.gestureRecognizer = gestureRecognizer + self.view = view + + self.view?.isUserInteractionEnabled = true + self.view?.addGestureRecognizer(gestureRecognizer) + self.gestureRecognizer.addTarget(self, action: #selector(handleGesture)) + } + + func request(_ demand: Subscribers.Demand) { + } + + func cancel() { + subscriber = nil + view?.removeGestureRecognizer(gestureRecognizer) + } + + @objc private func handleGesture() { + _ = subscriber?.receive(gestureRecognizer) + } +} From f33c643057f1cf1aa00a3dd82ed8c72d5edfac3f Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 19:01:42 +0900 Subject: [PATCH 13/16] =?UTF-8?q?[Feat]=20#87=20-=20=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=99=84=EB=A3=8C=20=ED=9B=84=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=ED=81=B4=EB=A1=9C=EC=A0=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Write/ViewController/WriteViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift b/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift index 0ac16f5..12c7138 100644 --- a/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift +++ b/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift @@ -20,6 +20,7 @@ final class WriteViewController: UIViewController { private var cancelBag = CancelBag() private let viewModel: WriteViewModel private var transparency: Int = 0 + var writeViewDidDisappear: (() -> Void)? private lazy var postButtonTapped = self.rootView.writeTextView.postButton.publisher(for: .touchUpInside) .debounce(for: .seconds(0.5), scheduler: RunLoop.main) @@ -93,7 +94,7 @@ final class WriteViewController: UIViewController { self.tabBarController?.tabBar.isTranslucent = false NotificationCenter.default.removeObserver(self, name: UITextView.textDidChangeNotification, object: nil) - + writeViewDidDisappear?() } } From 4169f18e2e033e98dafc694205026627edbc03cd Mon Sep 17 00:00:00 2001 From: binisnull Date: Wed, 15 Jan 2025 20:38:19 +0900 Subject: [PATCH 14/16] =?UTF-8?q?[Fix]=20#87=20-=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=ED=9B=84=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=8B=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/ViewModels/MigratedHomeViewModel.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift b/Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift index e04274c..111db54 100644 --- a/Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift +++ b/Wable-iOS/Presentation/Home/ViewModels/MigratedHomeViewModel.swift @@ -10,6 +10,7 @@ import Combine final class MigratedHomeViewModel { private var cursor: Int = -1 + private var deletedFeedCount: Int = 0 let feedSubject = CurrentValueSubject<[HomeFeedDTO], Never>([]) private let service: HomeAPI @@ -62,9 +63,11 @@ extension MigratedHomeViewModel: ViewModelType { let feedPublisher = lastContentIDPublisher .filter { [weak self] lastContentID in // 페이지네이션 조건 필터링 - (self?.feedSubject.value.count ?? 0) % 20 == 0 && + guard let self else { return false } + let count = feedSubject.value.count + deletedFeedCount + return count % 20 == 0 && lastContentID != -1 && - lastContentID != self?.cursor ?? .zero + lastContentID != cursor } .flatMap { [weak self] lastContentID -> AnyPublisher<[HomeFeedDTO], Never> in guard let self else { @@ -79,7 +82,6 @@ extension MigratedHomeViewModel: ViewModelType { return previousFeeds } - // feedPublisher를 feedSubject에 구독 feedPublisher .subscribe(feedSubject) @@ -162,6 +164,7 @@ extension MigratedHomeViewModel: ViewModelType { func deleteFeed(at contentID: Int) { feedSubject.value.removeAll { $0.contentID == contentID } + deletedFeedCount += 1 } func updateGhostState(for memberID: Int) -> [HomeFeedDTO] { @@ -263,6 +266,7 @@ private extension MigratedHomeViewModel { func resetCursorAndGetHomeFeed() -> AnyPublisher<[HomeFeedDTO], Never> { cursor = -1 + deletedFeedCount = 0 return getHomeFeed(cursor: cursor) } } From 4fb3fcbd549b5fa0b54d09b1bde33f5b1c3e27b9 Mon Sep 17 00:00:00 2001 From: binisnull Date: Thu, 16 Jan 2025 21:34:21 +0900 Subject: [PATCH 15/16] =?UTF-8?q?[Fix]=20#87=20-=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Wable-iOS.xcodeproj/project.pbxproj | 4 ++ .../Home/Supports/PopupType.swift | 51 +++++++++++++ .../FeedDetailViewController.swift | 2 +- .../HomePopupViewController.swift | 35 ++++----- .../ViewController/HomeViewController.swift | 2 +- .../Home/ViewModels/PopupViewModel.swift | 16 ++--- .../Views/Cells/MigratedHomeFeedCell.swift | 33 ++++----- .../Home/Views/MigratedHomeView.swift | 18 ++--- .../ViewController/LoginViewController.swift | 2 +- .../MyPagePostViewController.swift | 2 +- .../MyPageReplyViewController.swift | 2 +- .../MyPageSignOutConfirmViewController.swift | 2 +- .../ViewController/MyPageViewController.swift | 2 +- .../UIComponents/WablePopupView.swift | 72 ++++--------------- .../ViewController/WriteViewController.swift | 2 +- 15 files changed, 128 insertions(+), 117 deletions(-) create mode 100644 Wable-iOS/Presentation/Home/Supports/PopupType.swift diff --git a/Wable-iOS.xcodeproj/project.pbxproj b/Wable-iOS.xcodeproj/project.pbxproj index a392223..c11775c 100644 --- a/Wable-iOS.xcodeproj/project.pbxproj +++ b/Wable-iOS.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ 0573B8C42CEC63EC00B5A434 /* FlattenReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0573B8C32CEC63EC00B5A434 /* FlattenReplyModel.swift */; }; 0586D89B2D09A68200436080 /* Pretendard-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0586D8992D09A68200436080 /* Pretendard-Regular.otf */; }; 0586D89C2D09A68200436080 /* Pretendard-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0586D89A2D09A68200436080 /* Pretendard-SemiBold.otf */; }; + 0589AECF2D38311A004F531E /* PopupType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0589AECE2D383119004F531E /* PopupType.swift */; }; 0593F6D62C96D6C100FFAD82 /* WablePhotoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0593F6D52C96D6C100FFAD82 /* WablePhotoDetailView.swift */; }; 0593F6DB2C96E75600FFAD82 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 0593F6DA2C96E75600FFAD82 /* FirebaseAnalytics */; }; 0593F6DD2C96E75600FFAD82 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 0593F6DC2C96E75600FFAD82 /* FirebaseMessaging */; }; @@ -315,6 +316,7 @@ 0573B8C32CEC63EC00B5A434 /* FlattenReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlattenReplyModel.swift; sourceTree = ""; }; 0586D8992D09A68200436080 /* Pretendard-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Regular.otf"; sourceTree = ""; }; 0586D89A2D09A68200436080 /* Pretendard-SemiBold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-SemiBold.otf"; sourceTree = ""; }; + 0589AECE2D383119004F531E /* PopupType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupType.swift; sourceTree = ""; }; 0593F6D52C96D6C100FFAD82 /* WablePhotoDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WablePhotoDetailView.swift; sourceTree = ""; }; 0593F6E32C9AFC1B00FFAD82 /* WablePushAlarmHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WablePushAlarmHelper.swift; sourceTree = ""; }; 05AD1EB42CE4C1D900F36D6B /* Dev.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Dev.xcconfig; sourceTree = ""; }; @@ -522,6 +524,7 @@ isa = PBXGroup; children = ( 050E0CCC2C74B95B00326EEA /* Team.swift */, + 0589AECE2D383119004F531E /* PopupType.swift */, ); path = Supports; sourceTree = ""; @@ -1678,6 +1681,7 @@ 3C8B4E182C78E21900174943 /* SocialLoginResponseDTO.swift in Sources */, 0547F49A2C60D968001E3039 /* AppDelegate.swift in Sources */, 050E0CC32C74B8B600326EEA /* FeedBottomView.swift in Sources */, + 0589AECF2D38311A004F531E /* PopupType.swift in Sources */, 0547F4DF2C62486C001E3039 /* APIConstants.swift in Sources */, 050E0CD92C74BFC200326EEA /* InfoLogoView.swift in Sources */, DE54C3E42CF4EB6B00753129 /* NoticeCell.swift in Sources */, diff --git a/Wable-iOS/Presentation/Home/Supports/PopupType.swift b/Wable-iOS/Presentation/Home/Supports/PopupType.swift new file mode 100644 index 0000000..f3ce885 --- /dev/null +++ b/Wable-iOS/Presentation/Home/Supports/PopupType.swift @@ -0,0 +1,51 @@ +// +// PopupType.swift +// Wable-iOS +// +// Created by 박윤빈 on 1/16/25. +// + +import Foundation + +enum PopupViewType { + case delete + case report + case ghost + case ban + + var title: String { + switch self { + case .delete: return StringLiterals.Home.deletePopupTitle + case .report: return StringLiterals.Home.reportPopupTitle + case .ghost: return StringLiterals.Home.ghostPopupTitle + case .ban: return "밴하기 ㅋㅋ" + } + } + + var content: String { + switch self { + case .delete: return StringLiterals.Home.deletePopupContent + case .report: return StringLiterals.Home.reportPopupContent + case .ghost: return "" + case .ban: return "너이놈밴머거랏!!!" + } + } + + var leftButtonTitle: String { + switch self { + case .delete: return StringLiterals.Home.deletePopupUndo + case .report: return StringLiterals.Home.reportPopupUndo + case .ghost: return StringLiterals.Home.ghostPopupUndo + case .ban: return "함봐줌" + } + } + + var rightButtonTitle: String { + switch self { + case .delete: return StringLiterals.Home.deletePopupDo + case .report: return StringLiterals.Home.reportPopupDo + case .ghost: return StringLiterals.Home.ghostPopupDo + case .ban: return "밴ㄱㄱ" + } + } +} diff --git a/Wable-iOS/Presentation/Home/ViewController/FeedDetailViewController.swift b/Wable-iOS/Presentation/Home/ViewController/FeedDetailViewController.swift index 9578731..5ab61aa 100644 --- a/Wable-iOS/Presentation/Home/ViewController/FeedDetailViewController.swift +++ b/Wable-iOS/Presentation/Home/ViewController/FeedDetailViewController.swift @@ -837,7 +837,7 @@ extension FeedDetailViewController { extension FeedDetailViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { if nowShowingPopup == "ghost" { AmplitudeManager.shared.trackEvent(tag: "click_withdrawghost_popup") self.ghostPopupView?.removeFromSuperview() diff --git a/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift b/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift index df5446e..15d2182 100644 --- a/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift +++ b/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift @@ -6,15 +6,20 @@ // import Combine -import CombineCocoa import UIKit +import CombineCocoa import SnapKit final class HomePopupViewController: UIViewController { // MARK: - Properties + var deleteButtonDidTapAction: ((Int) -> Void)? + var ghostButtonDidTapAction: ((Int) -> Void)? + var banButtonDidTapAction: ((Int) -> Void)? + var reportButtonDidTapAction: (() -> Void)? + private let viewModel: PopupViewModel private let rootView: WablePopupView private let cancelBag = CancelBag() @@ -24,11 +29,6 @@ final class HomePopupViewController: UIViewController { private let banButtonDidTapSubject = PassthroughSubject() private let ghostButtonDidTapSubject = PassthroughSubject() - var deleteButtonDidTapAction: ((Int) -> Void)? - var ghostButtonDidTapAction: ((Int) -> Void)? - var banButtonDidTapAction: ((Int) -> Void)? - var reportButtonDidTapAction: (() -> Void)? - // MARK: - Initializer init(viewModel: PopupViewModel, popupType: PopupViewType) { @@ -43,26 +43,25 @@ final class HomePopupViewController: UIViewController { // MARK: - Life Cycles + override func loadView() { + super.loadView() + view = rootView + } + override func viewDidLoad() { super.viewDidLoad() - view = rootView setupBinding() rootView.delegate = self } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - } private extension HomePopupViewController { func setupBinding() { let input = PopupViewModel.Input( - deleteButtonDidTapSubject: deleteButtonTapSubject.eraseToAnyPublisher(), - reportButtonDidTapSubject: reportButtonDidTapSubject.eraseToAnyPublisher(), - banButtonDidTapSubject: banButtonDidTapSubject.eraseToAnyPublisher(), - ghostButtonDidTapSubject: ghostButtonDidTapSubject.eraseToAnyPublisher() + deleteButtonDidTap: deleteButtonTapSubject.eraseToAnyPublisher(), + reportButtonDidTap: reportButtonDidTapSubject.eraseToAnyPublisher(), + banButtonDidTap: banButtonDidTapSubject.eraseToAnyPublisher(), + ghostButtonDidTap: ghostButtonDidTapSubject.eraseToAnyPublisher() ) let output = viewModel.transform(from: input, cancelBag: cancelBag) @@ -106,8 +105,10 @@ extension HomePopupViewController { } } +// MARK: - WablePopupDelegate + extension HomePopupViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { self.dismiss(animated: true) } diff --git a/Wable-iOS/Presentation/Home/ViewController/HomeViewController.swift b/Wable-iOS/Presentation/Home/ViewController/HomeViewController.swift index 2510229..68a45bf 100644 --- a/Wable-iOS/Presentation/Home/ViewController/HomeViewController.swift +++ b/Wable-iOS/Presentation/Home/ViewController/HomeViewController.swift @@ -577,7 +577,7 @@ extension HomeViewController: UITableViewDelegate, UITableViewDataSource { extension HomeViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { if nowShowingPopup == "ghost" { AmplitudeManager.shared.trackEvent(tag: "click_withdrawghost_popup") self.ghostPopupView?.removeFromSuperview() diff --git a/Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift b/Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift index 74cda8f..3b316dc 100644 --- a/Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift +++ b/Wable-iOS/Presentation/Home/ViewModels/PopupViewModel.swift @@ -19,10 +19,10 @@ final class PopupViewModel { extension PopupViewModel: ViewModelType { struct Input { - let deleteButtonDidTapSubject: AnyPublisher - let reportButtonDidTapSubject: AnyPublisher - let banButtonDidTapSubject: AnyPublisher - let ghostButtonDidTapSubject: AnyPublisher + let deleteButtonDidTap: AnyPublisher + let reportButtonDidTap: AnyPublisher + let banButtonDidTap: AnyPublisher + let ghostButtonDidTap: AnyPublisher } struct Output { @@ -31,7 +31,7 @@ extension PopupViewModel: ViewModelType { func transform(from input: Input, cancelBag: CancelBag) -> Output { let dismissViewSubject = PassthroughSubject<(HomeFeedDTO, PopupViewType), Never>() - input.deleteButtonDidTapSubject + input.deleteButtonDidTap .flatMap { [weak self] _ -> AnyPublisher in guard let self else { return Just(EmptyDTO()).eraseToAnyPublisher() @@ -49,7 +49,7 @@ extension PopupViewModel: ViewModelType { } .store(in: cancelBag) - input.banButtonDidTapSubject + input.banButtonDidTap .flatMap { [weak self] _ -> AnyPublisher in guard let self else { return Just(EmptyDTO()).eraseToAnyPublisher() @@ -72,7 +72,7 @@ extension PopupViewModel: ViewModelType { } .store(in: cancelBag) - input.reportButtonDidTapSubject + input.reportButtonDidTap .flatMap { [weak self] _ -> AnyPublisher in guard let self else { return Just(EmptyDTO()).eraseToAnyPublisher() @@ -91,7 +91,7 @@ extension PopupViewModel: ViewModelType { } .store(in: cancelBag) - input.ghostButtonDidTapSubject + input.ghostButtonDidTap .flatMap { [weak self] _ -> AnyPublisher in guard let self else { return Just(EmptyDTO()).eraseToAnyPublisher() diff --git a/Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift b/Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift index 77b316e..41933c2 100644 --- a/Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift +++ b/Wable-iOS/Presentation/Home/Views/Cells/MigratedHomeFeedCell.swift @@ -26,6 +26,10 @@ final class MigratedHomeFeedCell: UICollectionViewCell{ // MARK: - Components + let infoView = FeedInfoView() + let feedContentView = FeedContentView() + let bottomView = FeedBottomView() + let divideLine = UIView().makeDivisionLine() let grayView: UIView = { let view = UIView() view.backgroundColor = .wableWhite @@ -34,31 +38,26 @@ final class MigratedHomeFeedCell: UICollectionViewCell{ return view }() - var infoView = FeedInfoView() - var feedContentView = FeedContentView() - var bottomView = FeedBottomView() - var divideLine = UIView().makeDivisionLine() - - var profileImageView: UIImageView = { + let profileImageView: UIImageView = { let imageView = UIImageView() imageView.image = ImageLiterals.Image.imgProfileSmall imageView.isUserInteractionEnabled = true return imageView }() - private var menuButton: UIButton = { - let button = UIButton() - button.setImage(ImageLiterals.Icon.icMeatball, for: .normal) - return button - }() - - var seperateLineView: UIView = { + let seperateLineView: UIView = { let view = UIView() view.backgroundColor = .gray200 view.isHidden = true return view }() + private let menuButton: UIButton = { + let button = UIButton() + button.setImage(ImageLiterals.Icon.icMeatball, for: .normal) + return button + }() + // MARK: - inits override init(frame: CGRect) { @@ -202,12 +201,8 @@ final class MigratedHomeFeedCell: UICollectionViewCell{ let memberGhost = adjustGhostValue(data.memberGhost) - if data.isGhost { - print("\(data.memberNickname)\n\(data.isGhost)") - grayView.alpha = 0.85 - } else { - grayView.alpha = CGFloat(Double(-memberGhost) / 100) - } + grayView.alpha = data.isGhost ? 0.85 : CGFloat(Double(-memberGhost) / 100) + } func changeButtonState(isLiked: Bool) { diff --git a/Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift b/Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift index a1c994d..6b47a6e 100644 --- a/Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift +++ b/Wable-iOS/Presentation/Home/Views/MigratedHomeView.swift @@ -52,20 +52,22 @@ final class MigratedHomeView: UIView { // MARK: - Extensions -extension MigratedHomeView { - private func setUI() { +private extension MigratedHomeView { + func setUI() { backgroundColor = .wableWhite loadingView.isHidden = true } - private func setHierarchy() { - self.addSubviews(homeTabView, - collectionView, - writeFeedButton, - loadingView) + func setHierarchy() { + self.addSubviews( + homeTabView, + collectionView, + writeFeedButton, + loadingView + ) } - private func setLayout() { + func setLayout() { loadingView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview() $0.top.equalToSuperview() diff --git a/Wable-iOS/Presentation/Login/ViewController/LoginViewController.swift b/Wable-iOS/Presentation/Login/ViewController/LoginViewController.swift index 50b59c7..3892092 100644 --- a/Wable-iOS/Presentation/Login/ViewController/LoginViewController.swift +++ b/Wable-iOS/Presentation/Login/ViewController/LoginViewController.swift @@ -194,7 +194,7 @@ extension LoginViewController { } extension LoginViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { } diff --git a/Wable-iOS/Presentation/MyPage/ViewController/MyPagePostViewController.swift b/Wable-iOS/Presentation/MyPage/ViewController/MyPagePostViewController.swift index f12f89f..621c399 100644 --- a/Wable-iOS/Presentation/MyPage/ViewController/MyPagePostViewController.swift +++ b/Wable-iOS/Presentation/MyPage/ViewController/MyPagePostViewController.swift @@ -435,7 +435,7 @@ extension MyPagePostViewController: UIScrollViewDelegate { extension MyPagePostViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { // if ghostPopupView != nil { // self.ghostPopupView?.removeFromSuperview() // } diff --git a/Wable-iOS/Presentation/MyPage/ViewController/MyPageReplyViewController.swift b/Wable-iOS/Presentation/MyPage/ViewController/MyPageReplyViewController.swift index f092328..4e9c4f9 100644 --- a/Wable-iOS/Presentation/MyPage/ViewController/MyPageReplyViewController.swift +++ b/Wable-iOS/Presentation/MyPage/ViewController/MyPageReplyViewController.swift @@ -414,7 +414,7 @@ extension MyPageReplyViewController: UIScrollViewDelegate { extension MyPageReplyViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { // if ghostPopupView != nil { // self.ghostPopupView?.removeFromSuperview() // } diff --git a/Wable-iOS/Presentation/MyPage/ViewController/MyPageSignOutConfirmViewController.swift b/Wable-iOS/Presentation/MyPage/ViewController/MyPageSignOutConfirmViewController.swift index 3171e80..a6a8af0 100644 --- a/Wable-iOS/Presentation/MyPage/ViewController/MyPageSignOutConfirmViewController.swift +++ b/Wable-iOS/Presentation/MyPage/ViewController/MyPageSignOutConfirmViewController.swift @@ -202,7 +202,7 @@ extension MyPageSignOutConfirmViewController { } extension MyPageSignOutConfirmViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { self.signOutPopupView.isHidden = true } diff --git a/Wable-iOS/Presentation/MyPage/ViewController/MyPageViewController.swift b/Wable-iOS/Presentation/MyPage/ViewController/MyPageViewController.swift index 2ff993c..4981de1 100644 --- a/Wable-iOS/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/Wable-iOS/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -667,7 +667,7 @@ extension MyPageViewController: UICollectionViewDelegate, UITableViewDelegate { } extension MyPageViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { if ghostPopupView != nil { self.ghostPopupView?.removeFromSuperview() } diff --git a/Wable-iOS/Presentation/UIComponents/WablePopupView.swift b/Wable-iOS/Presentation/UIComponents/WablePopupView.swift index 8df7fd2..4a1591c 100644 --- a/Wable-iOS/Presentation/UIComponents/WablePopupView.swift +++ b/Wable-iOS/Presentation/UIComponents/WablePopupView.swift @@ -9,15 +9,8 @@ import UIKit import SnapKit -enum PopupViewType { - case delete - case report - case ghost - case ban -} - protocol WablePopupDelegate: AnyObject { - func cancleButtonTapped() + func cancelButtonTapped() func confirmButtonTapped() func singleButtonTapped() } @@ -27,7 +20,7 @@ final class WablePopupView: UIView { // MARK: - Properties weak var delegate: WablePopupDelegate? - var cancelBag: CancelBag? + var cancelBag = CancelBag() var popupType: PopupViewType // MARK: - UI Components @@ -128,7 +121,7 @@ final class WablePopupView: UIView { self.popupType = popupType super.init(frame: .zero) - setInitialPopup(type: popupType) + configurePopup(type: popupType) setUI() setHierarchy() setLayout() @@ -151,7 +144,6 @@ extension WablePopupView { func setHierarchy() { self.addSubview(container) - // 팝업뷰 내용이 없는 경우 if popupContentLabel.text == "" { container.addSubviews(popupTitleLabel, buttonStackView) } else { @@ -173,7 +165,6 @@ extension WablePopupView { $0.height.equalTo(48.adjusted) } - // 팝업뷰 내용이 없는 경우 if popupContentLabel.text == "" { popupTitleLabel.snp.makeConstraints { $0.top.equalToSuperview().inset(32.adjusted) @@ -201,57 +192,24 @@ extension WablePopupView { } func setAddTarget() { - self.cancleButton.addTarget(self, action: #selector(cancleButtonTapped), for: .touchUpInside) + self.cancleButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) self.confirmButton.addTarget(self, action: #selector(confirmButtonTapped), for: .touchUpInside) } - func setInitialPopup(type: PopupViewType) { - switch type { - case .delete: - popupTitleLabel.setTextWithLineHeight( - text: StringLiterals.Home.deletePopupTitle, - lineHeight: 28.8.adjusted, - alignment: .center - ) - popupContentLabel.text = StringLiterals.Home.deletePopupContent - cancleButton.setTitle(StringLiterals.Home.deletePopupUndo, for: .normal) - confirmButton.setTitle(StringLiterals.Home.deletePopupDo, for: .normal) - - case .report: - popupTitleLabel.setTextWithLineHeight( - text: StringLiterals.Home.reportPopupTitle, - lineHeight: 28.8.adjusted, - alignment: .center - ) - popupContentLabel.text = StringLiterals.Home.reportPopupContent - cancleButton.setTitle(StringLiterals.Home.reportPopupUndo, for: .normal) - confirmButton.setTitle(StringLiterals.Home.reportPopupDo, for: .normal) - - case .ghost: - popupTitleLabel.setTextWithLineHeight( - text: StringLiterals.Home.ghostPopupTitle, - lineHeight: 28.8.adjusted, - alignment: .center - ) - popupContentLabel.text = "" - cancleButton.setTitle(StringLiterals.Home.ghostPopupUndo, for: .normal) - confirmButton.setTitle(StringLiterals.Home.ghostPopupDo, for: .normal) - case .ban: - popupTitleLabel.setTextWithLineHeight( - text: "밴하기 ㅋㅋ", - lineHeight: 28.8.adjusted, - alignment: .center - ) - popupContentLabel.text = "너이놈밴머거랏!!!" - cancleButton.setTitle("함봐줌", for: .normal) - confirmButton.setTitle("밴ㄱㄱ", for: .normal) - } + private func configurePopup(type: PopupViewType) { + popupTitleLabel.setTextWithLineHeight( + text: type.title, + lineHeight: 28.8.adjusted, + alignment: .center + ) + popupContentLabel.text = type.content + cancleButton.setTitle(type.leftButtonTitle, for: .normal) + confirmButton.setTitle(type.rightButtonTitle, for: .normal) } func setSingleHierarchy() { self.addSubview(container) - // 팝업뷰 내용이 없는 경우 if popupContentLabel.text == "" { container.addSubviews(popupTitleLabel, buttonStackView) } else { @@ -304,8 +262,8 @@ extension WablePopupView { } @objc - func cancleButtonTapped() { - delegate?.cancleButtonTapped() + func cancelButtonTapped() { + delegate?.cancelButtonTapped() } @objc diff --git a/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift b/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift index 12c7138..a2ddbd1 100644 --- a/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift +++ b/Wable-iOS/Presentation/Write/ViewController/WriteViewController.swift @@ -280,7 +280,7 @@ extension WriteViewController: PHPickerViewControllerDelegate { } extension WriteViewController: WablePopupDelegate { - func cancleButtonTapped() { + func cancelButtonTapped() { self.writeCanclePopupView?.removeFromSuperview() } From 4fbfb66ec3125e1c1a9a2a5cf65d0233b8e60969 Mon Sep 17 00:00:00 2001 From: binisnull Date: Fri, 17 Jan 2025 18:51:39 +0900 Subject: [PATCH 16/16] =?UTF-8?q?[Fix]=20#87=20-=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=B5=9C=EC=A2=85=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/ViewController/HomePopupViewController.swift | 1 - Wable-iOS/Presentation/UIComponents/WablePopupView.swift | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift b/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift index 15d2182..dbd6cda 100644 --- a/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift +++ b/Wable-iOS/Presentation/Home/ViewController/HomePopupViewController.swift @@ -44,7 +44,6 @@ final class HomePopupViewController: UIViewController { // MARK: - Life Cycles override func loadView() { - super.loadView() view = rootView } diff --git a/Wable-iOS/Presentation/UIComponents/WablePopupView.swift b/Wable-iOS/Presentation/UIComponents/WablePopupView.swift index 4a1591c..094ddf9 100644 --- a/Wable-iOS/Presentation/UIComponents/WablePopupView.swift +++ b/Wable-iOS/Presentation/UIComponents/WablePopupView.swift @@ -20,7 +20,7 @@ final class WablePopupView: UIView { // MARK: - Properties weak var delegate: WablePopupDelegate? - var cancelBag = CancelBag() + private var cancelBag = CancelBag() var popupType: PopupViewType // MARK: - UI Components @@ -136,7 +136,7 @@ final class WablePopupView: UIView { // MARK: - Extensions -extension WablePopupView { +private extension WablePopupView { func setUI() { self.backgroundColor = .wableBlack.withAlphaComponent(0.5) } @@ -196,7 +196,7 @@ extension WablePopupView { self.confirmButton.addTarget(self, action: #selector(confirmButtonTapped), for: .touchUpInside) } - private func configurePopup(type: PopupViewType) { + func configurePopup(type: PopupViewType) { popupTitleLabel.setTextWithLineHeight( text: type.title, lineHeight: 28.8.adjusted,