Remove back button text from navigationbar in SwiftUI - ios

I've recently started working in SwiftUI, came to the conclusion that working with navigation isn't really great yet. What I'm trying to achieve is the following. I finally managed to get rid of the translucent background without making the application crash, but now I ran into the next issue. How can I get rid of the "back" text inside the navbaritem?
I achieved the view above by setting the default appearance in the SceneDelegate.swift file like this.
let newNavAppearance = UINavigationBarAppearance()
newNavAppearance.configureWithTransparentBackground()
newNavAppearance.setBackIndicatorImage(UIImage(named: "backButton"), transitionMaskImage: UIImage(named: "backButton"))
newNavAppearance.titleTextAttributes = [
.font: UIFont(name: GTWalsheim.bold.name, size: 18)!,
.backgroundColor: UIColor.white
]
UINavigationBar.appearance().standardAppearance = newNavAppearance
One possible way that I could achieve this is by overriding the navigation bar items, however this has one downside (SwiftUI Custom Back Button Text for NavigationView) as the creator of this issue already said, the back gesture stops working after you override the navigation bar items. With that I'm also wondering how I could set the foregroundColor of the back button. It now has the default blue color, however I'd like to overwrite this with another color.

Piggy-backing on the solution #Pitchbloas offered, this method just involves setting the backButtonDisplayMode property to .minimal:
extension UINavigationController {
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}

It's actually really easy. The following solution is the fastest and cleanest i made.
Put this at the bottom of your SceneDelegate for example.
extension UINavigationController {
// Remove back button text
open override func viewWillLayoutSubviews() {
navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
This will remove the back button text from every NavigationView (UINavigationController) in your app.

I have found a straightforward approach to remove the back button text using SwiftUI only, and keeping the original chevron.
A drag gesture is added to mimic the classic navigation back button
when user wants to go back by swiping right. Following this, an extension of View is created to create a SwiftUI like modifier.
This is how to use it in code:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
// Your main view code here with a ZStack to have the
// gesture on all the view.
}
.navigationBarBackButtonTitleHidden()
}
}
This is how to create the navigationBarBackButtonTitleHidden() modifier:
import SwiftUI
extension View {
func navigationBarBackButtonTitleHidden() -> some View {
self.modifier(NavigationBarBackButtonTitleHiddenModifier())
}
}
struct NavigationBarBackButtonTitleHiddenModifier: ViewModifier {
#Environment(\.dismiss) var dismiss
#ViewBuilder #MainActor func body(content: Content) -> some View {
content
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: Button(action: { dismiss() }) {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
.imageScale(.large) })
.contentShape(Rectangle()) // Start of the gesture to dismiss the navigation
.gesture(
DragGesture(coordinateSpace: .local)
.onEnded { value in
if value.translation.width > .zero
&& value.translation.height > -30
&& value.translation.height < 30 {
dismiss()
}
}
)
}
}

Standard Back button title is taken from navigation bar title of previous screen.
It is possible the following approach to get needed effect:
struct TestBackButtonTitle: View {
#State private var hasTitle = true
var body: some View {
NavigationView {
NavigationLink("Go", destination:
Text("Details")
.onAppear {
self.hasTitle = false
}
.onDisappear {
self.hasTitle = true
}
)
.navigationBarTitle(self.hasTitle ? "Master" : "")
}
}
}

So I actually ended up with the following solution that actually works. I am overwriting the navigation bar items like so
.navigationBarItems(leading:
Image("backButton")
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
)
The only issue with this was that the back gesture wasn't working so that was solved by actually extending the UINavigationController
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
Now it's looking exactly the way I want it, the solution is kinda hacky... but it works for now, hopefully SwiftUI will mature a little bit so this can be done easier.

Using the Introspect framework, you can easily gain access to the underlying navigation item and set the backButtonDisplayMode to minimal.
Here’s how you might use that in the view that was pushed
var body: some View {
Text("Your body here")
.introspectNavigationController { navController in
navController.navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}

If you want to:
Do it globally
Keep the standard back button (along with custom behaviours like an ability to navigate a few screens back on a long press)
Avoid introducing any third party frameworks
You can do it by setting the back button text color to Clear Color via appearance:
let navigationBarAppearance = UINavigationBarAppearance()
let backButtonAppearance = UIBarButtonItemAppearance(style: .plain)
backButtonAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.highlighted.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear]
navigationBarAppearance.backButtonAppearance = backButtonAppearance
//Not sure you'll need both of these, but feel free to adjust to your needs.
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
You can do it once when the app starts and forget about it.
A potential downside (depending on your preferences) is that the transition to the clear color is animated as the title of the current window slides to the left as you move to a different one.
You can also experiment with different text attributes.

