SwiftUI: Navigating to a view within the view hierarchy - swift

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/ .

Related

SwiftUI: UITabBarController that contains UIHostingControllers(rootView: SomeSwiftUIView). How to make full screen cover View

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

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.

How does the modifier .navigationTitle in SwiftUI work?

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
}

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.