skip to Main Content

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!

2

Answers


  1. I adapted your example to accept input from a TextField. When run in a simulator, I see that the first message has a strange animation that starts in the middle of the screen. Subsequent animations seem to work fine (these being simple "move" transitions).

    To fix the animation for the first message, I would suggest some small changes:

    • The .frame setting maxHeight: .infinity has no effect inside a vertical ScrollView. However, it would be useful to set a maximum width.

    • The Spacer is redundant too and can be removed.

    // DialogView
    
    ScrollView {
        VStack(alignment: .leading) {
            ForEach(dialog) { bubble in
                BubbleView(bubble: bubble)
            }
            .onChange(of: dialog.count, initial: false) { _,_  in
                scrollDown(proxy: proxy)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding()
    }
    

    Here is the adapted ContentView that can be used for testing in a simulator:

    struct ContentView: View {
        @State private var dialog = [Bubble]()
        @State private var text = ""
        @FocusState private var hasFocus: Bool
    
        var body: some View {
            VStack {
                DialogView(dialog: $dialog)
                Spacer()
                TextField("Text", text: $text)
                    .focused($hasFocus)
                    .textFieldStyle(.roundedBorder)
                    .padding()
                    .onSubmit {
                        if !text.isEmpty {
                            dialog.append(Bubble(sender: .bot, text: text))
                            text = ""
                        }
                        hasFocus = true
                    }
                    .onAppear {
                        hasFocus = true
                    }
                Button("Reset") {
                    dialog.removeAll()
                    hasFocus = true
                }
            }
        }
    }
    

    Animation

    Login or Signup to reply.
  2. 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()
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search