4

I have a SwiftUI Form with a custom chart view (not Swift Charts). A long press toggles to a different type of chart. These charts use the .transition(.slide) modifier. In iOS 15 these transitioned as expected on a long press, but in iOS 16 they do not.

Persisted state property (an enum):

@AppStorage("chartType") var chartType: ChartType = .chartA

The Form part of the body property:

Form {
    
    // Other sections
    
    Section {
        switch chartType {
            case .chartA:
                ChartViewA()
                    .transition(.slide)
            case .chartB:
                ChartViewB()
                    .transition(.slide)
    }
        .onLongPressGesture {
            if chartType == .chartA {
                withAnimation {
                    summaryChartType = .chartB
                }
            } else {
                withAnimation {
                    summaryChartType = .chartA
                }
            }
        }

Unfortunately adding animation modifiers like .animation(.spring(), value: chartType) makes no difference.

I would be grateful for advice on why this might have worked in iOS 15 but not in iOS 16, and what I could do to restore animation here.

Chris
  • 4,009
  • 3
  • 21
  • 52
  • 2
    This might be an issue with `@AppStorage`. Does the animation work correctly if `chartType` is `@State`? – vacawama Sep 14 '22 at 08:27
  • @vacawama Thanks for the tip. I’ll check this when I’m at my computer. I thought `@AppStorage` behaved like `@State` (it certainly worked in iOS15). If that works, how could I persist the value? Would it be a `didSet` block, with loading from `UserDefaults` on `init()`? – Chris Sep 14 '22 at 09:02
  • 2
    I saw someone complaining that @AppStorage wasn’t animating correctly. I couldn’t reproduce it, but I had tried iOS 15 and your question makes me wonder that user was using iOS 16 and perhaps iOS 16 changed something about @AppStorage. If so, you could keep the @State var for animation and manually load/save it to user defaults. You have to use `.onChange {}` with `@State vars`. `didSet` will not work. – vacawama Sep 14 '22 at 09:12
  • @vacawama Great - thanks for the info. I’ll try it and update you later. – Chris Sep 14 '22 at 09:19
  • 2
    I have a related problem with `@AppStorage` in iOS 16. onChange is not triggered on an `@AppStorage` variable. It was working in iOS 15. – awct Oct 15 '22 at 05:18

1 Answers1

8

In iOS 16, there appears to be a problem with @AppStorage vars and animation. Here is one possible workaround. Use @State var for animation, and save it to an @AppStorage variable with .onChange():

enum ChartType: String {
    case chartA, chartB
}

struct ChartViewA: View {
    var body: some View {
        Color.red
    }
}

struct ChartViewB: View {
    var body: some View {
        Color.blue
    }
}

struct ContentView: View {
    @AppStorage("chartType") var chartTypeAS: ChartType = .chartA
    @State private var chartType: ChartType = .chartA
    
    init() {
        // load initial value from persistent storage
        _chartType = State(initialValue: chartTypeAS)
    }
    
    var body: some View {

        Form {
            
            // Other sections
            
            Section {
                VStack {
                    switch chartType {
                    case .chartA:
                        ChartViewA()
                            .transition(.slide)
                    case .chartB:
                        ChartViewB()
                            .transition(.slide)
                    }
                }
                .onLongPressGesture {
                    if chartType == .chartA {
                        withAnimation {
                            chartType = .chartB
                        }
                    } else {
                        withAnimation {
                            chartType = .chartA
                        }
                    }
                }
            }
            .onChange(of: chartType) { value in
                // persist chart type
                chartTypeAS = value
            }
        }
    }
}

Tested in Xcode 14.0 with iPhone 14 simulator running iOS 16.

Alternatively, you could perform the saving to/restoring from UserDefaults manually.

vacawama
  • 150,663
  • 30
  • 266
  • 294
  • Thank you! This certainly restores animation; the sections below move smoothly based on the different heights of the chart views and the charts themselves fade in. However, I still cannot get the transitions to work. Any idea what might be happening here? – Chris Sep 14 '22 at 22:06
  • I'm not sure what you mean. In the example above, the views slide in, which shows they're using the transitions. Can you expand on my example to show your issue? – vacawama Sep 14 '22 at 22:51
  • So I think I must have something else in my view hierarchy interfering with the transition. Your example does work as expected, and certainly restores animation for me. Thanks. – Chris Sep 15 '22 at 09:53
  • Had the same problem today and i was thinking to bypass it like proposed solution. Is this a bug that Apple will fix? – christostsang Sep 16 '22 at 17:02
  • @christostsang, I sure hope Apple fixes it. It makes AppStorage less useful, but who knows how long that will take? – vacawama Sep 16 '22 at 17:17
  • 1
    I opened a FB: FB11608015. – alpennec Sep 28 '22 at 10:39
  • 1
    A similar problem appears with (at)Published and (at)Binding. The animation isn't as fluid and I get the error "Publishing changes from within view updates is not allowed, this will cause undefined behavior.". – christostsang Oct 15 '22 at 08:09