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.
Related
I have a UITabBarController that contains 2 Items. FirstVC is UINavigationController that has UIHostingController a SwiftUI HomeView. HomeView has a navigationLink that can navigate to SecondView().
I am trying to implement a full cover view as a loadingView that gets triggered from SecondView().
Since my RootView is the UITabBarController what is the best way to access it in order to show a view on top of UITabBarController to cover everything??
Solution should support iOS 13 and above.
Thank you 🙏
tabBarCnt.tabBar.tintColor = UIColor.black
let firstVc = UINavigationController(rootViewController: UIHostingController(rootView: HomeView()))
firstVc.title = "First"
let secondVc = UIViewController()
secondVc.title = "Second"
tabBarCnt.setViewControllers([firstVc, secondVc], animated: false)
UIApplication.shared.windows.first?.rootViewController = tabBarCnt
UIApplication.shared.windows.first?.makeKeyAndVisible()
HomeView
struct HomeView: View {
var body: some View {
NavigationView {
VStack{
Text("Home View")
NavigationLink {
SecondView()
} label: {
Text("Press here to go to second page")
}
}
}
.navigationBarTitle("NAVIGATION TITLE")
}
}
SecondView
struct SecondView: View {
var body: some View {
VStack(spacing: 20) {
Text("Second View")
Text("How can I make a modal view that covers the entire view and the tabBar ??? the View should get triggered from there. BAsically I want to make a loading view that covers everything.")
}
}
}
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
}
I am presenting and dismissing a swiftUI view with a button, and it works fine.
The swiftUI view:
struct SmartG_SwiftUI: View {
var dismissAction: (() -> Void)
var body: some View {
Button(action: {
dismissAction()
}) {}
}
}
I am presenting the SwiftUI view controller from UIKit, this way:
let hostingVC = UIHostingVC(rootView: SmartG_SwiftUI(dismissAction: {
vc?.dismiss( animated: true, completion: nil )
}))
vc?.present(hostingVC, animated: true, completion: nil)
My question is, how could I put this button in a separate struct? So in order to have something like:
struct SmartG_SwiftUI: View {
var dismissAction: (() -> Void)
Header()
}
struct Header: View {
Button(action: {
dismissAction() //unknown here
}) {}
}
Rather than hand-rolling your own dismiss action and passing it in, SwiftUI does have its own dismiss action available via an environment variable. I’ve not yet used it within a hosting controller basis, but conversely haven’t seen any indication that it doesn’t work... (edit: have double-checked and definitely works for a SwiftUI view wrapped in a UIHostingController and presented via UIViewController.present(_:animation:completion:).)
The general approach is:
struct MyView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
Button {
dismiss()
} label: {
Text("Close")
}
}
}
But this doesn’t have to be the topmost view within your hosting controller; because the dismiss action is in the environment, it’ll be available in your Header view as well.
Note that for iOS 13 or 14, the syntax is a little more verbose:
struct MyView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Close")
}
}
}
Is there a way I can do programatical navigation with more than two views using NavigationView?
Like this: View 1 -> View 2 -> View 3
This is the sample code for what I'm trying to do:
class Coordinator: ObservableObject {
#Published var selectedTag: String?
}
struct ContentView: View {
#EnvironmentObject var coordinator: Coordinator
let things = ["first", "second", "third"]
var body: some View {
NavigationView {
List(things, id: \.self) { thing in
NavigationLink(destination: SecondView(thing: thing),
tag: thing,
selection: self.$coordinator.selectedTag) {
Text(thing)
}
}
}
}
}
struct SecondView: View {
#EnvironmentObject var coordinator: Coordinator
let thing: String
let things = ["fourth", "fifth", "sixth"]
var body: some View {
List(things, id: \.self) { thing2 in
NavigationLink(destination: ThirdView(thing: self.thing, thing2: thing2),
tag: thing2,
selection: self.$coordinator.selectedTag) {
Text(thing2)
}
}
}
}
struct ThirdView: View {
let thing: String
let thing2: String
var body: some View {
Text("\(thing) \(thing2)")
}
}
I hoped that I could select a specific tag and deep navigate to the ThirdView but even the simple navigation won't work. If I select a link on the SecondView it will navigate forward and then back, instead of just navigating forward as expected.
I also tried using 2 variables, one to represent the tag of each screen but it also doesn't work.
Is there a way to make this work? Am I doing something wrong?
Currently, your code is acting like there's another flow of navigation from SecondView to ThirdView, which I assume you're not intending to. If that's the intended case, you should also wrap SecondView's body into a NavigationView, too.
Caveat: You'll have 2 navigation bars.
Without tag and selection (in SecondView or also in ContentView), your code works as intended. Is there a specific reason for using tag and selection here? I couldn't find a proper solution that both uses them and have only one NavigationView.
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/ .