最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - Font is twitching, animation bug - SwiftUI - Stack Overflow

programmeradmin3浏览0评论

I've been testing for 3 hours now, and I don't understand why this behavior persists. I've tried:

  1. All animation types (.linear, .smooth), transitions.
  2. mainactor, async, dispatchQueue, on main etc.

(Deselect of the same element is animated acceptably)

Minimal Reproducible Example:

struct ChipsView: View {
    
    let title: String
    @Binding var selectedString: String

    private var isSelected: Bool { selectedString == title }
    private let selectedColor = Color.red
    private let unselectedColor = Color.gray
    private let selectedFont: Font.Weight = .bold
    private let unselectedFont: Font.Weight = .light
    
    var body: some View {
        Text(title)
            .font(.system(
                size: 56,
                weight: isSelected ? selectedFont : unselectedFont
            ))
            .foregroundColor(isSelected ? selectedColor : unselectedColor)
            .contentShape(Rectangle())
            .onTapGesture {
                withAnimation(.bouncy(duration: 0.3)) {
                    selectedString = isSelected ? "" : title
                }
            }
    }
}

struct ChipsView_CustomPreview: View {
    @State var selectedString: String = ""
    
    var body: some View {
        HStack {
            ForEach(["11","22","33","44","55"], id: \.self) { str in
                ChipsView(title: str, selectedString: $selectedString)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.black)
    }
}

#Preview {
    ChipsView_CustomPreview()
}

I've been testing for 3 hours now, and I don't understand why this behavior persists. I've tried:

  1. All animation types (.linear, .smooth), transitions.
  2. mainactor, async, dispatchQueue, on main etc.

(Deselect of the same element is animated acceptably)

Minimal Reproducible Example:

struct ChipsView: View {
    
    let title: String
    @Binding var selectedString: String

    private var isSelected: Bool { selectedString == title }
    private let selectedColor = Color.red
    private let unselectedColor = Color.gray
    private let selectedFont: Font.Weight = .bold
    private let unselectedFont: Font.Weight = .light
    
    var body: some View {
        Text(title)
            .font(.system(
                size: 56,
                weight: isSelected ? selectedFont : unselectedFont
            ))
            .foregroundColor(isSelected ? selectedColor : unselectedColor)
            .contentShape(Rectangle())
            .onTapGesture {
                withAnimation(.bouncy(duration: 0.3)) {
                    selectedString = isSelected ? "" : title
                }
            }
    }
}

struct ChipsView_CustomPreview: View {
    @State var selectedString: String = ""
    
    var body: some View {
        HStack {
            ForEach(["11","22","33","44","55"], id: \.self) { str in
                ChipsView(title: str, selectedString: $selectedString)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.black)
    }
}

#Preview {
    ChipsView_CustomPreview()
}

Share Improve this question edited Feb 15 at 10:40 Benzy Neez 21.3k3 gold badges14 silver badges36 bronze badges asked Feb 15 at 2:00 zef_szef_s 671 silver badge7 bronze badges 1
  • It's as easy as this: just change the layout so that each label has a FIXED WIDTH. At the moment it's correctly doing what you tell it to, animating everything together. (Alternately, and this would be more normal, you do the two animations one after the other.) – Fattie Commented Feb 15 at 16:01
Add a comment  | 

1 Answer 1

Reset to default 0

The way that SwiftUI performs animations is to examine the start state and end state and then interpolate the stages in-between. For some reason, it is getting confused with the end state for either the text being selected or the text being de-selected, when these are changing simultaneously. I would say it's a bug.

A workaround is to use different animations for selection and de-selection. This means using a second flag. The second flag (which I've called isNowSelected) can be made dependent on the first flag using an .onChange handler.

Another issue is that the numbers move a little when the animation is happening. This is because the text is wider when the bold font is in effect. A way to prevent the movement is to determine the footprint for the bold font using a hidden placeholder, then show the visible text as an overlay.

Here is an updated version with the workarounds applied. I found that it is important for the de-selection animation to have a slightly different duration to the selection animation.

struct ChipsView: View {

    let title: String
    @Binding var selectedString: String

    private var isSelected: Bool { selectedString == title }
    private let selectedColor = Color.red
    private let unselectedColor = Color.gray
    private let selectedFont: Font.Weight = .bold
    private let unselectedFont: Font.Weight = .light
    @State private var isNowSelected = false

    var body: some View {
        Text(title)
            .fontWeight(selectedFont)
            .hidden()
            .overlay {
                Text(title)
                    .fontWeight(isSelected || isNowSelected  ? selectedFont : unselectedFont)
                    .foregroundStyle(isSelected || isNowSelected ? selectedColor : unselectedColor)
                    .contentShape(Rectangle())
                    .onTapGesture {
                        selectedString = isSelected ? "" : title
                    }
                    .onChange(of: isSelected) { oldVal, newVal in
                        isNowSelected = newVal
                    }
                    // Animation used for selection
                    .animation(.bouncy(duration: 0.3), value: isSelected)

                    // Animation used for de-selection. It is important that
                    // the duration is different to the selection animation
                    .animation(.bouncy(duration: 0.28), value: isNowSelected)
            }
            .font(.system(size: 56))
    }
}

Ps. I would always recommend using a simulator for testing animations, not Preview.

发布评论

评论列表(0)

  1. 暂无评论