Reverse animation for NavigationLink? - swift

I am currently trying to implement a back button in my app. I have some Views linked through NavigationLinks and I'm using the default animation. I am pretty sure the default is .easeIn but tbh I can't really tell the difference between them. I tried making it .easeOut bc I assumed it would go the opposite way but it doesn't. I want my back button to essentially have the reverse direction of the normal animation to move forward in the app, but I can't seem to find a good answer in the documentation. Is there a good way to just reverse the default animation so it looks like it's going backwards?

I have mimicked the NavigationLink behaviour in one of my apps, because I needed more control on the behaviours and UI elements.
Basically, I have given a transition to the List view and an opposite transition to the detail view. If you add some "chevron" system images and place the buttons in the right position, a good eye can catch the trick, but I think it works pretty good for the average user.
Here is the code, check out the .transition() in each view (just remember to use withAnimation() when changing the state):
struct Example: View {
#State private var detail: String? = nil
let details = ["First", "Second", "Third"]
var body: some View {
VStack {
if detail == nil {
List(details, id: \.self) { item in
Text("Tap to see the detail \(item)")
.onTapGesture {
withAnimation {
self.detail = item
}
}
}
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
} else {
VStack {
Button {
withAnimation {
detail = nil
}
} label: {
Text("Back")
}
Text("Now you see only \(detail ?? "not found")")
.padding()
}
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
}
}
}
}

Related

Can you control the progress of a SwiftUI animation

Is it possible to control the progress of withAnimation
Ideal API would look something like this
withAnimation(progress: percentDragged) { showSecondView.toggle() }
Here's a full code example of the above line in context.
The goal is to smoothly slide between different app states with a user controlled gesture.
struct ContentView: View {
#State var showSecondView = false
var body: some View {
VStack {
ZStack {
if !showSecondView {
Text("First view")
.padding()
.background(Color.blue)
.transition(.slide)
} else {
Text("Second view")
.padding()
.background(Color.red)
.transition(.slide)
}
}
}
.gesture(
DragGesture()
.onChanged { value in
let percentDragged = value.translation.width / UIScreen.main.bounds.width
// Here I want to gradually slide (interpolate) between the two states
// Ideal API would look like this:
withAnimation(progress: percentDragged) { showSecondView.toggle() }
}
)
}
}
Yes.. I know I can trigger an animation whenever I want (like this). but that's not what I'm looking for! I want to control the progress
if percentDragged > 0.5 {
withAnimation { showSecondView.toggle() }
}
Yes.. I also know I could manually store a #State var offset: Double and control my own slide.
This just gets nasty when my app state is tied to tons of different visual changes. Views appearing/disappearing, alignments shifting, etc.

Nested transitions / animations in SwiftUI

The goal is to create a view on top on another view, that has elements and changes color while moving away to the top of the screen. So we have one variable / state that triggers an animation where both views have sub transitions / animations.
Problem is: there is either no movement or no colortransition at some point in the animation / transition because adding a transition somehow overwrites all the individual transitions of the elements in the stack.
EDIT: Due to simplicity I rearranged my code avoiding the described behaviour. But it seems like a really bad solution (repeating lots of code, if's ...) Has someone an idea how to write the following working example in a more compact/advanced way?
Working, but probably unnecessary complicated:
struct ContentView: View {
#State var signUpLoginView_active: Bool = true
var body: some View {
ZStack{
Color.orange
if signUpLoginView_active {
ZStack {
Color.white
}
.transition(.move(edge: .top))
.zIndex(1)
}
if signUpLoginView_active {
ZStack {
Color.red
}
.transition(AnyTransition.move(edge: .top).combined(with: AnyTransition.opacity))
.zIndex(2)
}
Button("test"){
withAnimation(.easeInOut(duration: 2)){
signUpLoginView_active.toggle()
}
}.zIndex(3)
}.ignoresSafeArea()
}
}
My Approach to simplify (not working correctly):
struct ContentView: View {
#State var signUpLoginView_active: Bool = true
var body: some View {
ZStack{
Color.orange
if signUpLoginView_active {
ZStack {
Color.white
Color.red.opacity(signUpLoginView_active ? 1 : 0)
}
.transition(AnyTransition.move(edge: .top))
}
Button("test"){
withAnimation(.easeInOut){
signUpLoginView_active.toggle()
}
}
}.ignoresSafeArea()
}
}
Although it might get more tricky than this simple answer, you can combine transitions together so they take effect seamlessly:
.transition(AnyTransition.move(edge: .top)
.combined(with: AnyTransition.scale))
In an optimal case, you should not need those AnyTransitions:
.transition(.move(edge: .top)
.combined(with: .scale))
but it sometimes results in an Xcode error, which might or might not give any clue of the real problem that Xcode has with your code.
Optionally you might need to define whether your transitions should take effect only on removal or insertion, or ask for different Transitions on each of removal or insertion:
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .top),
removal: AnyTransition.move(edge: .bottom))
.combined(with: AnyTransition.asymmetric(insertion: .scale,
removal: .identity)))
// `.identity` means no transition

