🥸 해결하고자 하는 상황
만약 아래와 같은 간단한 NavgationStack이 있다고 해보자. 여기서 < Back 부분이 맘에 안들어서 이 부분을 바꾸고 싶다고 하자.
물론 다양한 방법으로 커스텀이 가능하지만 아래와 같은 방법으로 바꿔보았다고 하자. 뒤로가기 버튼을 숨기고 툴바아이템을 추가해주었다. 액션은 dismiss를 활용했다.
struct NextView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Text("Some View")
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("뒤로", systemImage: "star.fill") {
dismiss()
}
}
}
}
}
하지만 여기서 욕심이 생겼다. 나는 처음에 네비게이션 뒤로가기가 동작하는것과 정확히 똑같은 동작을 원한다. 기존의 네비게이션 뒤로가기는 가장자리를 드래그해도 동작하게 된다. 이것을 어떻게 할 수 있을까?
💡 해결 과정
야속하게 back버튼을 숨기면 해당 기능도 없어지게 되어있다. 그렇다면 해당 제스처 자체를 추가할수도 있다는 이야기로 생각되었다. 결국 UIKit을 사용해서 커스텀 네비게이션 스택을 만들어주면 된다...! UIScreenEdgePanGestureRecognizer, interactivePopGestureRecognizer를 사용하면 된다. 전체 코드는 아래와 같다. 생각보다 아주 간단하다.
import SwiftUI
struct CustomNavigationStack<Content: View>: View {
@ViewBuilder var content: () -> Content
private let gesture = UIScreenEdgePanGestureRecognizer()
var body: some View {
NavigationStack {
content()
.background {
PopGestureView(gesture: gesture)
}
}
}
}
struct PopGestureView: UIViewRepresentable {
let gesture: UIScreenEdgePanGestureRecognizer
func makeUIView(context: Context) -> some UIView {
gesture.name = UUID().uuidString
gesture.edges = UIRectEdge.left
return UIView()
}
func updateUIView(_ uiView: UIViewType, context: Context) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
let viewControllers = sequence(first: uiView) { $0.next }.compactMap { $0 as? UIViewController }
guard let parentViewController = viewControllers.first,
let navigationController = parentViewController.navigationController else { return }
/// 제스처가 중복되지 않도록 확인
guard let gestureRecognizers = navigationController.view.gestureRecognizers,
!gestureRecognizers.contains(where: { $0.name == gesture.name }) else { return }
/// 기존 interactivePopGestureRecognizer의 동작을 복사하여 새로운 제스처에 할당
/// 애플이 지정한 키 "targets" 사용
guard let gestureRecognizer = navigationController.interactivePopGestureRecognizer else { return }
gesture.setValue(gestureRecognizer.value(forKey: "targets"), forKey: "targets")
navigationController.view.addGestureRecognizer(gesture)
}
}
}
🧐 코드 뜯어보기
SwiftUI View는 제스쳐뷰를 background에 넣어주는 부분이다.
struct CustomNavigationStack<Content: View>: View {
@ViewBuilder var content: () -> Content
private let gesture = UIScreenEdgePanGestureRecognizer()
var body: some View {
NavigationStack {
content()
.background {
PopGestureView(gesture: gesture)
}
}
}
}
실제 핵심은 아래의 코드이다.
struct PopGestureView: UIViewRepresentable {
let gesture: UIScreenEdgePanGestureRecognizer
func makeUIView(context: Context) -> some UIView {
gesture.name = UUID().uuidString
gesture.edges = UIRectEdge.left
return UIView()
}
func updateUIView(_ uiView: UIViewType, context: Context) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
let viewControllers = sequence(first: uiView) { $0.next }.compactMap { $0 as? UIViewController }
guard let parentViewController = viewControllers.first,
let navigationController = parentViewController.navigationController else { return }
/// 제스처가 중복되지 않도록 확인
guard let gestureRecognizers = navigationController.view.gestureRecognizers,
!gestureRecognizers.contains(where: { $0.name == gesture.name }) else { return }
/// 기존 interactivePopGestureRecognizer의 동작을 복사하여 새로운 제스처에 할당
/// 애플이 지정한 키 "targets" 사용
guard let gestureRecognizer = navigationController.interactivePopGestureRecognizer else { return }
gesture.setValue(gestureRecognizer.value(forKey: "targets"), forKey: "targets")
navigationController.view.addGestureRecognizer(gesture)
}
}
}
제스처를 받아서 기본적인 설정을 해준다. 여기서 name을 설정하는 이유는 제스처가 중복되지 않게 하기 위해서 필요하다.
let gesture: UIScreenEdgePanGestureRecognizer
func makeUIView(context: Context) -> some UIView {
gesture.name = UUID().uuidString
gesture.edges = UIRectEdge.left
return UIView()
}
아래의 코드는 부모뷰의 네비게이션 컨트롤러를 찾아서 interactivePopGestureRecognizer를 등록해주는 과정이다. 이 과정에서 targets라는 키로 추가하지 않으면 오류가 생긴다. 애플에서 지정한 키인 것 같다.
func updateUIView(_ uiView: UIViewType, context: Context) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
let viewControllers = sequence(first: uiView) { $0.next }.compactMap { $0 as? UIViewController }
guard let parentViewController = viewControllers.first,
let navigationController = parentViewController.navigationController else { return }
/// 제스처가 중복되지 않도록 확인
guard let gestureRecognizers = navigationController.view.gestureRecognizers,
!gestureRecognizers.contains(where: { $0.name == gesture.name }) else { return }
/// 기존 interactivePopGestureRecognizer의 동작을 복사하여 새로운 제스처에 할당
/// 애플이 지정한 키 "targets" 사용
guard let gestureRecognizer = navigationController.interactivePopGestureRecognizer else { return }
gesture.setValue(gestureRecognizer.value(forKey: "targets"), forKey: "targets")
navigationController.view.addGestureRecognizer(gesture)
}
}
✨ 결과
아래와 같이 기존 네비게이션 뒤로가기 버튼과 동일하게 작동하는 것을 볼 수 있다. 좀더 자유롭게 커스텀이 가능해졌다!! 😌
struct ContentView: View {
var body: some View {
CustomNavigationStack {
NavigationLink("Next view") {
NextView()
}
}
}
}
'→ SwiftUI' 카테고리의 다른 글
[SwiftUI] 이미지 압축하기 (0) | 2025.03.04 |
---|---|
[SwiftUI] UIKit 활용하여 달력 넣고 사용하기 (UICalendarView) (0) | 2024.01.19 |