How to change DragGesture of sheet for a specific area? - swiftui

If I have a button that opens a sheet:
Button(action: { show = true }, label: { Text("Open" })
.sheet(isPresented: $show){
Text("Sheet view")
}
the complete sheet is "movable"/has a DragGesture. Is there any way to deactivate this or define the area of this DragGesture? I would like to have it at the upper 10% of the sheet.

The possible solution is to use background of sheet (of needed area) with blocked DragGesture.
Here is a demo of approach. Tested with Xcode 12.4 / iOS 14.4
struct TestSheetDraggableArea: View {
#State private var show = false
var body: some View {
Button(action: { show = true }, label: { Text("Open") })
.sheet(isPresented: $show){
SheetView()
}
}
}
struct SheetView: View {
var body: some View {
GeometryReader { gp in
VStack(spacing: 0) {
Color.red.overlay(Text("Draggable Area"))
.frame(height: gp.size.height * 0.1)
Color.clear.overlay(
Text("Sheet view") // << content here
)
.contentShape(Rectangle()) // makes hit testable
.gesture(DragGesture()) // << blocks sheet dragging !!!
}
}.ignoresSafeArea()
}
}

Related

SwiftUI: Touches not working after returning from background

Got a strange bug/error. Touches stops working at the top after closing and open the app.
To reproduce:
Click the blue bar to trigger "onTapGesture"
Swipe up to go back to springboard
Open the app
Drag down to close the modal
Click the blue bar (Will not work)
Interesting, if I remove the "Color.red.ignoresSafeArea()" It works as expected. In iOS 15, it also works as expected.
Is this a bug in SwiftUI?
Any suggestion for a workaround?
public struct TestView: View {
#State private var showModal = false
public var body: some View {
ZStack {
Color.red.ignoresSafeArea()
VStack(spacing: 0) {
Color.blue
.frame(height: 20)
.onTapGesture {
showModal = true
}
Color.white
}
}
.sheet(isPresented: $showModal, content: {
Text("HELLO")
})
}
}
I see the same happening on iPhone 14 Pro, iOS 16.2, Xcode 14.2
A workaround could be to dismiss the sheet when the app goes into the background:
struct TestView: View {
#State private var showModal = false
#Environment(\.scenePhase) var scenePhase
public var body: some View {
ZStack {
Color.red.ignoresSafeArea()
VStack(spacing: 0) {
Color.blue
.frame(height: 20)
.onTapGesture {
showModal = true
}
Color.white
}
}
.sheet(isPresented: $showModal, content: {
Text("HELLO")
})
.onChange(of: scenePhase) { scenePhase in
if scenePhase == .background {
showModal = false
}
}
}
}

Animate SwiftUI View when .sheet is shown

I have a SheetAnimationView from which I want to show a sheet called SheetContentView.
When the sheet appears, I want to show a transition animation of its content (start the animation when the content appears) but am unable to make it work. All 3 views in VStack should ideally end their animation at the same time.
SheetContentView:
struct SheetContentView: View {
#Binding var showSheet: Bool
var body: some View {
VStack(spacing: 8) {
Text("A great content of my new sheet")
Label("still not done", systemImage: "guitars")
Text("I'm done now")
}
.transition(.asymmetric(insertion: .scale, removal: .opacity)) // <--- I want this to work
.animation(Animation.easeInOut(duration: 2)) // <--- for 2 seconds
}
}
SheetAnimationView:
struct SheetAnimationView: View {
#State var showSheet: Bool = false
var body: some View {
Button("show my sheet with animated content (hopefully)") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
SheetContentView(showSheet: $showSheet)
}
}
}
Transition works when view is appeared in view hierarchy (not on screen), so to solve this we need another container and state.
Here is a fixed variant. Tested with Xcode 12.1 / iOS 14.1
struct SheetContentView: View {
#Binding var showSheet: Bool
#State private var isShown = false
var body: some View {
VStack { // container to animate transition !!
if isShown {
VStack(spacing: 8) {
Text("A great content of my new sheet")
Label("still not done", systemImage: "guitars")
Text("I'm done now")
}
.transition(.asymmetric(insertion: .scale, removal: .opacity))
}
}
.animation(Animation.easeInOut(duration: 2))
.onAppear {
isShown = true // << activate !!
}
}
}

