SwiftUI: Animation problem - fading instead of moving

Viewed 50

I’m trying to build a custom sidebar menu that animates out when a button in it is tapped. For debugging purposes the animation is deliberately slow at 2.0 seconds. As you can see the animation does not work properly:

enter image description here

I suspect there are two parts to this problem:

  1. The background of the newly selected button is moving out faster than the menu. I think this is rooted in the default system animation of Button.
  2. When I replace Button with Text and use an .onTapGesture, there is still the fading animation, so I assume there is something structurally wrong in the way I’m setting selected in FeatureButton.

Sorry the example code is a bit long, tried to simplify my app architecture as much as possible. The reason for using MenuState as an EnvironmentObject is to be able to the change its properties from various places throughout the app.

Here’s the code:

class MenuState: ObservableObject {
    @Published var currentFeature: Feature = .featureA
    @Published var menuOffset: CGFloat = 0
}

enum Feature: String, CaseIterable {
    case featureA = "Feature A"
    case featureB = "Feature B"
    case featureC = "Feature C"
}

extension Feature: Identifiable {
    var id: RawValue { rawValue }
}

struct ContentView: View {
    @StateObject var menuState = MenuState()

    var body: some View {
        ZStack(alignment: .leading) {
            content
            menu
        }
        .environmentObject(menuState)
        .animation(.easeOut(duration: 2.0), value: menuState.menuOffset)
    }

    var menu: some View {
        VStack {
            ForEach(Feature.allCases) { feature in
                FeatureButton(feature: feature)
            }
        }
        .frame(maxHeight: .infinity)
        .frame(width: 200)
        .background(.thinMaterial)
        .offset(x: menuState.menuOffset)
    }

    var content: some View {
        VStack {
            Button("Show Menu") {
                menuState.menuOffset = 0
            }
            Text(menuState.currentFeature.rawValue)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

struct FeatureButton: View {
    @EnvironmentObject var menuState: MenuState

    let feature: Feature
    var selected: Bool {
        return menuState.currentFeature == feature
    }

    var body: some View {
        Button(feature.rawValue) {
            menuState.currentFeature = feature
            menuState.menuOffset = -200
        }
        .buttonStyle(FeatureButtonStyle(selected: selected))
    }
}

struct FeatureButtonStyle: ButtonStyle {
    @EnvironmentObject var menuState: MenuState

    var selected: Bool
    public func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .frame(maxWidth: .infinity, minHeight: 44)
            .foregroundColor(selected ? .blue : .primary)
            .background(Color.gray.opacity(selected ? 0.4 : 0))
            .contentShape(Rectangle())
    }
}

EDIT: For some reason making the animation explicit solves the issue, see answer below.

1 Answers

The problem can be solved by making the animation explicit instead of using the .animation modifier:

Button(feature.rawValue) {
    menuState.currentFeature = feature
        withAnimation(.easeOut) {
            menuState.menuOffset = -200
        }
    }
.buttonStyle(FeatureButtonStyle(selected: selected))

I don't understand why it only works like this though.

Related