Is it possible to make the navigation title of SwiftUI editable?
Unfortunately the navigationTitle modifier only accepts Text views and not TextField views.
I want to do this instead of just using a text field below the navigation bar because I still want the nice default behaviour of having the modified title appear in the navigation bar inline when the user scrolls down and the navigation bar allocates space for the navigation title whether you define one or not.
iOS/iPadOS 16+, macOS 13+
The navigationTitle modifier now accepts a Binding<String> argument, as well as the more usual String-based initializer.
When using a bound value and the navigation bar is in its inline form, the title gains a drop-down menu with a Rename option. Tapping this allows the user to edit the view's title:
struct EditableTitleView: View {
#State private var title = "View Title"
var body: some View {
Text("Editable Title View")
.navigationTitle($title)
.navigationBarTitleDisplayMode(.inline)
}
}
This isn't exactly the same UX as always using a text field, but as it's a standard SwiftUI implementation it's a lot easier to implement.
For earlier versions of iOS
You can place a custom view in your NavigationView at the position where the title might be expected to go by specifying a ToolbarItem with a placement value of .principal, for example in the code below. I've added the RoundedBorderTextFieldStyle to make the text field more visible:
struct EditableTitleView: View {
#State private var editableTitle: String = "My Title"
var body: some View {
NavigationView {
Text("View with editable title")
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Title", text: $editableTitle)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
}
}
Note that if you also add a navigationTitle modifier into your view, its default large style on iOS will still display beneath the toolbar, but if it scrolls off the page it while disappear while your principal item will remain on screen. If you set .navigationBarTitleDisplayMode(.inline) then the larger style title will never display.
I mention this because you should consider keeping a title for your view anyway, for a couple of reasons:
If a NavigationLink pushes a view on the stack, you want a meaningful name to appear in the back button and the list of stacked views that appear on long press.
I haven't checked what happens with VoiceOver when you have a navigation subview without a title. The more you override native behaviour, the more you need to consider whether you are making your app less accessible than the SwiftUI defaults provide.
Try a TextField inside of a ToolbarItem in the principal slot of the toolbar. Create a computed property for the destination and give it an editable navigation title, too.
struct TextFieldNavigationTitleView: View {
#State var mainTitle = "Main Menu"
#State var areaOneTitle = "Area One"
var body: some View {
NavigationView {
NavigationLink("App Area One", destination: areaOne)
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Navigation Title", text: $mainTitle)
}
}
}
}
var areaOne : some View {
Text("AREA ONE")
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Area One Title", text: $areaOneTitle)
}
}
}
}
Related
In SwiftUI, if you embed a NavigationLink inside a button, then clicking the button will trigger the button's action and the navigation as well.
struct LoginView: View {
#StateObject private var viewModel = LoginViewModel()
Button(action: viewModel.doLogin, label: {
NavigationLink(value: viewModel.userInfo) {
Text("Log in")
}
.buttonStyle(.plain)
})
.buttonStyle(.plain)
}
However the reverse only triggers the button's action. Why is that?
struct LoginView: View {
#StateObject private var viewModel = LoginViewModel()
NavigationLink(value: viewModel.userInfo) {
Button(action: viewModel.doLogin, label: {
Text("Log in")
})
.buttonStyle(.plain)
}
.buttonStyle(.plain)
}
When the Button is created, it takes a label: parameter in its initialiser that is, in this case, of type view.
So what gets dispalyed on the screen is the label.
In the first case, the label is a NavigationLink and clicking on a it performs the navigation.
In the second case, the NavigationLink has the label parameter, that displays a Button and because of the style, it completely hides the navigation link area, as opposed to, if you had used a Text view or so.
So the answer is, the NavigationLink simply does not get clicked on because it hides entirely beneath the Button.
I want a large title in the navigationbar on a pushed view in SwiftUI and an inline title on the parent view.
When the parent navigation bar display mode is not set, it works:
Working without display mode on parent
But when I set the display mode in the parent view to inline, the title on the second screen is inline, instead of large. You can drag the list and the title will stay large. (You can see a small example in the code below)
With display mode to inline on the parent, the child is also inline.
Here is a small example:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DestinationView()) {
Text("Next Screen")
}
.navigationBarTitle("Start screen", displayMode: .inline)
}
}
}
struct DestinationView: View {
var body: some View {
ScrollView {
VStack{
ForEach((1...10), id: \.self) {
Text("\($0)")
}
}
}
.navigationBarTitle("Second screen", displayMode: .large)
}
}
There are several post with similar questions:
https://www.reddit.com/r/iOSProgramming/comments/g2knmp/large_title_collapses_after_a_push_segue/
-> Same problem but with UIKit and we don't have prefersLargeTitles in SwiftUI.
Large title doesn't appear large
-> Same problem with UIKit and marked as answered with preferesLargeTitles.
Navigation bar title stays inline in iOS 15
-> Here was a fix from apple side, but it was a back navigation
just place .navigationViewStyle(.stack) in NavigationView in ContentView()
SwiftUI helpfully gives you NavigationView which easily lets you define a sidebar and main content for iPad apps that automatically collapse for iPhones.
I have an app and everything works as expected except on iPad, in portrait mode, the sidebar is hidden by default and you are forced click a button to show it.
All I want is to force the sidebar to always be visible, even in portrait mode. And make it work the same way as the settings app.
I’m even willing to use a UIKit view wrapped for SwiftUI, but wrapping NavigationController seems very very challenging.
Is there a better way?
Without your specific code I can't be sure but I think you are describing the initial screen that shows on iPad. That is actually a "ThirdView". You can see it with the code below.
And I am answering the "better way" portion of your question. Which for me is a way of just "graciously" dealing with it.
struct VisibleSideBar2: View {
var body: some View {
NavigationView{
List(0..<10){ idx in
NavigationLink("SideBar \(idx)", destination: Text("Secondary \(idx)"))
}
Text("Welcome Screen")
}
}
}
It is even more apparent if you have a default selection in your "Secondary View" because now you have to click "Back" twice to get to the SideBar
struct VisibleSideBar1: View {
#State var selection: Int? = 1
var body: some View {
NavigationView{
List(0..<10){ idx in
NavigationLink(
destination: Text("Secondary \(idx)"),
tag: idx,
selection: $selection,
label: {Text("SideBar \(idx)")})
}
Text("Third View")
}
}
}
A lot of the "solutions" out there for this just turn the NavigationView into a Stack but then you can't get the double column.
One way of dealing with it is the what is depicted in VisibleSideBar2. You can make/embrace a nice "Welcome Screen" so the user isn't greeted with a blank screen and then the natural navigation instincts kick in. You only see the "Welcome Screen" on iPad Portrait and on Catalyst/MacOS where Stack is unavailable.
Or you can bypass the third screen by using isActive in a NavigationLink and using the Sidebar as a menu like View
struct VisibleSidebar3: View{
#State var mainIsPresented = true
var body: some View {
NavigationView {
ScrollView{
NavigationLink(
destination: Text("Main View").navigationTitle("Main"),
isActive: $mainIsPresented,
label: {
Text("Main View")
})
NavigationLink("List View", destination: ListView())
}.navigationTitle("Sidebar")
//Not visible anymore
Text("Welcome Screen")
}
}
}
struct ListView: View{
var body: some View {
List(0..<10){ idx in
NavigationLink("SideBar \(idx)", destination: Text("Secondary \(idx)"))
}.navigationTitle("List")
}
}
Like I said my answer isn't really a way of "fixing" the issue. Just dealing with it. To fix it we would have to somehow dismiss the "Third Screen/Welcome Screen". Then manipulate the remaining UISplitViewController (Several SO questions on this) to show both the SideBar/Master and the Detail View.
In UIKit it seems to have been done a lot, if you search SO, you will find a way to create a UISplitViewController that behaves like Settings.
in the HIG Apple writes: In iOS 14 and later, a button can display a pull-down menu that lists items or actions from which people can choose
This is exactly what I want for my project. This picture where they have a "more" bar button with a drop-down menu fits the bill perfectly. Does anyone have an example, though, of how to create a pull-down menu (not context-menu) from a button with SwiftUI?
You can simply use the Menu view that is new for iOS in iOS 14.
It acts as a button and when pressed presents the context menu. You can use a Label if you want an image and even nest different views, as shown in the
example in the documentation.
struct ContentView: View {
#State var text = "Hello World"
var body: some View {
NavigationView {
Text("Hello World")
.navigationTitle("Hello")
.navigationBarItems(trailing: {
Menu {
Button(action: { text = "Hello there" }) {
Label("Hello", systemImage: "pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}())
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Instead of navigationBarItems one should probably use toolbar, however I found that to be quite unreliable starting with Beta 4.
Here is my current demo UI written in SwiftUI:
I want to make custom navigation and display decisions based on if the cell (View 1, View 2, ...) was tapped or if one of the buttons (arrows) in the cell was tapped.
For example, each cell represents a different view I want to navigate to and each arrow button represents some custom modal I want to present.
How do I keep track of context and propagate the different taps up to the view that will ultimately make the navigation decisions. Here is the view that makes the list and would make the navigation decisions:
import SwiftUI
struct ContentView: View {
#State var options: [String] = ["View 1", "View 2", "View 3", "View 4"]
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
ScrollView {
ForEach(self.options, id: \.self) { element in
OptionCell(optionDescription: element)
}
}
}
}
}
And here is my OptionCell class. I try and illustrate my problem in some comments in the button action here.
struct OptionCell {
//MARK: Input Properties
#State var optionDescription: String
//MARK: Computed Properties
//MARK: Init
//MARK: Functions
func cellTapped() {
///What can I call here to propagate the context of this cell along with the fact that it was tapped to something that needs to make UI transition decisions?
print("\(self.optionDescription) Tapped")
//What can I do to make micro navigation decisions if one of the "ArrowButtons" is tapped?
}
}
extension OptionCell: View {
var body: some View {
Button(action: { self.cellTapped() }) {
VStack {
HStack{
Text(self.optionDescription).foregroundColor(Color.black).padding()
Spacer()
HStack {
//These Views have a Button view in them with a call to a function in that struct. How do I get the call for the tap on each of these buttons propagated up to the decisions making view?
ArrowButton(direction: .LEFT)
ArrowButton(direction: ArrowDirection.RIGHT)
ArrowButton(direction: ArrowDirection.DOWN)
ArrowButton(direction: ArrowDirection.UP)
}.padding()
}
///Another custom view that I have
SeparatorView(separatorViewStyle: SeparatorViewStyle.init(weight: .THIN, gapPosition: .RIGHT, color: Colors.grey.medium))
}
}
}
}
In UIKit I would add button actions on the custom UITableViewCell and then create a delegate for the custom cell. Tapping the button would call the appropriate delegate function (Delegate.leftArrow(), Delegate.upArrow(), ...) and the UIViewController would become the delegate for the custom cell and make navigation decisions there.
I presume the question is:
How do I get the call for the tap on each of these buttons propagated up to the decisions making view?
In ContentView you setup something like this:
#ObservedObject var myChoice = MyChoiceClass()
where:
class MyChoiceClass: ObservableObject {
#Published var choice: String = ""
}
pass it around to your OptionCell(...) and then to the ArrowButton(...)
So when the ArrowButton(...) change the "choice" it is reflected everywhere.