So my goal is to disable touch events for everything on screen and I don't want to use .disable on every view or to use Color.black.opacity(0.00001) because of the code smell. I want it to be a block that isn't visible for the user like if I would overlay Color.clear over the whole view. And I want it to behave like if I were to use Color.black.opacity(0.1) with it disabling touch events on every view underneath.
If I for example use a ZStack with:
Color.black.opacity(0.2) every view underneath will no longer register touch events. (I want this, but it should be transparent)
Color.black.opacity(0) every view underneath will register touch events.
Color.black.opacity(0).contentShape(Rectangle()), some events will register, some won't, for example buttons won't work though scrolling in a ScrollView, or using a toggle will still work.
Here is some example code
struct ContentView: View {
#State var numberOfRows: Int = 10
var body: some View {
ZStack {
Color.white
ScrollView {
VStack {
ForEach(0..<numberOfRows, id: \.self) { (call: Int) in
Text(String(call))
.frame(maxWidth: .infinity)
}
}
}
Button(action: {
numberOfRows += 1
}) {
Color.blue
.frame(width: 300, height: 300)
}
Color.black.opacity(0) // <- change this to 0.x to disable all touch
.contentShape(Rectangle()) // <- Remove this line to make blue button work (opacity needs to be 0)
}
}
}
Why is scrollview still receiving touch events and why is buttons not?
Is there a way to make my touch events for every view underneath, disabled?
Use instead (tested with Xcode 13.4 / iOS 15.5)
Color.clear
// .ignoresSafeArea(.all) // << if need all screen space
.contentShape(Rectangle())
.highPriorityGesture(DragGesture(minimumDistance: 0))
Related
Automatic keyboard avoidance seems to work fine if it's a regular TextField (i.e. one that doesn't expand on an axis), whether or not it is contained in a ScrollView
Keyboard avoidance also seems to work with the new TextField(_:text:axis) introduced in iOS 16 if it's simply placed in a VStack without being wrapped in a ScrollView. It will even continue to avoid the keyboard correctly as the height expands with more text.
But I can't seem to get keyboard avoidance to work with TextField(_:text:axis) if it is placed inside a ScrollView
I can employ the hacky method of using a ScrollViewReader combined with DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) to wrap the proxy.scrollTo() when the TextField is focused. This sort of works when you first focus the field, but I can't seem to get the ScrollView to continue to adjust its position as the TextField expands.
Here is an example:
struct KeyboardAvoidingView: View {
#State var text = ""
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
Color.red
.frame(height: 400)
Color.blue
.frame(height: 400)
TextField("Name", text: $text, axis: .vertical)
.padding(.vertical)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) {
withAnimation(.default) {
proxy.scrollTo(0)
}
}
}
.onChange(of: text) { newValue in
proxy.scrollTo(0) // This doesn't seem to do anything
}
Spacer()
.frame(height: 0)
.id(0)
}
}
}
}
}
I guess I'm wondering whether this is expected behavior, or a bug. And regardless if it's one or the other, I'm wondering if I can have an auto-expanding text field inside a scroll view that I can make avoid the keyboard even as the height of the field expands?
UPDATE: It turns out, the issue was with placing the TextField inside a VStack instead of a LazyVStack. I assume ScrollView doesn't know what to do with just a regular VStack in certain situations. If I replace the VStack with a LazyVStack in my example, everything works as expected!
I answered the question with the update posted above. The issue was with using VStack instead of LazyVStack
This is a long time known bug in the TextField component, but you may achieve the desired behavior by using an anchor: .bottom in the proxy.scrollTo call of your onChange.
it'll look like this:
// ...
TextField("Name", text: $text, axis: .vertical)
.padding(.vertical)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) {
withAnimation(.default) {
proxy.scrollTo(0)
}
}
}
.onChange(of: text) { newValue in
// This will always scroll to the bottom of the text editor,
// just make sure to pass the right value in the first parameter
// that will identify your TextEditor
proxy.scrollTo(MyTextEditorId, anchor: .bottom)
}
// ...
You may need some additional work to handle the editing of upper parts of the text editor when it's taller than your screen
What I want
I have a "ScrollView" in a custom sheet that I can drag down to close. Of course, the ScrollView is above the drag area, so when I drag over the ScrollView it scrolls down or up. I want to disable the ScrollView when I am at the top of the ScrollView and scroll up so that the sheet is dragged and starts to close. This is similar to the behaviour of the Shazam sheet.
The Problem
If the ScrollView is deactivated, the current drag action is not applied to the sheet, but does nothing. Only when dragging again (on the now deactivated ScrollView) the sheet is focused on the dragging. So is there a way to transfer the focus of the drag action from the ScrollView to the outer sheet view without starting a new one?
Showcase
I created a simplified version of the problem for better understanding.
We have a container (inner) that is draggable along the y-axis. On drag release, we let it jump back to offset 0.
#State var offsetY: CGFloat = .zero
var body: some View {
Inner(outerOffset: $offsetY)
.offset(y: offsetY)
.gesture(DragGesture().onChanged { value in
offsetY = value.translation.height
}.onEnded { _ in
offsetY = 0
}
)
}
Inside the container we have our own ScrollView (ScrollViewOffset) which takes a callback with the current scroll offset. If the offset is positive (i.e. if we are scrolling upwards, even if we are at the top of the content), we want to disable the scroll and let the outer container drag along the y-axis instead of the ScrollView. To activate the ScrollView we listen for the outerOffset (the drag value) and activate the ScrollView when the offset is 0 again (this happens when releasing the drag as described before).
struct Inner: View {
#Binding var outerOffset: CGFloat
#State var disableScroll = false
var body: some View {
ScrollViewReader { proxy in
ScrollViewOffset {
ForEach(0 ... 10, id: \.self) { e in
Text(String(e))
.id(e)
.padding()
Divider()
}
} onOffsetChange: { offset in
if offset > 0 {
disableScroll = true
}
}
.background(.red)
.padding()
.frame(width: nil, height: 400)
.onChange(of: outerOffset) { _ in
if outerOffset == 0 {
proxy.scrollTo(0)
disableScroll = false
}
}
.disabled(disableScroll)
}
}
}
When we run this, it is easy to see the problem I mentioned before. The ScrollView is getting disabled as it should, but the container is not focused on the drag. Only after starting a new drag, the container is focused.
My actual case
Last but not least the shazam equivalent (left) and my project (right).
How can I have an overlaying view in SwiftUI with a different transition on different portions? E.g. in my concrete example, I want to achieve the following:
one subview of the overlay view slides in from the bottom
another subview of the overlay view has no transition at all (appears instantly)
the background of the overlay does not slide in from the bottom but fades in
I've already tried all sorts of things and it seems, that I have to set animation and transition on the view that wraps the if showOverlay condition. However, it looks like all the .transition() and .animation() modifiers set on the sub-views of the overlay are ignored then.
Is it actually possible?
The following code is what I thought should somewhat work but apparently doesn't at all:
// any observable external variable, that enables / disables the overlay
#State showOverlay: bool = false
// surrounding view
VStack { /* .. */ }
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(
Group {
if showOverlay {
Overlay()
}
}
)
// overlay view
struct Overlay: View {
var body: some View {
VStack {
// style 1
Text("OverlayContent that slides in from bottom")
.transition(.move(edge: .bottom).combined(with: .opacity))
.animation(.easeInOut(duration: 0.28))
// style 2
Text("OverlayContent that appears instant")
.animation(nil)
}
.background( // style 3
Color.black.opacity(0.2)
.ignoresSafeArea()
.transition(.opacity) // <-- background should not slide up but fade in
)
}
}
I am developing an iOS app with list by SwiftUI. I am implementing .onDelete to enable user to delete the rows. However, I have found that when I add a .onTapGesture to the VStack View containing the List, the onDelete function is not called when the user tapped the "Delete" button after slide the row left. However, it stills works when the user slide the row to the left side to delete this. It seems that the .onTapGesture blocks .onDelete to receive user input. How to solve this?
NavigationView {
VStack(alignment: .leading) {
List {
ForEach(things) { thing in
Text(thing)
}
.onDelete(perform: { indexSet in
things.remove(atOffsets: indexSet)
})
}
.listStyle(SidebarListStyle())
}
.onTapGesture {
}
}
Here is some code that can show my problem.
This is probably useless and very hacky but... If you must have an area that is tappable, you could place everything in a ZStack and put a random view over the List and make that tappable. Then you could set a good amount of padding to free the area where the delete button would be tapped, like this:
NavigationView {
ZStack {
List {
ForEach(things, id: \.self) { thing in
Text(thing)
}
.onDelete(perform: { indexSet in
things.remove(atOffsets: indexSet)
})
}
.zIndex(2)
.listStyle(SidebarListStyle())
Rectangle()
.zIndex(3)
.fill(Color.red)
.padding(.trailing, 80)
.allowsHitTesting(true)
.onTapGesture {
print("Blocking you")
}
}
}
I put the color in so you can see exactly where you are working, the code above would give you a look like this:
Tappable Rectangle on List
When you're happy with the area that the rectangle covers, you can just set the Opacity to a very small amount, just by replacing the color with
.opacity(0.000001)
or something. Unfortunately taps don't work with Color.clear or hidden() modifiers.
I am developing an app based on a Tabview with three TabItems. Each TabItem is a List and I would be able to show a kind of modal view over those Lists. The problem becomes when I can not call a Sheet as modal view because Sheets are almost full windowed. I need some kind of bottom modal view, so I create a View that I present over a List with higher ZIndex. It seems to work until you click in the tabbar and select another TabItem having deployed the "modal" view. The error is:
[TableView] Warning once only: UITableView was told to layout its
visible cells and other contents without being in the view hierarchy
(the table view or one of its superviews has not been added to a
window). This may cause bugs by forcing views inside the table view to
load and perform layout without accurate information (e.g. table view
bounds, trait collection, layout margins, safe area insets, etc), and
will also cause unnecessary performance overhead due to extra layout
passes.
So, I would like as solution to restrict the tappable area to the "modal" view area. ¿Is there a way to achieve this?
Probably you have some condition state depending on which you present your "modal-like" view, so depending on the same condition you can disable below TabView, like below
TabView {
// ... tabs content here
}.disabled(showingModal)
Update: Here is a demo of approach that I meant (tested with Xcode 11.3+)
struct TestTabViewModal: View {
#State private var selectedTab = 0
#State private var modalShown = false
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
VStack {
Button("Show Modal") { self.modalShown = true }
.padding(.top, 40)
Spacer()
}
.tabItem {
Image(systemName: "1.circle")
}.tag(0)
Text("2").tabItem {
Image(systemName: "1.circle")
}.tag(1)
}.disabled(modalShown)
if modalShown {
RoundedRectangle(cornerRadius: 10)
.fill(Color.yellow)
.frame(width: 320, height: 240)
.overlay(Button("CloseMe") { self.modalShown = false })
}
}
}
}