SwiftUI: Navigation View/Bar/Link and ScrollView issues since iOS 14.5 - ios

Since iOS 14.5, the behaviour when pushing a new view onto a scrolled navigation view appears to have changed and no longer works the way it did on iOS 14.4
Image of the differences here
On iOS 14.4, when pushing a new view onto a NavigationView via a NavigationLink, the new view renders (as expected) with the .large NavigationBar in it's expanded state.
On iOS 14.5, with the exact same code, the new view renders with the .large NavigationBar in either an expanded or collapsed state (with the content scrolled up) depending on the state from the parent view.
If you have scrolled the parent view, the navigation bar will be collapsed. If you have not scrolled the parent view the navigation bar will be expanded.
This is not the behaviour I want or expect. I expect the navigation bar on the child view to always be expanded.
Code below:
struct RootView: View {
var body: some View {
NavigationView() {
ScrollView() {
VStack() {
Text("Root view")
.navigationTitle("Root")
.navigationBarTitleDisplayMode(.large)
.padding()
ForEach(0...50, id: \.self){ i in
NavigationLink(
destination:DetailView(title: "Link \(i)")
){
Text("Link \(i)")
}
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailView: View {
var title: String
var body: some View {
ScrollView() {
VStack() {
Text(title)
.navigationTitle("Detail")
.navigationBarTitleDisplayMode(.large)
ForEach(0...50, id: \.self) { x in
Text("Scroll content! \(x)")
}
}
}
}
}
Am I doing something wrong in how I set up the views?

Related

SwiftUI - Empty bottom bar after tapping NavigationLink

I have a NavigationView with a toolbar that contains a ToolBarItem with a .bottomBar placement and a search field. This NavigationView contains a ScrollView with content that exceeds the screen's vertical size, which means that the bottom bar has a background, as seen below:
When the user taps the "Root View" text element they navigate to a new view, in this case, just another text displaying "Detail View". The problem, however, is that the bottom toolbar's background remains in the screen instead of vanishing as expected. See the screenshot below:
This behavior is not seen if I remove the search bar or shrink the ScrollView's height to fit the vertical dimension of the device. I tried googling this issue to see if it was a known bug with a workaround but maybe I'm not searching the right keywords. How can I fix this issue?
Please see the bare minimum to replicate the issue below:
struct BugView: View {
#State var searchPattern: String = ""
var body: some View {
NavigationView {
ScrollView {
VStack {
NavigationLink(destination: Text("Detail View")) {
Text("Root View").foregroundColor(Color.blue)
}
Spacer()
Text("Root View Bottom").foregroundColor(Color.blue)
}.frame(maxWidth: .infinity, minHeight: 1000)
}
.searchable(text: self.$searchPattern, prompt: "Search Here")
.toolbar(content: {
ToolbarItem(placement: .bottomBar) {
Text("Toolbar text")
}
})
}
}
}
Setup:
XCode Version: 13.4.1
Simulator: iPhone 13
You can use the .toolbar(.hidden, for: .bottomBar) for the destination view as shown in the code below:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Text("Destination View")
} label: {
// hiding the toolbar for the destination view
Text("Root View").toolbar(.hidden, for: .bottomBar)
}
}.toolbar {
ToolbarItem(placement: .bottomBar) {
Text("Toolbar Text")
.background {
Color.gray
}
}
}
}
}

Draw a view over the navigation bar

I'm working on a bottom sheet that can be invoked from any other screen. The bottom sheet will be displayed on top of a half-opaque overlay and I would like the overlay to render full screen over any other view including the navigation bar and the tab bar.
However, I can't seem to be able to figure out how to get the content of the navigation bar to be behind the overlay. Here is what a demo of my current implementation looks like. As you can see, it's possible to interact with the content of the navigation bar even though it is visually displayed behind the overlay.
Half Screen
Full Screen
Back button is still active
And here is the simplified code of my current implementation:
import SwiftUI
struct MainNavigationView: View {
var body: some View {
NavigationView {
NavigationLink(destination: AnoterView()) {
Text("Navigate to the next screen")
}
}
}
}
struct AnoterView: View {
var body: some View {
ZStack {
Color(uiColor: .red)
.edgesIgnoringSafeArea(.all)
.navigationTitle("Test")
.navigationBarTitleDisplayMode(.inline)
ViewWithOverlay()
}
}
}
struct ViewWithOverlay: View {
var body: some View {
ZStack {
// I'd like this overlay to be rendered over the navigation bar
Color(uiColor: .blue)
.edgesIgnoringSafeArea(.all)
Color(uiColor: .green)
}
}
}
And the outcome:
As you can see, while the blue color, which represent my overlay, is drawn over the red color, the title and the back button are still displayed on top of the blue color.
I understand why this is happening, but I cannot think of any workaround in SwiftUI to fix this that can be invoked from any view.
Any help is appreciated.
If you want to overlay everything then it should be on root, including over NavigationView as well, ie.
ZStack {
NavigationView {
Color(uiColor: .red).edgesIgnoringSafeArea(.all)
}
ViewWithOverlay() // << here !!
}
.edgesIgnoringSafeArea(.all)
One thing you can do is to put the NavigationView inside a ZStack. This way it will be in a lower layer hidden by the layer above. Here is the code that completely hides the NavigationBar on the tap of the button.
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
ZStack {
NavigationView {
Text("Hello World")
.navigationTitle("Welcome")
}
VStack {
}.frame(maxWidth: isPresented ? .infinity: 0, maxHeight: isPresented ? .infinity: 0)
.background(.green)
Button("Animate") {
withAnimation {
isPresented.toggle()
}
}
}
}
}

