Dismissing Presented View Crops while keyboard is active - ios

I am trying to present a view as bottom sheet but it is behaving weirdly while closing the view using drag down. Whenever the keyboard is active it crops the view while dragging down but when keyboard is not active it behaves perfectly. I want to stop this cropping view when dropping down. You can more under stand in the GIFs.
When keyboard is not active [This what I want achieve when keyboard is active]:
When keyboard is active [Focus on edges of sheet] :
I have tried changing method of presenting but using SwiftUIX and iOS 16 sheet modifier. But I have not found the cause of this. And I am not getting any idea why this is happening and yes this behaviour only reproduces in iOS 16.
struct ContentView: View {
#State var presented: Bool = false
var body: some View {
Button("Show",action: {
presented.toggle()
})
.ignoresSafeArea()
.sheet(isPresented: $presented) {
view2
}
}
private var view2: some View {
VStack(spacing: 0) {
TextField(text: .constant("123"))
.frame(height: 70)
.background(.gray)
.padding()
TextField(text: .constant("456"))
.frame(height: 70)
.background(.gray)
.padding()
Spacer()
}
.ignoresSafeArea()
.background(.black)
}
}

I don't know why this issue is happening, but I have solved this issue by changing presenting approach.
First reason that making cropping issues is ignoring the safe area using any method will reproduce the same issue. So, you have to remove ignoreSafeArea() or edgesIgnoringSafeArea(). It will solve your problem but there's a chance you have to redesign your screen.
If it will still not work, try presenting the view using ViewController's present method by Creating an object of UIHostingController() by passing your view in it and presenting that UIHostingConrtoller() object.
AdaptToKeyboard() solution in the question comment works but not in every scenario. I had three points that had the same issue adaptsTokeyboard()solved the issue in two points but not o the third point.
Here's example of UIHostingController() approach
extension UIApplication {
public var firstKeyWindow: UIWindow? {
windows.first(where: { $0.isKeyWindow })
}
#available(macCatalystApplicationExtension, unavailable)
#available(iOSApplicationExtension, unavailable)
#available(tvOSApplicationExtension, unavailable)
public var topmostViewController: UIViewController? {
UIApplication.shared.firstKeyWindow?.rootViewController?.topmostViewController
}
func present<V: View>(_ view: V) {
previousTopmostViewController = UIApplication.shared.topmostViewController
let controller = UIHostingController(rootView: view)
previousTopmostViewController?.present(controller, animated: true)
}
}

Related

SwiftUI navigation bar items frame are misaligned after sheet dismiss

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())
}
}

Can't get removal of view to animate

I have a VStack where I have a title above some other views. The title is shown/hidden based on the value of an #Published variable from an environment object. Ideally, when the title should be hidden, I want it to transition by fading out and moving the views below it in the VStack up. However, what actually happens is it just disappears immediately and moves the rest of the views up without animation. How can I fix this?
struct MyView: View {
#EnvironmentObject var modelController: MyModelController
var body: some View {
VStack {
title()
//..other views here
}
}
#ViewBuilder
func title() -> some View {
if let currentPage = modelController.currentPage,
currentPage >= 6 {
EmptyView()
} else {
Text("Create Event")
}
}
}
You'll need to use the .transition() modifier to tell the system that you want to animate when the view appears or disappears. Additionally, I don't think you need to return EmptyView when you want to hide the title.
#ViewBuilder
func title() -> some View {
if modelController.currentPage ?? 0 < 6 {
Text("Create Event")
.transition(.opacity)
}
}
I've used the opacity transition but it's a very customizable modifier, and you can pass an asymmetric transition that does different animations on insert and removal. I would suggest googling it or looking at the documentation to learn more about it.
Your code snapshot is not testable, but try the following
VStack {
title()
//..other views here
}
.animation(.default, value: modelController.currentPage) // << here !!

Why does adding .navigationBarBackButtonHidden(true) to my ContentView breaking the core NavigationView animation to switch views? Is this a bug?

