SwiftUI navigation bar items frame are misaligned after sheet dismiss - ios

Navigation bar buttons are not tappable after dismissing a sheet in SwiftUI. Below is the steps to reproduce the issue
Present a sheet,
Move the app to background for a short duration (2 seconds)
Resume the app & dismiss the sheet by swiping down
Now the navigation bar button frames are misaligned. Tap is working at different frame than visible frame of the button. This is easily reproducible on iOS 16 simulator, but intermittently on actual iOS devices. Below is the minimal code to reproduce the issue
struct ContentView: View {
#State private var showSheetView = false
var body: some View {
NavigationView {
VStack {
navigationBarView
Color.blue
}
.sheet(isPresented: $showSheetView) {
FilterView()
}
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
private var navigationBarView: some View {
HStack(spacing: 0) {
Spacer()
Button {
showSheetView = true
} label: {
Text("Filter")
.padding()
.background(Color.red)
}
}
}
}
struct FilterView: View {
var body: some View {
Color.green
}
}

I've also struggled a lot with this issue, and it's clearly a bug from apple.
I found an ugly hack to workaround this issue. But to make it work I have to redraw my main view every time I enter background. For my project it's ok until they fix an actual bug. I hope it will help you as well
When you detect app is moving to background, you force app to redraw based on UUID()
struct BackgroundModeSheetBugApp: App {
#State private var id = UUID()
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
id = UUID()
}
.id(id)
}
}
}
I've tested it with your code and it's working correctly on both simulator and device
P.S. I also took courage to use your example to submit a bug to Apple
P.P.S. For my app I've also had to wrap my main view to hack UIViewControllerRepresentable to fix the padding issues.
struct UIHackMainTabView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIHostingController<MainTabView> {
let mainTab = MainTabView()
return UIHostingController(rootView: mainTab)
}
func updateUIViewController(_ pageViewController: UIHostingController<MainTabView>, context: Context) {
}
}

Another workaround for now is to make use of presentationDetents()
It's break UI a little, since we're loosing nice shrink effect and it works only on iOS 16, but at least app will work there :(
struct SheetHackModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.presentationDetents([.fraction(0.99)])
} else {
content
}
}
}
extension View {
func sheetHackModifier() -> some View {
self.modifier(SheetHackModifier())
}
}

Related

SwiftUI - Intermittent crash when presenting sheet on a sheet

I have a crash happening unpredictably when I'm presenting a sheet over a view that is already in a sheet. I have been able to slowly strip out parts of my view and custom types until I've got the very simple and generic view structure below that still exhibits the crash.
The crash happens when I interact with the TextField then interact with one of the buttons that shows the sub-sheet. It sometimes takes a lot of tapping around between the buttons and the text field to trigger the crash, and sometimes it happens right away (as in the GIF below). Sometimes I can't get the crash to happen at all, but my users keep reporting it.
In the gif below the crash occurs the minute the bottom button is pressed. You can see the button never comes out of its "pressed" state and the sheet never appears.
Xcode doesn't give any helpful info about the crash (screenshots included below).
I've only gotten it to happen on an iPhone XR running 13.4.1 and Xcode 11.4.1. I have tried on an iPhone 6s and several simulators and can't trigger the crash, but users have reported it on several devices.
struct ContentView: View {
#State var showingSheetOne: Bool = false
var body: some View {
Button(action: { self.showingSheetOne = true }) {
Text("Show")
}
.sheet(isPresented: $showingSheetOne) {
SheetOne(showingSheetOne: self.$showingSheetOne)
}
}
}
struct SheetOne: View {
#Binding var showingSheetOne: Bool
#State var text = ""
var body: some View {
VStack {
SheetTwoButton()
SheetTwoButton()
SheetTwoButton()
TextField("Text", text: self.$text)
}
}
}
struct SheetTwo: View {
#Binding var showing: Bool
var body: some View {
Button(action: {
self.showing = false
}) {
Text("Hide")
.frame(width: 300, height: 100)
.foregroundColor(.white)
.background(Color.blue)
}
}
}
struct SheetTwoButton: View {
#State private var showSheetTwo: Bool = false
var body: some View {
Button(action: { self.showSheetTwo = true } ) {
Image(systemName: "plus.circle.fill")
.font(.headline)
}.sheet(isPresented: self.$showSheetTwo) {
SheetTwo(showing: self.$showSheetTwo)
}
}
}
I ran into a similar problem a few weeks ago. Turns out that when I presented the new sheet with the keyboard open it would lead to a crash.
I found using UIApplication.shared.endEditing() before showing the second sheet would solve the problem
UPDATE
For iOS 14 I’ve created an extension because the above function is no longer available
extension UIApplication {
static func endEditing() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
}
The usage is similar UIApplication.endEditing()
I'm using this in Swift 5.7, iOS 16.0:
#if !os(watchOS)
import UIKit
func dismissKeyboard() {
// Dismiss text editing context menu
UIMenuController.shared.hideMenu()
// End any text editing - dismisses keyboard.
UIApplication.shared.endEditing()
}
#endif
However, since iOS 16.0 there's now a warning:
'UIMenuController' was deprecated in iOS 16.0:
UIMenuController is deprecated. Use UIEditMenuInteraction instead.
I haven't yet figured out how to use UIEditMenuInteraction to do the same.

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.

