66//
77
88import UIKit
9+
10+ import EATSSUDesign
11+
912import 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