SwiftUI StackNavigationViewStyle issue when rotating iPhone - ios

I want to implement a Settings view which can be opened taping on a gear icon button in the navigation tool bar.
This button opens a SwiftUI sheet with on Ok button to validate settings and close the settings window.
It works well if you use it without rotating the iPhone.
But if you rotate the phone when the settings window is opened, the Ok button does not work anymore and the window stays on screen (even if you rotate back the phone).
In the console, an error appears when I rotate the phone. Here is the message:
[Presentation] Attempt to present <…> on <…> (from <…>) which is already presenting
This issue seems to be linked to StackNavigationViewStyle() modifier I use to not have 2 columns on landscape mode.
If I remove the following line, the bug disappears but the layout is no more the one I want.
.navigationViewStyle(StackNavigationViewStyle())
Here is a sample code I wrote to reproduce the problem:
import SwiftUI
struct ContentView: View {
// Size class
#Environment(\.verticalSizeClass) var sizeClassV
#State private var showGearView: Bool = false
var gearButton: some View {
HStack {
Button(action: {
self.showGearView.toggle()
}) {
Image(systemName: "gear")
.imageScale(.large)
.accessibility(label: Text("Settings"))
}
.sheet(isPresented: self.$showGearView, onDismiss: {
}, content: {
gearView()
})
}
}
var body: some View {
return NavigationView {
// if sizeClassV == .regular {
VStack {
Text("Click on Gear and rotate your iPhone: here is the bug when clicking on Ok: the sheet does not collapse!")
.multilineTextAlignment(.center)
.padding(.all)
}
.padding(.all)
.navigationTitle("Bug")
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: { gearButton })
}
// The bug only happens when adding the StackNavigationViewStyle below
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct gearView: View {
#Environment(\.presentationMode) var presentationMode
var OKButton: some View {
HStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("OK")
}
}
}
var body: some View {
return NavigationView {
VStack {
Form {
Section(header: Text("Settings")) {
Text("No Settings")
}
}
}
.navigationTitle(Text("Settings"))
.toolbar(content: {
ToolbarItem(placement: .primaryAction) {
OKButton
}
})
}
}
}

Related

SwiftUI - Navigation Link pops out on iPhone, but not in Simulator

