Navigation bug when dismissing view while focussing on empty .searchable() modifier - swiftui

When trying to navigate back from a view using the environment Dismiss value while also focussing on an empty searchable modifier the view you navigated back to becomes unresponsive. This is due to an empty UIView blocking any interaction with the view as seen in this screenshot:
Empty UIView blocking view after navigating back
This only occurs when the searchbar is focussed and empty when trying to navigate back. When there's a value in the searchbar everything works:
GIF of the bug
Am I doing something wrong here?
Tested on Xcode 14.2 iPhone 14 Pro (iOS 16.0) simulator.
import SwiftUI
struct MainPage: View {
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
Text("Main view")
NavigationLink(destination: DetailView()) {
Text("Click me")
}
}
}
}
}
struct DetailView: View {
#Environment(\.dismiss) private var dismiss
#State private var searchText = ""
var body: some View {
VStack {
Text("Detail view")
Button("Go back") {
dismiss()
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}
This bug only seems to happen when using NavigationStack or NavigationView with a .navigationViewStyle(.stack). When using NavigationView without a navigationViewStyle it seems to work fine. Currently I can work around this using the latter but I would prefer to use NavigationStack as NavigationView has become deprecated since iOS 16.0.
Any help is appreciated.

Related

Using a custom background behind NavigationStack in SwiftUI

This ought to be straightforward enough, but I cannot find out how to place a background behind a NavigationStack. With NavigationView, it was simply a matter of embedding in a ZStack with the background view called before the NavigationView (as described in an older post: How change background color if using NavigationView in SwiftUI?)
The same technique does not work for me with NavigationStack.
Here's what I have:
struct MyAngularGradient: View {
var body: some View {
ZStack {
AngularGradient(gradient: Gradient(colors: [.red, .orange , .yellow, .green, .cyan, .blue, .indigo, .purple, .red]), center: .leading)
AngularGradient(gradient: Gradient(colors: [.red, .orange , .yellow, .green, .cyan, .blue, .indigo, .purple, .red]), center: .leading)
.offset(x: -8)
}
.ignoresSafeArea()
}
}
var body: some View {
ZStack{
MyAngularGradient()
NavigationStack {
...
}
.navigationViewStyle(.stack)
} // end ZStack
FYI, I've used the same MyAngularGradient()in other apps (with NavigationView)
Any ideas? Thanks.
The closest I've managed to get so far is this:
struct MyView: View {
var body: some View {
NavigationStack {
ZStack {
Color.pink.ignoresSafeArea()
List {
NavigationLink("Hello") {
Text("Hello")
}
}
.navigationTitle("Title")
}
}
.scrollContentBackground(.hidden)
}
}
Which is somewhat dissatisfying for the following reasons:
When you scroll up the navigation bar appears, as expected, but ruins the effect imo.
I guess you can experiment changing this in a limited way using UINavigationBarAppearance() by updating it in the constructor of the view.
You can't apply a background to the whole app if you have multiple NavigationStackView based views in a TabView. (My example above was in a TabView)
When a new view is pushed on the stack the custom background disappears.
Only works on iOS 16+ due to the .scrollContentBackground(.hidden) modifier.
Does not work if you use ForEach to populate the List. Interestingly if you debug the hierarchy with and without the ForEach you'll see that SwiftUI adds a new view controller which is opaque in the case of ForEach.

NavigationStack and TabView in Swiftui iOS 16: bug or improper usage?

[Xcode 14.1, iOS 16.1]
I have a NavigationStack with a navigationTitle and a TabView with 2 Views. Each View has a ScrollView (see image below):
NavigationStack and TabView problem image
When I tap on Tab1 (#1 in red on the image above), then swipe up, the behavior is as expected (#2), i.e. the big navigationTitle move to the center, and my view passes below and becomes blurry. Perfect.
However, when I tap ton Tab2 (#3) and then swipe up (#4), the big title stays big, and the view doesn't become blurry.
Then I tap on Tab1 again (#5) and it works as expected.
Please help!
Here is my code:
ContentView:
import SwiftUI
struct ContentView: View {
#State private var selection: Tab = .tab1
enum Tab {
case tab1
case tab2
}
#State private var mainTitle = "Tab1"
var body: some View {
NavigationStack {
TabView(selection: $selection) {
Tab1(mainTitle: $mainTitle)
.tabItem {
Label("Tab1", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab1)
Tab2(mainTitle: $mainTitle)
.tabItem {
Label("Tab2", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab2)
} .navigationTitle(mainTitle)
}
}
}
Tab1:
import SwiftUI
struct Tab1: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 1")
.padding(.all,100)
.background(.blue)
} .onAppear {
mainTitle = "Tab1"
}
}
}
Tab2:
import SwiftUI
struct Tab2: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 2")
.padding(.all,100)
.background(.green)
} .onAppear {
mainTitle = "Tab2"
}
}
}
I tried a hack that is supposed to fix the transparency bug for Tab bars, but it doesn't work.
.onAppear {
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground()
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
}
TabViews are designed to sit at the top of the navigation hierarchy. They're intended to allow users to switch between independent sections of your app at any time.
You would generally put a separate navigation stack within each tab that then handles pushing and popping of views. And then, you can use the navigationTitle modifier to manage the screen's title.
So your structure (which might be split over multiple custom views) should look something like:
TabView {
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 1")
}
.tabItem { Label("Tab1", ...) }
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 2")
}
.tabItem { Label("Tab2", ...) }
}
This structure is by design, to align with Apple's Human Interface Guidelines. It's worth reading the HIG to get a handle on where Apple are coming from, and how working on the same principles can really help your app feel like it belongs on your users' device.

SwiftUI extra space at top of list above section header. On testing device only

I have been trying to figure out what is causing the space at the top of the screen in my production app, so I made this test app to see if it is a bug or not. The code works as intended on a simulator but when a testing device runs the code it adds extra space. The space goes away after you start scrolling, and does not comeback until the view reloads. I have tried restarting the device and other devices. I took out .navigationTitle and .navigationBarTitleDisplayMode and it did not fix the problem. So far my best guess is that there is some problem with changing the section header in .onAppear(). Changing it to .task() seems to be a workaround for now.
struct DetailView: View {
#State var item: Item
#State private var headerText = "Header"
var body: some View {
List {
Section(header: Text("\(headerText)")) {
Text("Text")
}
HStack {
Text("Red Text")
}.listRowBackground(Color.red)
// Change to .task instead
}.onAppear {
headerText = "Change Header"
}
}
}
Edit: Here is the code for the list view, it is the default new project setup.
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: DetailView(item: item), label: {
Text(item.timestamp!, formatter: itemFormatter)
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
In my case this behaviour was caused by .navigationViewStyle(StackNavigationViewStyle()).
Looks like it's a bug with NavigationView in SwiftUI 4 since it wasn't like this before.
If you are on iOS 16 use NavigationStack instead NavigationView. This fixed all my problems and I didn't even need to use the navigationViewStyle anymore.
NavigationView is deprecated in favor of NavigationStack.

Changing EnvironmentObject published property changes TabView

I have a an app I'm writing (first one) that has developed an issue. I have a tabbed view with multiple views, one of which is settings. On the settings view, various elements (mostly Toggles) bind to the global environment object appData. When I select something on the settings view, the TabView changes to the first tab away from the settings view.
The playground example below shows the issue. It does it the first time but not subsequent times. In my application, it does it every time. Not sure why it's different. To see the issue, load the playground code, click on settings and click the "Setting" toggle. It immediately jumps to MainView. The value for isSet in the application data does appear to get set.
I also see this on the emulator (set for iPhone SE) for my application.
I'm using XCode 13.4.
import SwiftUI
import Combine
import Foundation
import PlaygroundSupport
class ApplicationData: ObservableObject
{
#Published var isSet: Bool = false
}
struct MainView: View
{
var body: some View
{
Text("Main View")
}
}
struct SettingsView: View
{
#EnvironmentObject var appData : ApplicationData
var body: some View
{
Toggle("Setting", isOn: $appData.isSet)
}
}
struct TabbedView: View
{
#StateObject var appData = ApplicationData()
#State var selection: Int = 0
var body: some View
{
TabView(selection: $selection)
{
MainView().tabItem({Label("Main", systemImage: "menucard")})
SettingsView().tabItem({Label("Settings", systemImage: "gear")})
}
.environmentObject(appData)
}
}
let view = TabbedView()
let hostingVC = UIHostingController(rootView: view)
PlaygroundPage.current.liveView = hostingVC
I think the issue comes from TabView(selection:) itself. As you don't provide tags to identify the tabs, the view looses track when it is redrawn.
Either use TabView without selection:
TabView // here
{
MainView()
.tabItem({Label("Main", systemImage: "menucard")})
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
}
Or – if with selection – you have to provide a .tag() so selection can track what is selected:
TabView(selection: $selection)
{
MainView()
.tabItem({Label("Main", systemImage: "menucard")})
.tag(0) // here
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1) // here
}

SwiftUI Navigation bar items disappear on iOS 14

Discovered in my app that navigation bar items in some views disappear when orientation of the device changes. This seems to occur only in a view that is opened using NavigationLink, on main view navigation bar items work as expected. It appears that something has changed between iOS 13.7 and iOS 14.2 related to this. Also, it does not seem to matter whether using leading or trailing items, both disappear.
Example snippet where this occurs:
struct ContentView: View {
var detailView: some View {
Text("This is detail view")
.navigationBarTitle("Detail view title", displayMode: .inline)
.navigationBarItems(trailing: Button(action: {}, label: {
Image(systemName: "pencil.circle.fill")
}))
}
var body: some View {
NavigationView {
NavigationLink(
destination: detailView,
label: {
Text("Open detail view")
})
.navigationBarTitle("Main view")
}.navigationViewStyle(StackNavigationViewStyle())
}
}
The issue occurs only when running on a real device. (iPhone 11 in my case) On simulator everything works as expected.
Anyone else seen similar issues? Workarounds/fixes?
.navigationBarTitle and .navigationBarItems are being deprecated. I think that the best "fix" is to switch to .toolbar
It's a weird issue but I guess there is a hack to make it work.
SwiftUI doesn't call body property when rotations happen. So you can add #State UIDeviceOrientation property to your view and update it every time orientation changes. One tricky thing is to use that property somewhere in the body of the view since SwiftUI smart enough to ignore #State that is not used in the body.
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: DetailsView(),
label: {
Text("Open detail view")
}).navigationBarTitle("Main view")
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailsView: View {
#State var orientation: UIDeviceOrientation = UIDevice.current.orientation
var body: some View {
Text("This is detail view")
.navigationBarTitle("Detail view title")
.navigationBarItems(trailing: button)
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientation = UIDevice.current.orientation
}.background(Text("\(orientation.rawValue)"))
}
var button: some View {
Button(action: { print("123") }, label: {
Image(systemName: "pencil.circle.fill")
}).id("123")
}
}
In my experience when I change a Button in the toolbar from disabled to enabled, they disappear. But if I scroll to the bottom of the View, they re-appear. If I am already at the end of the View when the button is enabled, it acts normally, until I then scroll away from the bottom, the button again disappears.
Try scrolling to the bottom of your view, if you use landscape.