Works on iOS 16
Solutions above didn't work for me. I wanted to make changes specific to view without any global (appearance or extension) and with minimal boilerplate code.
Since you can update NavigationItem inside the init of the View. You can solve this in 2 steps:
Get visible View Controller.
// Get Visible ViewController
extension UIApplication {
static var visibleVC: UIViewController? {
var currentVC = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
while let presentedVC = currentVC?.presentedViewController {
if let navVC = (presentedVC as? UINavigationController)?.viewControllers.last {
currentVC = navVC
} else if let tabVC = (presentedVC as? UITabBarController)?.selectedViewController {
currentVC = tabVC
} else {
currentVC = presentedVC
}
}
return currentVC
}
}
Update NavigationItem inside init of the View.
struct YourView: View {
init(hideBackLabel: Bool = true) {
if hideBackLabel {
// iOS 14+
UIApplication.visibleVC?.navigationItem.backButtonDisplayMode = .minimal
// iOS 13-
let button = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
UIApplication.visibleVC?.navigationItem.backBarButtonItem = button
}
}
}

custom navigationBarItems and self.presentationMode.wrappedValue.dismiss() worked but you are not allow to perform swiping back
You can either add the following code to make the swipe back again
//perform gesture go back
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
but the problem is, sometimes it will make your app crashed when you swipe half the screen and then cancel.
I would suggest the other way to remove the "Back" text.
Adding the isActive state to monitor whether the current screen is active or not. :)
struct ContentView: View {
#State var isActive = false
var body: some View {
NavigationView() {
NavigationLink(
"Next",
destination: Text("Second Page").navigationBarTitle("Second"),
isActive: $isActive
)
.navigationBarTitle(!isActive ? "Title" : "", displayMode: .inline)
}
}
}

I am accomplishing this by changing the title of the master screen before pushing the detail screen and then setting it back when it re-appears. The only caveat is when you go back to the master screen the title's re-appearance is a little noticeable.
Summary:
on master view add state var (e.g. isDetailShowing) to store if detail screen is showing or not
on master view use the navigationTitle modifier to set the title based on the current value of isDetailShowing
on master view use onAppear modifier to set the value of isDetailShowing to false
on the NavigationLink in master screen use the simultaneousGesture modifier to set the isDetailShowing to true
struct MasterView: View {
#State var isDetailShowing = false
var body: some View {
VStack {
Spacer()
.frame(height: 20)
Text("Master Screen")
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
.frame(height: 20)
NavigationLink(destination: DetailView()) {
Text("Go to detail screen")
}
.simultaneousGesture(TapGesture().onEnded() {
isDetailShowing = true
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(isDetailShowing ? "" : "Master Screen Title")
.onAppear() {
isDetailShowing = false
}
}
}
struct DetailView: View {
var body: some View {
Text("This is the detail screen")
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Detail Screen Title")
}
}

you can use .toolbarRole(.editor)

Why not use Custom BackButton with Default Back Button Hidden
You could use Any Design You Prefer !
Works on iOS 16
First View
struct ContentView: View {
var body: some View {
NavigationView {
VStack(){
Spacer()
NavigationLink(destination: View2()) {
Text("Navigate")
.font(.title)
}
Spacer()
}
}
}
}
Second View
struct View2: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack{
VStack{
HStack(alignment:.center){
//Any Design You Like
Image(systemName: "chevron.left")
.font(.title)
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
.padding()
Spacer()
}
Spacer()
}
}
}
.navigationBarBackButtonHidden(true)
}
}

Related

iOS 16 keyboard safe area not updated on push

