Change colour of Picker within NavigationView - ios

I'm trying to embed this NavigationView within a larger view but nothing I seem to set will change to background colour of the Picker within this view.
When I do the following, everything other than the Picker itself is set to black, but the picker remains white, like so...
example image
There may be a much better setup to get the effect I am after but not knowing that, how do I change the Picker Colour also?
struct ContentView: View {
#State var value = ""
init(){
UITableView.appearance().backgroundColor = .clear
UINavigationBar.appearance().backgroundColor = .clear
}
var body: some View {
NavigationView {
ZStack {
Color.black
.ignoresSafeArea()
Form {
Picker(selection: $value, label: Text("This")) {
Text("1").tag("1")
Text("2").tag("2")
Text("3").tag("3")
Text("4").tag("4")
}
}
}
}
}
}

Use listRowBackground for that.
Picker(selection: $value, label: Text("this")) {
...
}.listRowBackground(Color.green)
In order to change the background color of the cells in the opened picker, you'll have to set them through UIKit.
extension View {
func cellBackgroundColor(_ uiColor: UIColor) -> some View {
background(TableCellGrabber { cell in
cell.backgroundView = UIView()
cell.backgroundColor = uiColor
})
}
}
struct TableCellGrabber: UIViewRepresentable {
let configure: (UITableViewCell) -> Void
func makeUIView(context: Context) -> UIView {
UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
DispatchQueue.main.async {
if let cell: UITableViewCell = uiView.parentView() {
configure(cell)
}
}
}
}
extension UIView {
func parentView<T: UIView>() -> T? {
if let v = self as? T {
return v
}
return superview?.parentView()
}
}
Usage:
Picker(selection: $value, label: Text("this")) {
Text("1").tag("1").cellBackgroundColor(.red)
Text("2").tag("2").cellBackgroundColor(.red)
Text("3").tag("3").cellBackgroundColor(.red)
Text("4").tag("4").cellBackgroundColor(.red)
}
Or you can use special Group view to apply it to all grouped items.
Picker(selection: $value, label: Text("this")) {
Group {
Text("1").tag("1")
Text("2").tag("2")
Text("3").tag("3")
Text("4").tag("4")
}.cellBackgroundColor(.red)
}

Related

How do I change the background colour of a list in swiftui? [duplicate]

