How does the modifier .navigationTitle in SwiftUI work? - swift

I've known the .navigationTitle is the extension function of View, but how to explain the following examples?
var body: some View {
NavigationView{
ScrollView{
ForEach(1..<100){ item in
Text("Hello, \(item)!")
.navigationTitle("Test\(item)")
}
}
}
.navigationTitle("title in navigation")
}
The result show that only the modifier of first widget inside NavigationView effected.
code results
I think the best choice is: modifier .navigationTitle is effective in NavigationView instead of the first widget inside NavigationView.

iOS will show the first innermost .navigationTitle.
Your outer title will never show, as it cannot be attached to "NavigationViewitself.
From the docs:
A view’s navigation title is used to visually display the current navigation state of an interface. On iOS and watchOS, when a view is navigated to inside of a navigation view, that view’s title is displayed in the navigation bar. On iPadOS, the primary destination’s navigation title is reflected as the window’s title in the App Switcher. Similarly on macOS, the primary destination’s title is used as the window title in the titlebar, Windows menu and Mission Control.

It doesn't really matter if .navigationTitle is inside a NavigationView or not. What .navigationTitle does is finds the UIView that the View is being displayed in, then searches for the UIViewController containing that UIView and it sets its navigationItem.title. That fact that only the first title param is used is probably some implementation detail, e.g. if this value is already set don't set it again, because obviously searching the UIView and UIViewController hierarchy is an expensive operation they would want to avoid.
You can verify this by implementing a UINavigationController in SwiftUI using UIViewControllerRepresentable. Then when you put a UIHostingController in the stack, if the SwiftUI View uses .navigationTitle then it still works. e.g.
struct NavigationControllerTestView: View {
var body: some View {
MyNavigation {
Text("Test Text")
.navigationTitle("Test Title") // works despite no NavigationView
.toolbar {
EditButton()
}
}
}
}
struct MyNavigation<Content: View>: UIViewControllerRepresentable {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> UINavigationController {
let hc = UIHostingController(rootView: content)
hc.rootView = content
let vc = UINavigationController(rootViewController: hc)
return vc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
typealias UIViewControllerType = UINavigationController
}

Related

Run action when view is 'removed'

I am developing an app which uses UIKit. I have integrated a UIKit UIViewController inside SwiftUI and everything works as expected. I am still wondering if there is a way to 'know' when a SwiftUI View is completely gone.
My understanding is that a #StateObject knows this information. I now have some code in the deinit block of the corresponding class of the StateObject. There is some code running which unsubscribes the user of that screen.
The problem is that it is a fragile solution. In some scenario's the deinit block isn't called.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)? I don't want to get notified with the .onDisppear modifier because that is also called when the user taps somewhere on the screen which adds another view to the navigation stack. I want to run some code once when the screen is completely gone.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)?
This implies you're using a NavigationView and presenting your view with a NavigationLink.
You can be notified when the user goes “back” from your view by using one of the NavigationLink initializers that takes a Binding. Create a custom binding and in its set function, check whether the old value is true (meaning the child view was presented) and the new value is false (meaning the child view is now being popped from the stack). Example:
struct ContentView: View {
#State var childIsPresented = false
#State var childPopCount = 0
var body: some View {
NavigationView {
VStack {
Text("Child has been popped \(childPopCount) times")
NavigationLink(
"Push Child",
isActive: Binding(
get: { childIsPresented },
set: {
if childIsPresented && !$0 {
childPopCount += 1
}
childIsPresented = $0
}
)
) {
ChildView()
}
}
}
}
}
struct ChildView: View {
var body: some View {
VStack {
Text("Sweet child o' mine")
NavigationLink("Push Grandchild") {
GrandchildView()
}
}
}
}
struct GrandchildView: View {
var body: some View {
VStack {
Text("👶")
.font(.system(size: 100))
}
}
}
Note that these initializers, and NavigationView, are deprecated if your deployment target is iOS 16. In that case, you'll want to use a NavigationStack and give it a custom Binding that performs the pop-detection.

SwiftUI Editable NavigationTitle

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)
}
}
}
}

How to push onto an existing UINavigationController stack from SwiftUI?