There's a strange keyboard issue on iOS 16, when pushing new screens. It seems the keyboard safe area is not updated when you come back from the pushed screen.
It's even reproducible with this chunk of code on an empty project:
struct ContentView: View {
#State var text = ""
var body: some View {
NavigationView {
VStack {
Spacer()
NavigationLink {
Text("test")
} label: {
Text("Tap me")
}
TextField("", text: $text)
.textFieldStyle(.roundedBorder)
}
.padding()
}
}
}
Steps to reproduce:
Open the keyboard
Press the button "tap me" and navigate to the other screen
Quickly come back to the previous screen
The keyboard is dismissed, but there's a large gap that fits the keyboard size.
Anyone else had a similar issue?
I found 2 ways to solve this problem and both will need to hide the keyboard before you go to the next screen
Add hide keyboard to the button which activates navigation to another view
#State var isActive: Bool = false
var body: some View {
NavigationView {
ZStack {
NavigationLink(isActive: $isActive, destination: { Text("Hello") }, label: EmptyView.init)
VStack {
TextField("Text here", text: .constant(""))
Button("Press me") {
resignFirstResponder()
isActive.toggle()
}
}
}
}
}
Add hide keyboard to onChange block
#State var isActive: Bool = false
var body: some View {
NavigationView {
ZStack {
NavigationLink(isActive: $isActive, destination: { Text("Hello") }, label: EmptyView.init)
.onChange(of: isActive) { newValue in
if newValue {
resignFirstResponder()
}
}
VStack {
TextField("Text here", text: .constant(""))
Button("Press me") {
isActive.toggle()
}
}
}
}
}
Code for hide keyboard:
public func resignFirstResponder() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
I have found a temporary workaround, it's not pretty but it does the job of removing the empty space that was previously occupied by keyboard. The solution is to call parent view from child in onDisappear, then in parent have a hidden TextField that is focused and almost immediately unfocused.
In parent view add properties:
#State private var dummyText = ""
#FocusState private var dummyFocus: Bool
And put a TextField somewhere in the parent view, inside a ZStack for example:
ZStack {
TextField("", text: $dummyText)
.focused($dummyFocus)
.opacity(0.01)
... your other layout ...
}
then call/navigate to the child view with completion block like this:
ChildView(didDismiss: {
if #available(iOS 16.0, *) {
dummyFocus = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
dummyFocus = false
}
}
})
In child view add property:
var didDismiss: () -> Void
and call the completion block in child's onDisappear:
.onDisappear {
didDismiss()
}
onDisappear and didDismiss() will only be called after the whole interactive swipe back animation completes. The code checks for iOS 16 so that it doesn't unnecessarily execute on prior versions.
I have come to another fix based on Frin's solution. In my case, all our SwiftUI views are embedded into some parent UIViewController since we have an app that is partially migrated to SwiftUI. What I did is to have a small class (KeyboardLayoutGuideFix) that creates a dummy textfield to capture the focus and then observes the view controller lifecycle to do:
On view disappear: if iOS16, put focus on the dummy textfield
On view appear: remove the focus from dummy textfield
This way, the keyboard layout seems to work as expected, although the keyboard will be dismissed next time you come back to the screen (this is the expected behavior in our case).
Here is the code:
public class KeyboardLayoutGuideFix: Behavior {
private weak var viewController: UIViewController?
private lazy var dummyTextField: UITextField = {
UITextField(frame: .zero).apply { text in
viewController?.view.addSubview(text)
text.alpha = 0
}
}()
private var needsEndEditing = false
private var disposeBag = Set<AnyCancellable>()
private init(viewController: UIViewController, lifeCycle: ControllerLifeCycle) {
self.viewController = viewController
super.init(frame: .zero)
lifeCycle.$isPresented.sink { [weak self] presented in
guard let self else { return }
if presented {
if self.needsEndEditing {
self.needsEndEditing = false
DispatchQueue.main.async {
self.viewController?.view.endEditing(true)
}
}
} else {
self.dummyTextField.becomeFirstResponder()
self.needsEndEditing = true
}
}.store(in: &disposeBag)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public static func apply(viewController: PlaytomicViewController) {
apply(viewController: viewController, lifeCycle: viewController.lifecycle)
}
public static func apply(viewController: UIViewController, lifeCycle: ControllerLifeCycle) {
if #available(iOS 16, *) {
let fix = KeyboardLayoutGuideFix(viewController: viewController, lifeCycle: lifeCycle)
fix.owner = viewController
}
}
}
and then use it in the container VC like:
override func viewDidLoad() {
super.viewDidLoad()
KeyboardLayoutGuideFix.apply(viewController: self)
}
Note that you will miss the following objects to make this work in your project, but you can adapt it to your own codebase:
Behavior: a class that allows you to assign dynamically other objects to a parent one, in this case it assigns the fix to the associated view controller, preventing the deallocation. You can remove it and use a local variable in your VC containing a reference to the fix
ControllerLifeCycle: A class that exposes a publisher to track the presentation state of the ViewController. You can replace it by explicit calls in viewWillAppear and viewWillDisappear
PlaytomicViewController: Base class that provides the lifecycle and updates the published property when appear/disappear

