How to properly animate a Slider in SwiftUI? - ios

I want to animate a Slider whenever its value changes with a custom animation (with a custom duration). But somehow it doesn't work. No matter what animation I specify, it always uses the same (very short) animation that is almost not noticeable.
Here's a minimal code example:
struct SliderView: View {
#State var value: Float = 0
var body: some View {
VStack {
Slider(value: $value)
.animation(.linear(duration: 3), value: value) // ← here 🎬
Button("Toggle") {
value = value < 0.5 ? 1 : 0
}
}
.frame(width: 300, height: 200)
.padding(16)
}
}
What am I doing wrong?
Note 1: Without the animation modifier, I get no animation at all and the knob immediately jumps to its new position. With the modifier, it animates but it doesn't seem to respect the concrete animation I specify.
Note 2: I know that I could use an explicit animation and wrap the change in a withAnimation closure. But even if that worked, it's not an option for me because the in my concrete case, the value is not a #State but a #Binding and is thus set externally from a view model that should not know anything about SwiftUI.

Related

SwiftUI: animate scale of view back to initial state when done?

I have this simple view:
struct ContentView: View {
#State private var scale:Double = 1
var body: some View {
Rectangle()
.fill(.red)
.frame(width: 200, height: 200)
.scaleEffect(scale)
.animation(Animation.easeInOut(duration: 1).repeatCount(4, autoreverses: true).delay(1), value: scale)
.onAppear {
scale = 1.2
}
}
}
It produces this animation:
You'll notice at the end of the animation it snaps to scale 1.2 since that is the value of the state scale effect follows.
I am want to trigger the scale animation on appear, and make it "pulse" 4 times from state 1 to 1.2 and back, 4 times. Right now it does start at scale 1, pulses, but at the end it snaps to 1.2. How can I fix this so it creates a "pulse" animation that starts at 1, scales to 1.2 and back 4 times?

SwiftUI transmit drag focus to outer view (ScrollView inside dragable)

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).

SwiftUI animation problem with a binding to a StateObject inside a NavigationView

I have an interesting situation in regards to animations in SwiftUI. In its simplified form, I have a view that shows a rectangle which, when tapped, should toggle a Bool binding and represent the change with an animated transition of color. But it seems like the animation doesn't happen when the view is inside a NavigationView and the binding is coming from a StateObject instead of simple local state. I can't explain why that would be the case, and would appreciate any thoughts.
Below is the code that shows a simplified case that reproduces the issue. The app code isn't particularly interesting; it's the default code that creates a WindowGroup and shows an instance of ContentView in it.
import SwiftUI
class AppState: ObservableObject {
#Published var isRed = false
}
struct RectView: View {
#Binding var isRed: Bool
var body: some View {
Rectangle()
.fill(isRed ? Color.red : Color.gray)
.frame(width: 75, height: 75, alignment: .center)
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) {
isRed.toggle()
}
}
}
}
struct ContentView: View {
#StateObject var appState = AppState()
#State private var childViewIsRed = false
var body: some View {
VStack {
NavigationView {
List {
NavigationLink("Link with binding to state object", destination: RectView(isRed: $appState.isRed))
NavigationLink("Link with binding to state variable", destination: RectView(isRed: $childViewIsRed))
}
}
.frame(height: 300)
RectView(isRed: $appState.isRed)
RectView(isRed: $childViewIsRed)
}
}
}
The gif/video below is me demonstrating four things, tapping on these views from bottom to top:
First I tap on the very bottom rectangle - the one with a binding to a #State property. It toggles with animation as expected. I tap again to leave it gray.
Then I tap the second rectangle from the bottom - one with a binding to a #Published property in the #StateObject. All is well.
Next, I tap on the NavigationLink that leads to a rectangle that is bound to the local #State property. When I tap on the rectangle the transition is animated fine. The very bottom rectangle also animates, which makes sense since they are bound to the same property.
Finally I tap on the top NavigationLink, which leads to a rectangle bound to the #Published property in the #StateObject. When I tap on this rectangle though, there is no animation. The rectangle snaps to red. The rectangle below it (which is bound to the same property) animates fine, proving that the property is indeed toggled. But there is no animation inside the NavigationView. Why? What am I missing?
I've searched for existing questions. I'm aware that there are some around NavigationView (like this) but that doesn't explain why one type of binding would work fine inside a NavigationView and another wouldn't. Similarly, there are ones around animating changes to #ObservedObjects (like this, would a #StateObject be similar?) but I don't follow why animating changes to such a binding would work fine outside of a NavigatonView and not inside one.
In case it's relevant I'm using Xcode 13.2.1, running on macOS 12.2.1, which is in turn running on a 16-inch M1 Max MacBook Pro. The simulator shown in the gif/video is an iPhone 11 Pro Max. I get the same results if I deploy this tiny test app to a physical iPhone 7 running iOS 15.3.1.
To be honest with you, I am not sure why, but utilizing the animation modifier on the RectView allows the animation to occur without any issues.
struct RectView: View {
#Binding var isRed: Bool
var body: some View {
Rectangle()
.fill(isRed ? Color.red : Color.gray)
.frame(width: 75, height: 75, alignment: .center)
.animation(.easeOut(duration: 1), value: isRed)
.onTapGesture { isRed.toggle() }
}
}
screen recording example