Crash when using hiding a ProgressView in a Picker label

I am trying to put a ProgressView inside a Picker label. When I tap the Hide Spinner button, this (intermittently) crashes with EXC_BAD_ACCESS (code=EXC_I386_GPFLT).
struct ContentView: View {
#State private var selectedCity = ""
#State private var showSpinner = true
let cities = [
"Calgary",
"Edmonton",
"Toronto"
]
var body: some View {
NavigationView {
VStack(spacing: 0) {
Form {
Picker(selection: $selectedCity, label:
HStack {
Text("Your City")
if showSpinner {
ProgressView()
.padding(.horizontal, 2)
}
}
) {
ForEach(cities, id: \.self) { city in
Text(city).tag(city)
}
}
Button("Hide Spinner", action: { showSpinner = false })
}
}
.navigationBarTitle("ProgressView Crash", displayMode: .inline)
}
}
}
Am I doing anything wrong? I'm guessing this is a SwiftUI bug. I get the same behaviour when wrapping a UIActivityIndicatorView in a UIViewRepresentable.
Yes, it looks like a bug with auto-generated accessibility label. The safe workaround is to use explicitly provided accessibility.
Tested with Xcode 12 / iOS 14
Picker(selection: $selectedCity, label:
HStack {
Text("Your City")
if showSpinner {
ProgressView()
.padding(.horizontal, 2)
}
}.accessibility(label: Text("Your City")) // << here !!
) {
ForEach(cities, id: \.self) { city in
Text(city).tag(city)
}
}

SwiftUI Navigation Controller stuttering with two Navigationlinks per List row

I am trying to create two NavigationLinks in a repeating List. Each has a separate destination. The code all works fine until I imbed the call to the root view in a List/ForEach loop. At which point the navigation becomes very strange.
Try to click on either link and then click the back indicator at the top. It will go to one NavigationLink, and then the other. Sometimes in a different order, and sometimes it will auto-return from one of the links, and othertimes it won't open the second detail view until you return from the first detail view. It does this both in Preview, as well as if you build and run the application.
I have distilled down the code to the most basic below. If you comment the 2 lines as indicated in ContentView, you will then see correct behavior.
I am running Catalina 10.15.5, xCode 11.6, with the application target of IOS 13.6.
How can I modify the code, so that it will work with the List/ForEach loop?
import SwiftUI
struct DetailView1: View {
var body: some View {
HStack {
Text("Here is Detail View 1." )}
.foregroundColor(.green)
}
}
struct DetailView2: View {
var body: some View {
HStack {
Text( "Here is Detail View 2.") }
.foregroundColor(.red)
}
}
struct RootView: View {
var body: some View {
HStack {
VStack {
NavigationLink(destination: DetailView1())
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}.buttonStyle(PlainButtonStyle())
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
NavigationLink(destination: DetailView2())
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
// Comment the following line for correct behavior
List { ForEach(0..<3) {_ in
RootView()
// Comment the following line for correct behavior
} }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
.navigationBarTitle("Strange Behavior")
}
}
}
In your case both navigation links are activated at once user tap a row, to avoid this below is possible approach
Tested with Xcode 12 / iOS 14
The idea is to have one link which is activated programmatically and destination is selected dynamically depending on which button is clicked
struct RootView: View {
#State private var isFirst = false
#State private var isActive = false
var body: some View {
HStack {
VStack {
Button(action: {
self.isFirst = true
self.isActive = true
})
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
Button(action: {
self.isFirst = false
self.isActive = true
})
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
.background(
NavigationLink(destination: self.destination(), isActive: $isActive) { EmptyView() }
)
}
.buttonStyle(PlainButtonStyle())
}
#ViewBuilder
private func destination() -> some View {
if isFirst {
DetailView1()
} else {
DetailView2()
}
}
}

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}