SwiftUI: How to ignore taps on background when menu is open?

I am currently struggling to resolve a SwiftUI issue:
In a very abstract way, this is how the code of my application looks like (not the actual code to simply things for the discussion here):
struct SwiftUIView: View {
#State private var toggle: Bool = true
var body: some View {
VStack {
Spacer()
if toggle {
Text("on")
} else {
Text("off")
}
Spacer()
Rectangle()
.frame(height: 200)
.onTapGesture { toggle.toggle() }
Spacer()
Menu("Actions") {
Button("Duplicate", action: { toggle.toggle() })
Button("Rename", action: { toggle.toggle() })
Button("Delete", action: { toggle.toggle() })
}
Spacer()
}
}
}
So what's the essence here?
There is an element (rectangle) in the background that reacts to tap input from the user
There is a menu that contains items that also carry out some action when tapped
Now, I am facing the following issue:
When opening the menu by tapping on "Actions" the menu opens up - so far so good. However, when I now decide that I don't want to trigger any of the actions contained in the menu, and tap somewhere on the background to close it, it can happen that I tap on the rectangle in the background. If I do so, the tap on the rectangle directly triggers the action defined in onTapGesture.
However, the desired behavior would be that when the menu is open, I can tap anywhere outside the menu to close it without triggering any other element.
Any idea how I could achieve this? Thanks!
(Let me know in the comments if further clarification is needed.)
You can implement an .overlay which is tappable and appears when you tap on the menu.
Make it cover the whole screen, it gets ignored by the Menu.
When tapping on the menu icon you can set a propertie to true.
When tapping on the overlay or a menu item, set it back to false.
You can use place it in your root view and use a viewmodel with #Environment to access it from everywhere.
The only downside is, that you need to place isMenuOpen = false in every menu button.
Apple is using the unexpected behaviour itself, a.ex in the Wether app.
However, I still think it's a bug and filed a report. (FB10033181)
#State var isMenuOpen: Bool = false
var body: some View {
NavigationView{
NavigationLink{
ChildView()
} label: {
Text("Some NavigationLink")
.padding()
}
.toolbar{
ToolbarItem(placement: .navigationBarTrailing){
Menu{
Button{
isMenuOpen = false
} label: {
Text("Some Action")
}
} label: {
Image(systemName: "ellipsis.circle")
}
.onTapGesture {
isMenuOpen = true
}
}
}
}
.overlay{
if isMenuOpen {
Color.white.opacity(0.001)
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
isMenuOpen = false
}
}
}
}
It's not amazing, but you can manually track the menu's state with a #State var and set this to true in the .onTap for the Menu.
You can then apply .disabled(inMenu) to background elements as needed. But you need to ensure all exits out of the menu properly set the variable back to false. So that means a) any menu items' actions should set it back to false and b) taps outside the menu, incl. on areas that technically are "disabled" also need to switch it back to false.
There are a bunch of ways to achieve this, depending on your view hierarchy. The most aggressive approach (in terms of not missing a menu exit) might be to conditionally overlay a clear blocking view with an .onTap that sets inMenu back to false. This could however have Accessibility downsides. Optimally, of course, there would just be a way to directly bind to the menu's presentationMode or the treatment of surrounding taps could be configured on the Menu. In the meantime, the approach above has worked ok for me.
I think I have a solution, but it’s a hack… and it won’t work with the SwiftUI “App” lifecycle.
In your SceneDelegate, instead of creating a UIWindow use this HackedUIWindow subclass instead:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = HackedWindow(windowScene: windowScene) // <-- here!
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
}
class HackedUIWindow: UIWindow {
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if type(of: subview) == NSClassFromString("_UIContextMenuContainerView") {
if let rootView = self.rootViewController?.view {
rootView.isUserInteractionEnabled = false
}
}
}
override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
if type(of: subview) == NSClassFromString("_UIContextMenuContainerView") {
if let rootView = self.rootViewController?.view {
rootView.isUserInteractionEnabled = true
}
}
}
}
The subclass watches for subviews being added/removed, looking for one of type _UIContextMenuContainerView that’s used by context menus. When it sees one being added, it grabs the window’s root view and disables user interaction; when the context menu is removed, it re-enables user interaction.
This has worked in my testing but YMMV. It may also be wise to obfuscate the "_UIContextMenuContainerView" string so App Review doesn’t notice you referencing a private class.
You can have the behavior you want by using a Form or a List instead of a "plain view". All buttons will then be disabled by default when the menu is on screen, but they need to be buttons, and only one per cell, it won't work with a tapGesture because what you are actually doing is tapping on the cell, and SwiftUI is disabling TableView taps for you.
The key elements to achieve this are:
Use a Form or a List
Use an actual Button. In your example you use a Rectangle with a tapGesture.
I modified the code you provided and if you open the menu you can't hit the button:
struct SwiftUIView: View {
#State private var toggle: Bool = true
var body: some View {
VStack {
Spacer()
if toggle {
Text("on")
} else {
Text("off")
}
Spacer()
/// We add a `List` (this could go at whole screen level)
List {
/// We use a `Button` that has a `Rectangle`
/// rather than a tapGesture
Button {
toggle.toggle()
} label: {
Rectangle()
.frame(height: 200)
}
/// Important: Never use `buttonStyle` or the
/// default behavior for buttons will stop working
}
.listStyle(.plain)
.frame(height: 200)
Spacer()
Menu("Actions") {
Button("Duplicate", action: { toggle.toggle() })
Button("Rename", action: { toggle.toggle() })
Button("Delete", action: { toggle.toggle() })
}
Spacer()
}
}
}
Bonus:
Bonus: Don't use a buttonStyle. I lost so many hours of code because of this and I want to share it here too. In my app all buttons have a buttonStyle. It turns out that by using a style, you remove some of the behaviors you get by default (like the one we are discussing).
Instead of using a buttonStyle use an extension like this:
extension Button {
func withRedButtonStyle() -> some View {
self.foregroundColor(Color(UIColor.primary.excessiveRed))
.font(Font(MontserratFont.regular.fontWithSize(14)))
}
}
And add the withRedButtonStyle() at the end of the button.
In my case an alert was prevented from showing in a similar scenario, conflicting with the Menu as well as Datepicker. My workaround was using a slight delay with DispatchQueue.
Rectangle()
.frame(height: 200)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05){
toggle.toggle()
}
}
The only real solution will happen when Apple fixes/refines SwiftUI regarding Menu (and Datepicker) behaviours.