Are there any ways to copy gestures in SwiftUI or handle them and pass them down to child views?

I have two TextEditors next to each other and I want to synchronize their scroll state, for example, if I scroll on the left one, the right one should show the same.
//basic code
#State private var textLeft: String = "view left"
#State private var textRight: String = "view right"
var body: some View {
HStack() {
TextEditor(text: $textLeft)
TextEditor(text: $textLeft)
.gesture(DragGesture(coordinateSpace: .global)
.updating($gestureState, body: { (value, state, transaction) in
print(value.translation)
})
.onChanged { value in
//this blocks the TextEditor to scroll
//or won't be called if the gesture is handled by the editor
//
//if I apply the offset to the other Editor
//it results in weird looking behavior
print(offset)
}
)
}
}
I tried several things like applying a gesture to it and capture the movement and set it to the other as an .offset(value), but either the DragGesture blocks the TextEditor or the TextEditor blocks the DragGesture.
Are there any approaches like copying gestures and apply them to another view or pass through the .gesture() to the view itself?
Thanks for any help!
You can try extracting the Gesture to a computed property:
var dragGesture: some Gesture {
DragGesture(coordinateSpace: .global)
.updating($gestureState) { value, state, transaction in
print(value.translation)
}
.onChanged { value in
// this blocks the TextEditor to scroll
// or won't be called if the gesture is handled by the editor
//
// if I apply the offset to the other Editor
// it results in weird looking behavior
print(offset)
}
}
var body: some View {
HStack {
TextEditor(text: $textLeft)
.gesture(dragGesture)
TextEditor(text: $textLeft)
.gesture(dragGesture)
}
}

Animate removing/adding SwiftUI view while animating offset

I'm trying to animate the offset of a SwiftUI view, while at the same time fading out and removing a subview of that view. The problem I'm running into is that SwiftUI performs the offset and fade-out animations, but doesn't combine them.
What I want to achieve to animate the position of the whole SubView, while simultaneously fading out the subtitle text, so that the subtitle text moves vertically while fading in or out. I can achieve this by animating the opacity of the Text instead of removing it, but that means the text will still take up "layout space".
Is there a way to achieve this animation with the if showSubtitle statement?
The following code and GIF demonstrate the problem:
struct ContentView: View {
#State private var showSubtitle = true
var body: some View {
SubView(showSubtitle: showSubtitle)
.animation(.default)
.offset(y: showSubtitle ? 100 : 0)
.onTapGesture {
self.showSubtitle.toggle()
}
}
}
struct SubView: View {
let showSubtitle: Bool
var body: some View {
VStack {
Text("Header")
if showSubtitle {
Text("Subtitle")
}
}
}
}
Actually the observed behaviour is because .offset does not change layout, the view is stand at the same place. So when you remove subview it is removed in-place and animating that removal (with default .opacity transition). The part that starts offsetting does not contain already subview, so you don't see it in moving up part.
Here is something that might give some kind of effect you expect, but transitions are based on source size, so it is not so far and manually specified distance of offset. Anyway, try:
if showSubtitle {
Text("Subtitle")
.transition(AnyTransition.opacity.combined(with: AnyTransition.move(edge: .top)))
}
Tested with Xcode 12 / iOS 14

Resources