SwiftUI Segmented Control selected segment text animation on view refresh
Asked Answered
S

2

6

I am experiencing the following animation of the text in the selected segment of Segmented Controls when the View is refreshed after changing some other data in the View:

segmented control text animation

Is this a bug/feature or is there a way to eliminate this behaviour?

This is the code to reproduce the effect:

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

This was run under iOS 13.4 / Xcode 11.4

Stjohn answered 29/3, 2020 at 9:15 Comment(2)
Yes, worth submitting feedback to Apple.Arrange
I'm not using SwiftUI (just placing widget in storyboard) and seeing the same issue when I update the text in the segments. I display a count of items in my segment titles so they need to be updated regularly. I haven't found a solution to this yet.Laicize
H
4

rearrange you code base ... (this helps SwiftUI to "refresh" only necessary Views)

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

result

enter image description here

Hindsight answered 29/3, 2020 at 12:0 Comment(1)
Thank you. Your workaround solves the question above, so I will accept the answer. My actual issue however is a view with a Core Data object passed as ObservableObject and the Segmented Controls are used to set attributes of that object; so I will need to find a way to make this approach work there accordingly, if possible. I have nonetheless submitted feedback, let's hope this is solved by Apple.Stjohn
S
0

I created a custom SegmentControl to solve this problem:

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

Seventieth answered 31/8, 2020 at 15:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.