SwiftUI - How to close the sheet view, while dismissing that view

I want to achieve the function. Like, "Look up" view that is from Apple.
My aim is when the sheet view push another view by navigation, the user can tap the navigation item button to close the sheet view. Like, this below gif.
I try to achieve this function.
I found a problem that is when the user tap the "Done" button. The App doesn't close the sheet view. It only pop the view to parent view. Like, this below gif.
This is my code.
import SwiftUI
struct ContentView: View {
#State var isShowSheet = false
var body: some View {
Button(action: {
self.isShowSheet.toggle()
}) {
Text("Tap to show the sheet")
}.sheet(isPresented: $isShowSheet) {
NavView()
}
}
}
struct NavView: View {
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView()) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("Done")
}
)
}
}
How did I achieve this function? :)
Please help me, thank you. :)
UPDATE: Restored original version - provided below changes should be done, as intended, to the topic starter's code. Tested as worked with Xcode 13 / iOS 15
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 11.2 / iOS 13.2)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var showingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
}.sheet(isPresented: $isShowSheet) {
NavView()
.environment(\.showingSheet, self.$isShowSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.showingSheet) var showingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
self.showingSheet?.wrappedValue = false
}
)
}
}
I haven't tried SwiftUI ever, but I came from UIKit + RxSwift, so I kinda know how binding works. I read quite a bit of sample codes from a SwiftUI Tutorial, and the way you dismiss a modal is actually correct, but apparently not for a navigation stack.
One way I learned just now is use a #Binding var. This might not be the best solution, but it worked!
So you have this $isShowSheet in your ContentView. Pass that object to your NavView struct by declaring a variable in that NavView.
ContentView
.....
}.sheet(isPresented: $isShowSheet) {
NavView(isShowSheet: self.$isShowSheet)
}
NavView
struct NavView: View {
#Binding var isShowSheet: Bool
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView(isShowSheet: self.$isShowSheet)) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
and finally, do the same thing to your subView.
NavSubView
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isShowSheet: Bool
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
//self.presentationMode.projectedValue.wrappedValue.dismiss()
self.isShowSheet = false
}){
Text("Done")
}
)
}
}
Now as you can see, you just need to send a new signal to that isShowSheet binding var - false.
self.isShowSheet = false
Voila!
Here's an improved version of Asperi's code from above since they won't accept my edit. Main credit goes to them.
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 13 / iOS 15)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var isShowingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
#State var isShowingSheet = false
...
Button("open sheet") {
isShowingSheet?.wrappedValue = true
}
// note no $ in front of isShowingSheet
.sheet(isPresented: isShowingSheet ?? .constant(false)) {
NavView()
.environment(\.isShowingSheet, self.$isShowingSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.isShowingSheet) var isShowingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
isShowingSheet?.wrappedValue = false
}
)
}
}

