Swift: Custom NavigationView에서 Swipe-back 가능하게 하기

Heechan
HcleeDev
Published in
9 min readFeb 26, 2021

이전에 Custom Navigation Bar 만들기 글에서 DragGesture를 이용해 현재 View에서 이전 View로 돌아갈 수 있는 방법을 제시한 바 있다.

하지만 이 방법은 그렇게 좋은 방법은 아니었다. 애플이 기본적으로 제공하고 있는 NavigationView에서 Swipe로 되돌아가는 방식과 비슷하지 않았고, Drag하고 있음을 시각적으로 보여주지 못했다. 따라서 그 이후에도 수많은 앱들처럼 애플이 기본적으로 제공하는 NavigationView의 기능을 Custom한 경우에도 적용시킬 수 있는 방법을 찾아보았고, 해결했기에 글로 남기고자 한다.

문제 상황

기존 DragGesture를 이용하는 방식의 로직은, 사용자가 왼쪽 끄트머리 어딘가(x 좌표 < 30)로부터 x 방향으로 100 이상 움직이는 DragGesture를 수행한 경우 현재 View를 dismiss()하는 방식이었다.

.gesture(DragGesture().updating($dragOffset) { (value, state, transaction) in
if (value.startLocation.x < 30 && value.translation.width > 100) {
self.presentationMode.wrappedValue.dismiss()
}
})
지난 게시물의 움짤을 그대로 가져왔다. 커서에 맞춰 움직이는 것이 아닌 100만 넘어가면 슥 넘어가는 애니메이션만 나온다.

하지만 이는 Drag를 하고 있음이 시각적으로 드러나지 않고, 원래 기본 NavigationView에서 제공하는 기능과 확연히 차이가 있다는 점을 알 수 있다. 아래 사진을 보자. 드래그해서 끌면 이전 View의 모습이 조금씩 보이고, 그에 따른 애니메이션이 제공된다.

따라서 이는 모종의 이유 때문에 기본적으로 제공하는 기능이 막히게 되었음을 의미하고, 이를 다시 이용, 활성화할 수 있는 방법을 찾아다녔다.

NavigationView와 그 구성

NavigationView는 SwiftUI이긴 하지만, 그 기능 자체가 완전히 새롭게 만들어진 것은 아니다.

NavigationView의 기능은 기본적으로 UIKit의 UINavigationController의 기능을 가지고 있다. 좀 더 편하게 사용할 수 있는 SwiftUI 식으로 감싼 것이지, 그 속을 보면 실제로는 UINavigationController를 포함하고 있다. (다 확인은 해보지 않았지만 다른 SwiftUI의 경우에도 UIKit의 기능을 적절히 더하고 빼고 감싼 것이 아닐까 싶다.)

실제로 NavigationView를 View hierarchy 디버그 창에서 뜯어보면 UINavigation~을 내부에 가지고 있는 것을 확인할 수 있다.

그렇다면 Swipe-Back은 무엇과 관련있는 것일까?

Custom NavigationView에서 Swipe 기능이 없어지는 이유는, NavigationBar를 없애기 때문이다. 우리가 원하는 Navigation Bar를 만들기 위해서, NavigationBar를 숨기고 NavigationTitle도 빈 문자열을 넣어준다.

.navigationBarTitle("")
.navigationBarHidden(true)

하지만 이 방법을 사용하면 Swipe 기능도 비활성화된다는 문제가 있다. 이를 다시 활성화시킬 수 있는 방법이 있을까 둘러봤지만 NavigationView에는 딱히 보이지 않았다.

그래서 검색하다보니, NavigationView의 근본인 UINavigationController를 직접 건드리면 이 문제를 해결할 수 있다고 한다. 바로 아래 메서드를 사용하는 것이다.

interactivePopGestureRecognizer?.delegate = self

UINavigationController에는 PopGesture를 인식하고 상호작용해주는 프로퍼티가 존재한다. 이 코드는 PopGesture를 인식해 전달하는 delegate를 self, 즉 해당 NavigationView로 연결한다는 의미를 가지고 있다.

근데 왜 이걸 PopGesture라고 부를까? UINavigationController는 Stack 형태로 View들의 상하관계를 정리한다. 흔히 navigation stack이라고 부르는 데이터 구조에 View를 쌓는다고 생각하면 된다. Stack이 참 어울리는 이유가, 가장 위에 있는 View만 화면에 보이면 되고, 뒤로 가기를 선택하면 그 바로 이전 View로 돌아가면 되니 Stack에 쌓고 빼는 것이 꽤 어울린다. Stack에 뭔가를 넣을 때를 Push, 가장 위에 있는 것을 빼낼 때를 Pop이라고 하는데, 아마 PopGesture라고 부르는 이유가 이것이 아닐까 싶다.