I have an app that contains several views with NavigationLinks inside.
The main view looks like this, calling a Toolbar view I have created.
struct CountListView: View {
#StateObject var vm = CountListViewModel()
let navigationBar = HomePageNavigationBar()
var body: some View {
NavigationView {
List {
ForEach(vm.count, id: \.uid) { item in
NavigationLink(destination: CountView(count: item)) {
CountListItemView(name: item.name)
}
}
}
.toolbar {
navigationBar.rightSideOfBar()
navigationBar.leftSideOfBar()
}
.navigationBarTitle("Count")
The navigation bar function that is playing up looks like this
func leftSideOfBar() -> some ToolbarContent {
ToolbarItemGroup(placement: .navigationBarLeading) {
NavigationLink(destination: SettingsView()) {
Label("Settings", systemImage: "gear")
}
}
}
And the SettingsView is as follows:
struct SettingsView: View {
var body: some View {
List {
NavigationLink(destination: NameSettingView()) {
Text("Name")
}
.buttonStyle(PlainButtonStyle())
NavigationLink(destination: PrivacyPolicyView()) {
Text("Privacy Policy")
}
.buttonStyle(PlainButtonStyle())
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
When I open the Privacy Policy View on device, the returns to the SettingsView without any user intervention. But this problem doesn't exist in the simulator.

Navigation bar title is stuck

If I use a toolbar for the keyboard which has a ScrollView inside it messes up the navigation bar title which will just be positioned stuck at the screen instead of moving in the navigation bar.
Does anyone have a solution for this issue?
(Xcode 13.4.1)
Minimal reproducible code:
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var numbers = Array(1...100).map { String($0) }
var body: some View {
NavigationView {
List($numbers, id: \.self) { $number in
TextField("", text: $number)
}
.toolbar {
ToolbarItem(placement: .keyboard) {
ScrollView(.horizontal) {
HStack {
Text("Hello")
Text("World")
}
}
}
}
.navigationTitle("Messed up title")
}
}
}
It seems like you are trying to add 100 toolbar item elements inside your keyboard which is causing performance issue and impacting on your navigation bar which could be issue in lower Xcode version compatibility. if you want to show 100 toolbar item elements then instead of adding inside keyboard add separate View and add on top of it and then based on keyboard appear or disappear hide/show 100 elements view accordingly. So I modify your code which is adding two toolbar items elements inside your keyboard and that seems to be working fine without any navigation title stuck issue eg below:-
var body: some View {
NavigationView {
List($numbers, id: \.self) { $number in
TextField("", text: $number)
}.toolbar {
ToolbarItem(placement: .keyboard) {
HStack {
Button("Cancel") {
print("Pressed")
}
Spacer()
Button("Done") {
print("Pressed")
}
}
}
}.navigationTitle("Messed up title")
}
}
Edited Answer if you want to use ScrollView, instead of using List use ScrollView like below
Please note this changes are required only if you are using lower Xcode version prior than Xcode 14
var body: some View {
NavigationView {
ScrollView {
ForEach($numbers, id: \.self) { number in
VStack {
TextField("", text: number)
}
}
}.toolbar {
ToolbarItem(placement: .keyboard) {
ScrollView(.horizontal) {
HStack {
Text("Hello")
Text("World")
}
}
}
}.navigationTitle("Messed up title")
}
}

SwiftUI disappear back button with navigationLink

I have 3 views. One of these have NavigationView second have NavigationLink and last just a child with toolbar.
So my problem when I added toolbar in last view backButton elegant disappear. How can I solve this?
Screen recording of my problem
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
NavigationLink(destination: ListView()) {
Image(systemName: "trash")
.font(.largeTitle)
.foregroundColor(.red)
}
}.navigationBarHidden(true)
.navigationTitle("Image")
}
}
}
import SwiftUI
struct ListView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
List {
NavigationLink(destination: DetailView()) {
Text("Detail")
}
}
}.navigationBarTitle(Text("Data"), displayMode: .large)
.toolbar {
Button("Save") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
import SwiftUI
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Text("DetailView")
.padding()
}.navigationBarTitle(Text("Data"), displayMode: .large)
.toolbar {
Button("Save") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
In the console, you'll notice this message:
2021-04-27 12:37:36.862733-0700 MyApp[12739:255441] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
The default style for NavigationView is usually DefaultNavigationViewStyle, which is really just DoubleColumnNavigationViewStyle. Use StackNavigationViewStyle instead, and it works as expected.
Edit: You are right that StackNavigationViewStyle will break iPad split view. But thankfully, DoubleColumnNavigationViewStyle works fine in iPad and doesn't hide the back button. We can then just use a different NavigationStyle depending on the device, as shown in this answer.
struct ResponsiveNavigationStyle: ViewModifier {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#ViewBuilder
func body(content: Content) -> some View {
if horizontalSizeClass == .compact { /// iPhone
content.navigationViewStyle(StackNavigationViewStyle())
} else { /// iPad or larger iPhone in landscape
content.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
NavigationLink(destination: ListView()) {
Image(systemName: "trash")
.font(.largeTitle)
.foregroundColor(.red)
}
}
.navigationBarHidden(true)
.navigationTitle("Image")
}
.modifier(ResponsiveNavigationStyle()) /// here!
}
}
Result:
iPad
iPhone
I don't know why, but it's what worked for me:
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button { } label: { } // button to the right
}
ToolbarItem(placement: .navigationBarLeading) {
Text("") // empty text in left to prevent back button to disappear
}
}
I already tried to replace the empty text with EmptyView() but the button keeps disappearing.
FYI: I have this problem only on my device with iOS 14, but in another device with iOS 15 the back button never disappears.

NavigationTitle visual glitches - transparent and not changing state from .large to .inline on scroll

