Create a custom navigation bar and link in SwiftUI | Advanced Learning #12
PreferenceKey
: 네비게이션 타이틀, 네비게이션 서브타이틀, 네비게이션 백버튼히든 등 값 전달 → 네비게이션 링크의 destination
단의 네비게이션 바 뷰의 값 상태 변경 가능Preference
값 등록 자동화struct CustomNavView<Content:View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
NavigationView {
CustomNavBarContainerView {
content
}
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
}
@ViewBuilder
프로토콜을 준수하는 뷰 컨텐츠를 클로저 형태로 받아 뷰를 그리기import SwiftUI
struct CustomNavLink<Label: View, Destination: View>: View {
let destination: Destination
let label: Label
init(destination: Destination, @ViewBuilder label: () -> Label) {
self.destination = destination
self.label = label()
}
var body: some View {
NavigationLink {
CustomNavBarContainerView {
destination
.navigationBarHidden(true)
}
} label: {
label
}
}
}
@ViewBuilder
프로토콜을 준수하는 라벨 뷰 컨텐츠를 클로저로 받아 뷰를 그리기import SwiftUI
struct CustomNavBarView: View {
@Environment(\.presentationMode) var presentationMode
let showBackButton: Bool
let title: String
let subtitle: String?
var body: some View {
HStack {
if showBackButton {
backButton
}
Spacer()
titleSection
Spacer()
if showBackButton {
backButton
.opacity(0)
}
}
.padding()
.tint(.white)
.foregroundColor(.white)
.font(.headline)
.background(.blue)
}
}
extension CustomNavBarView {
private var backButton: some View {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "chevron.left")
}
}
private var titleSection: some View {
VStack(spacing: 4) {
Text(title)
.font(.title)
.fontWeight(.semibold)
if let subtitle = subtitle {
Text(subtitle)
}
}
}
}
HStack
으로 백버튼, 타이틀, 서브타이틀을 그리는 뷰presentationMode
환경변수 사용 → dismiss
직접 사용도 가능import SwiftUI
struct CustomNavBarContainerView<Content: View>: View {
let content: Content
@State private var showBackButton: Bool = true
@State private var title: String = ""
@State private var subtitle: String? = nil
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(spacing: 0) {
CustomNavBarView(showBackButton: showBackButton, title: title, subtitle: subtitle)
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onPreferenceChange(CustomNavBarTitlePreferenceKey.self) { value in
title = value
}
.onPreferenceChange(CustomNavBarSubtitlePreferenceKey.self) { value in
subtitle = value
}
.onPreferenceChange(CustomNavBarBackButtonHiddenPreferenceKey.self) { value in
showBackButton = !value
}
}
}
PreferenceKey
로 등록, 값 변화에 따라 상태값을 주기 위한 @State
선언@ViewBuilder
프로토콜을 준수, 뷰 자체를 그리기 위한 클로저를 입력값으로 받기import SwiftUI
struct CustomNavBarTitlePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
struct CustomNavBarSubtitlePreferenceKey: PreferenceKey {
static var defaultValue: String? = nil
static func reduce(value: inout String?, nextValue: () -> String?) {
value = nextValue()
}
}
struct CustomNavBarBackButtonHiddenPreferenceKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) {
defaultValue = nextValue()
}
}
extension View {
func customNavigationTitle(_ title: String) -> some View {
preference(key: CustomNavBarTitlePreferenceKey.self, value: title)
}
func customNavigationSubtitle(_ subtitle: String?) -> some View {
preference(key: CustomNavBarSubtitlePreferenceKey.self, value: subtitle)
}
func customNavigationBarBackButtonHidden(_ hidden: Bool) -> some View {
preference(key: CustomNavBarBackButtonHiddenPreferenceKey.self, value: hidden)
}
func customNavBarItems(title: String = "", subtitle: String? = nil, hidden: Bool = false) -> some View {
self
.customNavigationTitle(title)
.customNavigationSubtitle(subtitle)
.customNavigationBarBackButtonHidden(hidden)
}
}
PreferenceKey
등록import SwiftUI
struct CustomNavView<Content:View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
NavigationView {
CustomNavBarContainerView {
content
}
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
}
struct CustomNavLink<Label: View, Destination: View>: View {
let destination: Destination
let label: Label
init(destination: Destination, @ViewBuilder label: () -> Label) {
self.destination = destination
self.label = label()
}
var body: some View {
NavigationLink {
CustomNavBarContainerView {
destination
.navigationBarHidden(true)
}
} label: {
label
}
}
}
extension UINavigationController {
open override func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = nil
}
}
interactivePopGestureRecognizer
적용. 디폴트 네비게이션 뷰가 살아 있기 때문에 적용 가능import SwiftUI
struct AppNavBarView: View {
var body: some View {
CustomNavView {
ZStack {
Color.orange.ignoresSafeArea()
CustomNavLink(destination: customDestinationView
.customNavigationTitle("SECOND TITLE")
.customNavigationSubtitle("SUBTITLE")
.customNavigationBarBackButtonHidden(false)
) {
Text("NAVIGATE")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
.customNavBarItems(title: "NEW TITLE", subtitle: "SUBTITLE", hidden: true)
}
}
}
extension AppNavBarView {
private var customDestinationView: some View {
ZStack {
Color.green.ignoresSafeArea()
Text("DESTINATION VIEW")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
}
private var defaultNavView: some View {
NavigationView {
ZStack {
Color.red.ignoresSafeArea()
NavigationLink {
ZStack {
Color.green.ignoresSafeArea()
.navigationTitle("NAV TITLE2")
.navigationBarBackButtonHidden(false)
Text("DESTINATION VIEW")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
} label: {
Text("NAVIGATE")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
.navigationTitle("NAV TITLE")
}
}
탭바를 커스텀해서 구현하는 것보다는 이해하기 쉬웠지만, 전반적으로 쉽지만은 않았다! SwiftUI로 네비게이션 뷰를 사용할 때 발생하는 디스미스 오류 또는 백버튼 스와이프 등 기능 소실이 걱정스러웠는데, 커스텀을 통해 해결할 수 있는 것을 배웠다.