This SwiftUI animation should only fade out. Why does it move to the right?
Asked Answered
U

2

21

I'm having a hard time understanding why this happens. I reduced the problem to its minimum expression.

I have a single Text view, that when removed, should just fade out. The .transition(.opacity) has been added for clarity only. It should not be needed as it is the default. The result, however, is that in addition to the fade-out, the text view slides to the right.

By playing with the text length, I realised that during the transition, its left margin wants to be aligned with the left margin of the CHANGE button. But why?!

On the contrary, when added back, it works fine and there is no movement. Just a nice fade-in effect.

The problem not only occurs with iOS, but macOS too. Using Xcode 11 beta 2.

enter image description here

import SwiftUI

struct ContentView : View {
    @State private var showText = true
    
    var body: some View {
        VStack {
            Spacer()
            
            if showText {
                Text("I should always be centered!")
                    .font(.largeTitle)
                    .transition(.opacity)
            }
            
            Spacer()
            
            Button {
                withAnimation(.basic(duration: 1.5)) {
                    self.showText.toggle() 
                }
            } label: {
                Text("CHANGE")
                    .font(.title)
            }
            
            Spacer()
        }
    }
}
Unswear answered 20/6, 2019 at 6:15 Comment(0)
U
33

I'll answer my own question... It turns out, the parent view shrinks during the transition, making the text view to move with it. To illustrate, I added some borders to the views:

enter image description here

In order to solve the problem, I must ensure the parent view does not shrink. It was as simple as adding this:

HStack { Spacer() }

enter image description here

The modified code would look like this:

import SwiftUI

struct ContentView : View {
    @State private var showText = true
    
    var body: some View {
        VStack {
            Spacer()
            
            if showText {
                Text("I should always be centered!")
                    .font(.largeTitle)
                    .transition(.opacity)
                    .border(Color.blue)
            }
            
            Spacer()
            
            Button {
                withAnimation(.basic(duration: 1.5)) {
                    self.showText.toggle() 
                }
            } label: {
                Text("CHANGE")
                    .font(.title)
                    .border(Color.blue)
            }
            
            Spacer()
            
            // This ensures the parent is kept wide to avoid the shift.
            HStack { Spacer() }   
        }
        .border(Color.green)
    }
}

I still think this is a bug, otherwise, the fade-in should have the same behavior, and it doesn't. If this is not a bug, it is not what one would expect. I'll file a bug report.

Unswear answered 20/6, 2019 at 10:48 Comment(2)
I also noticed this! Did you try changing the alignment of the text itself, if that is even a property these days?Manofwar
I think I tried all alignment options, but that was when I was just starting with SwiftUI, I will probably have to revisit it. However, since a new beta will be dropping any minute now, I though I should wait and concentrate on other things.Unswear
L
4

SwiftUI animates layout changes automatically.

My theory is that a Spacer replaces the Text when you hide it, and by expanding to fill the superview, it pushes out the Text towards the trailing edge of the screen.

You can fix the issue with this approach:

Text("I should always be centered!")
                .font(.largeTitle)
                .opacity(showText ? 1 : 0)

The Text will fade in/out without moving this way.

Lalita answered 20/6, 2019 at 7:53 Comment(1)
I like your way of thinking about the extra spacer, and although it does not seem to be the case, it put me in the right track (check my posted answer). Your workaround certainly works for this specific case, but that is just an animation. I want the transition to work. The example is simplified, but I in my own code I am using more complex custom transitions than just opacity. Additionally, your solution keeps the view around (hidden and using space). On the other side, with a properly working transition, the view is removed completely from the hierarchy at the end of the animation.Unswear

© 2022 - 2025 — McMap. All rights reserved.