The .navigationTitle on some views seem to be having some problems. On some views (and only some of the time), the .navigationTitle will not change from .large to .inline as would be expected. Instead, the title stays in place when scrolling up, and the navigation bar is completely invisible (as outlined in the video below). This is all reproducible every time.
Video of reproducible .navigationTitle bugs
I haven't found any people on stack overflow or the Apple Developer forums who have run into this exact issue. There have some people who have produced similar results as this, but those were all fixed by removing some stylizing code to the .navigationbar, of which I am not making any modifications to it anywhere in my code.
Below are some snippets of my code:
import SwiftUI
struct WelcomeUI: View {
var body: some View {
NavigationView {
VStack {
//NavigationLink(destination: SignupUI(), label: {
//Text("Sign Up")
//}
NavigationLink(destination: LoginUI(), label: {
Text("Log In")
})
}
}
}
}
struct LoginUI: View {
var body: some View {
VStack {
NavigationLink(destination: MainUI(), label: { Text("Log In") })
//Button(action: { ... }
}
.navigationBarHidden(false)
}
}
struct MainUI: View {
#State var selectedTab: Views = .add
var body: some View {
TabView(selection: $selectedTab) {
SpendingView()
.tabItem {
Image(systemName: "bag.circle")
Text("Spending")
}.tag(Views.spending)
Text("Adding View")
.tabItem {
Image(systemName: "plus")
Text("Add")
}.tag(Views.add)
Text("Edit View")
.tabItem {
Image(systemName: "pencil")
Text("Edit")
}.tag(Views.edit)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}.tag(Views.settings)
}
.navigationBarTitle(Text(selectedTab.rawValue))
.navigationBarBackButtonHidden(true)
}
}
enum Views: String {
case spending = "Spending"
case add = "Add"
case edit = "Edit"
case settings = "Settings"
}
struct SettingsView: View {
var body: some View {
VStack{
ZStack {
Form {
Section(header: Text("Section Header")) {
NavigationLink(destination: WelcomeUI()) {
Text("Setting Option")
}
}
Section {
//Button("Log Out") {
//self.logout()
//}
Text("Log Out")
}
}
Button("say-high", action: {print("Hi")})
}
}
}
}
struct SpendingView: View {
var body: some View {
ScrollView{
Text("SpendingView")
NavigationLink("subSpending", destination: SubSpendingView())
}.padding()
}
}
struct SubSpendingView: View {
var body: some View {
ScrollView{
Text("SubSpendingView")
}.navigationBarTitle("SubSpending")
}
}
It almost seems like a bug in SwiftUI itself just because the fact that bringing down the control centre makes it kind of work, but with no animation (as seen in the video). Also, changing which view is selected first in #State var selectedTab: Views seems to let the view selected to work as expected, but lets the rest of the tabs mess up.
When I build and run the app on my iPad, it behaves as expected with no bugs, it's only when run on my iPhone and the iOS simulator on Mac that it does this, any way to fix this?
For this to work flawlessly the ScrollView needs to be the direct child of the NavigationView. I ran into a similar issue with wanting to dismiss the TabView when I navigating but SwiftUI won't let that happen. Each tab needs to be a NavigationView and you need to dismiss the TabView creatively if that is what you want.
TabView {
NavigationView {
ScrollView {
// your view here
}
}.tabItem {
// tab label
}
// etc
}
Essentially the navigation view needs to be a child (in the brackets) of the tab view and the scrollview needs to be the direct child of the navigation view.
Use navigationBarTitle("Title") and navigationBarBackButtonHidden(true) on the TabView's sub-view, not on itself.
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
}
.navigationBarTitle("Title")
.navigationBarBackButtonHidden(true)
}
}
}

SwiftUI modal presentation works only once from navigationBarItems

Here is a bug in SwiftUI when you show modal from button inside navigation bar items.
In code below Button 1 works as expected, but Button 2 works only once:
struct DetailView: View {
#Binding var isPresented: Bool
#Environment (\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Text("OK")
.navigationBarTitle("Details")
.navigationBarItems(trailing: Button(action: {
self.isPresented = false
// or:
// self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done").bold()
})
}
}
}
struct ContentView: View {
#State var showSheetView = false
var body: some View {
NavigationView {
Group {
Text("Master")
Button(action: { self.showSheetView.toggle() }) {
Text("Button 1")
}
}
.navigationBarTitle("Main")
.navigationBarItems(trailing: Button(action: {
self.showSheetView.toggle()
}) {
Text("Button 2").bold()
})
}.sheet(isPresented: $showSheetView) {
DetailView(isPresented: self.$showSheetView)
}
}
}
This bug is from the middle of the last year, and it still in Xcode 11.3.1 + iOS 13.3 Simulator and iOS 13.3.1 iPhone XS.
Is here any workaround to make button work?
EDIT:
Seems to be tap area goes somewhere down and it's possible to tap below button to show modal.
Temporary solution to this is to use inline navigation bar mode:
.navigationBarTitle("Main", displayMode: .inline)
Well, the issue is in bad layout (seems broken constrains) of navigation bar button after sheet has closed
It is clearly visible in view hierarchy debug:
Here is a fix (workaround of course, but safe, because even after issue be fixed it will continue working). The idea is not to fight with broken layout but just create another button, so layout engine itself remove old-bad button and add new one refreshing layout. The instrument for this is pretty known - use .id()
So modified code:
struct ContentView: View {
#State var showSheetView = false
#State private var navigationButtonID = UUID()
var body: some View {
NavigationView {
Group {
Text("Master")
Button(action: { self.showSheetView.toggle() }) {
Text("Button 1")
}
}
.navigationBarTitle("Main")
.navigationBarItems(trailing: Button(action: {
self.showSheetView.toggle()
}) {
Text("Button 2").bold() // recommend .padding(.vertical) here
}
.id(self.navigationButtonID)) // force new instance creation
}
.sheet(isPresented: $showSheetView) {
DetailView(isPresented: self.$showSheetView)
.onDisappear {
// update button id after sheet got closed
self.navigationButtonID = UUID()
}
}
}
}

Resources