I've got a super simple SwiftUI master-detail app:
import SwiftUI
struct ContentView: View {
@State private var imageNames = [String]()
var body: some View {
NavigationView {
MasterView(imageNames: $imageNames)
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation {
// simplified for example
self.imageNames.insert("image", at: 0)
}
}
) {
Image(systemName: "plus")
}
)
}
}
}
struct MasterView: View {
@Binding var imageNames: [String]
var body: some View {
List {
ForEach(imageNames, id: \.self) { imageName in
NavigationLink(
destination: DetailView(selectedImageName: imageName)
) {
Text(imageName)
}
}
}
}
}
struct DetailView: View {
var selectedImageName: String
var body: some View {
Image(selectedImageName)
}
}
I'm also setting the appearance proxy on the SceneDelegate for the navigation bar's colour"
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithOpaqueBackground()
navBarAppearance.shadowColor = UIColor.systemYellow
navBarAppearance.backgroundColor = UIColor.systemYellow
navBarAppearance.shadowImage = UIImage()
UINavigationBar.appearance().standardAppearance = navBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Now, what I'd like to do is for the navigation bar's background colour to change to clear when the detail view appears. I still want the back button in that view, so hiding the navigation bar isn't really an ideal solution. I'd also like the change to only apply to the Detail view, so when I pop that view the appearance proxy should take over and if I push to another controller then the appearance proxy should also take over.
I've been trying all sorts of things:
- Changing the appearance proxy on didAppear
- Wrapping the detail view in a UIViewControllerRepresentable
(limited success, I can get to the navigation bar and change its colour but for some reason there is more than one navigation controller)
Is there a straightforward way to do this in SwiftUI?
Update: in iOS 16 we have a new modifier .toolbarBackground()
that allows us to set custom background of a navigation bar.
For older iOS Versions: I prefer using ViewModifer for this. Below is my custom ViewModifier
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
init(backgroundColor: UIColor?) {
self.backgroundColor = backgroundColor
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = backgroundColor
coloredAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
UINavigationBar.appearance().tintColor = .white
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}}
You can also initialize it with different text color and tint color for your bar, I have added static color for now.
You can call this modifier from any. In your case
NavigationLink(
destination: DetailView(selectedImageName: imageName)
.modifier(NavigationBarModifier(backgroundColor: .green))
)
Below is the screenshot.
In my opinion, this is the straightforward solution in SwiftUI.
problem: framework adds back buttom in the DetailView solution: Custom back button and nav bar are rendered
struct DetailView: View {
var selectedImageName: String
@Environment(\.presentationMode) var presentationMode
var body: some View {
CustomizedNavigationController(imageName: selectedImageName) { backButtonDidTapped in
if backButtonDidTapped {
presentationMode.wrappedValue.dismiss()
}
} // creating customized navigation bar
.navigationBarTitle("Detail")
.navigationBarHidden(true) // Hide framework driven navigation bar
}
}
If framework driven navigation bar is not hidden in the detail view, we get two navigation bars like this: double nav bars
Using UINavigationBar.appearance() is quite unsafe in scenarios like when we want to present both of these Master and Detail views within a popover. There is a chance that all other nav bars in our application might acquire the same navbar configuration of the Detail view.
struct CustomizedNavigationController: UIViewControllerRepresentable {
let imageName: String
var backButtonTapped: (Bool) -> Void
class Coordinator: NSObject {
var parent: CustomizedNavigationController
var navigationViewController: UINavigationController
init(_ parent: CustomizedNavigationController) {
self.parent = parent
let navVC = UINavigationController(rootViewController: UIHostingController(rootView: Image(systemName: parent.imageName).resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.blue)))
navVC.navigationBar.isTranslucent = true
navVC.navigationBar.tintColor = UIColor(red: 41/255, green: 159/244, blue: 253/255, alpha: 1)
navVC.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.red]
navVC.navigationBar.barTintColor = .yellow
navVC.navigationBar.topItem?.title = parent.imageName
self.navigationViewController = navVC
}
@objc func backButtonPressed(sender: UIBarButtonItem) {
self.parent.backButtonTapped(sender.isEnabled)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UINavigationController {
// creates custom back button
let navController = context.coordinator.navigationViewController
let backImage = UIImage(systemName: "chevron.left")
let backButtonItem = UIBarButtonItem(image: backImage, style: .plain, target: context.coordinator, action: #selector(Coordinator.backButtonPressed))
navController.navigationBar.topItem?.leftBarButtonItem = backButtonItem
return navController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
//Not required
}
}
Here is the link to view the full code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With