iPad navigation automatically imposes slideout

I have a NavigationView with three NavigationLinks, on portrait iPhone, it works as expected where one taps a link, a new view is presented with a Back button. However on an iPad in landscape (which this app is designed for only), iOS forces a slideout menu from the left side instead of showing the three links on the view.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
HStack {
NavigationLink(destination: ContentView()) {
Text("Item 1")
}
NavigationLink(destination: ContentViewC()) {
Text("Item 2")
}
NavigationLink(destination: ContentViewB()) {
Text("Item 3")
}
}
}
}
}
Any suggestions for overriding this behavior?
You can solve this issue by adding this
.navigationViewStyle(StackNavigationViewStyle())
to your NavigationView
NavigationView {
...
}.navigationViewStyle(StackNavigationViewStyle())
This will force it to render a standard stack (like you see on the phone) rather than defaulting to the split view layout.

SwiftUI: How to make a hidden UITabBar display correctly on View appearing?

I have a TabView, with one of the tabs being a NavigationView. I want the tab bar to be hidden on the navigation destination view. I have achieved this, but the view only appears properly after the first rotation. How do I get it to appear properly the first time (2nd image)?
struct ContentView: View {
var rowIndexes : [Int] = [0,1,2,3,4,5,6]
var body: some View {
TabView {
NavigationView {
List {
ForEach(self.rowIndexes, id: \.self) {i in
NavigationLink(
destination: Color(.blue)
.onAppear(perform: {
Global.tabBar!.isHidden = true
})
.onDisappear(perform: {
Global.tabBar!.isHidden = false
})
) {
Text("\(i)")
}
}
}
}.tabItem {
Image(systemName: "list.number")
Text("List View")
}
NavigationView {
Text("Options View")
}.tabItem {
Image(systemName: "wrench")
Text("Options")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Global {
static var tabBar : UITabBar?
}
extension UITabBar {
override open func didMoveToSuperview() {
super.didMoveToSuperview()
Global.tabBar = self
print("Tab Bar moved to superview")
}
}
Here's what the screen looks like after clicking on a link in the list for the first time (INCORRECT, with gap at the bottom where the tab bar would be if it wasn't hidden):
Here's what the screen looks like after rotating it to landscape, then back to portrait (CORRECT, blue View extending all the way to the bottom):
Is there a way to force the redraw, or simulate a rotation and back? I have tried various #State, #EnvironmentObect, and #ObservedObject solutions, but none work.
Adding ignore bottom safe area gives behaviour as you want.
NavigationLink(
destination: Color(.blue).edgesIgnoringSafeArea(.bottom) // << here !!
.onAppear(perform: {
MyGlobal.tabBar!.isHidden = true
Tested with Xcode 11.4 / iOS 13.4.

Conditional Navigation Links in Parent View Forcing Navigation From Child View back to Parent View When Condition is No Longer Met

import SwiftUI
struct ConditionalNavigationParentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
#Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var body: some View {
NavigationView {
// iPhone Portrait
if horizontalSizeClass == .compact && verticalSizeClass == .regular {
VStack {
Text("This is the Parent View.")
.foregroundColor(Color.black)
.font(.headline)
NavigationLink(destination: ConditionalNavigationChildView()) {
Text("Navigate to Child View.")
.font(.body)
}
}
}
// iPhone Landscape
if verticalSizeClass == .compact {
HStack {
Text("This is the Parent View.")
.foregroundColor(Color.black)
.font(.headline)
NavigationLink(destination: ConditionalNavigationChildView()) {
Text("Navigate to Child View.")
.font(.body)
}
}
}
}
}
}
struct ConditionalNavigationChildView: View {
var body: some View {
Text("This is the Child View.")
.font(.headline)
}
}
In the above code, we have a Parent View and a Child View. The parent view holds a conditional title and navigation link. The child view holds a title. When you navigate to the child view, navigation works fine. However, once the condition that holds the navigation link in the parent view is no longer met (by rotating your device from portrait to landscape), the connection keeping the child and parent views navigatable breaks, and forces navigation back to the parent view. This appears to be because SwiftUI as it is now needs to redraw the components from the parent view. With the example above, one solution would be to make only the text condtional, and have the navgation link be held in both a vstack and hstack simultaneously, like this:
import SwiftUI
struct ConditionalNavigationParentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
#Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var body: some View {
NavigationView {
VStack {
// iPhone Portrait
if horizontalSizeClass == .compact && verticalSizeClass == .regular {
Text("This is the Parent View.")
.foregroundColor(Color.black)
.font(.headline)
}
HStack {
// iPhone Landscape
if verticalSizeClass == .compact {
Text("This is the Parent View.")
.foregroundColor(Color.black)
.font(.headline)
}
NavigationLink(destination: ConditionalNavigationChildView()) {
Text("Navigate to Child View.")
.font(.body)
}
}
}
}
}
}
struct ConditionalNavigationChildView: View {
var body: some View {
Text("This is the Child View.")
.font(.headline)
}
}
While this does fix the navigation issue in this case, other cases with multiple navigation links seem to be completely impossible. For example, say you replace the title with another navigation link. The second one will still work fine, as it is not conditional, but the first one, and all of its subviews, will get pushed back to the parent view on rotation.
In UIKit, this would never be a problem, as conditional navigation doesn't push back to the parent view like that. So is this a SwiftUI bug? Or is it intended behavior? If it is intended behavior, what solution is there for a view with multiple conditional navigation links?

Resources