We're pushing to a SwiftUI view embedded in a UIHostingViewController in UIKit land like this:
First making the UIViewController:
func hostingController() -> UIViewController {
let swiftUIView = swiftUIView(viewModel: viewModel(data: [Data, Data, Data]))
return UIHostingController(rootView: swiftUIView)
}
Then pushing it onto the UINavigationView stack:
[self.navigationController pushViewController:hostingViewController animated:YES];
This gets us to SwiftUI's environment. But, if our SwiftUI environment has it's own navigation stack with NavigationView and NavigationLink, the original navigationBar's back button can only navigate back to the original presenting UIViewController.
Is there a way to push a SwiftUI view embedded in a NavigationView onto an existing UINavigationController stack?
The only thought we've had so far is creating a new UIHostingViewController for each new SwiftUI screen, and pushing that onto the stack via some kind of delegate method.
What we're looking for is something like this:
UINavigationStack: [UIViewController -> UIViewController -> SwiftUIView -> SwiftUIView]
Where the back < arrow in the navigationBar will behave as expected.
Please let us know if we can clarify further!
I had a similar problem - I added a SwiftUI view with NavigationView to a UIKit NavigationController which lead to two layers of navigation bars when continuing navigation in the SwiftUI View: UIKit NavigationController -> MyListNavigationView(MyListView) -> DetailView.
Adding SwiftUI view to UIKit NavigationController:
let swiftUIView = MyListNavigationView(content: contentList)
let hostingViewController = UIHostingController(rootView: swiftUIView)
self.navigationController?.pushViewController(hostingViewController, animated: true)
My simplified SwiftUI views (did not validate if it compiles after simplifications):
struct MyListNavigationView: View {
#State var content: [String]
var body: some View {
NavigationView { //replace with Group {
MyListView(content: $content)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink {
DetailView(data: "New Data")
} label: {
Image(systemName: "plus")
}
}
}
}
.navigationViewStyle(.stack)
.navigationTitle("My Title")
}
}
struct MyListView: View {
#Binding var content: [String]
var body: some View {
List(content, id: \.self) { data in
NavigationLink(destination: DetailsView(data: data) {
MyRow(data: data)
}
}
}
}
struct MyRow: View {
let data: String
var body: some View {
Text(data)
}
}
struct DetailView: View {
let data: String
var body: some View {
Text(data)
}
}
So with that setup I had 2 layers of navigation bars - and with a simple change it all worked out for me:
As I only added MyListNavigationView to a UIKit NavigationController, I did not need a standalone SwiftUI NavigationView and just replaced it with a Group - and all navigation/toolbar settings from my SwiftUI views have been adopted by the parent UIKit NavigationController and there was only one navigation bar.

Navigation between SwiftUI Views

I don't know how to navigate between views with buttons.
The only thing I've found online is detail view, but I don't want a back button in the top left corner. I want two independent views connected via two buttons one on the first and one on the second.
In addition, if I were to delete the button on the second view, I should be stuck there, with the only option to going back to the first view being crashing the app.
In storyboard I would just create a button with the action TouchUpInSide() and point to the preferred view controller.
Also do you think getting into SwiftUI is worth it when you are used to storyboard?
One of the solutions is to have a #Statevariable in the main view. This view will display one of the child views depending on the value of the #Statevariable:
struct ContentView: View {
#State var showView1 = false
var body: some View {
VStack {
if showView1 {
SomeView(showView: $showView1)
.background(Color.red)
} else {
SomeView(showView: $showView1)
.background(Color.green)
}
}
}
}
And you pass this variable to its child views where you can modify it:
struct SomeView: View {
#Binding var showView: Bool
var body: some View {
Button(action: {
self.showView.toggle()
}) {
Text("Switch View")
}
}
}
If you want to have more than two views you can make #State var showView1 to be an enum instead of a Bool.

SwiftUI: Navigating to a view within the view hierarchy

What I have so far:
I have two views, the first one my ContentView will be loaded inside the scene(_:willConnectTo:options:) method in SceneDelegate (which is standard). I have embedded a NavigationView to the body property of my ContentView. The second view DetailView can be navigated to via a NavigationLink, to navigate back you can use the back button created by the NavigationView.
struct ContentView: View {
var body: some View {
NavigationView {
Form {
NavigationLink(destination: DetailView()) {
Text("Navigate")
}
}
.navigationBarTitle("Some Title")
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail")
.navigationBarTitle("Another Title")
}
}
Inside scene(_:willConnectTo:options:) method in SceneDelegate:
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
What I am trying to do:
Now I want to navigate to the DetailView right away depending on the connectionOptions inside the scene(_:willConnectTo:options:) method in SceneDelegate.
My Problem:
When I just replace let contentView = ContentView() with let contentView = DetailView depending on the connectionOptions, my ContentView is not inside my view hierarchy anymore and therefore I don't have the NavigationView layout.
Question:
How can I navigate to a certain view inside my view hierarchy without losing any of the navigation feature (including the navigation bar, back button, and being able to navigate deeper) created by a NavigationView inside another view?
Use NavigationLink(_,destination:,tag:,selection:) and bind selection to the relevant data from connectionOptions.
For a detailed explanation and example code, see https://nalexn.github.io/swiftui-deep-linking/ .