Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions Keychy/Keychy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@
38F832CD2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F832CC2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift */; };
38F832CF2EC914C900D3A248 /* InvenExpandPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F832CE2EC914C900D3A248 /* InvenExpandPopup.swift */; };
40WF8CXMLHGD9B5S521VX89Y /* BundleViewModel+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DTQCH5OWZZJ8N03KVZERHJKR /* BundleViewModel+Cache.swift */; };
4C004F912F164D5500D9063E /* TabBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F902F164D5500D9063E /* TabBarManager.swift */; };
4C004FA12F177C4600D9063E /* BundleVideoGenerator+Rendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F982F177C4600D9063E /* BundleVideoGenerator+Rendering.swift */; };
4C004FA22F177C4600D9063E /* KeyringVideoGenerator+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F9E2F177C4600D9063E /* KeyringVideoGenerator+Setup.swift */; };
4C004FA32F177C4600D9063E /* BundleVideoGenerator+Metal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C004F972F177C4600D9063E /* BundleVideoGenerator+Metal.swift */; };
Expand Down Expand Up @@ -130,6 +129,9 @@
4C2526232F3B290A003CC5AD /* KeyringScene+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526172F3B290A003CC5AD /* KeyringScene+Setup.swift */; };
4C2526242F3B290A003CC5AD /* KeyringCellScene+Capture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C25260F2F3B290A003CC5AD /* KeyringCellScene+Capture.swift */; };
4C2526262F3B2BBE003CC5AD /* KeyringScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526252F3B2BBE003CC5AD /* KeyringScale.swift */; };
4C25262B2F3B97D6003CC5AD /* UIViewController+Find.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526292F3B97D6003CC5AD /* UIViewController+Find.swift */; };
4C25262C2F3B97D6003CC5AD /* TabBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526272F3B97D6003CC5AD /* TabBarManager.swift */; };
4C25262D2F3B97D6003CC5AD /* TabBarSwipeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2526282F3B97D6003CC5AD /* TabBarSwipeObserver.swift */; };
4C3687F72EBFA87800C64E75 /* Pretendard-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4C3687F62EBFA87800C64E75 /* Pretendard-Medium.ttf */; };
4C3687FA2EBFC0FB00C64E75 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3687F82EBFC0FB00C64E75 /* NotificationManager.swift */; };
4C3687FC2EC05E6800C64E75 /* AccountAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3687FB2EC05E6800C64E75 /* AccountAlert.swift */; };
Expand Down Expand Up @@ -549,7 +551,6 @@
38F832CC2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WidgetOnboardingStepView+Helpers.swift"; sourceTree = "<group>"; };
38F832CE2EC914C900D3A248 /* InvenExpandPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvenExpandPopup.swift; sourceTree = "<group>"; };
40QZ1H4Y8EH2YZZUOT7WN7MX /* BundleViewModel+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleViewModel+Helpers.swift"; sourceTree = "<group>"; };
4C004F902F164D5500D9063E /* TabBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarManager.swift; sourceTree = "<group>"; };
4C004F962F177C4600D9063E /* BundleVideoGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleVideoGenerator.swift; sourceTree = "<group>"; };
4C004F972F177C4600D9063E /* BundleVideoGenerator+Metal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleVideoGenerator+Metal.swift"; sourceTree = "<group>"; };
4C004F982F177C4600D9063E /* BundleVideoGenerator+Rendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BundleVideoGenerator+Rendering.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -590,6 +591,9 @@
4C2526182F3B290A003CC5AD /* KeyringScene+Swipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyringScene+Swipe.swift"; sourceTree = "<group>"; };
4C2526192F3B290A003CC5AD /* KeyringScene+Touch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyringScene+Touch.swift"; sourceTree = "<group>"; };
4C2526252F3B2BBE003CC5AD /* KeyringScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyringScale.swift; sourceTree = "<group>"; };
4C2526272F3B97D6003CC5AD /* TabBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarManager.swift; sourceTree = "<group>"; };
4C2526282F3B97D6003CC5AD /* TabBarSwipeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarSwipeObserver.swift; sourceTree = "<group>"; };
4C2526292F3B97D6003CC5AD /* UIViewController+Find.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Find.swift"; sourceTree = "<group>"; };
4C3687F62EBFA87800C64E75 /* Pretendard-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Medium.ttf"; sourceTree = "<group>"; };
4C3687F82EBFC0FB00C64E75 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
4C3687FB2EC05E6800C64E75 /* AccountAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAlert.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1212,6 +1216,16 @@
path = KeyringScene;
sourceTree = "<group>";
};
4C25262A2F3B97D6003CC5AD /* TabBar */ = {
isa = PBXGroup;
children = (
4C2526272F3B97D6003CC5AD /* TabBarManager.swift */,
4C2526282F3B97D6003CC5AD /* TabBarSwipeObserver.swift */,
4C2526292F3B97D6003CC5AD /* UIViewController+Find.swift */,
);
path = TabBar;
sourceTree = "<group>";
};
4C3687F52EBFA86B00C64E75 /* pretendard */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1958,9 +1972,9 @@
4CEC620D2EAE08DA0099ECEE /* Navigation */ = {
isa = PBXGroup;
children = (
4CEC620B2EAE08DA0099ECEE /* Routes */,
4CEC620C2EAE08DA0099ECEE /* NavigationRouter.swift */,
4C004F902F164D5500D9063E /* TabBarManager.swift */,
4C25262A2F3B97D6003CC5AD /* TabBar */,
4CEC620B2EAE08DA0099ECEE /* Routes */,
);
path = Navigation;
sourceTree = "<group>";
Expand Down Expand Up @@ -2809,6 +2823,9 @@
38173D0A2EB8AD7900E36F7E /* CategoryTabBarWithLongPress.swift in Sources */,
4CEBB1552EFACFA900CF53E2 /* HomeTab.swift in Sources */,
4CEBB1572EFACFA900CF53E2 /* CollectionTab.swift in Sources */,
4C25262B2F3B97D6003CC5AD /* UIViewController+Find.swift in Sources */,
4C25262C2F3B97D6003CC5AD /* TabBarManager.swift in Sources */,
4C25262D2F3B97D6003CC5AD /* TabBarSwipeObserver.swift in Sources */,
4CEBB1592EFACFA900CF53E2 /* WorkshopTab.swift in Sources */,
38F832CD2EC90DEF00D3A248 /* WidgetOnboardingStepView+Helpers.swift in Sources */,
38A22A9D2EC27AC400B4C7C5 /* PackagedKeyringView+SaveImage.swift in Sources */,
Expand Down Expand Up @@ -2850,7 +2867,6 @@
4C6530502EBB2A90000F8154 /* LoadingAlert.swift in Sources */,
4C6530722EBCFD10000F8154 /* BangmarkAlert.swift in Sources */,
389080192ED3F32700D7A49F /* FestivalKeyringDetailView+Sheet.swift in Sources */,
4C004F912F164D5500D9063E /* TabBarManager.swift in Sources */,
C665DDF02EAF08D000CE4495 /* PurchaseManager.swift in Sources */,
4CEC62332EAE08DA0099ECEE /* BundleGridItem.swift in Sources */,
AA69DD242F14C56F00C0A41C /* BundleViewModel+CRUD.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ struct MultiKeyringSceneView: View {
loadBackgroundImage()
setupScene()
}

// 키링이 없으면 즉시 준비 완료 콜백 호출
if keyringDataList.isEmpty {
onAllKeyringsReady?()
}
}
.onChange(of: backgroundImageURL) { _, _ in
loadBackgroundImage()
Expand Down
72 changes: 72 additions & 0 deletions Keychy/Keychy/Core/Navigation/TabBar/TabBarManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// TabBarManager.swift
// Keychy
//
// Created by 길지훈 on 2/11/26.
//
// 탭바 표시/숨김을 관리하는 매니저
// - show(): 탭바 표시 (애니메이션)
// - hide(): 탭바 숨김 + 스와이프 백 제스처 연동 설정
// - setAlpha(): 탭바 투명도 직접 설정
//

import UIKit

enum TabBarManager {

// MARK: - Tab Index

enum Tab: Int {
case home = 0
case workshop = 1
case collection = 2
case festival = 3
}

// MARK: - Public Methods

/// 탭바 표시 (fade in 애니메이션)
static func show() {
animate(toAlpha: 1.0)
}

/// 탭바 숨김 (fade out) + 스와이프 백 제스처 연동
static func hide() {
animate(toAlpha: 0.0)
setupSwipeGestureObserver()
}

/// 탭바 투명도 직접 설정 (애니메이션 없음)
static func setAlpha(_ alpha: CGFloat) {
tabBar?.alpha = alpha
}

/// 특정 탭으로 전환
static func switchTo(_ tab: Tab) {
tabBarController?.selectedIndex = tab.rawValue
}

// MARK: - Private 변/함

private static var tabBarController: UITabBarController? {
UIApplication.shared.rootViewController?.findTabBarController()
}

private static var tabBar: UITabBar? {
tabBarController?.tabBar
}

private static func animate(toAlpha alpha: CGFloat) {
UIView.animate(withDuration: alpha > 0 ? 0.25 : 0.2) {
tabBar?.alpha = alpha
}
}

private static func setupSwipeGestureObserver() {
guard let selectedVC = tabBarController?.selectedViewController,
let navController = selectedVC.findNavigationController(),
let gesture = navController.interactivePopGestureRecognizer else { return }

TabBarSwipeObserver.shared.attach(to: gesture)
}
}
90 changes: 90 additions & 0 deletions Keychy/Keychy/Core/Navigation/TabBar/TabBarSwipeObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// TabBarSwipeObserver.swift
// Keychy
//
// Created by 길지훈 on 2/11/26.
//
// 스와이프 백 제스처 진행도를 탭바 투명도에 연동
//
// [동작 흐름]
// 1. TabBarManager.hide() 호출 시 attach(to:) 실행
// 2. 스와이프 시작 → handleGesture 호출
// 3. 스와이프 진행 → 진행도(0~1)를 탭바 alpha에 반영
// 4. 스와이프 완료(50%↑ or 빠른 스와이프) → TabBarManager.show()
// 5. 스와이프 취소 → TabBarManager.hide()
//

import UIKit

final class TabBarSwipeObserver: NSObject {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

탭바 스와이프 정로를 감지할 수 있는 건 첨 알았네요...
진짜 아직까지도 UIKit이 이것저것 커스텀하기엔 좋은듯...


static let shared = TabBarSwipeObserver()

// MARK: - Constants

private enum Threshold {
static let completeProgress: CGFloat = 0.5 // 50% 이상이면 완료
static let fastVelocity: CGFloat = 500 // 빠른 스와이프 판정 속도
}

// MARK: - Properties

private weak var currentGesture: UIGestureRecognizer?

// MARK: - Init

private override init() {
super.init()
}

// MARK: - Public

/// 제스처에 옵저버 연결
func attach(to gesture: UIGestureRecognizer) {
guard currentGesture !== gesture else { return }

detach()
currentGesture = gesture
gesture.addTarget(self, action: #selector(handleGesture(_:)))
}

/// 현재 제스처 연결 해제
func detach() {
currentGesture?.removeTarget(self, action: #selector(handleGesture(_:)))
currentGesture = nil
}

// MARK: - Private

private func calculateProgress(from gesture: UIPanGestureRecognizer) -> CGFloat {
let translationX = gesture.translation(in: gesture.view).x
let screenWidth = gesture.view?.window?.screen.bounds.width ?? gesture.view?.bounds.width ?? 393
return min(max(translationX / screenWidth, 0), 1)
}

private func shouldComplete(progress: CGFloat, velocity: CGFloat) -> Bool {
progress > Threshold.completeProgress || velocity > Threshold.fastVelocity
}

// MARK: - Gesture Handler

@objc private func handleGesture(_ gesture: UIPanGestureRecognizer) {
let progress = calculateProgress(from: gesture)

switch gesture.state {
case .changed:
TabBarManager.setAlpha(progress)

case .ended, .cancelled:
let velocity = gesture.velocity(in: gesture.view).x
if shouldComplete(progress: progress, velocity: velocity) {
TabBarManager.show()
} else {
TabBarManager.hide()
}

default:
break
}
}
}
50 changes: 50 additions & 0 deletions Keychy/Keychy/Core/Navigation/TabBar/UIViewController+Find.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// UIViewController+Find.swift
// Keychy
//
// Created by 길지훈 on 2/11/26.
//
// UIViewController 계층에서 특정 컨트롤러 탐색
//

import UIKit

extension UIViewController {

/// 하위 계층에서 UITabBarController 탐색
func findTabBarController() -> UITabBarController? {
if let tabBar = self as? UITabBarController { return tabBar }

for child in children {
if let found = child.findTabBarController() { return found }
}

return parent?.findTabBarController()
}

/// 하위 계층에서 UINavigationController 탐색
func findNavigationController() -> UINavigationController? {
if let nav = self as? UINavigationController { return nav }

for child in children {
if let found = child.findNavigationController() { return found }
}

return nil
}
}

// MARK: - UIApplication Extension

extension UIApplication {

/// 현재 활성 윈도우의 rootViewController
var rootViewController: UIViewController? {
connectedScenes
.compactMap { $0 as? UIWindowScene }
.first?
.windows
.first?
.rootViewController
}
}
69 changes: 0 additions & 69 deletions Keychy/Keychy/Core/Navigation/TabBarManager.swift

This file was deleted.

Loading