I'm trying to recreate an UI I built with UIKit in SwiftUI but I'm running into some minor issues.
I want the change the color of the List here, but no property seems to work as I expects. Sample code below:
struct ListView: View {
#EnvironmentObject var listData: ListData
var body: some View {
NavigationView {
List(listData.items) { item in
ListItemCell(item: item)
}
.content.background(Color.yellow) // not sure what content is defined as here
.background(Image("paper-3")) // this is the entire screen
}
}
}
struct ListItemCell: View {
let item: ListItem
var body: some View {
NavigationButton(destination: Text(item.name)) {
Text("\(item.name) ........................................................................................................................................................................................................")
.background(Color.red) // not the area I'm looking for
}.background(Color.blue) // also not the area I'm looking for
}
}
Ok, I found the solution for coloring the list rows:
struct TestRow: View {
var body: some View {
Text("This is a row!")
.listRowBackground(Color.green)
}
}
and then in body:
List {
TestRow()
TestRow()
TestRow()
}
This works as I expect, but I have yet to find out how to then remove the dividing lines between the rows...
This will set the background of the whole list to green:
init() {
UITableView.appearance().separatorStyle = .none
UITableViewCell.appearance().backgroundColor = .green
UITableView.appearance().backgroundColor = .green
}
struct ContentView: View {
var strings = ["a", "b"]
var body: some View {
List {
ForEach(strings, id: \.self) { string in
Text(string)
}.listRowBackground(Color.green)
}
}
}
You can do it by changing UITableView's appearance.
UITableView.appearance().backgroundColor = UIColor.clear
just put this line in Appdelegate's didFinishLaunchingWithOptions method.
In replace of UIColor.clear set whatever color you want to add in background color of list.
Changing Background Color
As other have mentioned, changing the UITableView background will affect all other lists in your app.
However if you want different background colors you can set the default to clear, and set the background color in swiftui views like so:
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
// Ignore safe area to take up whole screen
.background(Color.purple.ignoresSafeArea())
.onAppear {
// Set the default to clear
UITableView.appearance().backgroundColor = .clear
}
You probably want to set the tableview appearance earlier, such as in the SceneDelegate or root view like so:
// SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else {
print("Returning because screne does not exist")
return
}
// Set here
UITableView.appearance().backgroundColor = .clear
let contentView = ContentView()
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
// Root App View
#main
struct ListBackgroundApp: App {
init() {
UITableView.appearance().backgroundColor = .clear
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
2022
MacOS Solution
The following code makes ALL OF Lists background color transparent:
// Removes background from List in SwiftUI
extension NSTableView {
open override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
backgroundColor = NSColor.clear
if let esv = enclosingScrollView {
esv.drawsBackground = false
}
}
}
..........
..........
..........
the following code makes ALL OF TextEditors background color transparent:
extension NSTextView {
open override var frame: CGRect {
didSet {
backgroundColor = .clear
drawsBackground = true
}
}
}
There is an argument: listRowBackground() in SwiftUI, but if you use List directly to iterate the data collection, it doesn't work.
Here is my workaround:
List {
// To make the background transparent, we have we use a ForEach as a wrapper
ForEach(files) {file in
Label(
title: { Text(file.name ?? fileOptionalFiller).lineLimit(listRowTextLineLimit) },
icon: { AppIcon.doc.foregroundColor(.primary) }
)
}
.listRowBackground(Color.primary.colorInvert())
}
Basically, listRowBackground() works if you use a ForEach inside List.
I was able to get the whole list to change color by using colorMultiply(Color:). Just add this modifier to the end of the list view, and then the padding will push the table to the device edges. For example:
List {...}.colorMultiply(Color.green).padding(.top)
https://www.hackingwithswift.com/quick-start/swiftui/how-to-adjust-views-by-tinting-and-desaturating-and-more
I do not know what is the connection but if you wrap the list with Form it is working.
Form {
List(viewModel.currencyList, id: \.self) { currency in
ItemView(item: currency)
}
.listRowBackground(Color("Primary"))
.background(Color("Primary"))
}
iOS 16 provides a modifier to control the background visibility of List (and other scrollable views): scrollContentBackground(_:)
You can hide the standard system background via .hidden. If you provide a background as well, that will become visible.
List {
Text("One")
Text("Two")
}
.background(Image("MyImage"))
.scrollContentBackground(.hidden)
You may also want to customize the background of list rows - the individual cells - and separators. This can be done like so:
List {
Section("Header") {
Text("One")
Text("Two")
.listRowBackground(Color.red)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
.scrollContentBackground(.hidden)
struct Details: View {
var body: some View {
Spacer().overlay(
List {
Text("Hello World!").font(.title2)
.listRowBackground(Color.clear)
Text("Hello World again").font(.title2)
.listRowBackground(Color.clear)
}.onAppear() {
UITableView.appearance().backgroundColor = UIColor.green
UITableViewCell.appearance().backgroundColor = UIColor.green
}
)
}
}
The answer by Islom Alimov https://stackoverflow.com/a/59970379/9439097 seems to be the best implementation so far in my opinion.
Only drawback: this also changes the background color of all other list views in your app, so you need to manually change them back unless you want the same color everywhere.
Here is an example view:
import SwiftUI
struct TestView1: View {
init(){
UITableView.appearance().backgroundColor = UIColor(Color.clear)
}
#State var data = ["abc", "def"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self) {element in
Text("\(String(describing: element))")
}
.background(Color.green)
.listRowBackground(Color.blue)
}
.background(Color.yellow)
Spacer()
Color.red
}
}
}
struct TestView1_Previews: PreviewProvider {
static var previews: some View {
TestView1()
}
}
produces:
Someone may find this useful if attempting to create a floating type cell with SwiftUI using .listRowBackground and applying .padding
var body: some View {
NavigationView {
List {
ForEach (site) { item in
HStack {
Text(String(item.id))
VStack(alignment: .leading) {
Text(item.name)
Text(item.crop[0])
}
}.listRowBackground(Color.yellow)
.padding(.trailing, 5)
.padding(.leading, 5)
.padding(.top, 2)
.padding(.bottom, 2))
}
}
.navigationBarTitle(Text("Locations"))
}
}
I assume the listRowPlatterColor modifier should do this, but isn't as of Xcode 11 Beta 11M336w
var body: some View {
List(pokemon) { pokemon in
PokemonCell(pokemon: pokemon)
.listRowPlatterColor(.green)
}
}
.colorMultiply(...)
As an option you can .colorMultiply(Color.yourColor) modifier.
Warning: this does not change the color! This only applies the Multiply modifier to the current color. Please read the question before any action, because you are probably looking for: "How to CHANGE the background color of a List in SwiftUI" and this will not work for you. ❄️
Example:
List (elements, id:\.self ) { element in
Text(element)
}
.colorMultiply(Color.red) <--------- replace with your color
For me, a perfect solution to change the background of List in SwiftUI is:
struct SomeView: View {
init(){
UITableView.appearance().backgroundColor = UIColor(named: "backgroundLight")
}
...
}
List is not perfect yet.
An option would be to use it like this -> List { ForEach(elements) { }} instead of List($elements)
On my end this is what worked best up to now.
Like #FontFamily said, it shouldn't break any List default behaviors like swiping.
Simply Add UITableView appearance background color in init() method and add list style (.listStyle(SidebarListStyle()). Don't forget to import UIKit module
struct HomeScreen: View {
init() {
UITableView.appearance().backgroundColor = .clear
}
let tempData:[TempData] = [TempData( name: "abc"),
TempData( name: "abc"),
TempData( name: "abc"),
TempData( name: "abc")]
var body: some View {
ZStack {
Image("loginBackgound")
.resizable()
.scaledToFill()
List{
ForEach(tempData){ data in
Text(data.name)
}
}
.listStyle(SidebarListStyle())
}
.ignoresSafeArea(edges: .all)
}
}
Using UITableView.appearance().backgroundColor is not a good idea as it changes the backgroundColor of all tables. I found a working solution for color changing at the exact table you selected in iOS 14, 15.
We will change the color using a modifier that needs to be applied inside the List
extension View {
func backgroundTableModifier(_ color: UIColor? = nil) -> some View {
self.modifier(BackgroundTableModifier(color: color))
}
}
Our task is to find the UITableView and after that change the color.
private struct BackgroundTableModifier: ViewModifier {
private let color: UIColor?
#State private var tableView: UITableView?
init(color: UIColor?) {
self.color = color
}
public func body(content: Content) -> some View {
if tableView?.backgroundColor != color {
content
.overlay(BackgroundTableViewRepresentable(tableBlock: { tableView in
tableView.backgroundColor = color
self.tableView = tableView
}))
} else {
content
}
}
}
private struct BackgroundTableViewRepresentable: UIViewRepresentable {
var tableBlock: (UITableView) -> ()
func makeUIView(context: Context) -> BackgroundTableView {
let view = BackgroundTableView(tableBlock: tableBlock)
return view
}
func updateUIView(_ uiView: BackgroundTableView, context: Context) {}
}
class BackgroundTableView: UIView {
var tableBlock: (UITableView) -> ()
init(tableBlock: #escaping (UITableView) -> ()) {
self.tableBlock = tableBlock
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let tableView = findTableView(in: self) {
tableBlock(tableView)
}
}
private func findTableView(in view: UIView) -> UITableView? {
if let tableView = view as? UITableView {
return tableView
}
if let superView = view.superview {
return findTableView(in: superView)
}
return nil
}
}
In order to find UITableView, the modifier must be inside the List. Naturally, you need to ensure that the modifier is called only once, you do not need to apply it to each row. Here is an example of usage
List {
rows()
.backgroundTableModifier(.clear)
}
func rows() -> some View {
ForEach(0..<10, id: \.self) { index in
Row()
}
}
In iOS 16, we got a native way to do this via scrollcontentbackground modifier.
You can either change the color by setting a color (ShapeStyle) to scrollcontentbackground.
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.scrollContentBackground(Color.pink)
Or you can hide the background .scrollContentBackground(.hidden) and set a custom one with .backgroud modifier.
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.background {
Image("ventura")
}
.scrollContentBackground(.hidden)
I've inspired some of the configurator used to config per page NavigationView nav bar style and write some simple UITableView per page configurator not use UITableView.appearance() global approach
import SwiftUI
struct TableViewConfigurator: UIViewControllerRepresentable {
var configure: (UITableView) -> Void = { _ in }
func makeUIViewController(context: UIViewControllerRepresentableContext<TableViewConfigurator>) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TableViewConfigurator>) {
let tableViews = uiViewController.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()
for tableView in tableViews {
self.configure(tableView)
}
}
}
Then there is UIView extension needed to find all UITableViews
extension UIView {
func subviews<T:UIView>(ofType WhatType:T.Type) -> [T] {
var result = self.subviews.compactMap {$0 as? T}
for sub in self.subviews {
result.append(contentsOf: sub.subviews(ofType:WhatType))
}
return result
}
}
And usage at the end is:
List {
}.background(TableViewConfigurator {
$0.backgroundColor = .red
})
Maybe one thing should be improved that is usage of navigationController?.topViewController to make it work even without navigationController in view controllers hierarchy
If anyone came here looking for solutions for background in landscape not full width on iPhone X/11 try:
.listRowBackground(Color("backgroundColour").edgesIgnoringSafeArea(.all))
If you want to avoid setting the appearance for all table views globally, you can combine UITableView.appearance(whenContainedInInstancesOf:) with UIHostingController. Thanks DanSkeel for the comment you left above pointing this out. This is how I used it:
public class ClearTableViewHostingController<Content>: UIHostingController<Content> where Content: View {
public override func viewDidLoad() {
UITableView.appearance(whenContainedInInstancesOf: [ClearTableViewHostingController<Content>.self]).backgroundColor = .clear
}
}
You can use ClearTableViewHostingController like this:
let view = MyListView()
let viewController = ClearTableViewHostingController(coder: coder, rootView: view)
Then in your view you can set the list background color like so:
List {
Text("Hello World")
}
.background(Color.gray)
Make extension List like:
extension List{
#available(iOS 14, *)
func backgroundList(_ color: Color = .clear) -> some View{
UITableView.appearance().backgroundColor = UIColor(color)
return self
}
}
you can use introspect library from Github to set the background color for the underlying table view like this:
List { ... } .introspectTableView { tableView in
tableView.backgroundColor = .yellow
}
For some reason color change is not working, you can try the .listStyle to .plain
Code:
struct ContentView: View {
var body: some View {
VStack {
Text("Test")
List {
ForEach(1 ..< 4) { items in
Text(String(items))
}
}
.listStyle(.plain)
}
}
Changing background did not work for me, because of the system background. I needed to hide it.
List(examples) { example in
ExampleRow(example: example)
}.background(Color.white.edgesIgnoringSafeArea(.all))
.scrollContentBackground(.hidden)
Xcode Version 12.4
The Background property worked for me, but with the mandatory use of Opacity.
Without opacity it is not work.
List {
ForEach(data, id: \.id) { (item) in
ListRow(item)
.environmentObject(self.data)
}
}
.background(Color.black)
.opacity(0.5)

How to change the status bar text color in SwiftUI for each TabView Item?

You can find many (UIKit) solutions to set the text color of the status bar for a SwiftUI view. Unfortunately, in my experience, these solutions do not seem to work satisfactorily for TabViews at runtime.
Found Solutions:
SwiftUI: Set Status Bar Color For a Specific View
How can I change the status bar text color per view in SwiftUI?
Using these solutions, you will quickly find that the text color is not always set correctly when trying to switch between tabs.
Unfortunately, Apple currently doesn't seem to have a direct solution for SwiftUI to change the UIStatusBarStyle for each view like it was possible to do with UIKit.
I just can't find a stable way to change the status bar text color for each tab at runtime.
I read the other Solutions but did not try them. But the following works, and I also think it lets the other Solutions work.
Go to your Info.plist and set UIViewControllerBasedStatusBarAppearance to NO
From here on, I would assume that the solutions mentioned by you, would also work.
If you don't mind having a deprecation warning in your Code this would also be an option:
TabView {
Text("1")
.tabItem { Text("1") }
.onAppear {
UIApplication.shared.setStatusBarStyle(.lightContent, animated: false)
}
Text("2")
.tabItem { Text("2") }
.onAppear {
UIApplication.shared.setStatusBarStyle(.darkContent, animated: true)
}
}
But setStatusBarStyle is deprecated and therefore it might not be the best option.
I could help myself now. I found a solution that takes an approach that works with TabViews as well based on: https://github.com/xavierdonnellon/swiftui-statusbarstyle
My full Code Example where the states of the previous status bar styles are saved and restored upon disposal (it is important to keep an eye on the status bar style hierarchy during runtime):
ExampleApp.swift
import SwiftUI
#main
struct ExampleApp: App {
init() {
//navigation bar
let coloredNavAppearance = UINavigationBarAppearance()
coloredNavAppearance.configureWithTransparentBackground()
coloredNavAppearance.backgroundColor = .clear
coloredNavAppearance.backgroundEffect = nil
coloredNavAppearance.backgroundImage = UIImage()
coloredNavAppearance.shadowImage = UIImage()
coloredNavAppearance.shadowColor = .clear
coloredNavAppearance.titleTextAttributes = [
.foregroundColor: UIColor.black
]
coloredNavAppearance.largeTitleTextAttributes = [
.foregroundColor: UIColor.black
]
UINavigationBar.appearance().standardAppearance = coloredNavAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredNavAppearance
UINavigationBar.appearance().compactAppearance = coloredNavAppearance
}
var body: some Scene {
WindowGroup {
RootView(content: {
ContentView()
})
}
}
}
UIApplication+StatusBarStyle.swift
extension UIApplication {
static var hostingController: HostingController<AnyView>? = nil
static var statusBarStyleHierarchy: [UIStatusBarStyle] = []
static var statusBarStyle: UIStatusBarStyle = .darkContent
///Sets the App to start at rootView
func setHostingController(rootView: AnyView) {
let hostingController = HostingController(rootView: AnyView(rootView))
windows.first?.rootViewController = hostingController
UIApplication.hostingController = hostingController
}
static func setStatusBarStyle(_ style: UIStatusBarStyle) {
statusBarStyle = style
hostingController?.setNeedsStatusBarAppearanceUpdate()
}
}
class HostingController<Content: View>: UIHostingController<Content> {
override var preferredStatusBarStyle: UIStatusBarStyle {
return UIApplication.statusBarStyle
}
}
///By wrapping views in a RootView, they will become the app's main / primary view. This will enable setting the statusBarStyle.
struct RootView<Content: View> : View {
var content: Content
init(#ViewBuilder content: () -> (Content)) {
self.content = content()
}
var body:some View {
EmptyView()
.onAppear {
UIApplication.shared.setHostingController(rootView: AnyView(content))
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var tabRoute = TabRoute.green
enum TabRoute: String, Identifiable {
var id: String {
self.rawValue
}
case green, black, blue, purple
static var all: [TabRoute] {
[.green, .black, .blue, .purple]
}
var bgColor: Color {
switch self {
case .green:
return .green
case .black:
return .black
case .blue:
return .blue
case .purple:
return .purple
}
}
var statusBarStyle: UIStatusBarStyle {
switch self {
case .black:
return .lightContent
default:
return .darkContent
}
}
}
var body: some View {
TabView(selection: $tabRoute) {
ForEach(TabRoute.all) { route in
NavigationView {
ViewExample(isPresented: false, title: route.rawValue, bgColor: route.bgColor, statusBarStyle: route.statusBarStyle)
.navigationTitle(route.rawValue)
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Text(route.rawValue)
}
.tag(TabRoute(rawValue: route.rawValue))
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct ViewExample: View {
#Environment(\.presentationMode) var presentationMode
#State private var sheetIsPresented = false
#State var isPresented: Bool
var title: String
var bgColor: Color
#State var statusBarStyle: UIStatusBarStyle
var body: some View {
ZStack {
bgColor
}
.onAppear {
UIApplication.statusBarStyleHierarchy.append(statusBarStyle)
UIApplication.setStatusBarStyle(statusBarStyle)
}
.onDisappear {
guard UIApplication.statusBarStyleHierarchy.count > 1 else { return }
let style = UIApplication.statusBarStyleHierarchy[UIApplication.statusBarStyleHierarchy.count - 1]
UIApplication.statusBarStyleHierarchy.removeLast()
UIApplication.setStatusBarStyle(style)
}
.edgesIgnoringSafeArea(.all)
}
}
Key Point
Use .preferredColorScheme()
.preferredColorScheme() Use previous View's setting in NavigationStack
implementation
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: ContentViewFirst()) {
Text("navigation")
}
.isDetailLink(false)
}
}
}
struct ContentViewFirst: View {
#State var colorScheme:ColorScheme = .dark
var body: some View {
GeometryReader { geometryReader in
ZStack {
Color.black.ignoresSafeArea()
VStack {
NavigationLink(destination: ContentViewSecond(colorScheme: self.$colorScheme)) {
Text("ColorScheme = .dark")
.font(.largeTitle)
.bold()
.foregroundColor(.white)
}
.isDetailLink(false)
}
}
}
.preferredColorScheme(colorScheme)
}
}
struct ContentViewSecond: View {
#Binding var colorScheme:ColorScheme
var body: some View {
GeometryReader { geometryReader in
ZStack {
Color.white.ignoresSafeArea()
VStack {
Text("ColorScheme = .light")
.font(.largeTitle)
.bold()
}
}
}
.onAppear {
self.colorScheme = .light
}
.onDisappear {
self.colorScheme = .dark
}
}
}

How to set foreground color optionally to View in swiftUI using ViewModifier

I have an image view, based on a boolean value i should use tint, if condition is true i should use tint else I should use the original image, I tried in many way using ViewModifier but I can't find the solution. Is that possible to get the expected result using ViewModifier?
struct ViewColor: ViewModifier {
var tint: Bool
func body(content: Content) -> some View {
Group {
if self.tint {
content.foregroundColor(Color("colorText"))
}else
{
// content.foregroundColor(Color("colorAccent"))
}
}
}
}
Here is a solution (tested with Xcode 12b)
struct ViewColor: ViewModifier {
var tint: Bool
#ViewBuilder
func body(content: Content) -> some View {
if self.tint {
content.foregroundColor(Color("colorText"))
} else {
content
}
}
}
I used image inside the button.
To maintain image original color inside Button we should use renderingMode(.original) to tint the image we should userenderingMode(. template), For my scenario I should use conditional operator to get the exact result, Here is the code
struct ButtonIconView: View {
var icon : String
var tint : Bool = true
var body: some View {
Button(action: {
print("Button action")
}){
Image(icon)
.renderingMode(self.tint ? .template : .original) // To get the original image color we should use .original in rendering mode to tint image we should use .template
.resizable().scaledToFit() .modifier(ViewColor(tint: self.tint)).frame(width: 18, height: 18)
}.frame(width: 54, height:48)
}
}
To add and remove tint color using ViewModifier we should follow the #Asperi answer
struct ViewColor: ViewModifier {
var tint: Bool
#ViewBuilder
func body(content: Content) -> some View {
if self.tint {
content.foregroundColor(Color("colorText"))
} else {
content
}
}
}

PencilKit in SwiftUI

I'm trying to use PencilKit in SwiftUI.
How can I detect in the updateUIView-function, which Binding-variable was updated? For example I don't want to clear the canvas when changing the color.
And is there a better way to clear the canvas than toggling a boolean? Toggling a boolean forces the updateUIView-function to execute.
import SwiftUI
import UIKit
import PencilKit
struct ContentView: View {
#State var color = UIColor.black
#State var clear = false
var body: some View {
VStack{
PKCanvas(color: $color, clear:$clear)
VStack(){
Button("Change to BLUE"){ self.color = UIColor.blue }
Button("Change to GREEN"){ self.color = UIColor.green }
Button("Clear Canvas"){ self.clear.toggle() }
}
}
}
}
struct PKCanvas: UIViewRepresentable {
class Coordinator: NSObject, PKCanvasViewDelegate {
var pkCanvas: PKCanvas
init(_ pkCanvas: PKCanvas) {
self.pkCanvas = pkCanvas
}
}
#Binding var color:UIColor
#Binding var clear:Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.tool = PKInkingTool(.pen, color: color, width: 10)
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) {
// clears the canvas
canvasView.drawing = PKDrawing()
// sets a new color
canvasView.tool = PKInkingTool(.pen, color: color, width: 10)
}
}
This will do the trick:
func updateUIView(_ canvasView: PKCanvasView, context: Context) {
if clear != context.coordinator.pkCanvas.clear{
canvasView.drawing = PKDrawing()
}
canvasView.tool = PKInkingTool(.pen, color: color, width: 10)
}
I observed similar pattern many times, and here we get intercommunication between SwiftUI-value-declarative-state-managed world and xKit-OOP-imperative-action-managed world... and there are attempts either to put everything from one to another, or vice versa...
I've got a mind that probably it would be better to keep each nature for each and have some mediator or actor between two... so for your use-case I'd think that I would go different way, say (scratchy, not tested, for consideration):
struct ContentView: View {
//#State var color = UIColor.black // < both these have nothing to ContentView
//#State var clear = false
let pkActor = PKCanvasActor() // < mediator, reference type
var body: some View {
VStack{
PKCanvas(actor: pkActor)
VStack(){
Button("Change to BLUE"){ self.pkActor.use(color: UIColor.blue) }
Button("Change to GREEN"){ self.pkActor.use(color: UIColor.green) }
Button("Clear Canvas"){ self.pkActor.clear() }
}
}
}
}
somewhere here
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
self.actor.canvas = canvas // but think to avoid cycle reference
and pure-OOP part
class PKCanvasActor {
var canvas: PKCanvasView
func use(color: Color) {
// do anything with canvas color
}
func clear() {
canvas.drawing = PKDrawing()
}
// ... any more actions
}
Of course there simple approach I proposed for similar scenario in SwiftUI Button interact with Map... but above way looks more preferable for me.
Remark: one might say the Coordinator is for this purpose, but its life-time is managed by SwiftUI-internals and it participate in those internal workflows, so I'd avoid intruding into those relationships...

SwiftUI update navigation bar title color

How to change the navigation bar title color in SwiftUI
NavigationView {
List{
ForEach(0..<15) { item in
HStack {
Text("Apple")
.font(.headline)
.fontWeight(.medium)
.color(.orange)
.lineLimit(1)
.multilineTextAlignment(.center)
.padding(.leading)
.frame(width: 125, height: nil)
Text("Apple Infinite Loop. Address: One Infinite Loop Cupertino, CA 95014 (408) 606-5775 ")
.font(.subheadline)
.fontWeight(.regular)
.multilineTextAlignment(.leading)
.lineLimit(nil)
}
}
}
.navigationBarTitle(Text("TEST")).navigationBarHidden(false).foregroundColor(.orange)
}
I have tried with .foregroundColor(.orange) but it is not working
also tried .navigationBarTitle(Text("TEST").color(.orange))
Any help ?
It is not necessary to use .appearance() to do this globally.
Although SwiftUI does not expose navigation styling directly, you can work around that by using UIViewControllerRepresentable. Since SwiftUI is using a regular UINavigationController behind the scenes, the view controller will still have a valid .navigationController property.
struct NavigationConfigurator: UIViewControllerRepresentable {
var configure: (UINavigationController) -> Void = { _ in }
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationConfigurator>) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationConfigurator>) {
if let nc = uiViewController.navigationController {
self.configure(nc)
}
}
}
And to use it
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
Text("Don't use .appearance()!")
}
.navigationBarTitle("Try it!", displayMode: .inline)
.background(NavigationConfigurator { nc in
nc.navigationBar.barTintColor = .blue
nc.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.white]
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
In SwiftUI, you can not change the navigationTitleColor directly. You have to change UINavigation's appearance in init() like this,
struct YourView: View {
init() {
//Use this if NavigationBarTitle is with Large Font
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.red]
//Use this if NavigationBarTitle is with displayMode = .inline
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.red]
}
var body: some View {
NavigationView {
List{
ForEach(0..<15) { item in
HStack {
Text("Apple")
.font(.headline)
.fontWeight(.medium)
.color(.orange)
.lineLimit(1)
.multilineTextAlignment(.center)
.padding(.leading)
.frame(width: 125, height: nil)
Text("Apple Infinite Loop. Address: One Infinite Loop Cupertino, CA 95014 (408) 606-5775 ")
.font(.subheadline)
.fontWeight(.regular)
.multilineTextAlignment(.leading)
.lineLimit(nil)
}
}
}
.navigationBarTitle(Text("TEST")).navigationBarHidden(false)
//.navigationBarTitle (Text("TEST"), displayMode: .inline)
}
}
}
I hope it will work. Thanks!!
I have searched for this issue and find a great article about this, you could wrap the settings of navigation bar style as a view modifier.
Check this Link.
Notes: I believe you need to update some code in this example, add titleColor parameter.
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
var titleColor: UIColor?
init(backgroundColor: UIColor?, titleColor: UIColor?) {
self.backgroundColor = backgroundColor
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = backgroundColor
coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
}
extension View {
func navigationBarColor(backgroundColor: UIColor?, titleColor: UIColor?) -> some View {
self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
}
}
After that, apply like this:
.navigationBarColor(backgroundColor: .clear, titleColor: .white)
I hope it will work.
In iOS 14, SwiftUI has a way to customize a navigation bar with the new toolbar modifier.
We need to set ToolbarItem of placement type .principal to a new toolbar modifier. You can even set an image and much more.
NavigationView {
Text("My View!")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
Image(systemName: "sun.min.fill")
Text("Title")
.font(.headline)
.foregroundColor(.orange)
}
}
}
}
Building on the answer from Arsenius, I found that an elegant way to get it to work consistently was to subclass UIViewController and do the configuration in viewDidLayoutSubviews().
Usage:
VStack {
Text("Hello world")
.configureNavigationBar {
$0.navigationBar.setBackgroundImage(UIImage(), for: .default)
$0.navigationBar.shadowImage = UIImage()
}
}
Implementation:
extension View {
func configureNavigationBar(configure: #escaping (UINavigationController) -> Void) -> some View {
modifier(NavigationConfigurationViewModifier(configure: configure))
}
}
struct NavigationConfigurationViewModifier: ViewModifier {
let configure: (UINavigationController) -> Void
func body(content: Content) -> some View {
content.background(NavigationConfigurator(configure: configure))
}
}
struct NavigationConfigurator: UIViewControllerRepresentable {
let configure: (UINavigationController) -> Void
func makeUIViewController(
context: UIViewControllerRepresentableContext<NavigationConfigurator>
) -> NavigationConfigurationViewController {
NavigationConfigurationViewController(configure: configure)
}
func updateUIViewController(
_ uiViewController: NavigationConfigurationViewController,
context: UIViewControllerRepresentableContext<NavigationConfigurator>
) { }
}
final class NavigationConfigurationViewController: UIViewController {
let configure: (UINavigationController) -> Void
init(configure: #escaping (UINavigationController) -> Void) {
self.configure = configure
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let navigationController = navigationController {
configure(navigationController)
}
}
}
I took a slightly different approach; I wanted to change only the title text color, and nothing else about the NavigationBar. Using the above and this as inspiration, I landed on:
import SwiftUI
extension View {
/// Sets the text color for a navigation bar title.
/// - Parameter color: Color the title should be
///
/// Supports both regular and large titles.
#available(iOS 14, *)
func navigationBarTitleTextColor(_ color: Color) -> some View {
let uiColor = UIColor(color)
// Set appearance for both normal and large sizes.
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: uiColor ]
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: uiColor ]
return self
}
}
This requires iOS 14 because UIColor.init(_ color: Color) requires iOS 14.
Which can be leveraged as such:
struct ExampleView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationBarTitle("Example")
.navigationBarTitleTextColor(Color.red)
}
}
}
Which in turn yields:
Use Below Code for Color Customization in SwiftUI
This is for main body background color:-
struct ContentView: View {
var body: some View {
Color.red
.edgesIgnoringSafeArea(.all)
}
}
For Navigation Bar:-
struct ContentView: View {
#State var msg = "Hello SwiftUI😊"
init() {
UINavigationBar.appearance().backgroundColor = .systemPink
UINavigationBar.appearance().largeTitleTextAttributes = [
.foregroundColor: UIColor.white,
.font : UIFont(name:"Helvetica Neue", size: 40)!]
}
var body: some View {
NavigationView {
Text(msg)
.navigationBarTitle(Text("NAVIGATION BAR"))
}
}
}
For Other UI Elements Color Customization
struct ContentView: View {
#State var msg = "Hello SwiftUI😊"
var body: some View {
Text(msg).padding()
.foregroundColor(.white)
.background(Color.pink)
}
}
from iOS 14, You can have any custom view you want (including custom text with custom color and font)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
VStack {
Text("Yellow And Bold Title")
.bold()
.foregroundColor(.yellow)
}
}
}
Also you can set the navigation bar color from the iOS 16 like:
.toolbarBackground(.red, for: .navigationBar)
I have developed a small sample of a custom SwiftUI navigation that can provide full visual customisation and programatic navigation. It can be used as a replacement for the NavigationView.
Here is the NavigationStack class that deals with currentView and navigation stack:
final class NavigationStack: ObservableObject {
#Published var viewStack: [NavigationItem] = []
#Published var currentView: NavigationItem
init(_ currentView: NavigationItem ){
self.currentView = currentView
}
func unwind(){
if viewStack.count == 0{
return
}
let last = viewStack.count - 1
currentView = viewStack[last]
viewStack.remove(at: last)
}
func advance(_ view:NavigationItem){
viewStack.append( currentView)
currentView = view
}
func home( ){
currentView = NavigationItem( view: AnyView(HomeView()))
viewStack.removeAll()
}
}
You can have a look here: for the full example with explanation:
PS: I am not sure why this one was deleted. I think it answer the question as it is a perfect functional alternative to NavigationView.
Instead of setting appearance(), which affects all navigation bars, you can set them individually using SwiftUI-Introspect.
Example:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
Text("Hello world!")
}
.navigationTitle("Title")
}
.introspectNavigationController { nav in
nav.navigationBar.barTintColor = .systemBlue
}
}
}
Result:
init() {
// for navigation bar title color
UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor:UIColor.red]
// For navigation bar background color
UINavigationBar.appearance().backgroundColor = .green
}
NavigationView {
List {
ForEach(0..<15) { item in
HStack {
Text("Apple")
.font(.headline)
.fontWeight(.medium)
.color(.orange)
.lineLimit(1)
.multilineTextAlignment(.center)
.padding(.leading)
.frame(width: 125, height: nil)
Text("Apple Infinite Loop. Address: One Infinite Loop Cupertino, CA 95014 (408) 606-5775 ")
.font(.subheadline)
.fontWeight(.regular)
.multilineTextAlignment(.leading)
.lineLimit(nil)
}
}
}
.navigationBarTitle(Text("TEST")).navigationBarHidden(false)
}
Based on this https://stackoverflow.com/a/66050825/6808357 I created an extension where you can set the background color and the title color at the same time.
import SwiftUI
extension View {
/// Sets background color and title color for UINavigationBar.
#available(iOS 14, *)
func navigationBar(backgroundColor: Color, titleColor: Color) -> some View {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor(backgroundColor)
let uiTitleColor = UIColor(titleColor)
appearance.largeTitleTextAttributes = [.foregroundColor: uiTitleColor]
appearance.titleTextAttributes = [.foregroundColor: uiTitleColor]
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
return self
}
}
Here's how to use it:
var body: some View {
NavigationView {
Text("Hello world!") // This could be any View (List, VStack, etc.)
.navigationTitle("Your title here")
.navigationBar(backgroundColor: .blue, titleColor: .white)
}
}
Happy coding!
If you have your content as
struct MyContent : View {
...
}
then you can put it like this inside a navigation view with a red background:
NavigationView {
ZStack(alignment: .top) {
Rectangle()
.foregroundColor(Color.red)
.edgesIgnoringSafeArea(.top)
MyContent()
}
}
I will update my answer as soon as I know how to update the title text itself.
Definitely there are already a few good answers, but all of them will cover only part of the job:
Great solution from #arsenius - give the good point to start
Elegant way from #EngageTheWarpDrive - this definitely improve usability
For latest version of iOS and swiftUI #Thahir suggest to use toolbar
Few more suggestions propose to use UIAppearence global config for UINavigationBar - as for me global change is not a good idea and may be not always suitable.
I ended up combining all proposals in to the next code:
Create NavigationControllerRepresentable and modifier for navigationBar configuration:
struct NavigationControllerLayout: UIViewControllerRepresentable {
var configure: (UINavigationController) -> () = { _ in }
func makeUIViewController(
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) {
if let navigationContoller = uiViewController.navigationController {
configure(navigationContoller)
}
}
}
extension View {
func configureNavigationBar(_ configure: #escaping (UINavigationBar) -> ()) -> some View {
modifier(NavigationConfigurationViewModifier(configure: configure))
}
}
struct NavigationConfigurationViewModifier: ViewModifier {
let configure: (UINavigationBar) -> ()
func body(content: Content) -> some View {
content.background(NavigationControllerLayout(configure: {
configure($0.navigationBar)
}))
}
}
To modify navigationBar to meet u'r requirements (such as bg color and other props):
extension UINavigationBar {
enum Appearence {
case transparent
case defaultLight
case colored(UIColor?)
var color: UIColor {
...
}
var appearenceColor: UIColor {
...
}
var tint: UIColor {
....
}
var effect: UIBlurEffect? {
....
}
}
func switchToAppearence(_ type: Appearence) {
backgroundColor = type.color
barTintColor = type.tint
// for iOS 13+
standardAppearance.backgroundColor = type.appearenceColor
standardAppearance.backgroundEffect = type.effect
// u can use other properties from navBar also simply modifying this function
}
}
As u can see, here we definitely need some bridge between Color and UIColor. Starting from iOS 14 - u can just UIColor.init(_ color: Color), but before iOS 14 there is not such way, so I ended up with simple solution:
extension Color {
/// Returns a `UIColor` that represents this color if one can be constructed
///
/// Note: Does not support dynamic colors
var uiColor: UIColor? {
self.cgColor.map({ UIColor(cgColor: $0) })
}
}
this will not work for dynamic colors
As result u can use this as following:
// modifier to `NavigationView`
.configureNavigationBar {
$0.switchToAppearence(.defaultLight)
}
Hopefully this may help to someone ;)
I still haven't figured out how to do the foreground color on a per-view basis, but I did figure out a simple workaround for the background color.
If using an .inline title, you can just use a VStack with a rectangle at the top of the NavigationView:
NavigationView {
VStack() {
Rectangle()
.foregroundColor(.red)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
List {
Text("Hello World")
Text("Hello World")
Text("Hello World")
}
}
.navigationBarTitle("Hello World", displayMode: .inline)
// ...
Note how the rectangle uses a frame height of 0 and .edgesIgnoringSafeArea(.top).
Here is the solution that worked for me. You need to start off with a UINavigationController as the rootViewController.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let nav = setupNavigationController()
window.rootViewController = nav
self.window = window
window.makeKeyAndVisible()
}
}
func setupNavigationController() -> UINavigationController {
let contentView = ContentView()
let hosting = UIHostingController(rootView: contentView)
let nav = NavigationController(rootViewController: hosting)
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
navBarAppearance.backgroundColor = UIColor.black
nav.navigationBar.standardAppearance = navBarAppearance
nav.navigationBar.scrollEdgeAppearance = navBarAppearance
nav.navigationBar.prefersLargeTitles = true
return nav
}
and then in your content view:
struct ContentView: View {
#State private var isModalViewPresented: Bool = false
var body: some View {
List(0 ..< 10, rowContent: { (index) in
NavigationLink(destination: DetailView()) {
Text("\(index)")
}
})
.navigationBarItems(trailing: Button("Model") {
self.isModalViewPresented.toggle()
})
.sheet(isPresented: $isModalViewPresented, content: {
ModalView()
})
.navigationBarTitle("Main View")
}
}
and if you want to change the color at some point, such as in a modal view, use the answer given here
struct ModalView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationBarTitle("Modal View")
.background(NavigationConfigurator { nc in
nc.navigationBar.backgroundColor = UIColor.blue
nc.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
})
}
}
}
you can subclass UINavigationController to change the status bar color
class NavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
}
override var preferredStatusBarStyle: UIStatusBarStyle
{
.lightContent
}
}
.foregroundColor(.orange) - изменяет внутренние представления NavigationView.
But to change the navigation view itself, you need to use UINavigationBar Appearance() in init()
I was looking for this problem and found a great article about it. And i modified your code by this article and came to success. Here, how i solve this problem:
struct ContentView: View {
init() {
let coloredAppearance = UINavigationBarAppearance()
// this overrides everything you have set up earlier.
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = .green
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.black]
// to make everything work normally
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
}
var body: some View {
NavigationView {
List{
ForEach(0..<15) { item in
HStack {
Text("Apple")
.font(.headline)
.fontWeight(.medium)
.lineLimit(1)
.multilineTextAlignment(.center)
.padding(.leading)
.frame(width: 125, height: nil)
.foregroundColor(.orange)
Text("Apple Infinite Loop. Address: One Infinite Loop Cupertino, CA 95014 (408) 606-5775 ")
.font(.subheadline)
.fontWeight(.regular)
.multilineTextAlignment(.leading)
.lineLimit(nil)
.foregroundColor(.orange)
}
}
}
.navigationBarTitle(Text("TEST"))
}
// do not forget to add this
.navigationViewStyle(StackNavigationViewStyle())
}
}
You can also take some examples here
update for 13.4
note: revisiting this the next day, it may be possible that some of my issues were caused by my somewhat nonstandard setup: i am still running mojave, but have manually added the 13.4 support files (normally available only via xcode 11.4, which requires catalina). i mention this because i am/was also having some tab bar custom color issues, but i just noticed that those are only manifesting when i have the phone actually plugged in and am running the app from xcode. if i unplug, and just run the app normally, i am not seeing the tab bar issues, so it may be possible that the nav bar issue had some similarity ...
(i would add this as a comment on arsenius' answer (the currently accepted one) above, but i don't have the rep, so ...)
i was using that solution, and it was working perfectly up until 13.4, which seems to have broken it, at least for me. after a lot of view hierarchy tracing, it looks like they changed things such that the implicit UINavigationController is no longer easily accessible via the passed UIViewController as described in the workaround. it's still there though (pretty far up the tree), we just have to find it.
to that end, we can just walk the view hierarchy until we find the navbar, and then set the desired parameters on it, as usual. this necessitates a new discovery function, and some minor changes to the NavigationConfigurator struct, and its instantiation ...
first up, the discovery function:
func find_navbar(_ root: UIView?) -> UINavigationBar?
{
guard root != nil else { return nil }
var navbar: UINavigationBar? = nil
for v in root!.subviews
{ if type(of: v) == UINavigationBar.self { navbar = (v as! UINavigationBar); break }
else { navbar = find_navbar(v); if navbar != nil { break } }
}
return navbar
}
modify the NavigationConfigurator as follows (note that we no longer care about passing in a view, since that's no longer reliable):
struct NavigationConfigurator: UIViewControllerRepresentable
{
#EnvironmentObject var prefs: Prefs // to pick up colorscheme changes
var configure: () -> Void = {}
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationConfigurator>) -> UIViewController { UIViewController() }
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationConfigurator>) { self.configure() }
}
(in my app, i have a Prefs object which keeps track of colors, etc.)
... then, at the instantiation site, do something like this:
MyView()
.navigationBarTitle("List", displayMode: .inline)
.navigationBarItems(trailing: navbuttons)
.background(NavigationConfigurator {
if self.prefs.UI_COLORSCHEME != Colorscheme.system.rawValue
{ if let navbar = find_navbar(root_vc?.view)
{ navbar.barTintColor = Colors.uicolor(.navbar, .background)
navbar.backgroundColor = .black
navbar.titleTextAttributes = [.foregroundColor: Colors.uicolor(.navbar, .foreground)]
navbar.tintColor = Colors.uicolor(.navbar, .foreground)
}
}
})
note that i capture the root view controller elsewhere in my app, and use it here to pass to find_navbar(). you might want to do it differently, but i already have that variable around for other reasons ... there's some other stuff there specific to my app, e.g., the color-related objects, but you get the idea.
https://stackoverflow.com/a/58427754/4709057 this answer works, but if you are experiencing issues with navigationController being nil in light or dark mode. Just add this.. no idea why it works.
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
Text("Don't use .appearance()!")
}
.navigationBarTitle("Try it!", displayMode: .inline)
.background(NavigationConfigurator { nc in
nc.navigationBar.barTintColor = .blue
nc.navigationBar.background = .blue
nc.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.white]
})
}
.navigationViewStyle(StackNavigationViewStyle())
.accentColor(.red) <------- DOES THE JOB
}
}
WatchOS navigation title color using SwiftUI
Side note for watchOS is that you don't need to fiddle with the navigation color. It's the Watch Accent color you need to change. In your project go into WatchProjectName->Asset->Accent and change this
https://developer.apple.com/documentation/watchkit/setting_the_app_s_tint_color
This solution builds on the accepted answer that doesn't use any library nor does it apply UINavigationBarAppearance globally.
This solution fixes the issues that the accepted answer has (such as not working for the initial view or not working for large display mode) by adding a hack.
Note I would personally not use this hack in production code, nevertheless it's interesting to see that the issues can be worked around. Use at own risk.
struct NavigationHackView: View {
#State private var isUsingHack = false
var body: some View {
NavigationView {
List {
NavigationLink {
Text("Detail view")
.navigationTitle("Detail view")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("Show details view")
}
}
.navigationTitle("Hack!")
.background(
NavigationConfigurator { navigationController in
// required for hack to work
_ = isUsingHack
navigationController.navigationBar.navigationBarColor(.red, titleColor: .white)
}
)
.onAppear {
// required for hack to work
DispatchQueue.main.async {
isUsingHack.toggle()
}
}
// required for hack to work, even though nothing is done
.onChange(of: isUsingHack) { _ in }
}
}
}
struct NavigationConfigurator: UIViewControllerRepresentable {
var configure: (UINavigationController) -> Void = { _ in }
func makeUIViewController(
context: UIViewControllerRepresentableContext<NavigationConfigurator>
) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: UIViewControllerRepresentableContext<NavigationConfigurator>
) {
guard let navigationController = uiViewController.navigationController else {
return
}
configure(navigationController)
}
}
extension UINavigationBar {
func navigationBarColor(
_ backgroundColor: UIColor,
titleColor: UIColor? = nil
) {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = backgroundColor
if let titleColor = titleColor {
appearance.titleTextAttributes = [.foregroundColor: titleColor]
appearance.largeTitleTextAttributes = [.foregroundColor: titleColor]
// back button appearance
tintColor = titleColor
}
standardAppearance = appearance
scrollEdgeAppearance = appearance
compactAppearance = appearance
if #available(iOS 15.0, *) {
compactScrollEdgeAppearance = appearance
}
}
}
The solution that worked for me was to use UINavigationBarAppearance() method, then add the .id() to the NavigationView. This will automatically redraw the component when the color changes.
Now you can have reactive color changes based on a state engine.
var body: some Scene {
let color = someValue ? UIColor.systemBlue : UIColor.systemGray3
let custom = UINavigationBarAppearance()
custom.configureWithOpaqueBackground()
custom.backgroundColor = color
UINavigationBar.appearance().standardAppearance = custom
UINavigationBar.appearance().scrollEdgeAppearance = custom
UINavigationBar.appearance().compactAppearance = custom
UINavigationBar.appearance().compactScrollEdgeAppearance = custom
return WindowGroup {
NavigationView {
content
}
.id(color.description)
}
}
Post iOS 14 easy way to do:
protocol CustomNavigationTitle: View {
associatedtype SomeView: View
func customNavigationTitle(_ string: String) -> Self.SomeView
}
extension CustomNavigationTitle {
func customNavigationTitle(_ string: String) -> some View {
toolbar {
ToolbarItem(placement: .principal) {
Text(string).foregroundColor(.red).font(.system(size: 18))
}
}
}
}
extension ZStack: CustomNavigationTitle {}
Suppose your root view of view is made with ZStack
it can be utilised below way
ZStack {
}. customNavigationTitle("Some title")
I have used ViewModifier to apply custom colour for navigation bar. I can't say below code modified actual navigation bar, but I find this work around better than above others.
Unlike UINavigationBar.appearance(), it is not applied to all view.
Create a ViewModifer - I have use ShapeStyle, so you can apply any style to navigation bar. (like - gradient, colour)
struct NavigationBarStyle<S: ShapeStyle>: ViewModifier {
private var bgStyle: S
private var viewBackgroundColor: Color
init(_ bgStyle: S, viewBackgroundColor: Color) {
self. bgStyle = bgStyle
self.viewBackgroundColor = viewBackgroundColor
}
func body(content: Content) -> some View {
ZStack {
Color(UIColor.systemBackground)
.ignoresSafeArea(.all, edges: .bottom)
content
}
.background(bgStyle)
}
}
extension View {
func navigationBarStyle<S: ShapeStyle>(_ bgStyle: S, viewBackgroundColor: Color = Color(UIColor.systemBackground)) -> some View {
modifier(NavigationBarStyle(bgStyle, viewBackgroundColor: viewBackgroundColor))
}
}
Note - you have to apply this modifier on the top most view to work. e.g -
struct NewView: View {
var body: some View {
NavigationView {
VStack {
HStack {
Text("H Stack")
}
// .navigationBarStyle(Color.orange) not the right place
Text("Hello World")
}
.navigationBarStyle(Color.orange) // right place to apply
}
}
}
The simplest way I found was:
init() {
UINavigationBar.appearance().tintColor = UIColor.systemBlue
}
instead of the systemBlue you can use any other colors that you wish.
You have to implement this outside the "var body: some View {}".
you can also add:
#Environment(/.colorScheme) var colorScheme
on top of the init() and then you can use the .dark or .light to change the color the way you want in dark mode and light mode. example:
init() {
UINavigationBar.appearance().tintColor = UIColor(colorScheme == .dark ? .white : Color(#colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1)))
}

Resources