SwiftUI - Intermittent crash when presenting sheet on a sheet - ios

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.

Related

ColorPicker on sheet taps outside

The bounty expires in 6 days. Answers to this question are eligible for a +50 reputation bounty.
Jaime Bocio wants to draw more attention to this question.
I have a problem with SwiftUI and the ColorPicker, only for iPad. On iPhone and Mac it works fine.
When I use a ColorPicker inside a sheet, I open the picker to choose the color and I click (without closing the picker) the button to close the sheet, the button is clicked, but the dismiss() is done on the picker and not on the sheet.
The problem is that once the picker is closed, the dismiss stops working on the sheet and I can't close it with the button, but by clicking outside of it.
I attach the code of the example, simplified to the maximum. The behavior in my app is exactly the same.
#available(iOS 16.0, *)
struct Pruebas: View {
#State private var showSheet: Bool = false
var body: some View {
Button("Show") {
showSheet.toggle()
}
.sheet(isPresented: $showSheet) {
Detail()
}
}
}
#available(iOS 16.0, *)
struct Detail: View {
#Environment(\.dismiss) var dismiss
#State private var color: Color = .orange
var body: some View {
NavigationStack {
VStack {
ColorPicker("Color", selection: $color)
.padding()
}
.navigationTitle("Color")
.toolbar {
Button("Cancel") {
dismiss()
}
}
}
}
}
Thanks in advance!

Dismissing Presented View Crops while keyboard is active

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

SwiftUI List Visual Bug with Keyboard and Safe Area

I'm experiencing a visual bug when using a List and TextFields in SwiftUI. After focusing on a TextField in the List and then removing the focus (I've tried various methods of doing this, like Buttons in the List rows/keyboard toolbar etc.), there is a visual bug where the black view behind the keyboard dismisses, hangs for a second over the safe area, then suddenly disappears, causing the animation not to appear smooth. Also, if you scroll to the bottom of the List and perform the same steps, after the black view lingers for a second, it then suddenly disappears again but causes the List to 'snap' down a bit and again ruins the animation.
Here is a minimal demonstration of the issue:
struct ContentView: View {
#State var text: String = ""
#FocusState private var focused: Int?
var body: some View {
List {
ForEach(0..<50) { item in
HStack {
TextField("", text: $text)
.id(item)
.focused($focused, equals: item)
.background(.white)
Button {
focused = nil
} label: {
Text("Hide")
}
}
}
}
.scrollContentBackground(.hidden)
.background(.red)
}
}
One solution is using the List in a VStack with something below the List, but this is something I'd like to avoid with my UI. Also, I could use a ScrollView, but there is then a separate issue, where after dismissing the keyboard, the extra padding under the List produced by the keyboard avoidance stays there until you try and scroll the List again. I would also like to use the native swipe actions in my actual project. Finally, ignoring the safe area of the List/ScrollView works but then this disables keyboard avoidance, which is something that I'd like to keep.
Note: experienced on iOS 16.0 and 16.1
I am not 100% sure why this is happening, since I have not worked a lot with lists. But I guess it has something to do with that you are not explicitly telling your view to ignore the safe area. Adding .edgesIgnoringSafeArea(.bottom) to your list when leaving focus, and then changing it to trailing on entering focus seems to solve it.
struct ContentView: View {
#State var text: String = ""
#State var ignoreSafeArea = false
#FocusState private var focused: Int?
var body: some View {
List {
ForEach(0..<50) { item in
HStack {
TextField("", text: $text)
.id(item)
.focused($focused, equals: item)
.background(.white)
Button {
focused = nil
} label: {
Text("Hide")
}
}
}
.onChange(of: focused) { focused in
if focused == nil {
ignoreSafeArea = true
} else {
ignoreSafeArea = false
}
}
}.edgesIgnoringSafeArea(ignoreSafeArea ? .bottom : .trailing)
}
}

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

SwiftUI: `Toggle`s within dynamic `List` breaking their layout upon reuse?

I'm trying to show a dynamic List with rows containing Toggle elements. The Toggles are laid out correctly initially, but their layout breaks when scrolling them in and out of view (i. e. upon cell reuse).
Minimal example code:
import SwiftUI
struct SwitchList: View {
var body: some View {
List(0..<20) { _ in
SwitchRow(value: Bool.random())
}
}
}
struct SwitchRow: View {
#State var value: Bool
var body: some View {
Toggle(isOn: $value) {
Text("A switch row")
}
}
}
Screen recording demonstrating the issue:
(This is using iOS 13.2.2 (17B102) on the Simulator.)
Am I doing something wrong, or is this a bug? How do I get the Toggles to show correctly?
This is a bug/regression in iOS 13.2+
Working - iOS 13.1 (17A844) / Xcode 11.1 (11A1027)
Broken - iOS 13.2.2 (17B102) / Xcode 11.2.1 (11B500)
Broken - iOS 13.3 beta (17C5032d) / Xcode 11.3 beta (11C24b)
Submit feedback to Apple
Workaround
This bug only appears to affect the List initializers which take a data parameter. This code is functionally equivalent, but is not affected by the bug.
struct SwitchList: View {
var body: some View {
List {
ForEach(0..<20) { _ in
SwitchRow(value: Bool.random())
}
}
}
}
I was able to reproduce the problem, but could not find out why this happens. When I use a ScrollView() with a Divider() I don't have the Problem anymore. Here is the code:
struct SwitchList: View {
var body: some View {
ScrollView {
ForEach(1...50, id: \.self) { item in
VStack {
SwitchRow(value: Bool.random())
Divider()
}
}
}
}
}
struct SwitchRow: View {
#State var value: Bool
var body: some View {
Toggle(isOn: $value) {
Text("A switch row")
}
}
}
I hope this helps!

Resources