SwiftUI: Setting individual colors for .toolbar ToolbarItem?

Viewed 3387

Generally I like to stick with default color options for accessibility reasons, but I have a specific use case and I am unable to change an individual .toolbar ToolbarItem color.

I can override all ToolbarItem colors by using the commented out code at top of my snippet, but I want to color individual icons.

import SwiftUI

struct ContentView: View {
    
//    init() {
//        UIBarButtonItem.appearance(whenContainedInInstancesOf: [UIToolbar.self]).tintColor = .systemRed
//    }
    
    var body: some View {
        
        NavigationView {
            
            Text("Hello, world!")
                .toolbar {
                    
                    // To be colored RED
                    ToolbarItem(placement: .bottomBar) {
                        Button(action: {}, label: {Label("Icon One", systemImage: "stop.fill")})
                    }
                    
                    // To be colored BLACK
                    ToolbarItem(placement: .bottomBar) {
                        Button(action: {}, label: {Label("Icon Two", systemImage: "play.fill")})
                    }
                    
                }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
5 Answers

Yep, I've been struggling with the same thing. It looks that Buttons are using system tint color and its style overall.

Here is my take:

content
    .toolbar {
        ToolbarItem(placement: .navigationBarLeading) {
            HStack {
                StyledButton(image: .system("arrow.left")) { ... }
                Spacer(minLength: 0)    // important to not fell button to default styling
            }
        }
    }

The idea is to not display Button only, but together with other UI elements. Looks like a SwiftUI bug to me. (Or just trying to out-smart you.)

The simplest solution I've found is to ditch Button and use .onTapGesture instead.

struct ContentView: View {
    
    var body: some View {
        NavigationView {
            Text("Hello World!")
                .toolbar {
                    
                    // To be colored RED
                    ToolbarItem(placement: .bottomBar) {
                        Label("Icon One", systemImage: "stop.fill")
                            .foregroundColor(.red)
                            .onTapGesture {
                                // Respond to tap
                            }
                    }
                    
                    // To be colored BLACK
                    ToolbarItem(placement: .bottomBar) {
                        Label("Icon One", systemImage: "play.fill")
                            .foregroundColor(.black)
                            .onTapGesture {
                                // Respond to tap
                            }
                    }
                    
                }
        }
    }
}

Output:

enter image description here

It looks like all standard types (button, image, text, etc) are intercepter by ToolbarItem and converted into appropriate internal representation. But custom view (eg. shape based)... is not. So see below a demo of possible approach.

Demo prepared & tested with Xcode 12 / iOS 14.

demo

ToolbarItem(placement: .bottomBar) {
    ShapeButton(shape: Rectangle(), color: .red) {
        print(">> works")
    }
}

and simple custom Shape-based button

struct ShapeButton<S:Shape>: View {
    var shape: S
    var color: Color
    var action: () -> ()

    @GestureState private var tapped = false
    var body: some View {
        shape
            .fill(color).opacity(tapped ? 0.4 : 1)
            .animation(.linear(duration: 0.15), value: tapped)
            .frame(width: 18, height: 18)
            .gesture(DragGesture(minimumDistance: 0)
                .updating($tapped) { value, state, _ in
                    state = true
                }
                .onEnded { _ in
                    action()
                })
    }
}

If you drop into UIKit it's working for me.

struct ButtonRepresentation: UIViewRepresentable {
    let sfSymbolName: String
    let titleColor: UIColor
    let action: () -> ()
    
    func makeUIView(context: Context) -> UIButton {
        let b = UIButton()
        let largeConfig = UIImage.SymbolConfiguration(scale: .large)
        let image = UIImage(systemName: sfSymbolName, withConfiguration: largeConfig)
        b.setImage(image, for: .normal)
        b.tintColor = titleColor
        b.addTarget(context.coordinator, action: #selector(context.coordinator.didTapButton(_:)), for: .touchUpInside)
        return b
    }
    
    func updateUIView(_ uiView: UIButton, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(action: action)
    }
    
    typealias UIViewType = UIButton
    
    class Coordinator {
        let action: () -> ()
        
        @objc
        func didTapButton(_ sender: UIButton) {
            self.action()
        }
        
        init(action: @escaping () -> ()) {
            self.action = action
        }
    }
}

Then you can add separate ButtonRepresentations with different colors.

.toolbar {
    ToolbarItem(placement: .cancellationAction) {
        ButtonRepresentation(sfSymbolName: closeIcon, titleColor: closeIconColor) { 
        presentationMode.wrappedValue.dismiss()
    }
}

Sadly this trick only works on iOS.

But on the bright side it also works for Menu as well:

Menu {
    Button("A title") {}
} label : {
    HStack {
            Label("Star"), systemImage: "star")
                .labelStyle(.iconOnly)
                .foregroundColor(.red)
                Spacer(minLength: 0)
    }
}

Related