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

SwiftUI Segmented Control selected segment text animation on view refresh

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

这是 bug/feature 还是有办法消除这种行为?

这是重现效果的代码:

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

重新安排您的代码库...(这有助于 SwiftUI "refresh" 仅需要必要的视图)

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()
    }
}

结果

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

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