SwiftUI Segmented Control 在视图刷新时选择段文本动画

Posted

技术标签:

【中文标题】SwiftUI Segmented Control 在视图刷新时选择段文本动画【英文标题】:SwiftUI Segmented Control selected segment text animation on view refresh 【发布时间】:2020-03-29 09:15:47 【问题描述】:

在更改视图中的一些其他数据后刷新视图时,我在选定的分段控件段中遇到以下文本动画:

这是一个错误/功能还是有办法消除这种行为?

这是重现效果的代码:

import SwiftUI

struct ContentView: View 
    let colorNames1 = ["Red", "Green", "Blue"]
    @State private var color1 = 0

    let colorNames2 = ["Yellow", "Purple", "Orange"]
    @State private var color2 = 0

    var body: some View 
        VStack 
            VStack 
                Picker(selection: $color1, label: Text("Color")) 
                    ForEach(0..<3, id: \.self)  index in
                        Text(self.colorNames1[index]).tag(index)
                    
                .pickerStyle(SegmentedPickerStyle())

                Text("Color 1: \(color1)")
            
            .padding()

            VStack 
                Picker(selection: $color2, label: Text("Color")) 
                    ForEach(0..<3, id: \.self)  index in
                        Text(self.colorNames2[index]).tag(index)
                    
                .pickerStyle(SegmentedPickerStyle())

                Text("Color 2: \(color2)")
            
            .padding()
        
    

这是在ios 13.4 / Xcode 11.4下运行的

【问题讨论】:

是的,值得向 Apple 提交反馈。 我没有使用 SwiftUI(只是将小部件放在情节提要中),并且在更新片段中的文本时看到了同样的问题。我在我的段标题中显示项目计数,因此需要定期更新它们。我还没有找到解决办法。 【参考方案1】:

重新排列代码库...(这有助于 SwiftUI 仅“刷新”必要的视图)

import SwiftUI

struct ContentView: View 
    let colorNames1 = ["Red", "Green", "Blue"]
    @State private var color1 = 0

    let colorNames2 = ["Yellow", "Purple", "Orange"]
    @State private var color2 = 0

    var body: some View 
        VStack 
            MyPicker(colorNames: colorNames1, color: $color1)
            .padding()

            MyPicker(colorNames: colorNames2, color: $color2)
            .padding()
        
    


struct MyPicker: View 
    let colorNames: [String]
    @Binding var color: Int
    var body: some View 
        VStack 
            Picker(selection: $color, label: Text("Color")) 
                ForEach(0..<colorNames.count)  index in
                    Text(self.colorNames[index]).tag(index)
                
            .pickerStyle(SegmentedPickerStyle())

            Text("Color 1: \(color)")
        
    


struct ContetView_Preview: PreviewProvider 
    static var previews: some View 
        ContentView()
    

结果

【讨论】:

谢谢。您的解决方法解决了上面的问题,所以我会接受答案。然而,我的实际问题是一个核心数据对象作为 ObservableObject 传递的视图,并且分段控件用于设置该对象的属性;因此,如果可能的话,我将需要找到一种方法使这种方法在那里相应地起作用。尽管如此,我还是提交了反馈,希望苹果能解决这个问题。【参考方案2】:

我创建了一个自定义 SegmentControl 来解决这个问题:

import SwiftUI

struct MyTextPreferenceKey: PreferenceKey 
    typealias Value = [MyTextPreferenceData]

    static var defaultValue: [MyTextPreferenceData] = []
    
    static func reduce(value: inout [MyTextPreferenceData], nextValue: () -> [MyTextPreferenceData]) 
        value.append(contentsOf: nextValue())
    


struct MyTextPreferenceData: Equatable 
    let viewIndex: Int
    let rect: CGRect


struct SegmentedControl : View 
    
    @Binding var selectedIndex: Int
    @Binding var rects: [CGRect]
    @Binding var titles: [String]

    var body: some View 
        ZStack(alignment: .topLeading) 
            SelectedView()
                .frame(width: rects[selectedIndex].size.width - 4, height: rects[selectedIndex].size.height - 4)
                .offset(x: rects[selectedIndex].minX + 2, y: rects[selectedIndex].minY + 2)
                .animation(.easeInOut(duration: 0.5))
            
            VStack 
                self.addTitles()
            
            .onPreferenceChange(MyTextPreferenceKey.self)  preferences in
                    for p in preferences 
                        self.rects[p.viewIndex] = p.rect
                    
            
        .background(Color(.red)).clipShape(Capsule()).coordinateSpace(name: "CustomSegmentedControl")
    
    
    func totalSize() -> CGSize 
        var totalSize: CGSize = .zero
        for rect in rects 
            totalSize.width += rect.width
            totalSize.height = rect.height
        
        return totalSize
    
    
    func addTitles() -> some View 
        
        HStack(alignment: .center, spacing: 8, content: 
           ForEach(0..<titles.count)  index in
            return SegmentView(selectedIndex: self.$selectedIndex, label: self.titles[index], index: index, isSelected: self.segmentIsSelected(selectedIndex: self.selectedIndex, segmentIndex: index))
            
        )
    
    
    func segmentIsSelected(selectedIndex: Int, segmentIndex: Int) -> Binding<Bool> 
        return Binding(get: 
            return selectedIndex == segmentIndex
        )  (value) in 
    


struct SegmentView: View 
    @Binding var selectedIndex: Int
    let label: String
    let index: Int
    @Binding var isSelected: Bool
    var body: some View 
        Text(label)
            .padding(.vertical, 6)
            .padding(.horizontal, 10)
            .foregroundColor(Color(.label))
            .background(MyPreferenceViewSetter(index: index)).onTapGesture 
                self.selectedIndex = self.index
        
    


struct MyPreferenceViewSetter: View 
    let index: Int
    
    var body: some View 
        GeometryReader  geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(key: MyTextPreferenceKey.self,
                            value: [MyTextPreferenceData(viewIndex: self.index, rect: geometry.frame(in: .named("CustomSegmentedControl")))])
        
    



struct SelectedView: View 
    var body: some View 
        Capsule()
            .fill(Color(.systemBackground))
            .edgesIgnoringSafeArea(.horizontal)
    

result

【讨论】:

以上是关于SwiftUI Segmented Control 在视图刷新时选择段文本动画的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI Segmented Picker 在点击时不会切换

Scroll Segmented Control(Swift)

Scroll Segmented Control(Swift)

《iOS Human Interface Guidelines》——Segmented Control

控制 3 分段

SwiftUI:使用 Picker(分段)更改 ForEach 源或获取请求