I want to avoid conflict with `Gesture` of `ScrollView` - ios

I want to avoid conflict with Gesture of ScrollView to meet a customer's complex animation requirements.
The reason I not want to conflict it is that when I simply gave Gesture as shown below, onEnded was not called.
In the first place, I feel that onChanged is called by accident, and I don't know if this way of writing is safe or not.
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("\(i)")
}
}
}
.gesture(
gestureA
)
}
var gestureA: some Gesture {
DragGesture().onChanged { value in
print("A onChanged:\(value)")
}
.onEnded { value in
print("A onEnded:\(value)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I wish I could use simultaneously(with:), but I don't know how to write with ScrollView.
If it's UIScrollView, I've found a way to remove the gesture recognizer as shown in the following with removeTarget:
https://stackoverflow.com/a/18337648/1979953
Please let me know if there is any way to avoid conflicts.
Edit: I tried the following after seeing https://developer.apple.com/forums/thread/655465, but still onEnded was not called.
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("\(i)")
}
}
}
.simultaneousGesture(
gestureA
)

Related

Exclusive gestures in SwiftUI when gestures not in the same hierarchy

Basically I would like this to work:
struct TestView: View {
var body: some View {
VStack {
Text("Should work simultaneously").onTapGesture {
print("Work simultaneously")
}
Text("Should work simultaneously").onTapGesture {
print("Work simultaneously")
}
Text("Should be exclusive").onTapGesture {
print("Work exclusively")
}
}
.simultaneousGesture(
TapGesture().onEnded {
print("Should work everywhere except for 3th child view")
}
)
}
}
The VStack has a gesture that should always be recognized, except when one their childs wants to handle the touches exclusively.
The pasted code is a minimal example.
In my real project, the 3th child view is a custom DatePicker that appears on some condition. I would like to dismiss the picker when one clicks outside, but if one clicks inside the picker it shouldn't be dismissed yet. For this reason I need handle the touches in the picker exclusively.
You can't block up the view hierarchy, only down. In order to have the simultaneous gesture on only part of the VStack, you will need to separate those views into their own composed view with the .simultaneousGesture() on that view. Presuming you don't want to change the look of your view, I would simply wrap the subviews you want the .simultaneousGesture() on in a Group{}. Something like this:
struct TestView: View {
var body: some View {
VStack {
Group {
Text("Should work simultaneously").onTapGesture {
print("Work simultaneously")
}
Text("Should work simultaneously").onTapGesture {
print("Work simultaneously")
}
}
.simultaneousGesture(
TapGesture().onEnded {
print("Should work everywhere except for 3th child view")
}
)
Text("Should be exclusive")
.onTapGesture {
print("Work exclusively")
}
}
}
}

Conditionally wrap code inside view SwiftUI

