1

In SwiftUI, for this code to toggle the display of view:

@State var show = true

Button { withAnimation { show.toggle() }} 
label: { Image(systemName: show ? "chevron.down" : "chevron.right") }

if show { ... }

The animation will be shown if the show is the @State variable.

However, I found that if show is changed to @AppStorage (so to keep the show state), the animation will not be shown.

Is there a way to keep the show state and also preserve the animation?

Lim Thye Chean
  • 8,704
  • 9
  • 49
  • 88

2 Answers2

2

You can also replace the withAnimation {} with the .animation(<#T##animation: Animation?##Animation?#>, value: <#T##Equatable#>) modifier and then it seems to work directly with the @AppStorage wrapped variable.

import SwiftUI
    
struct ContentView: View {
    @AppStorage("show") var show: Bool = true
    
    var body: some View {
        VStack {
            Button {
                self.show.toggle()
            }
            label: {
                Rectangle()
                    .fill(Color.red)
                    .frame(width: self.show ? 200 : 400, height: 200)
                    .animation(.easeIn, value: self.show)
            }
            
            Rectangle()
                .fill(Color.red)
                .frame(width: self.show ? 200 : 400, height: 200)
                .animation(.easeIn, value: self.show)
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

EDIT: Following the comments, another solution

import SwiftUI
    
struct ContentView: View {
    @State private var show: Bool
    
    init() {
        self.show = UserDefaults.standard.bool(forKey: "show")
        // Or self._show = State(initialValue: UserDefaults.standard.bool(forKey: "show"))
    }
    
    var body: some View {
        VStack {
            Button {
                withAnimation {
                    self.show.toggle()
                }
            }
            label: {
                Text("Toggle")
            }
            
            if show {
                Rectangle()
                    .fill(Color.red)
                    .frame(width: 200 , height: 200)
            }
        }
        .padding()
        .onChange(of: self.show) { newValue in
             UserDefaults.standard.set(newValue, forKey: "show")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
  • Thanks Marc! Tried if show { view.animation(.easeIn, value: self.show) } and it does not seem to work. – Lim Thye Chean Oct 03 '22 at 11:54
  • Perhaps `Group { if show { view } }.animation(.easeIn, value: self.show)` can be a workaround, or `view.opacity(self.show ? 1 : 0).animation(.easeIn, value: self.show)` – Marc Biosca Oct 03 '22 at 13:20
  • 1
    In any case this seems to be a bug with `@AppStorage` + Xcode 14 which should be fixed at some point... else you can do as someone else was mentioning, use `@State` and the didSet {} stuff to store in userDefaults, or the onChange modifier to do so. – Marc Biosca Oct 03 '22 at 13:29
  • Thanks Marc, but I can't get it to work. – Lim Thye Chean Oct 03 '22 at 13:44
  • I am trying the userDefaults method. Saving is no issue, but loading to be the value for show state causes issue. Anyway, I think I will give out for now and wait to see what happened. – Lim Thye Chean Oct 03 '22 at 13:47
  • If you can check my answer again, in the EDIT part at the bottom, that works for me – Marc Biosca Oct 03 '22 at 14:05
  • I still face issue.. in the init part. I got Variable 'self.show' used before being initialized. – Lim Thye Chean Oct 03 '22 at 14:35
  • Guess it's your Xcode version. Give this a try `self._show = State(initialValue: UserDefaults.standard.bool(forKey: "show"))` – Marc Biosca Oct 03 '22 at 14:45
  • This works but show does not seem to be persistent. I did one change and seems to work: rather than didSet, I change to onChange(of: show) which apply to the button itself. Thanks for all your help! – Lim Thye Chean Oct 04 '22 at 02:44
  • Good, that’s an option too indeed, not sure why it works for me the didset to be fair… – Marc Biosca Oct 04 '22 at 05:29
0

Try moving the code into a view model so the property is not inside the same view. Something like this should fix the animation issue:

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup { ContentView().environmentObject(ViewModel()) }
    }
}

final class ViewModel: ObservableObject {
    @AppStorage("show") var show = true
}

struct ContentView: View {
    @EnvironmentObject var viewModel: ViewModel
    var body: some View {
        VStack {
            Button {
                withAnimation { viewModel.show.toggle() }
            } label: {
                Image(systemName: "chevron.right")
                    .rotationEffect(Angle(degrees: viewModel.show ? 0 : 90))
            }
            if viewModel.show { /* ... */ }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Islam
  • 3,654
  • 3
  • 30
  • 40