in my ContentView, I have something analogous to this:
import SwiftUI
struct MainContentView : View {
var body: some View {
Text("Main Content View")
}
}
struct AlternateView : View {
var body: some View {
Text("Alternate View")
}
}
struct ContentView : View {
#State private var selection: String? = "Main"
var body: some View {
ZStack {
NavigationView {
VStack {
ZStack {
Color.clear
AlternateView()
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.top)
//button to go back to the MainContentView()
//AlternateViewBackButton(button_action: self.hide_alternate_view)
}
NavigationLink(destination:
MainContentView()
//why cant I do this without breaking NavigationView?
//.navigationBarBackButtonHidden(true)
,
tag: "Main",
selection: $selection)
{
EmptyView()
}
.navigationBarHidden(true)
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
My question is in the code commented above. Why does adding navigationBarBackButtonHidden(true) break the functionality where I can drag from the left of the screen to the right to animate between the two views? Is this something I am doing wrong? or is it a SwiftUI bug?
I encountered a solution which I thought would fix this problem, namely: Hide navigation bar without losing swipe back gesture in SwiftUI It works on the test case I have written above, but fails on the full program unless I deactivate all of the gestures I have installed on MainContentView with .simultaneousGesture
This partial solution may be included over the original code example with the following snippet (copied from the linked post)
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
It seems to work on the test example, but when I use it in my main project, it interferes with the gestures I have installed on MainContentView. I will try to figure out exactly why this is happening and include what I find below.
-edit-
The way this problem manifests in my codebase is as follows:
If I include the extension code snippet above, uncomment .navigationBarBackButtonHidden(true), and comment all gestures on MainContentView (including every one of its children), There is no back button (as intended) and I can drag from left to right to access the AlternateView (as intended). However, commenting the gestures on MainContentView and children is not possible, as those govern the core functionality of the application.
If I knew why this was happening, I would not have a question anymore.
It seems to me that .navigationBarBackButtonHidden should not change anything other than the fact that the back button is hidden.
-end of edit-
I need to be able to handle gestures on both MainContentView and AlternateView. Handling user input is a critical part of the program, after all.
One correlated question is this: why does the above code hide the navigationBar in portrait mode, but show it in landscape?
Thanks in advance for any help!

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: Button in NavigationBar won't fire after modal dismissal

I'm running into some weird behavior, trying to get a simple modal to pop after it has been dismissed.
I have an Add button in the NavigationBar that pops a modal. The modal has a button that will dismiss it, which works. However, I cannot interact with the Add button in the NavigationBar again until I interact with something else on the screen, such as scrolling the List below.
I have also placed another Add button, just for kicks, in the List itself, which always works.
Here's the code for the main view:
import SwiftUI
struct ContentView: View {
#State var displayModal: Bool = false
var body: some View {
NavigationView {
List {
Text("Hello again.")
Button(action: { self.displayModal = true }) {
Text("Add")
}
}
.sheet(isPresented: $displayModal) {
Modal(isPresented: self.$displayModal)
}
.navigationBarTitle("The Title")
.navigationBarItems(trailing: Button(action: { self.displayModal = true }) {
Text("Add")
})
}
}
}
And the modal, for completeness:
import SwiftUI
struct Modal: View {
#Binding var isPresented: Bool
var body: some View {
VStack {
HStack {
Button(action: {
self.isPresented = false
}) {
Text("Cancel")
}
.padding()
Spacer()
}
Text("I am the modal")
Spacer()
}
}
}
The only thing I can think of is that something invisible is preventing me from working with the NavigationBar button. So I fired up the UI Debugger, and here's what the ContentView looks like. Note the NavigationBar button.
Now, after I tap the button and display the modal, and then use the UI Debugger to see the ContentView again, all the same elements are in place, but the Button parent views are offset a bit, like this:
Once I drag the List up and down, the UI Debugger shows a view hierarchy identical to the first image.
Does anyone have any idea what's going on here?
I'm using Xcode 11.2.1 and iOS 13 on an iPhone 11 Pro simulator, but have also observed this on my iPhone.
It is really a bug. The interesting thing is that after 'drag to dismiss' the issue is not observed, so it is a kind of 'sync/async' state changing or something.
Workaround (temporary of course, decreases visibility almost completely)
.navigationBarItems(trailing: Button(action: { self.displayModal = true }) {
Text("Add").padding([.leading, .vertical], 4)
})
I ran into the same issue, and for me the workaround was to use an inline-style navigation bar title on the presenter.
.navigationBarTitle(Text("The Title"), displayMode: .inline)
HOWEVER, if you use a custom accent color on your ContentView (like .accentColor(Color.green)), this workaround no longer works.
Edit: the bug seems fixed in 13.4, and no workarounds are needed anymore.

Resources