I want to make my code inside a scrollview based on a boolean
Something like this :
ScrollView{
Vstack {
}
Hstack {
}
}.isWrapped(true)
So if the bool is true, the content is wrapped inside a ScrollView, if not, I just get the content as is. (without the scrollView parent)
I tried with multiples way, the only thing I see as possible is 2 blocks of code, not good practice thought.
One way is to always use a ScrollView, but tell it not to scroll. If you pass an empty set as the first argument to ScrollView, it won't scroll.
ScrollView(shouldScroll ? .vertical : []) {
// content here
}
Another way is to extract the content back out of the ScrollView in your proposed isWrapped modifier, like this:
extension ScrollView {
#ViewBuilder
func isWrapped(_ flag: Bool) -> some View {
if flag { self }
else { self.content }
}
}
Because of the nature of SwiftUI, you can't remove the parent without removing children. As you mentioned in your edit, this will require two blocks of code. But, you can make it much more friendly in two different ways.
The first way is a reusable component. It takes a Binding isVisible variable and the content.
struct ConditionalScrollView<Content: View>: View {
#Binding private var isVisible: Bool
private var builtContent: Content
init(isVisible: Binding<Bool>, content: () -> Content) {
self._isVisible = isVisible
builtContent = content()
}
var body: some View {
if isVisible {
ScrollView { builtContent }
} else {
builtContent
}
}
}
To use this new component is to simply replace the use of ScrollView in the area that you want to manually adjust. I.E:
struct ContentView: View {
#State var isVisible = true
var body: some View {
ConditionalScrollView(isVisible: $isVisible) {
Text("Hello, world!")
.padding()
}
}
}
But this is not your only option.
If you want to keep things as simple as possible, you can achieve this using a simple ViewBuilder. Create the view you don't want to change inside the ViewBuilder, and then create a condition around the ScrollView, all the while placing the variable as the content. It would look as follows:
struct ContentView: View {
#State private var isVisible = true
#ViewBuilder private var mainContent: some View {
Text("Hello, world!")
.padding()
}
var body: some View {
if isVisible {
ScrollView { mainContent }
} else {
mainContent
}
}
}
As you've probably come to realize, this is one of the limitations of SwiftUI. But it can be considered a strength because you will always know if a view's parent is there or not.
I hope this little tidbit helped, happy coding!
You can also make it even easier:
#State var isWraped : bool = true
var body: some View {
if isWrapped {
ScrollView {
YourView()
}
else {
YourView()
}

SwiftUI, how to stop List rolling immediately by code after dragging?

As the title described. In some case, I want stop the rolling List immediately.
That's easy with UIKit. But there seems no obvious way to do it in SwiftUI.
Just do UIScrollView.appearance().bounces = false.
struct ContentView: View {
init() {
UIScrollView.appearance().bounces = false /// here!
}
var body: some View {
ScrollView {
Rectangle()
.frame(height: 1000)
}
}
}

SwiftUI: attach an animation to a transition

According to the Apple documentation we should be able to attach an animation directly to a transition. For example:
.transition(AnyTransition.slide.animation(.linear))
documentation for the method:
extension AnyTransition {
/// Attach an animation to this transition.
public func animation(_ animation: Animation?) -> AnyTransition
}
says:
Summary
Attaches an animation to this transition.
But I can't manage to make it work. Take a look at this minimum viable example (you can copy-paste it and try yourself):
import SwiftUI
struct AnimationTest: View {
#State private var show = false
var body: some View {
VStack {
if show {
Color.green
.transition(AnyTransition.slide.animation(.linear))
} else {
Color.yellow
.transition(AnyTransition.slide.animation(.linear))
}
Button(action: {
self.show.toggle()
}, label: {
Text("CHANGE VIEW!")
})
}
}
}
struct AnimationTest_Previews: PreviewProvider {
static var previews: some View {
AnimationTest()
}
}
As you can see no animation happens at all. Any ideas? Thank you.
You need to wrap the boolean toggling within a withAnimation() closure:
withAnimation {
self.show.toggle()
}
Tested the transition working on Xcode 11.2.1 while on the simulator; the Canvas doesn't preview it.
Please note that animations/transitions applied directly to a view have an effect on that particular view and its children. Moreover, according to the docs:
func animation(Animation?) -> View
Applies the given animation to all animatable values within the view.
Since the animatable value, in this case the Bool toggle enabling the transition, is external to the Color views, it must be animated explicitly from where it's set in the button's action. Alternatively, one can effectively attach the transition directly to the target views, but apply the animation to their container, thus enabling interpolation of changes to show. So, this also achieves the desired result:
struct AnimationTest: View {
#State private var show = false
var body: some View {
VStack {
if show {
Color.green
.transition(.slide)
} else {
Color.yellow
.transition(.slide)
}
Button(action: {
self.show.toggle()
}, label: {
Text("CHANGE VIEW!")
})
}
.animation(.linear)
}
}

SwiftUI add subview dynamically but the animation doesn't work

I would like to create a view in SwiftUI that add a subview dynamically and with animation.
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
if isButtonVisible {
AnyView(DetailView())
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}else{
AnyView(Text("test"))
}
}
}
}
The above code works fine with the animation . however when i move the view selection part into a function, the animation is not working anymore (since i want to add different views dynamically, therefore, I put the logic in a function.)
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView().transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
func subView() -> some View {
if isButtonVisible {
return AnyView(DetailView())
}else{
return AnyView(Text("test"))
}
}
}
it looks totally the same to me, however, i don't understand why they have different result. Could somebody explain me why? and any better solutions? thanks alot!
Here's your code, modified so that it works:
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView()
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
}
func subView() -> some View {
Group {
if isButtonVisible {
DetailView()
} else {
Text("test")
}
}
}
}
Note two things:
Your two examples above are different, which is why you get different results. The first applies a transition and animation to a DetailView, then type-erases it with AnyView. The second type-erases a DetailView with AnyView, then applies a transition and animation.
Rather that using AnyView and type-erasure, I prefer to encapsulate the conditional logic inside of a Group view. Then the type you return is Group, which will animate properly.
If you wanted different animations on the two possibilities for your subview, you can now apply them directly to DetailView() or Text("test").
Update
The Group method will only work with if, elseif, and else statements. If you want to use a switch, you will have to wrap each branch in AnyView(). However, this breaks transitions/animations. Using switch and setting custom animations is currently not possible.
I was able to get it to work with a switch statement by wrapping the function that returns an AnyView in a VStack. I also had to give the AnyView an .id so SwiftUI can know when it changes. This is on Xcode 11.3 and iOS 13.3
struct EnumView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
view(for: viewModel.viewState)
.id(viewModel.viewState)
.transition(.opacity)
}
}
func view(for viewState: ViewModel.ViewState) -> AnyView {
switch viewState {
case .loading:
return AnyView(LoadingStateView(viewModel: self.viewModel))
case .dataLoaded:
return AnyView(LoadedStateView(viewModel: self.viewModel))
case let .error(error):
return AnyView(ErrorView(error: error, viewModel: viewModel))
}
}
}
Also for my example in the ViewModel I need to wrap the viewState changes in a withAnimation block
withAnimation {
self.viewState = .loading
}
In iOS 14 they added the possibility to use if let and switch statements in function builders. Maybe it helps for your issues:
https://www.hackingwithswift.com/articles/221/whats-new-in-swiftui-for-ios-14 (at the article's bottom)

Resources