diff --git a/ByeBoo-iOS/ByeBoo-iOS.xcodeproj/project.pbxproj b/ByeBoo-iOS/ByeBoo-iOS.xcodeproj/project.pbxproj index 16c57433..d7a3f226 100644 --- a/ByeBoo-iOS/ByeBoo-iOS.xcodeproj/project.pbxproj +++ b/ByeBoo-iOS/ByeBoo-iOS.xcodeproj/project.pbxproj @@ -298,7 +298,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.heartz.ByeBoo-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.heartz.ByeBoo-iOS"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.heartz.ByeBoo-iOS"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooTabBar.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooTabBar.swift index fc42776c..a18120c4 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooTabBar.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooTabBar.swift @@ -22,7 +22,7 @@ final class ByeBooTabBar: UITabBarController { private func setViewController() { let homeViewController = ViewControllerFactory.shared.makeHomeViewController() - let questViewController = ViewControllerFactory.shared.makeQuestViewController() + let questViewController = ViewControllerFactory.shared.makeParentQuestViewController() let myPageViewController = ViewControllerFactory.shared.makeMyPageViewController() self.viewControllers = [ diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TabItem.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TabItem.swift new file mode 100644 index 00000000..1e35e1f7 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TabItem.swift @@ -0,0 +1,14 @@ +// +// TabItem.swift +// ByeBoo-iOS +// +// Created by APPLE on 2/12/26. +// + +import UIKit + +protocol TabItem: CaseIterable { + var title: String { get } + var image: UIImage { get } + var viewController: UIViewController { get } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TopTabBar.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TopTabBar.swift new file mode 100644 index 00000000..c5f696a2 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TopTabBar.swift @@ -0,0 +1,71 @@ +// +// TopTabBar.swift +// ByeBoo-iOS +// +// Created by APPLE on 2/12/26. +// + +import UIKit + +final class TopTabBar: UIStackView { + + private let itemViews: [TopTabBarItemView] + var didTap: ((Int) -> Void)? + + init(items: [any TabItem]) { + self.itemViews = items.map { TopTabBarItemView(item: $0) } + super.init(frame: .zero) + + setStyle() + setUI() + setAction() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setStyle() { + self.do { + $0.backgroundColor = .grayscale900 + $0.axis = .horizontal + $0.spacing = 4 + $0.distribution = .fillEqually + } + } + + private func setUI() { + itemViews.forEach { self.addArrangedSubview($0) } + } + + private func setAction() { + itemViews.enumerated().forEach { index, itemView in + let tapGesture = UITapGestureRecognizer( + target: self, action: #selector(barDidTap(_:)) + ) + itemView.addGestureRecognizer(tapGesture) + itemView.tag = index + } + + if let firstIndex = itemViews.indices.first { + updateTabBar(tag: firstIndex) + } + } +} + +extension TopTabBar { + + @objc + private func barDidTap(_ sender: UITapGestureRecognizer) { + guard let tag = sender.view?.tag else { + return + } + + didTap?(tag) + updateTabBar(tag: tag) + } + + private func updateTabBar(tag: Int) { + itemViews.forEach { $0.updateBarItem(for: tag) } + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TopTabBarItemView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TopTabBarItemView.swift new file mode 100644 index 00000000..c9417ab1 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/TopTabBar/TopTabBarItemView.swift @@ -0,0 +1,92 @@ +// +// TopTabBarItemView.swift +// ByeBoo-iOS +// +// Created by APPLE on 2/12/26. +// + +import UIKit + +final class TopTabBarItemView: BaseView { + + private let tabStackView = UIStackView() + private let tabImageView = UIImageView() + private let tabNameLabel = UILabel() + private let underlineLabel = UILabel() + + init(item: any TabItem) { + tabImageView.image = item.image + tabNameLabel.text = item.title + + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func setStyle() { + tabStackView.do { + $0.axis = .horizontal + $0.spacing = 2 + $0.alignment = .center + } + tabNameLabel.do { + $0.textColor = .grayscale100 + $0.font = FontManager.body2M16.font + $0.textAlignment = .center + } + underlineLabel.do { + $0.layer.borderColor = UIColor.grayscale300.cgColor + $0.layer.borderWidth = 1 + } + } + + override func setUI() { + addSubviews( + tabStackView, + underlineLabel + ) + tabStackView.addArrangedSubviews( + tabImageView, + tabNameLabel + ) + } + + override func setLayout() { + tabStackView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.centerX.equalToSuperview() + $0.width.equalTo(108.adjustedW) + $0.height.equalTo(24.adjustedH) + } + tabImageView.snp.makeConstraints { + $0.size.equalTo(24.adjustedW) + $0.verticalEdges.equalToSuperview() + $0.leading.equalToSuperview().inset(9.5.adjustedW) + } + tabNameLabel.snp.makeConstraints { + $0.leading.equalTo(tabImageView.snp.trailing).offset(2.adjustedW) + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().inset(11.5.adjustedW) + } + underlineLabel.snp.makeConstraints { + $0.top.equalTo(tabStackView.snp.bottom).offset(4.adjustedH) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(1.adjustedH) + $0.bottom.equalToSuperview() + } + } +} + +extension TopTabBarItemView { + + func updateBarItem(for tag: Int) { + let condition = (self.tag == tag) + + tabImageView.layer.opacity = condition ? 1 : 0.44 + tabNameLabel.textColor = condition ? .grayscale100 : .grayscale600 + underlineLabel.layer.borderColor = condition ? UIColor.grayscale300.cgColor : UIColor.clear.cgColor + underlineLabel.layer.borderWidth = condition ? 1 : 0 + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/MyPage/ViewController/MyPageViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/MyPage/ViewController/MyPageViewController.swift index 60e036db..df69c7e5 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/MyPage/ViewController/MyPageViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/MyPage/ViewController/MyPageViewController.swift @@ -15,9 +15,10 @@ final class MyPageViewController: BaseViewController { private let viewModel: MyPageViewModel private var cancellables = Set() private var name: String? - private var beforeNotificationStatus = false private let rootView = MyPageView() + private lazy var beforeNotificationStatus = rootView.noticeView.noticeSwitch.isOn + private var didOpenSetting = false init(viewModel: MyPageViewModel) { self.viewModel = viewModel @@ -204,25 +205,29 @@ extension MyPageViewController { @objc private func checkNoticeAuthorizationWhenBack() { + guard didOpenSetting else { return } + UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in guard let self else { return } + let isAuthorized: Bool switch settings.authorizationStatus { case .authorized, .provisional, .ephemeral: - if !beforeNotificationStatus { - self.viewModel.action(.notificationSwitchDidTap) - } - beforeNotificationStatus = true + isAuthorized = true default: - if beforeNotificationStatus { - self.viewModel.action(.notificationSwitchDidTap) - } else { - DispatchQueue.main.async { - self.rootView.noticeView.noticeSwitch.setOn(false, animated: false) - } + isAuthorized = false + } + + if beforeNotificationStatus != isAuthorized { + viewModel.action(.notificationSwitchDidTap) + } else if !isAuthorized { + DispatchQueue.main.async { + self.rootView.noticeView.noticeSwitch.setOn(false, animated: false) } - beforeNotificationStatus = false } + + beforeNotificationStatus = isAuthorized + didOpenSetting = false } } @@ -425,6 +430,7 @@ extension MyPageViewController { private func moveSetting() { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) + didOpenSetting = true } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/Common/QuestTabItem.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/Common/QuestTabItem.swift new file mode 100644 index 00000000..31d1c67c --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/Common/QuestTabItem.swift @@ -0,0 +1,41 @@ +// +// QuestTabItem.swift +// ByeBoo-iOS +// +// Created by APPLE on 2/12/26. +// + +import UIKit + +enum QuestTabItem: TabItem { + + case myJourney + case commonJourney + + var title: String { + switch self { + case .myJourney: + return "나의 여정" + case .commonJourney: + return "공통 여정" + } + } + + var image: UIImage { + switch self { + case .myJourney: + return .myJourney + case .commonJourney: + return .commonJourney + } + } + + var viewController: UIViewController { + switch self { + case .myJourney: + return ViewControllerFactory.shared.makeQuestViewController() + case .commonJourney: + return ViewControllerFactory.shared.makeCommonQuestViewController() + } + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift new file mode 100644 index 00000000..2fefe288 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift @@ -0,0 +1,19 @@ +// +// CommonQuestViewController.swift +// ByeBoo-iOS +// +// Created by APPLE on 2/12/26. +// + +final class CommonQuestViewController: BaseViewController { + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: false) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.navigationController?.setNavigationBarHidden(false, animated: false) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/ParentQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/ParentQuestViewController.swift new file mode 100644 index 00000000..cdf32179 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/ParentQuestViewController.swift @@ -0,0 +1,94 @@ +// +// ParentQuestViewController.swift +// ByeBoo-iOS +// +// Created by APPLE on 2/14/26. +// + +import UIKit + +final class ParentQuestViewController: BaseViewController { + + private let tabBar: TopTabBar + private let containerView = UIView() + private let controllers: [UIViewController] + private var currentViewController: UIViewController? + + init(items: T.AllCases) { + self.tabBar = TopTabBar(items: Array(items)) + self.controllers = items.map { $0.viewController } + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setLayout() + bind() + + if let controller = controllers.first { + show(controller) + } + } + + override func setView() { + view.addSubviews( + tabBar, + containerView + ) + } +} + +extension ParentQuestViewController { + + private func setLayout() { + tabBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(20.adjustedH) + $0.centerX.equalToSuperview() + $0.width.equalTo(228.adjustedW) + $0.height.equalTo(28.adjustedH) + } + containerView.snp.makeConstraints { + $0.top.equalTo(tabBar.snp.bottom).offset(16.adjustedH) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + private func bind() { + tabBar.didTap = { [weak self] index in + guard let self, + index >= 0 && index < controllers.count + else { + return + } + show(controllers[index]) + } + } + + private func show(_ target: UIViewController) { + guard currentViewController != target else { return } + + if let currentViewController { + currentViewController.do { + $0.willMove(toParent: nil) + $0.view.removeFromSuperview() + $0.removeFromParent() + } + } + + addChild(target) + containerView.addSubview(target.view) + target.view.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + $0.horizontalEdges.equalToSuperview() + } + + target.didMove(toParent: self) + currentViewController = target + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift index 287556ae..2b1e6d38 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift @@ -189,6 +189,14 @@ final class ViewControllerFactory: ViewControllerFactoryProtocol { } return CompletedQuestsViewController(viewModel: viewModel) } + + func makeParentQuestViewController() -> ParentQuestViewController { + return ParentQuestViewController(items: QuestTabItem.allCases) + } + + func makeCommonQuestViewController() -> CommonQuestViewController { + return .init() + } } extension ViewControllerFactory { diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/Contents.json b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/common_journey.imageset/Contents.json b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/common_journey.imageset/Contents.json new file mode 100644 index 00000000..63aded90 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/common_journey.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "common_journey_sharp.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/common_journey.imageset/common_journey_sharp.svg b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/common_journey.imageset/common_journey_sharp.svg new file mode 100644 index 00000000..61f3b35e --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/common_journey.imageset/common_journey_sharp.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/my_journey.imageset/Contents.json b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/my_journey.imageset/Contents.json new file mode 100644 index 00000000..4c09197b --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/my_journey.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "my_journey_sharp.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/my_journey.imageset/my_journey_sharp.svg b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/my_journey.imageset/my_journey_sharp.svg new file mode 100644 index 00000000..9d84a147 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/TabBar/my_journey.imageset/my_journey_sharp.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +