diff --git a/SOPT-iOS/Projects/Features/AppMyPageFeature/Sources/AppMypageScene/AppMyPageViewController.swift b/SOPT-iOS/Projects/Features/AppMyPageFeature/Sources/AppMypageScene/AppMyPageVC.swift similarity index 93% rename from SOPT-iOS/Projects/Features/AppMyPageFeature/Sources/AppMypageScene/AppMyPageViewController.swift rename to SOPT-iOS/Projects/Features/AppMyPageFeature/Sources/AppMypageScene/AppMyPageVC.swift index 8ee194d9e..3800779f5 100644 --- a/SOPT-iOS/Projects/Features/AppMyPageFeature/Sources/AppMypageScene/AppMyPageViewController.swift +++ b/SOPT-iOS/Projects/Features/AppMyPageFeature/Sources/AppMypageScene/AppMyPageVC.swift @@ -227,27 +227,27 @@ extension AppMyPageVC { // TODO: - (@승호): 적절히 객체에 위임하기 private func addTabGestureOnListItems() { - self.servicePolicySectionGroup.addTapGestureRecognizer { - self.onPolicyItemTap?() + self.servicePolicySectionGroup.addTapGestureRecognizer { [weak self] in + self?.onPolicyItemTap?() } - self.termsOfUseListItem.addTapGestureRecognizer { - self.onTermsOfUseItemTap?() + self.termsOfUseListItem.addTapGestureRecognizer { [weak self] in + self?.onTermsOfUseItemTap?() } self.sendFeedbackListItem.addTapGestureRecognizer { openExternalLink(urlStr: ExternalURL.KakaoTalk.serviceProposal) } - self.alertListItem.addTapGestureRecognizer { - self.onAlertButtonTap?(UIApplication.openSettingsURLString) + self.alertListItem.addTapGestureRecognizer { [weak self] in + self?.onAlertButtonTap?(UIApplication.openSettingsURLString) } - self.editOnelineSentenceListItem.addTapGestureRecognizer { - self.onEditOnelineSentenceItemTap?() + self.editOnelineSentenceListItem.addTapGestureRecognizer { [weak self] in + self?.onEditOnelineSentenceItemTap?() } - self.resetStampListItem.addTapGestureRecognizer { + self.resetStampListItem.addTapGestureRecognizer { [weak self] in AlertUtils.presentAlertVC( type: .titleDescription, theme: .main, @@ -261,7 +261,7 @@ extension AppMyPageVC { ) } - self.logoutListItem.addTapGestureRecognizer { + self.logoutListItem.addTapGestureRecognizer { [weak self] in AlertUtils.presentAlertVC( type: .titleDescription, theme: .main, @@ -275,12 +275,12 @@ extension AppMyPageVC { ) } - self.withDrawalListItem.addTapGestureRecognizer { - self.onWithdrawalItemTap?(self.userType) + self.withDrawalListItem.addTapGestureRecognizer { [weak self] in + self?.onWithdrawalItemTap?(self?.userType ?? .visitor) } - self.loginListItem.addTapGestureRecognizer { - self.onShowLogin?() + self.loginListItem.addTapGestureRecognizer { [weak self] in + self?.onShowLogin?() } } } diff --git a/SOPT-iOS/Projects/Features/HomeFeature/Sources/CalendarDetailScene/VC/HomeCalendarDetailVC.swift b/SOPT-iOS/Projects/Features/HomeFeature/Sources/CalendarDetailScene/VC/HomeCalendarDetailVC.swift index 3625314a8..d28daac50 100644 --- a/SOPT-iOS/Projects/Features/HomeFeature/Sources/CalendarDetailScene/VC/HomeCalendarDetailVC.swift +++ b/SOPT-iOS/Projects/Features/HomeFeature/Sources/CalendarDetailScene/VC/HomeCalendarDetailVC.swift @@ -26,8 +26,7 @@ final class HomeCalendarDetailVC: UIViewController, HomeCalendarDetailViewContro // MARK: UI Components - private lazy var naviBar = OPNavigationBar(self, - type: .oneLeftButton, + private lazy var naviBar = OPNavigationBar(self, type: .oneLeftButton, backgroundColor: DSKitAsset.Colors.semanticBackground.color) .addMiddleLabel(title: I18N.Home.CalendarDetail.navigationTitle, font: DSKitFontFamily.Suit.medium.font(size: 16)) @@ -142,7 +141,7 @@ extension HomeCalendarDetailVC { private func bindViewModels() { let input = HomeCalendarDetailViewModel.Input( viewDidLoad: Just(()).asDriver(), - naviBackButtonTap: self.naviBar.leftButtonTapped.asDriver(), + naviBackButtonTap: self.naviBar.leftButtonTapped, onAttendanceButtonTap: self.attendanceButton .publisher(for: .touchUpInside) .withUnretained(self) @@ -157,7 +156,7 @@ extension HomeCalendarDetailVC { .sink { owner, calendarDetailInfo in owner.calendarDetailInfo = calendarDetailInfo owner.collectionView.reloadData() - self.scrollToRecentSchedule() + owner.scrollToRecentSchedule() }.store(in: cancelBag) } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeCoordinator.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeCoordinator.swift index cd787fc18..5b51c7218 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeCoordinator.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeCoordinator.swift @@ -57,21 +57,21 @@ final class PokeCoordinator: DefaultCoordinator { pokeMain.vm.onPokeButtonTapped = { [weak self] userModel in guard let self else { return .empty() } - return self.showMessageBottomSheet(userModel: userModel, on: pokeMain.vc.viewController) + return self.showMessageBottomSheet(userModel: userModel, on: self.rootController) } pokeMain.vm.onNewFriendMade = { [weak self] friendName in guard let self else { return } let pokeMakingFriendCompletedVC = self.factory.makePokeMakingFriendCompleted(friendName: friendName).viewController pokeMakingFriendCompletedVC.modalPresentationStyle = .overFullScreen - pokeMain.vc.viewController.present(pokeMakingFriendCompletedVC, animated: false) + self.rootController?.present(pokeMakingFriendCompletedVC, animated: false) } pokeMain.vm.onAnonymousFriendUpgrade = { [weak self] user in guard let self else { return } let pokeAnonymousFriendUpgradeVC = self.factory.makePokeAnonymousFriendUpgrade(user: user).viewController pokeAnonymousFriendUpgradeVC.modalPresentationStyle = .overFullScreen - pokeMain.vc.viewController.present(pokeAnonymousFriendUpgradeVC, animated: false) + self.rootController?.present(pokeAnonymousFriendUpgradeVC, animated: false) } pokeMain.vm.switchToOnboarding = { [weak self] in diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift index ac13017bc..95a34473a 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift @@ -103,11 +103,11 @@ public final class PokeMainVC: UIViewController, PokeMainViewControllable { public override func viewDidLoad() { super.viewDidLoad() - self.setUI() - self.setDelegate() - self.setStackView() - self.setLayout() - self.bindViewModel() + setUI() + setDelegate() + setStackView() + setLayout() + bindViewModel() } } @@ -298,8 +298,9 @@ extension PokeMainVC { }.store(in: cancelBag) output.isLoading - .sink { [weak self] isLoading in - isLoading ? self?.showLoading() : self?.stopLoading() - }.store(in: self.cancelBag) + .withUnretained(self) + .sink { owner, isLoading in + isLoading ? owner.showLoading() : owner.stopLoading() + }.store(in: cancelBag) } } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift index e11c61ded..acb1f9219 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift @@ -23,7 +23,7 @@ extension StampBuilder: StampFeatureViewBuildable { public func makeMissionListVC(sceneType: MissionListSceneType) -> MissionListViewControllable { let useCase = DefaultMissionListUseCase(repository: missionListRepository) let viewModel = MissionListViewModel(useCase: useCase, sceneType: sceneType) - let missionListVC = MissionListVC() + let missionListVC = MissionListVC(viewModel: viewModel) missionListVC.viewModel = viewModel return missionListVC } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/CollectionView/MissionListCompositionalLayout.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/CollectionView/MissionListCompositionalLayout.swift index f402f89a4..a4a87d7c1 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/CollectionView/MissionListCompositionalLayout.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/CollectionView/MissionListCompositionalLayout.swift @@ -12,10 +12,10 @@ extension MissionListVC { static let standardWidth = UIScreen.main.bounds.width - 40.adjusted func createLayout() -> UICollectionViewLayout { - return UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in switch MissionListSection.type(sectionIndex) { - case .sentence: return self.createSentenceSection() - case .missionList: return self.createMissionListSection() + case .sentence: return self?.createSentenceSection() + case .missionList: return self?.createMissionListSection() } } } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift index 6ac291846..0e5b0fb71 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift @@ -20,439 +20,448 @@ import StampFeatureInterface import BaseFeatureDependency public class MissionListVC: UIViewController, MissionListViewControllable { - - // MARK: - Properties - - public var viewModel: MissionListViewModel! - public var sceneType: MissionListSceneType { - return self.viewModel.missionListsceneType - } - private var cancelBag = CancelBag() - - private var missionTypeMenuSelected = CurrentValueSubject(.all) - private var viewWillAppear = PassthroughSubject() - private let swipeHandler = PassthroughSubject() - - lazy var dataSource: UICollectionViewDiffableDataSource! = nil - - // MARK: - MissionListCoordinatable - - public var onSwiped: (() -> Void)? - public var onNaviBackTap: (() -> Void)? - public var onPartRankingButtonTap: ((RankingViewType) -> Void)? - public var onCurrentGenerationRankingButtonTap: ((RankingViewType) -> Void)? - public var onGuideTap: (() -> Void)? - public var onCellTap: ((MissionListModel, String?) -> Void)? - public var onReportButtonTap: (() -> Void)? - - private var usersActiveGenerationStatus: UsersActiveGenerationStatusViewResponse? - - // MARK: - UI Components - - lazy var naviBar: STNavigationBar = { - switch sceneType { - case .default: - return STNavigationBar(type: .title) - .setTitle("전체 미션") - .setTitleTypoStyle(.SoptampFont.h2) - .setTitleButtonMenu(menuItems: self.menuItems) - .addLeftButtonToTitleMenu() - case .ranking(let username, _): - return STNavigationBar(type: .titleWithLeftButton) - .setTitle(username) - .setRightButton(.none) - .setTitleTypoStyle(.SoptampFont.h2) + + // MARK: - Properties + + public var viewModel: MissionListViewModel + public var sceneType: MissionListSceneType { + return self.viewModel.missionListsceneType } - }() - - private lazy var menuItems: [UIAction] = { - var menuItems: [UIAction] = [] - [("전체 미션", MissionListFetchType.all), - ("완료 미션", MissionListFetchType.complete), - ("미완료 미션", MissionListFetchType.incomplete)].forEach { menuTitle, fetchType in - menuItems.append(UIAction(title: menuTitle, - handler: { _ in - self.missionTypeMenuSelected.send(fetchType) - self.naviBar.setTitle(menuTitle) - })) + private var cancelBag = CancelBag() + + private var missionTypeMenuSelected = CurrentValueSubject(.all) + private var viewWillAppear = PassthroughSubject() + private let swipeHandler = PassthroughSubject() + + lazy var dataSource: UICollectionViewDiffableDataSource! = nil + + // MARK: - MissionListCoordinatable + + public var onSwiped: (() -> Void)? + public var onNaviBackTap: (() -> Void)? + public var onPartRankingButtonTap: ((RankingViewType) -> Void)? + public var onCurrentGenerationRankingButtonTap: ((RankingViewType) -> Void)? + public var onGuideTap: (() -> Void)? + public var onCellTap: ((MissionListModel, String?) -> Void)? + public var onReportButtonTap: (() -> Void)? + + private var usersActiveGenerationStatus: UsersActiveGenerationStatusViewResponse? + + // MARK: - UI Components + + lazy var naviBar: STNavigationBar = { + switch sceneType { + case .default: + return STNavigationBar(type: .title) + .setTitle("전체 미션") + .setTitleTypoStyle(.SoptampFont.h2) + .setTitleButtonMenu(menuItems: self.menuItems) + .addLeftButtonToTitleMenu() + case .ranking(let username, _): + return STNavigationBar(type: .titleWithLeftButton) + .setTitle(username) + .setRightButton(.none) + .setTitleTypoStyle(.SoptampFont.h2) + } + }() + + private lazy var menuItems: [UIAction] = { + var menuItems: [UIAction] = [] + [("전체 미션", MissionListFetchType.all), + ("완료 미션", MissionListFetchType.complete), + ("미완료 미션", MissionListFetchType.incomplete)].forEach { [weak self] menuTitle, fetchType in + menuItems.append(UIAction(title: menuTitle, + handler: { [weak self] _ in + self?.missionTypeMenuSelected.send(fetchType) + self?.naviBar.setTitle(menuTitle) + })) + } + return menuItems + }() + + private lazy var sentenceLabel: SentencePaddingLabel = { + let lb = SentencePaddingLabel() + if case let .ranking(_, sentence) = sceneType { + lb.text = sentence + } + lb.setTypoStyle(.SoptampFont.subtitle1) + lb.textColor = DSKitAsset.Colors.soptampGray900.color + lb.numberOfLines = 2 + lb.textAlignment = .center + lb.backgroundColor = DSKitAsset.Colors.soptampPurple100.color + lb.layer.cornerRadius = 9.adjustedH + lb.clipsToBounds = true + lb.setCharacterSpacing(0) + return lb + }() + + private lazy var missionListCollectionView: UICollectionView = { + let cv = UICollectionView(frame: .zero, collectionViewLayout: self.createLayout()) + cv.showsVerticalScrollIndicator = true + cv.backgroundColor = .white + cv.bounces = false + return cv + }() + + private let missionListEmptyView = MissionListEmptyView() + + private lazy var floatingButtonStackView = UIStackView(frame: self.view.frame).then { + $0.axis = .horizontal + $0.spacing = 0.f } - return menuItems - }() - - private lazy var sentenceLabel: SentencePaddingLabel = { - let lb = SentencePaddingLabel() - if case let .ranking(_, sentence) = sceneType { - lb.text = sentence + + private lazy var currentGenerationRankFloatingButton: UIButton = { + let bt = UIButton() + bt.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + bt.layer.cornerRadius = 27.adjustedH + bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color, for: .normal) + bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color.withAlphaComponent(0.2), for: .selected) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) + bt.tintColor = .white + bt.titleLabel?.setTypoStyle(.SoptampFont.h2) + bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) + bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) + return bt + }() + + private lazy var partRankingFloatingButton: UIButton = { + let bt = UIButton() + bt.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + bt.layer.cornerRadius = 27.adjustedH + bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color, for: .normal) + bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color.withAlphaComponent(0.2), for: .selected) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) + bt.tintColor = .white + bt.titleLabel?.setTypoStyle(.SoptampFont.h2) + let attributedStr = NSMutableAttributedString(string: "파트별 랭킹") + let style = NSMutableParagraphStyle() + attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) + attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) + bt.setAttributedTitle(attributedStr, for: .normal) + bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) + bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) + return bt + }() + + // MARK: - View Life Cycle + + public override func viewDidLoad() { + super.viewDidLoad() + self.setUI() + self.setLayout() + self.setDelegate() + self.setGesture() + self.registerCells() + self.setDataSource() + self.bindViews() + self.bindViewModels() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.viewWillAppear.send(()) + self.navigationController?.interactivePopGestureRecognizer?.delegate = self + } + + init(viewModel: MissionListViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - lb.setTypoStyle(.SoptampFont.subtitle1) - lb.textColor = DSKitAsset.Colors.soptampGray900.color - lb.numberOfLines = 2 - lb.textAlignment = .center - lb.backgroundColor = DSKitAsset.Colors.soptampPurple100.color - lb.layer.cornerRadius = 9.adjustedH - lb.clipsToBounds = true - lb.setCharacterSpacing(0) - return lb - }() - - private lazy var missionListCollectionView: UICollectionView = { - let cv = UICollectionView(frame: .zero, collectionViewLayout: self.createLayout()) - cv.showsVerticalScrollIndicator = true - cv.backgroundColor = .white - cv.bounces = false - return cv - }() - - private let missionListEmptyView = MissionListEmptyView() - - private lazy var floatingButtonStackView = UIStackView(frame: self.view.frame).then { - $0.axis = .horizontal - $0.spacing = 0.f - } - - private lazy var currentGenerationRankFloatingButton: UIButton = { - let bt = UIButton() - bt.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] - bt.layer.cornerRadius = 27.adjustedH - bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color, for: .normal) - bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color.withAlphaComponent(0.2), for: .selected) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) - bt.tintColor = .white - bt.titleLabel?.setTypoStyle(.SoptampFont.h2) - bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) - bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - return bt - }() - - private lazy var partRankingFloatingButton: UIButton = { - let bt = UIButton() - bt.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] - bt.layer.cornerRadius = 27.adjustedH - bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color, for: .normal) - bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color.withAlphaComponent(0.2), for: .selected) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) - bt.tintColor = .white - bt.titleLabel?.setTypoStyle(.SoptampFont.h2) - let attributedStr = NSMutableAttributedString(string: "파트별 랭킹") - let style = NSMutableParagraphStyle() - attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) - attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) - bt.setAttributedTitle(attributedStr, for: .normal) - bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) - bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - return bt - }() - - // MARK: - View Life Cycle - - public override func viewDidLoad() { - super.viewDidLoad() - self.setUI() - self.setLayout() - self.setDelegate() - self.setGesture() - self.registerCells() - self.setDataSource() - self.bindViews() - self.bindViewModels() - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.viewWillAppear.send(()) - self.navigationController?.interactivePopGestureRecognizer?.delegate = self - } } // MARK: - UI & Layouts extension MissionListVC { - - private func setUI() { - self.view.backgroundColor = .white - self.navigationController?.isNavigationBarHidden = true - } - - private func setLayout() { - self.view.addSubviews(naviBar, missionListCollectionView) - - naviBar.snp.makeConstraints { make in - make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) - } - - missionListCollectionView.snp.makeConstraints { make in - make.top.equalTo(naviBar.snp.bottom).offset(20.adjustedH) - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() + + private func setUI() { + self.view.backgroundColor = .white + self.navigationController?.isNavigationBarHidden = true } - - switch sceneType { - case .default: - self.view.addSubview(self.floatingButtonStackView) - - self.floatingButtonStackView.snp.makeConstraints { make in - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-18.adjustedH) - make.centerX.equalToSuperview() - } - - self.partRankingFloatingButton.snp.makeConstraints { - $0.width.equalTo(143.adjusted) - $0.height.equalTo(54.adjustedH) - } - - self.currentGenerationRankFloatingButton.snp.makeConstraints { - $0.width.equalTo(143.adjusted) - $0.height.equalTo(54.adjustedH) - } - - case .ranking: - self.view.addSubview(sentenceLabel) - - sentenceLabel.snp.makeConstraints { make in - make.top.equalTo(naviBar.snp.bottom).offset(10.adjustedH) - make.leading.trailing.equalToSuperview().inset(20.adjusted) - make.height.equalTo(64.adjustedH) - } - - missionListCollectionView.snp.remakeConstraints { make in - make.top.equalTo(sentenceLabel.snp.bottom).offset(16.adjustedH) - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } + + private func setLayout() { + self.view.addSubviews(naviBar, missionListCollectionView) + + naviBar.snp.makeConstraints { make in + make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) + } + + missionListCollectionView.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom).offset(20.adjustedH) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + + switch sceneType { + case .default: + self.view.addSubview(self.floatingButtonStackView) + + self.floatingButtonStackView.snp.makeConstraints { make in + make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-18.adjustedH) + make.centerX.equalToSuperview() + } + + self.partRankingFloatingButton.snp.makeConstraints { + $0.width.equalTo(143.adjusted) + $0.height.equalTo(54.adjustedH) + } + + self.currentGenerationRankFloatingButton.snp.makeConstraints { + $0.width.equalTo(143.adjusted) + $0.height.equalTo(54.adjustedH) + } + + case .ranking: + self.view.addSubview(sentenceLabel) + + sentenceLabel.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom).offset(10.adjustedH) + make.leading.trailing.equalToSuperview().inset(20.adjusted) + make.height.equalTo(64.adjustedH) + } + + missionListCollectionView.snp.remakeConstraints { make in + make.top.equalTo(sentenceLabel.snp.bottom).offset(16.adjustedH) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + } } - } } // MARK: - Methods extension MissionListVC { - private func bindViews() { - - naviBar.rightButtonTapped - .asDriver() - .withUnretained(self) - .sink { owner, _ in - owner.onGuideTap?() - }.store(in: self.cancelBag) - - naviBar.leftButtonTapped - .withUnretained(self) - .sink { owner, _ in - owner.onNaviBackTap?() - }.store(in: self.cancelBag) - - partRankingFloatingButton.publisher(for: .touchUpInside) - .withUnretained(self) - .sink { owner, _ in - owner.onPartRankingButtonTap?(.partRanking) - }.store(in: self.cancelBag) - - currentGenerationRankFloatingButton.publisher(for: .touchUpInside) - .withUnretained(self) - .sink { owner, _ in - guard let usersActiveGenerationStatus = owner.usersActiveGenerationStatus else { return } - - owner.onCurrentGenerationRankingButtonTap?(.currentGeneration(info: usersActiveGenerationStatus)) - }.store(in: self.cancelBag) - - swipeHandler - .first() - .withUnretained(self) - .sink { owner, _ in - owner.onSwiped?() - }.store(in: self.cancelBag) - } - - private func bindViewModels() { - let input = MissionListViewModel.Input( - viewDidLoad: Driver.just(()), - viewWillAppear: viewWillAppear.asDriver(), - missionTypeSelected: missionTypeMenuSelected - ) - - let output = self.viewModel.transform(from: input, cancelBag: self.cancelBag) - - naviBar.reportButtonTapped - .withUnretained(self) - .sink { owner, _ in - owner.onReportButtonTap?() - }.store(in: cancelBag) - - output.$missionListModel - .compactMap { $0 } - .sink { [weak self] model in - self?.setCollectionView(model: model) - }.store(in: self.cancelBag) - - output.$usersActivateGenerationStatus - .compactMap { $0 } - .sink { [weak self] generationStatus in - guard generationStatus.status == .ACTIVE else { return } - - self?.usersActiveGenerationStatus = generationStatus - self?.remakeButtonConstraint() - self?.configureCurrentGenerationButton(with: String(describing: generationStatus.currentGeneration)) - }.store(in: self.cancelBag) - - output.needNetworkAlert - .withUnretained(self) - .sink { owner, _ in - owner.showNetworkAlert() - }.store(in: self.cancelBag) - } + private func bindViews() { + + naviBar.rightButtonTapped + .asDriver() + .withUnretained(self) + .sink { owner, _ in + owner.onGuideTap?() + }.store(in: self.cancelBag) + + naviBar.leftButtonTapped + .withUnretained(self) + .sink { owner, _ in + owner.onNaviBackTap?() + }.store(in: self.cancelBag) + + partRankingFloatingButton.publisher(for: .touchUpInside) + .withUnretained(self) + .sink { owner, _ in + owner.onPartRankingButtonTap?(.partRanking) + }.store(in: self.cancelBag) + + currentGenerationRankFloatingButton.publisher(for: .touchUpInside) + .withUnretained(self) + .sink { owner, _ in + guard let usersActiveGenerationStatus = owner.usersActiveGenerationStatus else { return } + + owner.onCurrentGenerationRankingButtonTap?(.currentGeneration(info: usersActiveGenerationStatus)) + }.store(in: self.cancelBag) + + swipeHandler + .first() + .withUnretained(self) + .sink { owner, _ in + owner.onSwiped?() + }.store(in: self.cancelBag) + } + + private func bindViewModels() { + let input = MissionListViewModel.Input( + viewDidLoad: Driver.just(()), + viewWillAppear: viewWillAppear.asDriver(), + missionTypeSelected: missionTypeMenuSelected + ) + + let output = self.viewModel.transform(from: input, cancelBag: self.cancelBag) + + naviBar.reportButtonTapped + .withUnretained(self) + .sink { owner, _ in + owner.onReportButtonTap?() + }.store(in: cancelBag) + + output.$missionListModel + .compactMap { $0 } + .sink { [weak self] model in + self?.setCollectionView(model: model) + }.store(in: self.cancelBag) + + output.$usersActivateGenerationStatus + .compactMap { $0 } + .sink { [weak self] generationStatus in + guard generationStatus.status == .ACTIVE else { return } + + self?.usersActiveGenerationStatus = generationStatus + self?.remakeButtonConstraint() + self?.configureCurrentGenerationButton(with: String(describing: generationStatus.currentGeneration)) + }.store(in: self.cancelBag) + + output.needNetworkAlert + .withUnretained(self) + .sink { owner, _ in + owner.showNetworkAlert() + }.store(in: self.cancelBag) + } } extension MissionListVC { - - private func setDelegate() { - missionListCollectionView.delegate = self - } - - private func setGesture() { - self.setGesture(to: missionListCollectionView) - self.setGesture(to: missionListEmptyView) - } - - private func setGesture(to view: UIView) { - let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(swipeBack(_:))) - swipeGesture.delegate = self - view.addGestureRecognizer(swipeGesture) - } - - @objc - private func swipeBack(_ sender: UIPanGestureRecognizer) { - let velocity = sender.velocity(in: self.view) - let velocityMinimum: CGFloat = 1000 - guard let navigation = self.navigationController else { return } - let isScrollY: Bool = abs(velocity.x) > abs(velocity.y) + 200 - let isNotRootView = navigation.viewControllers.count >= 2 - if velocity.x >= velocityMinimum - && isNotRootView - && isScrollY { - self.missionListCollectionView.isScrollEnabled = false - swipeHandler.send(()) + + private func setDelegate() { + missionListCollectionView.delegate = self } - } - - private func registerCells() { - MissionListCVC.register(target: missionListCollectionView) - } - - private func setDataSource() { - dataSource = UICollectionViewDiffableDataSource(collectionView: missionListCollectionView, cellProvider: { collectionView, indexPath, itemIdentifier in - switch MissionListSection.type(indexPath.section) { - case .sentence: - guard let sentenceCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } - return sentenceCell - - case .missionList: - guard let missionListCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } - let missionListModel = itemIdentifier - missionListCell.initCellType = missionListModel.toCellType() - missionListCell.setData(model: missionListModel) - return missionListCell - } - }) - } - - func setCollectionView(model: [MissionListModel]) { - if model.isEmpty { - self.missionListCollectionView.isHidden = true - self.missionListEmptyView.isHidden = false - self.setEmptyView() - } else { - self.missionListCollectionView.isHidden = false - self.missionListEmptyView.isHidden = true - self.applySnapshot(model: model) + + private func setGesture() { + self.setGesture(to: missionListCollectionView) + self.setGesture(to: missionListEmptyView) } - } - - private func setEmptyView() { - missionListEmptyView.snp.removeConstraints() - missionListEmptyView.removeFromSuperview() - self.view.addSubviews(missionListEmptyView) - missionListEmptyView.snp.makeConstraints { make in - make.top.equalTo(naviBar.snp.bottom).offset(145.adjustedH) - make.centerX.equalToSuperview() - make.width.equalToSuperview() - make.bottom.equalToSuperview().priority(.low) + + private func setGesture(to view: UIView) { + let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(swipeBack(_:))) + swipeGesture.delegate = self + view.addGestureRecognizer(swipeGesture) } - bringRankingFloatingButtonToFront() - } - - private func bringRankingFloatingButtonToFront() { - self.view.subviews.forEach { view in - if view == self.partRankingFloatingButton { - self.view.bringSubviewToFront(partRankingFloatingButton) - } + + @objc + private func swipeBack(_ sender: UIPanGestureRecognizer) { + let velocity = sender.velocity(in: self.view) + let velocityMinimum: CGFloat = 1000 + guard let navigation = self.navigationController else { return } + let isScrollY: Bool = abs(velocity.x) > abs(velocity.y) + 200 + let isNotRootView = navigation.viewControllers.count >= 2 + if velocity.x >= velocityMinimum + && isNotRootView + && isScrollY { + self.missionListCollectionView.isScrollEnabled = false + swipeHandler.send(()) + } + } + + private func registerCells() { + MissionListCVC.register(target: missionListCollectionView) + } + + private func setDataSource() { + dataSource = UICollectionViewDiffableDataSource(collectionView: missionListCollectionView, cellProvider: { collectionView, indexPath, itemIdentifier in + switch MissionListSection.type(indexPath.section) { + case .sentence: + guard let sentenceCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } + return sentenceCell + + case .missionList: + guard let missionListCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } + let missionListModel = itemIdentifier + missionListCell.initCellType = missionListModel.toCellType() + missionListCell.setData(model: missionListModel) + return missionListCell + } + }) + } + + func setCollectionView(model: [MissionListModel]) { + if model.isEmpty { + self.missionListCollectionView.isHidden = true + self.missionListEmptyView.isHidden = false + self.setEmptyView() + } else { + self.missionListCollectionView.isHidden = false + self.missionListEmptyView.isHidden = true + self.applySnapshot(model: model) + } + } + + private func setEmptyView() { + missionListEmptyView.snp.removeConstraints() + missionListEmptyView.removeFromSuperview() + self.view.addSubviews(missionListEmptyView) + missionListEmptyView.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom).offset(145.adjustedH) + make.centerX.equalToSuperview() + make.width.equalToSuperview() + make.bottom.equalToSuperview().priority(.low) + } + bringRankingFloatingButtonToFront() + } + + private func bringRankingFloatingButtonToFront() { + self.view.subviews.forEach { view in + if view == self.partRankingFloatingButton { + self.view.bringSubviewToFront(partRankingFloatingButton) + } + } + } + + private func applySnapshot(model: [MissionListModel]) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.sentence, .missionList]) + snapshot.appendItems(model, toSection: .missionList) + dataSource.apply(snapshot, animatingDifferences: false) + self.view.setNeedsLayout() + } + + private func remakeButtonConstraint() { + self.floatingButtonStackView.addArrangedSubviews(self.currentGenerationRankFloatingButton, self.partRankingFloatingButton) + } + + private func configureCurrentGenerationButton(with generation: String) { + let attributedStr = NSMutableAttributedString(string: "\(generation)기 랭킹") + let style = NSMutableParagraphStyle() + attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) + attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) + self.currentGenerationRankFloatingButton.setAttributedTitle(attributedStr, for: .normal) + } + + private func showNetworkAlert() { + AlertUtils.presentNetworkAlertVC( + theme: .soptamp, + animated: true + ) } - } - - private func applySnapshot(model: [MissionListModel]) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.sentence, .missionList]) - snapshot.appendItems(model, toSection: .missionList) - dataSource.apply(snapshot, animatingDifferences: false) - self.view.setNeedsLayout() - } - - private func remakeButtonConstraint() { - self.floatingButtonStackView.addArrangedSubviews(self.currentGenerationRankFloatingButton, self.partRankingFloatingButton) - } - - private func configureCurrentGenerationButton(with generation: String) { - let attributedStr = NSMutableAttributedString(string: "\(generation)기 랭킹") - let style = NSMutableParagraphStyle() - attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) - attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) - self.currentGenerationRankFloatingButton.setAttributedTitle(attributedStr, for: .normal) - } - - private func showNetworkAlert() { - AlertUtils.presentNetworkAlertVC( - theme: .soptamp, - animated: true - ) - } } // MARK: - UICollectionViewDelegate extension MissionListVC: UICollectionViewDelegate { - public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - switch indexPath.section { - case 0: - return false - case 1: - switch self.sceneType { - case .default: - return true - case .ranking: - return true - } - default: - return false + public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + switch indexPath.section { + case 0: + return false + case 1: + switch self.sceneType { + case .default: + return true + case .ranking: + return true + } + default: + return false + } } - } - - public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - switch indexPath.section { - case 0: - return - case 1: - guard let tappedCell = collectionView.cellForItem(at: indexPath) as? MissionListCVC, - let model = tappedCell.model else { return } - onCellTap?(model, sceneType.usrename) - default: - return + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + switch indexPath.section { + case 0: + return + case 1: + guard let tappedCell = collectionView.cellForItem(at: indexPath) as? MissionListCVC, + let model = tappedCell.model else { return } + onCellTap?(model, sceneType.usrename) + default: + return + } } - } } extension MissionListVC: UIGestureRecognizerDelegate { - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/ViewModel/MissionListViewModel.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/ViewModel/MissionListViewModel.swift index eb8cdeb11..05678093b 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/ViewModel/MissionListViewModel.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/ViewModel/MissionListViewModel.swift @@ -101,7 +101,7 @@ extension MissionListViewModel { .asDriver() .sink { usersActivateGenerationStatus in output.usersActivateGenerationStatus = usersActivateGenerationStatus - }.store(in: self.cancelBag) + }.store(in: cancelBag) self.useCase.errorOccurred .asDriver() diff --git a/SOPT-iOS/Projects/Features/TabBarFeature/Sources/ViewModel/TabBarViewModel.swift b/SOPT-iOS/Projects/Features/TabBarFeature/Sources/ViewModel/TabBarViewModel.swift index ee91021b0..27fd39d75 100644 --- a/SOPT-iOS/Projects/Features/TabBarFeature/Sources/ViewModel/TabBarViewModel.swift +++ b/SOPT-iOS/Projects/Features/TabBarFeature/Sources/ViewModel/TabBarViewModel.swift @@ -10,7 +10,6 @@ import Foundation import Combine import Core -import TabBarFeatureInterface final public class TabBarViewModel: TabBarViewModelType { diff --git a/SOPT-iOS/Projects/Modules/DSKit/Sources/Components/SoptampComponents/STNavigationBar.swift b/SOPT-iOS/Projects/Modules/DSKit/Sources/Components/SoptampComponents/STNavigationBar.swift index 007dd456e..6a895242a 100644 --- a/SOPT-iOS/Projects/Modules/DSKit/Sources/Components/SoptampComponents/STNavigationBar.swift +++ b/SOPT-iOS/Projects/Modules/DSKit/Sources/Components/SoptampComponents/STNavigationBar.swift @@ -29,7 +29,6 @@ public class STNavigationBar: UIView { // MARK: - UI Component - private var vc: UIViewController? private let titleLabel = UILabel() private let titleButton = UIButton() private let leftButton = UIButton()