Moving view with DragGesture while having a ScrollView on top - swift

I have a view that can be dragged by the user. This view contains a ScrollView that shows a list of information. However, this view interferes with the DragGesture of the containing view.
The expected behavior would be that only when the user can actually scroll something, the gesture of the ScrollView would have priority over the container.
Here is the code that can be used to test the problem I'm referring to:
struct MultipleDrag: View {
#GestureState private var offset: CGFloat = 0
private var gesture: some Gesture {
DragGesture()
.updating($offset) { value, state, transaction in
state = -value.translation.height
}
}
var body: some View {
VStack {
ScrollView {
ForEach(1...20, id: \.self) { val in
Text(String(val))
.padding()
}
}
.background(Color.green)
.cornerRadius(10)
.padding()
}
.frame(height: 200)
.background(Color.orange)
.cornerRadius(10)
.offset(y: -offset)
.gesture(gesture)
}
}
As you can see, by default the scrollview starts at the top. That would mean that if the user swiped down, the orange container should move downwards, even if the gesture took place in the green area (ScrollView). The same should happen when the bottom of the ScrollView was reached or when there's not enough content on the scrollview to actually scroll.
Any ideas?

I think you can use this modifier:
.highPriorityGesture(Gesture)
This will make the gesture with a higher priority than other gestures.

Related

Vertical ScrollView disabled when cursor is inside horizontal ScrollView in SwiftUI

My SwiftUI app has a List of ScrollView(.horizontal). I would like to vertical scroll to go through each of the horizontal scroll views (Rows) and to horizontal scroll within the Row. The following code accomplishes this :
struct Row: View {
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(0..<100) { i in
Text(String(i))
.padding(5)
}
}
.padding(10)
.background(.red)
}
}
}
struct ContentView: View {
var body: some View {
List(0..<100) {_ in
Row()
}
.background(.blue)
}
}
The issue is, if my cursor is inside the bounds of a Row, the outer vertical scroll is disabled. How can I "promote" the vertical scroll gesture of the nested Row to the List it lives in?
The reason I want this behavior is so that if the user is scrolling through the List, I don't want them to reposition their cursor if it lands inside the bounds of a Row.
Vertical Scroll: GIF
Horizontal Scroll: GIF
Issue
If you exchange the List with another vertical ScrollView it works both ways (don't ask me why).
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
ForEach(0..<100) { row in
Row()
}
}
.background(.blue)
}
}

Add Padding to TabView Page Indicator

I have a view that is a TabView with the style PageTabViewStyle(indexDisplayMode: .always which looks great for my use case however the page indicator is bumping right up to the safe area near the bottom of the screen and it looks bad. I'd like to move the page indicator up to some n value. here's a sample of my view to reproducing it. If this view were built on any device without a Home Button, it will ride on top of the home indicator line.
var body: some View {
ZStack {
TabView(selection: $homeVM.selectedPageIndex) {
// Any number of views here.
}
.frame(width: UIScreen.main.bounds.width)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .never))
}.edgesIgnoringSafeArea(.all)
}
I attempted to add the padding to the ZStack, which does work but then my TabView is cut off on the bottom, which means my cells disappear prematurely.
Here's an image for what I'm trying to fix. Notice the page indicator sits on the home bar indicator. I need the indicators pushed up, without pushing up the background ScrollView
Update #1
This view is being presented by a base view that I use to handle my navigation stack. The view is as follows. The important thing to note here is the .ignoresSafeArea() that I have on this view. I did that because it's a containing view for my eventual TabView to be presented from. Interestingly if I remove this modifier the indicators move up to a more manageable position, but then my form becomes clipped at the top and bottom of the device when scrolling, and that's not ideal.
struct BaseLaunchView: View {
#StateObject var baseNavVM = BaseLaunchViewModel()
#State var shouldLogin = false
#State var shouldRegister = false
var body: some View {
VStack {
switch baseNavVM.loggedIn {
case true:
HomeView()
default:
NavigationView {
VStack{
Spacer()
VStack(spacing: 30) {
VStack {
Text("Stello")
.font(Fonts.title)
Text("Life Groups")
.font(Fonts.body)
}
StelloDivider()
Text("Connect with like minded people, to fellowship and find your home.")
.font(Fonts.subheading)
.multilineTextAlignment(.center)
}
Spacer()
NavigationLink(destination: RegisterOptionsView(isLoggingIn: true), isActive: $shouldLogin) {
Button(action: {
shouldLogin.toggle()
}, label: {
Text("Login")
.font(Fonts.button)
}).buttonStyle(StelloFillButtonStyle())
}
NavigationLink(destination: RegisterOptionsView(isLoggingIn: false), isActive: $shouldRegister) {
Button(action: {
shouldRegister.toggle()
}, label: {
Text("Register")
.font(Fonts.button)
}).buttonStyle(StelloHollowButtonStyle())
}
}
}.accentColor(.black)
}
}
.padding()
.environmentObject(baseNavVM)
.ignoresSafeArea()
}
}

