Skip to content

Commit 5c18a4b

Browse files
authored
Merge pull request #369 from EAT-SSU/feat/#359
[#359] 하단 탭바 iOS 26 적용
2 parents 8be9608 + 8feb37b commit 5c18a4b

File tree

3 files changed

+146
-315
lines changed

3 files changed

+146
-315
lines changed

EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift

Lines changed: 145 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -6,114 +6,134 @@
66
//
77

88
import UIKit
9+
10+
import EATSSUDesign
11+
912
import SnapKit
1013

11-
final class CustomTabBarContainerController: BaseViewController {
14+
final class CustomTabBarContainerController: UITabBarController {
15+
16+
// MARK: - Types
17+
18+
private enum Tab: Int {
19+
case home = 0
20+
case map = 1
21+
case myPage = 2
22+
}
1223

1324
// MARK: - Properties
1425

15-
private let contentContainerView = UIView()
16-
private let tabBarView = CustomTabBarView()
17-
private let viewControllers: [UINavigationController] = [
26+
private lazy var tabViewControllers: [UINavigationController] = [
1827
UINavigationController(rootViewController: HomeViewController()),
1928
UINavigationController(rootViewController: MainMapViewController()),
2029
UINavigationController(rootViewController: MyPageViewController())
2130
]
22-
private var currentIndex = 0
23-
private var contentBottomConstraint: Constraint?
2431

25-
// MARK: - View Setup
26-
27-
override func configureUI() {
28-
view.addSubview(contentContainerView)
29-
view.addSubview(tabBarView)
30-
31-
tabBarView.buttonTapped = { [weak self] index in
32-
guard let self = self else { return }
33-
34-
if index == 1 {
35-
// firebase - click_map 이벤트 호출
36-
MapAnalyticsManager.shared.logClickMap()
37-
}
38-
39-
// 마이페이지와 지도는 로그인 필요
40-
// TODO: 지도는 서버팀과 함께 나중에 둘러보기 상태에서보 "전체" 카테고리는 볼 수 있게 수정
41-
if (index == 1 || index == 2), RealmService.shared.isAccessTokenPresent() == false {
42-
self.presentLoginAlert()
43-
return
44-
}
45-
46-
// 같은 탭 다시 클릭 시 처리
47-
if index == self.currentIndex {
48-
if index == 0 {
49-
// 학식 탭: 오늘이 아니면 오늘로 이동
50-
if let nav = self.viewControllers[index] as? UINavigationController,
51-
let homeVC = nav.viewControllers.first as? HomeViewController {
52-
homeVC.resetToToday()
53-
}
54-
} else if index == 1 {
55-
// 지도 탭: 콘텐츠 리로드
56-
if let nav = self.viewControllers[index] as? UINavigationController,
57-
let mapVC = nav.viewControllers.first as? MainMapViewController {
58-
mapVC.reloadContent()
59-
}
60-
}
61-
}
32+
// MARK: - Life Cycle
6233

63-
self.switchToViewController(at: index)
64-
}
34+
override func viewDidLoad() {
35+
super.viewDidLoad()
6536

66-
// 각 네비게이션 컨트롤러의 delegate 설정
67-
viewControllers.forEach { navController in
68-
navController.delegate = self
69-
navController.setNavigationBarHidden(false, animated: false)
70-
}
37+
setupTabBar()
38+
setupViewControllers()
39+
delegate = self
7140
}
41+
42+
// MARK: - Setup
43+
44+
private func setupTabBar() {
45+
tabBar.tintColor = EATSSUDesignAsset.Color.Main.primary.color
46+
tabBar.unselectedItemTintColor = .gray500
47+
48+
let appearance = UITabBarAppearance()
7249

73-
override func setLayout() {
74-
tabBarView.snp.makeConstraints {
75-
$0.leading.trailing.bottom.equalToSuperview()
76-
$0.height.equalTo(80)
77-
}
50+
appearance.configureWithDefaultBackground()
51+
appearance.backgroundColor = .white
7852

79-
contentContainerView.snp.makeConstraints {
80-
$0.top.leading.trailing.equalToSuperview()
81-
contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint
82-
}
53+
tabBar.standardAppearance = appearance
54+
tabBar.scrollEdgeAppearance = appearance
8355
}
56+
57+
private func setupViewControllers() {
58+
let tabConfigurations: [(title: String, normal: UIImage, selected: UIImage, size: CGSize)] = [
59+
("학식", EATSSUDesignAsset.Images.tabMeal.image, EATSSUDesignAsset.Images.tabMealSelected.image, CGSize(width: 23, height: 23)),
60+
("지도", EATSSUDesignAsset.Images.tabMap.image, EATSSUDesignAsset.Images.tabMapSelected.image, CGSize(width: 23, height: 23)),
61+
("마이", EATSSUDesignAsset.Images.tabMypage.image, EATSSUDesignAsset.Images.tabMypageSelected.image, CGSize(width: 44, height: 23))
62+
]
63+
64+
tabViewControllers.enumerated().forEach { index, navController in
65+
let config = tabConfigurations[index]
66+
let normalImage = config.normal.resized(to: config.size).withRenderingMode(.alwaysOriginal)
67+
let selectedImage = config.selected.resized(to: config.size).withRenderingMode(.alwaysOriginal)
68+
69+
navController.tabBarItem = UITabBarItem(
70+
title: config.title,
71+
image: normalImage,
72+
selectedImage: selectedImage
73+
)
74+
}
8475

85-
// MARK: - Life Cycle
76+
// 폰트 설정
77+
let normalAttributes: [NSAttributedString.Key: Any] = [
78+
.font: EATSSUDesignFontFamily.Pretendard.regular.font(size: 11),
79+
.foregroundColor: UIColor.gray500
80+
]
81+
let selectedAttributes: [NSAttributedString.Key: Any] = [
82+
.font: EATSSUDesignFontFamily.Pretendard.bold.font(size: 11),
83+
.foregroundColor: EATSSUDesignAsset.Color.Main.primary.color
84+
]
85+
86+
tabViewControllers.forEach { nav in
87+
nav.tabBarItem.setTitleTextAttributes(normalAttributes, for: .normal)
88+
nav.tabBarItem.setTitleTextAttributes(selectedAttributes, for: .selected)
89+
}
8690

87-
override func viewDidLoad() {
88-
super.viewDidLoad()
89-
switchToViewController(at: currentIndex)
91+
self.viewControllers = tabViewControllers
9092
}
91-
92-
// MARK: - Navigation Control
93-
94-
/// 탭 전환 처리
95-
private func switchToViewController(at index: Int) {
96-
contentContainerView.subviews.forEach { $0.removeFromSuperview() }
93+
94+
// MARK: - Public Interface
95+
96+
/// 외부에서 탭 전환 요청 시 사용
97+
public func setTab(index: Int) {
98+
guard index < tabViewControllers.count else { return }
99+
selectedIndex = index
100+
}
101+
102+
/// 특정 인덱스의 네비게이션 컨트롤러를 반환
103+
public func getNavController(at index: Int) -> UINavigationController? {
104+
guard index < tabViewControllers.count else { return nil }
105+
return tabViewControllers[index]
106+
}
107+
108+
/// 공용 다이얼로그(팝업)를 표시하는 함수
109+
public func showDialog(
110+
title: String,
111+
message: String,
112+
cancelButtonTitle: String = "취소하기",
113+
confirmButtonTitle: String = "확인",
114+
confirmAction: @escaping () -> Void
115+
) {
116+
let dialogView = EATSSUDialogView()
97117

98-
let selectedNav = viewControllers[index]
118+
dialogView.configure(title: title, message: message)
119+
dialogView.setButtonTitles(cancel: cancelButtonTitle, confirm: confirmButtonTitle)
99120

100-
contentContainerView.addSubview(selectedNav.view)
101-
selectedNav.view.snp.makeConstraints {
102-
$0.edges.equalToSuperview()
103-
}
121+
dialogView.cancelButton.addAction(UIAction { _ in
122+
dialogView.removeFromSuperview()
123+
}, for: .touchUpInside)
104124

105-
tabBarView.setSelectedIndex(index)
106-
currentIndex = index
125+
dialogView.confirmButton.addAction(UIAction { _ in
126+
confirmAction()
127+
dialogView.removeFromSuperview()
128+
}, for: .touchUpInside)
107129

108-
// 현재 표시 중인 VC의 shouldHideTabBar 확인
109-
updateTabBarVisibility(for: selectedNav.topViewController)
130+
self.view.addSubview(dialogView)
131+
dialogView.snp.makeConstraints {
132+
$0.edges.equalToSuperview()
133+
}
110134
}
111135

112-
/// 탭바 가시성 업데이트
113-
private func updateTabBarVisibility(for viewController: UIViewController?) {
114-
guard let vc = viewController as? BaseViewController else { return }
115-
setTabBarHidden(vc.shouldHideTabBar, animated: true)
116-
}
136+
// MARK: - Private Helpers
117137

118138
/// 로그인 필요 시 알림창 표시
119139
private func presentLoginAlert() {
@@ -126,10 +146,7 @@ final class CustomTabBarContainerController: BaseViewController {
126146
let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in
127147
self?.navigateToLogin()
128148
}
129-
let cancelAction = UIAlertAction(title: "취소", style: .cancel) { [weak self] _ in
130-
guard let self = self else { return }
131-
self.tabBarView.setSelectedIndex(self.currentIndex)
132-
}
149+
let cancelAction = UIAlertAction(title: "취소", style: .cancel)
133150

134151
alert.addAction(confirmAction)
135152
alert.addAction(cancelAction)
@@ -146,102 +163,56 @@ final class CustomTabBarContainerController: BaseViewController {
146163
window.replaceRootViewController(loginVC)
147164
}
148165
}
149-
150-
/// 공용 다이얼로그(팝업)를 표시하는 함수
151-
public func showDialog(
152-
title: String,
153-
message: String,
154-
cancelButtonTitle: String = "취소하기",
155-
confirmButtonTitle: String = "확인",
156-
confirmAction: @escaping () -> Void
157-
) {
158-
let dialogView = EATSSUDialogView()
159-
160-
// 다이얼로그 내용 설정
161-
dialogView.configure(title: title, message: message)
162-
dialogView.setButtonTitles(cancel: cancelButtonTitle, confirm: confirmButtonTitle)
163-
164-
// '취소' 버튼 액션: 팝업 닫기
165-
dialogView.cancelButton.addAction(UIAction { _ in
166-
dialogView.removeFromSuperview()
167-
}, for: .touchUpInside)
168-
169-
// '확인' 버튼 액션: 전달받은 클로저 실행 후 팝업 닫기
170-
dialogView.confirmButton.addAction(UIAction { _ in
171-
confirmAction()
172-
dialogView.removeFromSuperview()
173-
}, for: .touchUpInside)
174-
175-
self.view.addSubview(dialogView)
176-
dialogView.snp.makeConstraints {
177-
$0.edges.equalToSuperview()
178-
}
179-
}
166+
}
180167

181-
// MARK: - Public Interface
168+
// MARK: - UITabBarControllerDelegate
182169

183-
/// 외부에서 탭 전환 요청 시 사용
184-
public func setTab(index: Int) {
185-
switchToViewController(at: index)
186-
}
187-
188-
/// 특정 인덱스의 네비게이션 컨트롤러를 반환
189-
public func getNavController(at index: Int) -> UINavigationController? {
190-
guard index < viewControllers.count else { return nil }
191-
return viewControllers[index]
192-
}
170+
extension CustomTabBarContainerController: UITabBarControllerDelegate {
171+
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
172+
guard let navController = viewController as? UINavigationController,
173+
let index = tabViewControllers.firstIndex(of: navController),
174+
let selectedTab = Tab(rawValue: index) else {
175+
return true
176+
}
193177

194-
/// 탭바를 숨기거나 표시하는 메서드
195-
public func setTabBarHidden(_ hidden: Bool, animated: Bool) {
196-
guard tabBarView.isHidden != hidden else { return }
197-
198-
// 제약 업데이트
199-
contentBottomConstraint?.deactivate()
200-
contentContainerView.snp.makeConstraints {
201-
if hidden {
202-
contentBottomConstraint = $0.bottom.equalToSuperview().constraint
203-
} else {
204-
contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint
205-
}
178+
// 마이페이지와 지도는 로그인 필요
179+
if (selectedTab == .map || selectedTab == .myPage), RealmService.shared.isAccessTokenPresent() == false {
180+
presentLoginAlert()
181+
return false
206182
}
207-
208-
if animated {
209-
UIView.animate(withDuration: 0.3) {
210-
self.tabBarView.alpha = hidden ? 0 : 1
211-
self.view.layoutIfNeeded()
212-
} completion: { _ in
213-
self.tabBarView.isHidden = hidden
183+
184+
// 지도 탭 클릭 시 Firebase 이벤트 호출 (로그인된 상태에서만)
185+
if selectedTab == .map {
186+
MapAnalyticsManager.shared.logClickMap()
187+
}
188+
189+
// 같은 탭 다시 클릭 시 처리
190+
if index == selectedIndex {
191+
switch selectedTab {
192+
case .home:
193+
// 학식 탭: 오늘이 아니면 오늘로 이동
194+
if let homeVC = navController.viewControllers.first as? HomeViewController {
195+
homeVC.resetToToday()
196+
}
197+
case .map:
198+
// 지도 탭: 콘텐츠 리로드
199+
if let mapVC = navController.viewControllers.first as? MainMapViewController {
200+
mapVC.reloadContent()
201+
}
202+
case .myPage:
203+
break
214204
}
215-
} else {
216-
self.tabBarView.alpha = hidden ? 0 : 1
217-
self.tabBarView.isHidden = hidden
218-
self.view.layoutIfNeeded()
219205
}
206+
207+
return true
220208
}
221209
}
222210

223-
// MARK: - UINavigationControllerDelegate
224-
225-
extension CustomTabBarContainerController: UINavigationControllerDelegate {
226-
func navigationController(
227-
_ navigationController: UINavigationController,
228-
willShow viewController: UIViewController,
229-
animated: Bool
230-
) {
231-
guard let vc = viewController as? BaseViewController else { return }
232-
let shouldHide = vc.shouldHideTabBar
233-
234-
contentBottomConstraint?.deactivate()
235-
contentContainerView.snp.makeConstraints {
236-
if shouldHide {
237-
contentBottomConstraint = $0.bottom.equalToSuperview().constraint
238-
} else {
239-
contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint
240-
}
211+
extension UIImage {
212+
func resized(to size: CGSize) -> UIImage {
213+
let renderer = UIGraphicsImageRenderer(size: size)
214+
return renderer.image { _ in
215+
self.draw(in: CGRect(origin: .zero, size: size))
241216
}
242-
243-
self.tabBarView.alpha = shouldHide ? 0 : 1
244-
self.tabBarView.isHidden = shouldHide
245-
self.view.layoutIfNeeded()
246217
}
247218
}

0 commit comments

Comments
 (0)