아무튼 문제가 무엇인지 찾을 수 있었다. NavigationBar를 가려버리면 interactivePopGestureRecognizer의 역할도 비활성화되고, 이를 다시 활성화하기 위해서는 이 프로퍼티의 delegate를 다시 연결시켜주기만 하면 된다.

문제 해결

위의 메서드를 활용하고, 인터넷을 찾다가 추가적으로 넣어줘야 하는 함수도 넣어줬다. extension이니까, 걍 코드 아무데나 적어줘도 상관없다. 따로 Representable 처리해줘야 하는 것도 없다.

이는 UINavigationController의 기능 자체에 접근해(extension) 내부의 viewDidLoad()를 override한 것이다. SwiftUI에서는 볼 일이 없다시피한 내용인데, UIKit에서는 View의 life cycle을 꽤 중요하게 여긴다. 따라서 ViewDidLoad, ViewWillAppear 등의 메서드로 View의 각 life cycle마다 할 수 있는 일을 설정해줄 수 있다.

ViewDidLoad는 전반적인 View life cycle에서 꽤 앞쪽에 위치하는 단계인데, View가 처음 그려질 때 정도로 생각하면 될 것 같다. 그때 수행해야 할 일을 원래 UINavigationController 내부에 정의해둔거지만, 그걸 override함으로써 우리가 원하는 동작을 수행할 수 있도록 덧씌워버린다.

아래 있는 gestureRecognizerShouldBegin 메서드는 Stack에 쌓인 View가 1개를 초과할 때 Gesture 인식을 시작하도록 설정해주는 것 같다. 한 개 밖에 없을 때 인식하면… 어떤 일이 일어나는지는 모르겠다.

아무튼, 이렇게 extension을 작성하고, NavigationBar를 숨긴 상태로 실행을 해보았다.

이제 굳이 추가적인 처리를 해주지 않아도 모든 Navigation Stack에서 Swipe-Back 기능을 사용할 수 있다.

추가적인 발견, NavigationBar 영구적으로 없애기

이렇게 문제를 해결하고 나서, 페어님께서 그러면 이처럼 extension를 이용해 NavigationBar 숨기기를 굳이 메서드를 쓰지 않아도 기본적으로 숨길 수 있냐고 하셔서 찾아보았다.

확실히 앱 전체적으로 View들을 커스텀해서 사용하기 때문에, 기본 NavigationBar를 사용할 일이 없다. 하지만 그럴 때마다 각 View마다 이를 숨기는 메서드를 붙여주는 것도 여간 귀찮은 일이 아니다.

.navigationBarTitle("")
.navigationBarHidden(true)

이를 지워주기 위해선 어떻게 해야 할까?

위에서 사용한 extension에 한 줄만 추가해주면 된다.

navigationBar.isHidden = true 를 추가했다. 딱 보면 느낌이 오겠지만, viewDidLoad() 에서 NavigationBar를 없앰으로써 모든 Navigation Stack의 View가 생길 때마다 NavigationBar가 기본적으로 보이지 않는다.

인터넷에서 찾아볼 때는 navigationController?.navigationBar.isHidden = true 이런 식으로 작성하는 경우가 많았던 것 같다. 하지만 이는 일반적인 View에서 UINavigationController를 따로 호출해서 사용할 때 navigationController?.navigationBar 를 가리켜야 하는 것 같고, 이 경우는 extension이라 이미 navigationBar가 프로퍼티로 UINavigationController 내부에 정의되어있어 바로 navigationBar에 접근해 숨겨버리면 된다.

이렇게 코드를 작성하면 굳이 매번 NavigationBar 숨기는 처리할 필요가 없고, 기본적으로 NavigationView에서 NavigationBar가 보이지 않게 된다.

결론

사실 UIKit 시절을 잘 모르고, SwiftUI만 알고 있어서 이 방법을 알아채는데 시간이 좀 걸렸다. 그래도 이를 통해 Life cycle도 살짝 알아볼 수 있었고, extension의 사용법도 조금 더 알게 된 것 같다. extension도 잘 활용할 수만 있다면 정말 강력한 툴이 되어줄텐데, 이를 사용하려면 그만큼 내부 구조를 이해하고 있어야 좀 더 자유자재로 쓸 수 있지 않을까, 생각이 든다. 앞으로도 공부할 것들이 많다.

참고한 것

UINavigationController | Apple Developer Documentation

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science