How to conditionally render a SwiftUI Form with animation using the selection from a Segmented Picker?

496 views Asked by At

I'm trying to conditionally render two Forms based on the selection from a segmented picker. It all works fine when there's no animations, but the moment I add them in, the first Form seems to break.

Code for the 1st Form:

struct CalculateView: View {
    @Binding var checkAmount: Double
    @Binding var numberOfPeople: Int
    @Binding var tipPercentage: Int
    @Binding var currentMode: DisplayMode
    @FocusState var amountIsFocused: Bool
    let tipPercentages: [Int]
    var currencyType: FloatingPointFormatStyle<Double>.Currency
    var grandTotal: Double
    var totalPerPerson: Double
    
    
    var body: some View {
        Form{
            Section{
                TextField("Amount", value: $checkAmount, format:
                                .currency(code: Locale.current.currencyCode ?? "USD"))
                    .keyboardType(.decimalPad)
                    .focused($amountIsFocused)
                
                Picker("Number of people", selection: $numberOfPeople) {
                    ForEach(1..<100, id: \.self){
                        Text($0 == 1 ? "\($0) Person" : "\($0) People")
                    }
                }
            }
            
            Section{
                Picker("Tip Percentage", selection: $tipPercentage){
                    ForEach(0..<101){
                        Text($0, format: .percent)
                    }
                }.pickerStyle(.wheel)

            } header: {
                Text("How much tip do you want to leave ?")
            }

            Section{
                HStack {
                    Text("Grand Total")
                        .foregroundColor(.secondary)
                        .font(.system(size:14, weight: .regular))
                        .textCase(.uppercase)

                    Spacer()

                    Text(grandTotal, format:
                            currencyType)
                        .foregroundColor(tipPercentage == 0 ? .red: .blue)
                        .font(.system(size: 20, weight: .regular))
                }.padding(.horizontal, 10)

                VStack(alignment: .leading){
                    Text("Amount per person")
                        .foregroundColor(.secondary)
                        .font(.system(size: 15, weight: .semibold))
                        .padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 0))
                        .textCase(.uppercase)

                    Text(totalPerPerson, format:
                                .currency(code: Locale.current.currencyCode ?? "USD"))
                        .frame(maxWidth: .infinity)
                        .font(.system(size: 55, weight: .thin))
                                            .padding(EdgeInsets(top: 15, leading: 0, bottom: 20, trailing: 0))
                        .foregroundColor(.green)
                }
            } header: {
                Text("Final Payment")
                    .foregroundColor(.orange)
            }
        }
    }
}

Code for the 2nd Form:

struct HistoryView: View {
    
    var body: some View {
        Form {
            Section {
                Text("test")
            }
        }
    }
}

Main ContentView code:

enum DisplayMode: String, Equatable, CaseIterable {
    case calculate
    case history
}

struct ContentView: View {
    @State private var checkAmount = 0.0
    @State private var numberOfPeople = 2
    @State private var tipPercentage = 20
    @State private var currentMode: DisplayMode = .calculate
    @FocusState private var amountIsFocused: Bool
    
    let tipPercentages = [10, 15, 20, 25, 0]
    
    var currencyType: FloatingPointFormatStyle<Double>.Currency {
        return .currency(code: Locale.current.currencyCode ?? "USD")
    }
    
    var grandTotal: Double {
        let tipValue = checkAmount / 100 * Double(tipPercentage)
        let total = checkAmount + tipValue
        
        return total
    }
    
    var totalPerPerson: Double {
        let peopleCount = Double(numberOfPeople)
        let amountPerPerson = grandTotal / peopleCount
        
        return amountPerPerson
    }
    
    var navigationTitle: String {
        let title = currentMode == .calculate ? "WeSplit" : currentMode.rawValue.capitalized
        
        return title
    }
    
    let swapRight: AnyTransition = .asymmetric(
        insertion: .move(edge: .trailing),
        removal: .move(edge: .leading)
    )
    let swapLeft: AnyTransition = .asymmetric(
        insertion: .move(edge: .leading),
        removal: .move(edge: .trailing)
    )
    
    var body: some View {
        NavigationView{
            VStack {
                    if currentMode == .calculate {
                        
                            CalculateView(checkAmount: $checkAmount, numberOfPeople: $numberOfPeople, tipPercentage: $tipPercentage, currentMode: $currentMode, amountIsFocused: _amountIsFocused, tipPercentages: tipPercentages, currencyType: currencyType, grandTotal: grandTotal, totalPerPerson: totalPerPerson)
                            .transition(swapLeft)
                            
                    } else {
                            HistoryView()
                            .transition(swapRight)
                    }
                
            }
            .animation(.easeInOut, value: currentMode)
            .navigationTitle(navigationTitle)
                .toolbar{
                    ToolbarItemGroup(placement: .keyboard) {
                        Button("Done"){
                            amountIsFocused = false
                        }
                    }
                    
                    ToolbarItemGroup(placement: .principal) {
                        
                        Picker("Mode", selection: $currentMode) {
                            ForEach(DisplayMode.allCases, id: \.self){ mode in
                                Text(mode.rawValue.capitalized)
                            }
                        }.pickerStyle(.segmented)
                            .fixedSize()
                    }
                    
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
                        if currentMode == .calculate {
                            Button {
                                print("Save")
                            } label: {
                                Text("Save")
                            }
                        }
                        
                    }
                }
        }
    }
}

Here's what it looks like on the simulator:

SwiftUI Form animation glitch

1

There are 1 answers

3
ChrisR On

I can reproduce the issue, this seems to be a bug, the console is showing some constraint warnings.

A workaround could be using TabView. A positive side effect is that you won't need custom transitions, they come with it:

   NavigationView{
        
        TabView(selection: $currentMode) {
            
            CalculateView(checkAmount: $checkAmount, numberOfPeople: $numberOfPeople, tipPercentage: $tipPercentage, currentMode: $currentMode, amountIsFocused: _amountIsFocused, tipPercentages: tipPercentages, currencyType: currencyType, grandTotal: grandTotal, totalPerPerson: totalPerPerson)
            
                .tag(DisplayMode.calculate)
//                    .transition(swapLeft)
            
            HistoryView()
                .tag(DisplayMode.history)
//                    .transition(swapRight)
            
        }
        .tabViewStyle(.page(indexDisplayMode: .never) )
        .animation(.easeInOut, value: currentMode)

// rest of your code as is