iosswiftanimationswiftuitransition

Can't get rid of a SwiftUI animation glitch


I can't get rid of an animation glitch. Here is my code:

// Bubble.swift

import SwiftUI

struct Bubble: Identifiable {
    var id = UUID()
    var sender: Sender
    var text: String
    var value: String?
    var inputType: String?
}

enum Sender {
    case user
    case bot
    case system
}


struct BubbleView: View {
    @State var bubble: Bubble
    @State var showBubble = false
    
    var body: some View {
        Section {
            if self.showBubble {
                switch bubble.sender {
                    case .user, .bot:
                        Text(bubble.text)
                            .padding(10)
                            .foregroundColor(bubble.sender == Sender.user ? .white : .black)
                            .background(bubble.sender == Sender.user ? .accentColor : Color(UIColor.systemGray5))
                            .clipShape(RoundedRectangle(cornerRadius: 20))
                            .frame(maxWidth: .infinity, alignment: bubble.sender == Sender.user ? .trailing : .leading)
                            .padding(.vertical, bubble.sender == Sender.user ? 10 : 0)
                            .transition(.move(edge: .bottom))
                    case .system:
                        Text(bubble.text.uppercased())
                            .padding(10)
                            .foregroundColor(.secondary)
                            .font(.caption)
                            .fontWeight(.semibold)
                            .frame(maxWidth: .infinity)
                            .transition(.move(edge: .bottom))
                }
            } else {
                Text("")
            }
        }.onAppear {
            withAnimation {
                self.showBubble.toggle()
            }
        }
    }
}

#Preview {
    VStack(alignment: .leading) {
        BubbleView(bubble: Bubble(sender: .bot, text: "It's-a me, Mario!"))
        BubbleView(bubble: Bubble(sender: .user, text: "And it's-a me, Luigi!"))
        BubbleView(bubble: Bubble(sender: .system, text: "10:30"))
    }
}
// Dialog.swift

import SwiftUI

struct DialogView: View {
    @Binding var dialog: [Bubble]
    
    func scrollDown(proxy: ScrollViewProxy) {
        if let lastID = dialog.last?.id {
            withAnimation {
                proxy.scrollTo(lastID)
            }
        }
    }
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(dialog) { bubble in
                        BubbleView(bubble: bubble)
                    }
                    .onChange(of: dialog.count, initial: false) { _,_  in
                        scrollDown(proxy: proxy)
                    }
                }
                .frame(maxHeight: .infinity)
                .padding()
                Spacer()
            }
        }
    }
}


#Preview("Dialog view") {
    DialogView(dialog: .constant([
        Bubble(sender: .bot, text: "Bubble 1"),
        Bubble(sender: .bot, text: "Bubble 2"),
        Bubble(sender: .bot, text: "Bubble 3")
    ]))
}

It is a part of a larger project, but the DialogView animation is buggy: on its first appearance, something strange happens. To be able to see it, you can just create a new SwiftUI iOS project, and paste my code into two new files. Then, go to the Dialog.swift file and see the preview. Nothing stranges happens on the first time but if you edit the DialogView body (for instance commenting) you will see the strange animation.

It happens constantly on my bigger project, and I don't know how to remove it. It is possible to test it on the simulator replacing the ContentView.swift content by the following:

// ContentView.swift

import SwiftUI

struct ContentView: View {
    @State var bubbles: [Bubble] = []
    
    var body: some View {
        DialogView(dialog: $bubbles)
        Button {
            if self.bubbles.count == 0 {
                self.bubbles = [
                    Bubble(sender: .bot, text: "Bubble 1"),
                    Bubble(sender: .bot, text: "Bubble 2"),
                    Bubble(sender: .bot, text: "Bubble 3")
                ]
            } else {
                self.bubbles = []
            }
        } label: {
            Text("Test")
        }
    }
}

#Preview {
    ContentView()
}

Can someone help me? Thanks!


Solution

  • Using withAnimation for a state change will cause animation for all the views that are updated during the state change.

    So the solution is to set animation for the specific views.

    var body: some View {
        Section {
            if self.showBubble {
                Group {
                    switch bubble.sender {
                    case .user, .bot:
                        Text(bubble.text)
                            .padding(10)
                            .foregroundColor(bubble.sender == Sender.user ? .white : .black)
                            .background(bubble.sender == Sender.user ? .accentColor : Color(UIColor.systemGray5))
                            .clipShape(RoundedRectangle(cornerRadius: 20))
                            .frame(maxWidth: .infinity, alignment: bubble.sender == Sender.user ? .trailing : .leading)
                            .padding(.vertical, bubble.sender == Sender.user ? 10 : 0)
                    case .system:
                        Text(bubble.text.uppercased())
                            .padding(10)
                            .foregroundColor(.secondary)
                            .font(.caption)
                            .fontWeight(.semibold)
                            .frame(maxWidth: .infinity)
                    }
                }
                .transition(.move(edge: .bottom).combined(with: .opacity))
                .transaction { transaction in
                    transaction.animation = .default
                }
            }
            else {
                Text("")
            }
        }
        .onAppear {
            self.showBubble.toggle()
        }
    }