Why won't a nested scrollview respond to scrolls in swiftUI?

I'm building an SwiftUI app with a dropdown menu with a vertical ScrollView within another vertical ScrollView. However, the dropdown menu one (the nested one) won't scroll. I would like to give it priority somehow. It seems like a simple problem, but I have scoured the internet but cannot find an adequate solution. Here is the basic code for the problem (the code is cleaner in the app but copy and pasting particular snippets did not work very well):
ScrollView{
VStack{
(other stuff)
DropdownSelector()
(other stuff)
}
}
struct DropdownSelector(){
ScrollView{
VStack(alignment: .leading, spacing: 0) {
ForEach(self.options, id: \.self) { option in
(do things with the option)
}
}
}
Creating nested ScrollViews in the first place is probably a bad idea. Nonetheless, there is a solution.
Because with ScrollView it scrolls as much as the content height, this is a problem when they are nested. This is because the inner ScrollView isn't limited in height (because the outer ScrollView height just changes), so it acts as if it wasn't there at all.
Here is a minimal example demonstrating the problem, just for comparison:
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("Top view")
DropdownSelector()
Text("Bottom view")
}
}
}
}
struct DropdownSelector: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< 10) { i in
Text("Item: \(i)")
}
}
}
}
}
To fix it, limit the height of the inner scroll view. Add this after DropdownSelector():
.frame(height: 100)

SwiftUI - ScrollView has 0 width and my content is not visible

I am trying to use a scroll view for scrollable content, but whenever I nest my views inside the Scroll View, I have noticed that the views from my stacks vanish back into the view hierarchy and nothing remains visible on the screen. I have also seen that whenever I am using a ScrollView, it adds another Hosting View Controller and I don't know if this is the normal behaviour.
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(bookLibrary.indices, id: \.self) { index in
HStack {
ForEach(self.bookLibrary[index], id: \.self) { book in
BookView(book: book)
}
}
}
}
}
}
}
Getting this view hierarchy. You can also see that the HostingScrollView has a width of 0.
If you don't want to use GeometryReader just insert zero height view with correct width like this
var body: some View {
ScrollView {
VStack {
Color.clear
.frame(maxWidth: .infinity, maxHeight: 0)
ForEach(...) { each in
...
}
}
}
}
While not a perfect solution, you can use GeometryReader to set the scroll view's frame to the same width as its superview.
NavigationView {
GeometryReader {geometry in
ScrollView(.vertical) {
// TODO: Add content
}
.frame(width: geometry.size.width)
}
}
This workaround was inspired by Rob Mayoff's answer on another question.

`withAnimation` only does animation once when adding first item to #State array

