Custom NavigationView in SwiftUI

Updated:

Custom NavigationView in SwiftUI

The default NavigationView comes with swiftUI is not that customizable. But you could build a custom Nav View and Bar are actually create wrappers and wrap them around the default navigation view and link

But on the screen it’s going to appear like we’re using our own custom navigationView. To be possible by using ViewBuilders and PreferenceKeys

// Default NavigationView in Apple's API
struct AppNavBarView: View {
// MARK: -  PROPERTY
// MARK: -  BODY
var body: some View {
NavigationView {
ZStack {
Color.green.ignoresSafeArea()

NavigationLink(destination: Text("Destination")
                .navigationTitle("Title 2")
                .navigationBarBackButtonHidden(false)) {
  Text("Navigate")
}
}
.navigationTitle("Nav title here")
} //: NAVIGATION
}
}

스크린샷

// in CustomNavBarTitlePreferenceKey
import Foundation
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) {
		value = nextValue()
	}
}

extension View {
	func customNavigationTile(_ title: String) -> some View {
		self
			.preference(key: CustomNavBarTitilePreferenceKey.self, value: title)
	}

	func customNavigationSubtitle(_ subtitle: String?) -> some View {
		self
			.preference(key: CustomNavBarSubtitlePreferenceKey.self, value: subtitle)
	}

	func customNavigationBarBackButtonHidden(_ hidden: Bool) -> some View {
		self
			.preference(key: CustomNavBarBackButtonHiddenPreferenceKey.self, value: hidden)
	}

	// combine above three functions
	func customNavBarItems(title: String = "", subtitle: String? = nil, backButtonHidden: Bool = false) -> some View {
		self
			.customNavigationTile(title)
			.customNavigationSubtitle(subtitle)
			.customNavigationBarBackButtonHidden(backButtonHidden)
	}
}

// in CustomNavLink
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(
  destination:
    CustomNavBarContainerView(content: {
      destination
    }).navigationBarHidden(true)){
  label
}
}
}

struct CustomNavLink_Previews: PreviewProvider {
static var previews: some View {
CustomNavView {
  CustomNavLink(
    destination: Text("Destination")) {
      Text("Click Me")
    }
}
}
}

// in CustomNavBarContainerView
// MARK: -  VIEW
struct CustomNavBarContainerView<Content: View>: View {
// MARK: -  PROPERTY
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()
}
// MARK: -  BODY
var body: some View {
  VStack (spacing: 0) {
    CustomNavBarView(showBackButton: showBackButton, title: title, subtitle: subtitle)
    content
      .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
  .onPreferenceChange(CustomNavBarTitilePreferenceKey.self) { value in
    self.title = value
  }
  .onPreferenceChange(CustomNavBarSubtitlePreferenceKey.self) { value in
    self.subtitle = value
  }
  .onPreferenceChange(CustomNavBarBackButtonHiddenPreferenceKey.self) { value in
    self.showBackButton = !value
  }
}
}

// MARK: -  PREVIEW
struct CustomNavBarContainerView_Previews: PreviewProvider {
static var previews: some View {
CustomNavBarContainerView {
ZStack {
  Color.green.ignoresSafeArea()

  Text("Hello")
    .foregroundColor(.white)
    .customNavigationTile("New Title")
    .customNavigationSubtitle("subtitle")
    .customNavigationBarBackButtonHidden(true)
}
}
}
}

// in CustomNavBarView
// MARK: -  VIEW
struct CustomNavBarView: View {
// MARK: -  PROPERTY
@Environment(\.presentationMode) var presentationMode
let  showBackButton: Bool
let title: String
let subtitle: String?
// MARK: -  BODY
var body: some View {
HStack {
  if showBackButton {
    backButton
  }
  Spacer()
  titleSection
  Spacer()
  if showBackButton {
    backButton
      .opacity(0)
  }

} //: HSTACK
.padding()
.accentColor(.white)
.foregroundColor(.white)
.font(.headline)
.background(Color.blue.ignoresSafeArea(edges: .top))
}
}

// MARK: -  PREVIEW
struct CustomNavBarView_Previews: PreviewProvider {
static var previews: some View {
VStack {
  CustomNavBarView(showBackButton: true, title: "Title here", subtitle: "Subtitle goes here")
  Spacer()
}
}
}

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)
  }

} //: VSTACK
}
}

// in CustomNavView
struct CustomNavView<Content:View>: View {
// MARK: -  PROPERTY
let content: Content

init(@ViewBuilder content: () -> Content) {
self.content = content()
}
// MARK: -  BODY
var body: some View {
NavigationView {
  CustomNavBarContainerView {
    content
  }
  .navigationBarHidden(true)
} //: NAVIGATION
.navigationViewStyle(.stack)
}
}

// MARK: -  PREVIEW
struct CustomNavView_Previews: PreviewProvider {
static var previews: some View {
CustomNavView {
  Color.red.ignoresSafeArea()
}
}
}

// enable drag back gesture in CustomNavBar
extension UINavigationController {
open override func viewDidLoad() {
  super.viewDidLoad()
  interactivePopGestureRecognizer?.delegate = nil
}
}

struct AppNavBarView: View {
// MARK: -  BODY
var body: some View {
CustomNavView {
ZStack {
Color.orange.ignoresSafeArea()

CustomNavLink(destination:
              Text("Destination")
              .customNavigationTile("Second Screen")
              .customNavigationSubtitle("Sibtitle should be showing!!")
) {
Text("Navigate")
}
} //: ZSTACK
.customNavBarItems(title: "New Title!", subtitle: nil, backButtonHidden: true)
}
}
}

// MARK: -  PREVIEW
struct AppNavBarView_Previews: PreviewProvider {
static var previews: some View {
AppNavBarView()
}
}

// MARK: -  EXTENSTION
extension AppNavBarView {
private var defaultNavView: some View {
NavigationView {
ZStack {
Color.green.ignoresSafeArea()

NavigationLink(destination: Text("Destination")
              .navigationTitle("Title 2")
              .navigationBarBackButtonHidden(false)) {
Text("Navigate")
}
}
.navigationTitle("Nav title here")
} //: NAVIGATION
}
}

스크린샷


🗃 Reference

SwiftUI Thinking - https://youtu.be/aIDT4uuMLHc

Categories:

Updated:

Leave a comment