Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI override navigation bar appearance in detail view

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?

like image 480
KerrM Avatar asked May 26 '20 16:05

KerrM


2 Answers

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. Detail View with green navigation bar

like image 172
user832 Avatar answered Oct 14 '22 15:10

user832


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.

like image 24
Vijay Varma Vegesna Avatar answered Oct 14 '22 15:10

Vijay Varma Vegesna