My goal is to have control over the type of animation when an object is added to the #State events array.
withAnimation only occurs on the first append to the events array. It is then ignored on additional appends.
I'm currently running this on Xcode 11 beta 4
I've tried adding the calling DispatchQueue.main.async, having the animation on the Text() object.
If I use a list it performs animation on addition, however I don't know how to modify those animations.
Goal
Have text slide in with each append and fade out on each remove.
struct Event: Identifiable {
var id = UUID()
var title: String
}
struct ContentView: View {
#State
var events = [Event]()
var body: some View {
VStack {
ScrollView {
ForEach(events) { event in
Text(event.title)
.animation(.linear(duration: 2))
}
}
HStack {
Button(action: {
withAnimation(.easeOut(duration: 1.5)) {
self.events.append(Event(title: "Animate Please"))
}
}) {
Image(systemName: "plus.circle.fill").resizable().frame(width: 40, height: 40, alignment: .center)
}
}
}
}
}
I'm expecting that each append has an animation that is described in the withAnimation block.
When SwiftUI layouts and animations behave in ways you think are not correct, I suggest you add borders. The outcome may surprise you and point you directly into the cause. In most cases, you'll see that SwiftUI was actually right! As in your case:
Start by adding borders:
ScrollView {
ForEach(events) { event in
Text(event.title)
.border(Color.red)
.animation(.linear(duration: 2))
}.border(Color.blue)
}.border(Color.green)
When you run your app, you'll see that before adding your first array element, the ScrollView is collapsed into zero width. That is correct, as the ScrollView is empty. However, when you add your first element, it needs to be expanded to accommodate the "Animate Please" text. The Text() view also starts with zero width, but as its containing ScrollView grows, it does too. These are the changes that get animated.
Now, when you add your second element, there is nothing to animate. The Text() view is placed with its final size right from the start.
If instead of "Animate Please", you change your code to use a random length text, you will see that when adding a largest view, animations do occur. This is because ScrollView needs to expand again:
self.events.append(Event(title: String(repeating: "A", count: Int.random(in: 0..<20))))
What next: You have not explained in your question what animation you expect to see. Is it a fade-in? A slide? Note that in addition to animations, you may define transitions, which determines the type of animation to perform when a view is added or removed from your hierarchy.
If after putting these tips into practice, you continue to struggle, I suggest you edit your question and tell us exactly what animation would you like to see when adding a new element to your array.
UPDATE
According to your comments, you want the text to slide. The simplest form, is using a transition. Unfortunately, the ScrollView seems to disable transitions on its children. I don't know if that is intended or a bug. Anyway, here I post two methods. One with transitions (does not work with ScrollView) and one using only animations, which does work inside a ScrollView, but requires more code:
With transitions (does not work inside a ScrollView)
struct ContentView: View {
#State private var events = [Event]()
var body: some View {
VStack {
ForEach(events) { event in
// A simple slide
Text(event.title).transition(.slide).animation(.linear(duration: 2))
// To specify slide direction
Text(event.title).transition(.move(edge: .trailing)).animation(.linear(duration: 2))
// Slide combined with fade-in
Text(event.title).transition(AnyTransition.slide.combined(with: .opacity)).animation(.linear(duration: 2))
}
Spacer()
HStack {
Button(action: {
self.events.append(Event(title: "Animate Please"))
}) {
Image(systemName: "plus.circle.fill").resizable().frame(width: 40, height: 40, alignment: .center)
}
}
}
}
}
Without transitions (works inside a ScrollView):
struct Event: Identifiable {
var id = UUID()
var title: String
var added: Bool = false
}
struct ContentView: View {
#State var events = [Event]()
var body: some View {
VStack {
ScrollView {
ForEach(0..<events.count) { i in
// A simple slide
Text(self.events[i].title).animation(.linear(duration: 2))
.offset(x: self.events[i].added ? 0 : 100).opacity(self.events[i].added ? 1 : 0)
.onAppear {
self.events[i].added = true
}
}
HStack { Spacer() } // This forces the ScrollView to expand horizontally from the start.
}.border(Color.green)
HStack {
Button(action: {
self.events.append(Event(title: "Animate Please"))
}) {
Image(systemName: "plus.circle.fill").resizable().frame(width: 40, height: 40, alignment: .center)
}
}
}
}
}