ColorPicker on sheet taps outside - ipad

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!

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

SwiftUI DatePicker breaks sheet dismiss?

Scenario:
RootScreen presents DateScreen modally though .sheet
DateScreen has a DatePicker with CompactDatePickerStyle() and a button to dismiss the modal
User opens the DatePicker
User taps the DatePicker to bring up the NumPad for manual keyboard input
User presses the button to dismiss the modal
SwiftUI will think the .sheet got dismissed, but in reality, only the DatePicker's modal got dismissed.
Minimum code example:
struct DateScreen: View {
#Binding var isPresented: Bool
#State var date: Date = Date()
var body: some View {
NavigationView {
VStack {
DatePicker("", selection: $date, displayedComponents: [.hourAndMinute])
.datePickerStyle(CompactDatePickerStyle())
}
.navigationBarItems(leading: Button("Dismiss") {
isPresented = false
})
}
}
}
#main
struct Main: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#State var isPresenting: Bool = false
var body: some Scene {
WindowGroup {
Button("Present modal", action: {
isPresenting = true
})
.sheet(isPresented: $isPresenting, content: {
DateScreen(isPresented: $isPresenting)
})
}
}
}
Gif showing the broken behavior:
Note, if the user doesn't open the NumPad, it seems to work well.
The only workaround I found is to ignore SwiftUI and go back to UIKit to do the dismissal.
Instead of isPresented = false I have to do UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true).
For iOS 15 this works to dismiss the sheet and doesn't generate the warning:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead
code:
UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
.first?
.windows
.first { $0.isKeyWindow }?
.rootViewController?
.dismiss(animated: true)
This is problem of provided code - the State is in Scene instead of view - state is not designed to update scene. The correct SwiftUI solution is to move everything from scene to a view and have only one root view there, ie.
Tested with Xcode 13.4 / iOS 15.5
#main
struct Main: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView() // << window root view, the one !!
}
}
}
struct ContentView: View {
#State var isPresenting: Bool = false
var body: some View {
Button("Present modal", action: {
isPresenting = true
})
.sheet(isPresented: $isPresenting, content: {
DateScreen(isPresented: $isPresenting)
})
}
}
// no more changes needed

SWIFTUI: Can't dismiss Sheet after changing ScreenSize classes

I toggle a sheet in SwiftUI with the following Button
Button(action: {
self.statusPopoverIsShown.toggle()
})
So the following sheet appears
.sheet(isPresented: self.$popoverIsShown) {
RandomSheet(popoverIsShown: self.$popoverIsShown)
}
Then I have a button inside the RandomSheetto dismiss the sheet (sets the popoverIsShown to false). Everything works fine.
But when I start using the app in splitscreen or somehow change the sizeclass SwiftUI transforms the sheet to a fullscreen iPhone-like sheet and the dismiss button/the binding does not work anymore.
Is there any solution to avoid this and keep the binding stable?
The following works with any size class changes. Tested with Xcode 12 / iOS 14
struct TestSheet: View {
#State private var popoverIsShown = false
var body: some View {
Button("Show Sheet") {
self.popoverIsShown = true
}
.sheet(isPresented: self.$popoverIsShown) {
RandomSheet(popoverIsShown: self.$popoverIsShown)
}
}
}
struct RandomSheet: View {
#Binding var popoverIsShown: Bool
var body: some View {
Button("Close") { self.popoverIsShown = false }
}
}

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 Modal Sheet Re-Opening After Dismiss

I have a list in a Navigation View, with a trailing navigation button to add a list item. The button opens a modal sheet. When I dismiss the sheet (by pulling it down), the sheet pops right back up again automatically and I can't get back to the first screen. Here's my code.
struct ListView: View {
#ObservedObject var listVM: ListViewModel
#State var showNewItemView: Bool = false
init() {
self.listVM = ListViewModel()
}
var body: some View {
NavigationView {
List {
ForEach(listVM.items, id: \.dateCreated) { item in
HStack {
Text(item.name)
Spacer()
Image(systemName: "arrow.right")
}
}
}
.navigationBarTitle("List Name")
.navigationBarItems(trailing: AddNewItemBtn(isOn: $showNewItemView))
}
}
}
struct AddNewItemBtn: View {
#Binding var isOn: Bool
var body: some View {
Button(
action: { self.isOn.toggle() },
label: { Image(systemName: "plus.app") })
.sheet(
isPresented: self.$isOn,
content: { NewItemView() })
}
}
I am getting this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c603b7c22SheetHostingControllerVS_7AnyView_: 0x7fc5e0c1f8f0> on which is already presenting (null)
I've tried toggling the bool within "onDismiss" on the button itself, but that doesn't work either. Any ideas?
Turns out putting the button in the navigationBarItems(trailing:) modifier is the problem. I just put the button in the list itself instead of in the nav bar and it works perfectly fine. Must be some kind of bug.

Resources