SwiftUI onTapGesture called only once

I am working on this audio player with multiple view components in it.
I added a way to hide/show the top view and the bottom view when we click anywhere in the middle view.
Before it was working fine, but recently when I tried again, it only dismiss it and doesn't trigger the onTapGesture again.
I believe the only difference with before is that the view is presented instead of pushed in a view controller.
I have tried to use a custom gesture with a TapGesture() on onEnded() but the same result.
I also tried to add a Rectangle shape like said [here][1].
struct PlayerView: View {
#ObservedObject private var playerState = PlayerState()
#Binding var isPlayerReduced: Bool
private let interfaceColor: Color = .gray//.black
private let interfaceOpacity: Double = 0.9
private let interfaceAnimationDuration: Double = 0.4
var body: some View {
ZStack(content: {
GeometryReader(content: { geometry in
VStack(content: {
if !self.playerState.isInterfaceHidden {
TopPlayerView(playerState: self.playerState,
isPlayerReduced: self.$isPlayerReduced)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
MiddlePlayerView(skipIntro: self.$playerState.skipIntro)
// Allow to spread the background zone for click purposes
.background(Color.clear)
// I want to have the middle under my TopPlayer and my BottomPlayer
.zIndex(-1)
.onTapGesture(perform: {
withAnimation(.easeInOut(duration: self.interfaceAnimationDuration)) {
self.playerState.isInterfaceHidden.toggle()
}
})
// .gesture(TapGesture()
// .onEnded({ _ in
// }))
if !self.playerState.isInterfaceHidden {
BottomPlayerView(playerState: self.playerState)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
})
})
})
.background(Color.black)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
I am kind of out of ideas here, any help is welcomed! thank you!
Alright, so after touching everything possible in this code. I ended up making it work.
The difference is where I put the padding to my views.
I switch the paddings to the VStack instead of my views in the VStack.
It seems to work now.
I post below the working code.
var body: some View {
ZStack(alignment: .center, content: {
GeometryReader(content: { geometry in
VStack(content: {
self.topMarker()
if !self.playerState.isInterfaceHidden {
TopPlayerView(playerState: self.playerState,
isPlayerReduced: self.$isPlayerReduced)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
MiddlePlayerView(skipIntro: self.$playerState.skipIntro)
// Allow to spread the background zone for click purposes
.background(Color.white.opacity(0.00000001))
// I want to have the middle under my TopPlayer and my BottomPlayer
.zIndex(-1)
.onTapGesture(perform: {
withAnimation(.easeInOut(duration: self.interfaceAnimationDuration)) {
self.playerState.isInterfaceHidden.toggle()
}
})
if !self.playerState.isInterfaceHidden {
BottomPlayerView(playerState: self.playerState)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
})
.padding(.top, 8)
.padding(.bottom, geometry.safeAreaInsets.bottom)
})
})
.background(Color.black)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("")
.navigationBarHidden(true)
}
To be honest, I have no idea what would be the difference here...
Even in the view debugger there is no difference..

SwiftUI: Animate changes in List without animating content changes

I have a simple app in SwiftUI that shows a List, and each item is a VStack with two Text elements:
var body: some View {
List(elements) { item in
NavigationLink(destination: DetailView(item: item)) {
VStack(alignment: .leading) {
Text(item.name)
Text(self.distanceString(for: item.distance))
}
}
}
.animation(.default)
}
The .animate() is in there because I want to animate changes to the list when the elements array changes. Unfortunately, SwiftUI also animates any changes to content, leading to weird behaviour. For example, the second Text in each item updates quite frequently, and an update will now shortly show the label truncated (with ... at the end) before updating to the new content.
So how can I prevent this weird behaviour when I update the list's content, but keep animations when the elements in the list change?
In case it's relevant, I'm creating a watchOS app.
The following should disable animations for row internals
VStack(alignment: .leading) {
Text(item.name)
Text(self.distanceString(for: item.distance))
}
.animation(nil)
The answer by #Asperi fixed the issue I was having also (Upvoted his answer as always).
I had an issue where I was animating the whole screen in using the below: AnyTransition.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))
And all the Text() and Button() sub views where also animating in weird and not so wonderful ways. I used animation(nil) to fix the issue after seeing Asperi's answer. However the issue was that my Buttons no longer animated on selection, along with other animations I wanted.
So I added a new State variable to turn on and off the animations of the VStack. They are off by default and after the view has been animated on screen I enable them after a small delay:
struct QuestionView : View {
#State private var allowAnimations : Bool = false
var body : some View {
VStack(alignment: .leading, spacing: 6.0) {
Text("Some Text")
Button(action: {}, label:Text("A Button")
}
.animation(self.allowAnimations ? .default : nil)
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.allowAnimations = true
}
}
}
}
Just adding this for anyone who has a similar issue to me and needed to build on Asperi's excellent answer.
Thanks to #Brett for the delay solution. My code needed it in several places, so I wrapped it up in a ViewModifier.
Just add .delayedAnimation() to your view.
You can pass parameters for defaults other than one second and the default animation.
import SwiftUI
struct DelayedAnimation: ViewModifier {
var delay: Double
var animation: Animation
#State private var animating = false
func delayAnimation() {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.animating = true
}
}
func body(content: Content) -> some View {
content
.animation(animating ? animation : nil)
.onAppear(perform: delayAnimation)
}
}
extension View {
func delayedAnimation(delay: Double = 1.0, animation: Animation = .default) -> some View {
self.modifier(DelayedAnimation(delay: delay, animation: animation))
}
}
In my case any of the above resulted in strange behaviours. The solution was to animate the action that triggered the change in the elements array instead of the list. For example:
#State private var sortOrderAscending = true
// Your list of elements with some sorting/filtering that depends on a state
// In this case depends on sortOrderAscending
var elements: [ElementType] {
let sortedElements = Model.elements
if (sortOrderAscending) {
return sortedElements.sorted { $0.name < $1.name }
} else {
return sortedElements.sorted { $0.name > $1.name }
}
}
var body: some View {
// Your button or whatever that triggers the sorting/filtering
// Here is where we use withAnimation
Button("Sort by name") {
withAnimation {
sortOrderAscending.toggle()
}
}
List(elements) { item in
NavigationLink(destination: DetailView(item: item)) {
VStack(alignment: .leading) {
Text(item.name)
}
}
}
}

How to animate hideable views with SwiftUI?

I'm trying out SwiftUI, and while I've found many of its features very elegant, I've had trouble with animations and transitions. Currently, I have something like
if shouldShowText { Text(str).animation(.default).transition(AnyTransition.opacity.animation(.easeInOut)) }
This label does transition properly, but when it's supposed to move (when another view above is hidden, for instance) it does not animate as I would have expected, but rather jumps into place. I've noticed that wrapping everything in an HStack works, but I don't see why that's necessary, and I was hoping that there is a better solution out there.
Thanks
If I correctly understood and reconstructed your scenario you need to use explicit withAnimation (depending on the needs either for "above view" or for both) as shown below
struct SimpleTest: View {
#State var shouldShowText = false
#State var shouldShowAbove = true
var body: some View {
VStack {
HStack
{
Button("ShowTested") { withAnimation { self.shouldShowText.toggle() } }
Button("HideAbove") { withAnimation { self.shouldShowAbove.toggle() } }
}
Divider()
if shouldShowAbove {
Text("Just some above text").padding()
}
if shouldShowText {
Text("Tested Text").animation(.default).transition(AnyTransition.opacity.animation(.easeInOut))
}
}
}
}