SwiftUI: attach an animation to a transition

According to the Apple documentation we should be able to attach an animation directly to a transition. For example:
.transition(AnyTransition.slide.animation(.linear))
documentation for the method:
extension AnyTransition {
/// Attach an animation to this transition.
public func animation(_ animation: Animation?) -> AnyTransition
}
says:
Summary
Attaches an animation to this transition.
But I can't manage to make it work. Take a look at this minimum viable example (you can copy-paste it and try yourself):
import SwiftUI
struct AnimationTest: View {
#State private var show = false
var body: some View {
VStack {
if show {
Color.green
.transition(AnyTransition.slide.animation(.linear))
} else {
Color.yellow
.transition(AnyTransition.slide.animation(.linear))
}
Button(action: {
self.show.toggle()
}, label: {
Text("CHANGE VIEW!")
})
}
}
}
struct AnimationTest_Previews: PreviewProvider {
static var previews: some View {
AnimationTest()
}
}
As you can see no animation happens at all. Any ideas? Thank you.
You need to wrap the boolean toggling within a withAnimation() closure:
withAnimation {
self.show.toggle()
}
Tested the transition working on Xcode 11.2.1 while on the simulator; the Canvas doesn't preview it.
Please note that animations/transitions applied directly to a view have an effect on that particular view and its children. Moreover, according to the docs:
func animation(Animation?) -> View
Applies the given animation to all animatable values within the view.
Since the animatable value, in this case the Bool toggle enabling the transition, is external to the Color views, it must be animated explicitly from where it's set in the button's action. Alternatively, one can effectively attach the transition directly to the target views, but apply the animation to their container, thus enabling interpolation of changes to show. So, this also achieves the desired result:
struct AnimationTest: View {
#State private var show = false
var body: some View {
VStack {
if show {
Color.green
.transition(.slide)
} else {
Color.yellow
.transition(.slide)
}
Button(action: {
self.show.toggle()
}, label: {
Text("CHANGE VIEW!")
})
}
.animation(.linear)
}
}

SwiftUI Navigation and status bar clash in color/transparency when inside TabView

My app consists of a few views across a few different tabs inside a TabView. These views create their own NavigationViews. Unfortunately the existence of the TabView is causing their colors and transparency to clash with the app's status bar, which is no longer in line with the navigation bar.
This is easily reproduced in a sample app using the following code.
struct ContentView: View {
var body: some View {
TabView {
NavView()
}
}
}
struct NavView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<10, id: \.self) { _ in
Section(header: Text("Foo")) {
Text("Bar")
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Foobar")
}
}
}
I'm using the grouped list style to make the styling changes more apparent, but it's the same with the default style.
Is there a SwiftUI API to access the status bar styling? Or possibly some other workaround?
As per Apple's documentation, edgesIgnoringSafeArea(_:) should be applied to TabView:
https://developer.apple.com/documentation/swiftui/vsplitview/3288813-edgesignoringsafearea
Extends the view out of the safe area on the specified edges.
struct ContentView: View {
var body: some View {
TabView {
NavView()
} // .edgesIgnoringSafeArea(.top) no longer necessary as of iOS 13.4
}
}
NOTE: It appears Apple changed the default behavior and this is no longer necessary with iOS 13.4.
For some reason, it stopped working properly for me. The situation was it was working without edgesIgnoringSafeArea(_:) applied just for the simulator but not a device. And vice versa when it is applied to it.
Created a little modifier to fix it.
struct EdgeIgnoringSafeAreaModifier: ViewModifier {
var edges: Edge.Set
func body(content: Content) -> some View {
#if targetEnvironment(simulator)
return content
#else
return content.edgesIgnoringSafeArea(self.edges)
#endif
}
}
extension View {
func edgeIgnoringSafeAreaForDevice(_ edges: Edge.Set) -> some View {
self.modifier(EdgeIgnoringSafeAreaModifier(edges: edges))
}
}

Resources