iOS/SwiftUI

커스텀 SwipePopNavigationStack 구현하기

Hamp 2025. 7. 13. 17:15
반응형

👋 들어가기 전

swiftUI를 하면서, 당황스러운 경험을 겪어서 한번 적어보려한다.

 

상황은 다음과 같다.

 

🧑‍💻: 나 너네 네비바 말고 내가 만들어 쓸래 가능?

 

🍎: ㅇㅇ 그럼 우리꺼 숨겨

 

🧑‍💻: ㅇㅋ!

 

🧑‍💻: 어 잠만, 숨기니깐 왜 swipe pop 기능이 안돼?

 

🍎: 우리꺼 안쓴다며

 

🧑‍💻: 아니 네비바만 안쓴다고, swipe pop 기능은 쓸꺼야

 

🍎: 안돼, 그것도 니가 만들어 쓰셈

 

🧑‍💻: ... 😭


🏁 학습할 내용

  • 상황 설명
  • 해결 과정
    • UIKit 힘 빌리기
    • 커스텀 뷰 만들기

기본 형태

toolbar를 숨기지 않을 때, swipe pop이 너무 잘 동작한다.

toolbar(.hidden)

 

다음 함수를 써서, Visibilityhidden 처리하면, 아무리 swipe을 해도 동작하지 않는다.


🌊 해결과정

SwiftUI는 SwiftUI 자체로 해결을 못하면, 자연럽게 UIKit쪽까지 고려를 해봐야한다.

 

이 문제 역시 마찬가지다.

🧰 UIKit 힘 빌리기

준비물은 다음과 같다.

  • UIViewRepresentable: UIView를 사용하기위해
  • UIPanGestureRecognizer: interactivePopGestureRecognizer에 동작을 가로챌 gesture

BackgroundSwipePopView.swift

PanGesture를 인식하기위해 뒤에 몰래 숨겨 놓는 뷰다.

이게 바로 이다.

import SwiftUI
import UIKit

struct BackgroundSwipePopView: UIViewRepresentable {
  @Binding var gesture: UIPanGestureRecognizer
  func makeUIView(context: Context) -> UIView {
    return UIView()
  }

  func updateUIView(_ uiView: UIView, context: Context) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
      if let parentVC = uiView.parentViewController {
        if let navigationController = parentVC.navigationController {
          if let _ = navigationController.view.gestureRecognizers?.first(where: { $0.name == gesture.name }) {
            // Already added
          } else {
            navigationController.addFullSwipeGesture(gesture)
          }
        }
      }
    }
  }
}

fileprivate extension UINavigationController {
  func addFullSwipeGesture(_ gesture: UIPanGestureRecognizer) {
    let key = "targets"
    guard let gestureSelector = interactivePopGestureRecognizer?.value(forKey: key) else { return }

    gesture.setValue(gestureSelector, forKey: key)
    view.addGestureRecognizer(gesture)
  }
}

fileprivate extension UIView {
  //responder chain을 따라 next를 순차적으로 타고 올라가면서 UIViewController를 탐색
  var parentViewController: UIViewController? {
    sequence(first: self) {
      $0.next
    }.first { $0 is UIViewController } as? UIViewController
  }
}

 

func UpdateUIView 

  • SwiftUI에서 이 뷰가 업데이트될 때마다 호출
  • DispatchQueue.main.asyncAfter(deadline: .now() + 1):
    • 살짝 지연시켜서 NavigationController가 완전히 생성된 이후에 실행
  • UIView의 parentViewController를 찾고 , parentViewController의 navigationController를 찾는다.
  • 이후 navigationController에 외부에서 받은 gesture를 등록한다,

func addFullswipeGesture

  • interactivePopGestureRecognizer의 내부 target-action("targets" key)을 가져와서 새로운 제스처에 그대로 복사
  • 이로써 gesture도 같은 방식으로 작동하도록 만듬

📚 커스텀 네비게이션 스택 만들기

위에서 만든 뷰를 뷔에 숨기는 컨테이너 뷰가 필요하다.

import SwiftUI
import UIKit

public struct SwipePopNavigationStack<Content: View>: View {
  private let content: () -> Content

  @State private var gesture: UIPanGestureRecognizer = {
    let gesture = UIPanGestureRecognizer()
    gesture.name = UUID().uuidString
    gesture.isEnabled = true
    return gesture
  }()

  public init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
  }

  public var body: some View {
    NavigationStack {
      content()
        .background {
          BackgroundSwipePopView(gesture: $gesture)
        }
    }
  }
}

 


✅ 결과


😂 쉬운 방법을 나중에 찾음

다음 코드를 사용하면, back버튼을 지워도 시스템에서 지원하는 swipe pop을 사용할 수 있다.

extension UINavigationController: @retroactive UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

출처

https://youtu.be/4ceKPSTlL4I?si=iLTvC8WQrHUD32xC

반응형