SwiftUI - dismissing keyboard on tapping anywhere in the view - issues with other interactive elements

I have a TextField and some actionable elements like Button, Picker inside a view. I want to dismiss the keyboard when the use taps outside the TextField. Using the answers in this question, I achieved it. However the problem comes with other actionable items.
When I tap a Button, the action takes place but the keyboard is not dismissed. Same with a Toggle switch.
When I tap on one section of a SegmentedStyle Picker, the keyboard is dimissed but the picker selection doesn't change.
Here is my code.
struct SampleView: View {
#State var selected = 0
#State var textFieldValue = ""
var body: some View {
VStack(spacing: 16) {
TextField("Enter your name", text: $textFieldValue)
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
Picker(selection: $selected, label: Text(""), content: {
Text("Word").tag(0)
Text("Phrase").tag(1)
Text("Sentence").tag(2)
}).pickerStyle(SegmentedPickerStyle())
Button(action: {
self.textFieldValue = "button tapped"
}, label: {
Text("Tap to change text")
})
}.padding()
.onTapGesture(perform: UIApplication.dismissKeyboard)
// .gesture(TapGesture().onEnded { _ in UIApplication.dismissKeyboard()})
}
}
public extension UIApplication {
static func dismissKeyboard() {
let keyWindow = shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(true)
}
}
As you can see in the code, I tried both options to get the tap gesture and nothing worked.
You can create an extension on View like so
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
and use it for the Views you want to dismiss the keyboard.
.onTapGesture {
self.endTextEditing()
}
I have just seen this solution in a recent raywenderlich tutorial so I assume it's currently the best solution.
Dismiss the keyboard by tapping anywhere (like others suggested) could lead to very hard to find bug (or unwanted behavior).
you loose default build-in TextField behaviors, like partial text
selection, copy, share etc.
onCommit is not called
I suggest you to think about gesture masking based on the editing state of your fields
/// Attaches `gesture` to `self` such that it has lower precedence
/// than gestures defined by `self`.
public func gesture<T>(_ gesture: T, including mask: GestureMask = .all) -> some View where T : Gesture
this help us to write
.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: (editingFlag) ? .all : .none)
Tap on the modified View will dismiss the keyboard, but only if editingFlag == true. Don't apply it on TextField! Otherwise we are on the beginning of the story again :-)
This modifier will help us to solve the trouble with Picker but not with the Button. That is easy to solve while dismiss the keyboard from its own action handler. We don't have any other controls, so we almost done
Finally we have to find the solution for rest of the View, so tap anywhere (excluding our TextFields) dismiss the keyboard. Using ZStack filled with some transparent View is probably the easiest solution.
Let see all this in action (copy - paste - run in your Xcode simulator)
import SwiftUI
struct ContentView: View {
#State var selected = 0
#State var textFieldValue0 = ""
#State var textFieldValue1 = ""
#State var editingFlag = false
#State var message = ""
var body: some View {
ZStack {
// TODO: make it Color.clear istead yellow
Color.yellow.opacity(0.1).onTapGesture {
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}
VStack {
TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
self.editingFlag = editing
}, onCommit: {
self.onCommit(txt: "salutation commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
TextField("Welcome message", text: $textFieldValue1, onEditingChanged: { editing in
self.editingFlag = editing
}, onCommit: {
self.onCommit(txt: "message commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
Picker(selection: $selected, label: Text(""), content: {
Text("Word").tag(0)
Text("Phrase").tag(1)
Text("Sentence").tag(2)
})
.pickerStyle(SegmentedPickerStyle())
.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: (editingFlag) ? .all : .none)
Button(action: {
self.textFieldValue0 = "Hi"
print("button pressed")
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}, label: {
Text("Tap to change salutation")
.padding()
.background(Color.yellow)
.cornerRadius(10)
})
Text(textFieldValue0)
Text(textFieldValue1)
Text(message).font(.largeTitle).foregroundColor(Color.red)
}
}
}
func onCommit(txt: String) {
print(txt)
self.message = [self.textFieldValue0, self.textFieldValue1].joined(separator: ", ").appending("!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you miss onCommit (it is not called while tap outside TextField), just add it to your TextField onEditingChanged (it mimics typing Return on keyboard)
TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
self.editingFlag = editing
if !editing {
self.onCommit(txt: "salutation")
}
}, onCommit: {
self.onCommit(txt: "salutation commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
I'd like to take Mark T.s Answer even further and add the entire function to an extension for View:
extension View {
func hideKeyboardWhenTappedAround() -> some View {
return self.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
}
Can then be called like:
var body: some View {
MyView()
// ...
.hideKeyboardWhenTappedAround()
// ...
}
#user3441734 is smart to enable the dismiss gesture only when needed. Rather than forcing every crevice of your forms to track state, you can:
Monitor UIWindow.keyboardWillShowNotification / willHide
Pass the current keyboard state via an EnvironmentKey set at the/a root view
Tested for iOS 14.5.
Attach dismiss gesture to the form
Form { }
.dismissKeyboardOnTap()
Setup monitor in root view
// Root view
.environment(\.keyboardIsShown, keyboardIsShown)
.onDisappear { dismantleKeyboarMonitors() }
.onAppear { setupKeyboardMonitors() }
// Monitors
#State private var keyboardIsShown = false
#State private var keyboardHideMonitor: AnyCancellable? = nil
#State private var keyboardShownMonitor: AnyCancellable? = nil
func setupKeyboardMonitors() {
keyboardShownMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillShowNotification)
.sink { _ in if !keyboardIsShown { keyboardIsShown = true } }
keyboardHideMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillHideNotification)
.sink { _ in if keyboardIsShown { keyboardIsShown = false } }
}
func dismantleKeyboarMonitors() {
keyboardHideMonitor?.cancel()
keyboardShownMonitor?.cancel()
}
SwiftUI Gesture + Sugar
struct HideKeyboardGestureModifier: ViewModifier {
#Environment(\.keyboardIsShown) var keyboardIsShown
func body(content: Content) -> some View {
content
.gesture(TapGesture().onEnded {
UIApplication.shared.resignCurrentResponder()
}, including: keyboardIsShown ? .all : .none)
}
}
extension UIApplication {
func resignCurrentResponder() {
sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
extension View {
/// Assigns a tap gesture that dismisses the first responder only when the keyboard is visible to the KeyboardIsShown EnvironmentKey
func dismissKeyboardOnTap() -> some View {
modifier(HideKeyboardGestureModifier())
}
/// Shortcut to close in a function call
func resignCurrentResponder() {
UIApplication.shared.resignCurrentResponder()
}
}
EnvironmentKey
extension EnvironmentValues {
var keyboardIsShown: Bool {
get { return self[KeyboardIsShownEVK] }
set { self[KeyboardIsShownEVK] = newValue }
}
}
private struct KeyboardIsShownEVK: EnvironmentKey {
static let defaultValue: Bool = false
}
You can set .allowsHitTesting(false) to your Picker to ignore the tap on your VStack
Apply this to root view
.onTapGesture {
UIApplication.shared.endEditing()
}

how to make the modal view with a transparent background in SwiftUI?

Anyone knows how to to make the modal view with a transparent background.
Exactly like the below link in swift
Swift Modal View Controller with transparent background
I am using coordinator pattern, creating view in assembly
let view = UIHostingController(rootView: swiftuiview)
view.view.backgroundColor = .clear
inside router just present this UIHostingController
module.modalPresentationStyle = .overCurrentContext
navigationController.present(module, animated: animated, completion: nil)
If you wanting to blur the background of a SwiftUI from UIKit Project and are possibly using SwiftUI View for a Modal View then I had the same problem recently and created a UIViewController that takes the UIHostController (UIViewController basically), then alters the HostingController View's alpha, puts a blur at the back and presents it to the parent view.
I Have created a gist with the file in it for public use
https://gist.github.com/Ash-Bash/93fd55d89c1e36f592d3868f6b29b259
Heres the working example:
// Initialises BlurredHostingController
var blurredHostingController = BlurredHostingController()
// Sets the Hosting View for the SwiftUI View Logic
blurredHostingController.hostingController = UIHostingController(rootView: ContentView())
// Blur Tweaks for blurredHostingController
blurredHostingController.blurEffect = .systemMaterial
blurredHostingController.translucentEffect = .ultrathin
// Presents View Controller as a Modal View Controller
self.present(blurredHostingController animated: true, completion: nil)
Here's the result from macCatalyst
Present:
let rootView = Text("Hello world")
let controller = UIHostingController(rootView: rootView)
controller.view.backgroundColor = .clear
UIApplication.shared.windows.first?.rootViewController?.present(controller, animated: true)
Dismiss:
UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true)
I didn't get the ideal way to do so, but I got a workaround for this.
So, In order to present a view modally, you can take a ZStack and group multiple views in it and handle it with a #State variable like this.
Here I have given the background colour to the view for better explanation.
struct ContentView : View {
#State private var showModally = false
var body : some View {
ZStack {
Color.red
VStack {
Button(action: {
withAnimation{
self.showModally = true
}
}) {
Text("Push Modally")
}
}
ModalView(show: $showModally)
.offset(y: self.showModally ? 0 : UIScreen.main.bounds.height)
.animation(.spring())
}
}
}
struct ModalView: View {
#Binding var show : Bool
var body: some View {
VStack {
Spacer()
VStack {
Color.white
}
.frame(height : 400)
.cornerRadius(10)
.padding(.horizontal)
}
.background(Color.clear)
.onTapGesture {
self.show = false
}
}
}
In this, the Modal View will be presented modally over the content view and will be dismissed by a tap.

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