Home > Net >  Changing icon causes a hitch in SwiftUI animation?
Changing icon causes a hitch in SwiftUI animation?

Time:09-28

I have a list of tags where when you select them a chip will appear.

When you click on the 'X' on each chip, the tag should slide out with an animation, and the tag in the list will be marked as unselected.

The problem I'm having is when I remove the last chip, the circle/circle-check to the left of the tag does not flow smoothly with the animation.

I believe that is because the icon is changing when selected vs. unselected, since if I keep the icon the same this is not a problem. It's also not a problem if I remove the slide animation on the chips, however I like this animation and would like to keep it.

I'm actually having this issue in a few places in my app that involve animations changing icons and was wondering if there's a workaround for this?

Gif of issue with animation

I've attached a reproducible example below.

import SwiftUI

struct ContentView: View {
    var body: some View {
        Icon_Animation()
    }
}

struct Icon_Animation: View {
    //All tags
    var testTags: [Tag] =
    [Tag("tag1"),
     Tag("tag2"),
     Tag("tag3")]
    
    //Only tags that have been selected
    @State var selectedTags = [Tag]()
    
    var body: some View {
        ScrollView{
            
            //Hstack of the tags that have been selected
            HStack{
                ForEach(selectedTags){ tag in
                        HStack(spacing: 0){
                            Button{
                                //Clicking on the X will remove from selectedTags array, and then make that tag's isSelected = false
                                withAnimation(.easeOut) {
                                    if let index = selectedTags.firstIndex(where: {$0.name == tag.name}){
                                        selectedTags.remove(at: index)
                                    }
                                }
                                
                                //PROBLEM: even though this statemnt isn't in the withAnimation block, it causes a weird behavior with the circle/check-circle icon
                                //If I remove the withAnimation statement from the above block, it works fine. However, I would like to keep the slide animation on the chips.
                                tag.isSelected = false
                                
                            }label:{
                                Image(systemName: "x.circle.fill")
                                    .font(.subheadline)
                                    .padding(.horizontal, 6)
                            }
                            
                            Image(systemName: "number")
                                .font(.footnote.weight(.bold))
                                .padding(.trailing, 2)
                            Text("\(tag.name)")
                                .font(.footnote)
                        }
                        .padding(.trailing, 20)
                        .padding(.vertical, 6)
                        .background(Color.blue.opacity(0.6), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
                        .transition(.slide)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            
            //List of tags where you can select each tag to create a chip
            ForEach(testTags){ tag in
                TagView(tag: tag)
                    .onTapGesture {
                        tag.isSelected.toggle()
                        
                        if(tag.isSelected == true){
                            selectedTags.append(tag)
                        }
                    }
            }
            .padding()
        }
        .padding()
    }
}

class Tag: Identifiable, ObservableObject {
    var id = UUID()
    @Published var name: String
    @Published var isSelected = false
    
    init(_ name: String){
        self.name = name
    }

}

struct TagView: View {
    @ObservedObject var tag: Tag = Tag("test")
  
    var body: some View {
        ZStack{
            //Overlay for when tag is selected
            Rectangle()
                .fill(Color.purple.opacity(0.6))
                .edgesIgnoringSafeArea(.all)
                .cornerRadius(5)
                .opacity(tag.isSelected ? 1 : 0)
            
            HStack(spacing: 8){
                
                //PROBLEM!!: I want to use a different icon based on whether tag isSelected, but it's causing a hitch in the animation when switching
                if(tag.isSelected){
                    Image(systemName: "checkmark.circle.fill")
                        .font(.title2.weight(.light))
                }else{
                    Image(systemName: "circle")
                        .font(.title2.weight(.light))
                }
                
                Image(systemName: "number")
                    .font(.body.weight(.bold))
                
                Text(tag.name)
                    .font(.headline)
                    .fontWeight(.bold)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            
        }
    }
}

CodePudding user response:

I think this is a common problem that comes down to the a parent view receiving two different child views from an if statement, and as they are two different views, with different ids, SwiftUI doesn't know how to animate between them.

The trick is is to use just one child view with variable content. You should be able to achieve this by replacing the if...else... in your TagView that generates the image with a ternary inside of the initialiser:

Image(systemName: tag.isSelected ? "checkmark.circle.fill" : "circle" )
               .font(.title2.weight(.light))
  • Related