Reputation: 459
I am building a custom SegmentedPicker in SwiftUI where the selector adjusts its size to fit the frame of each picker item. I did it already using PreferenceKey
s as inspired by this post (Inspecting the View Tree) for uniformly sized items like shown below:
I think I can simplify my implementation considerably and avoid using PreferencyKey
s altogether by using a .matchedGeometryEffect()
. My idea was to present a selector behind each item only when that item has been selected and sync the transition using the .matchedGeometryEffect()
. Almost everything is working except for an issue where the selector will be in front of the previously selected item. I tried explicitly setting the zIndex
, but it does not seem to affect the result:
The code:
struct MatchedGeometryPicker: View {
@Namespace private var animation
@Binding var selection: Int
let items: [String]
var body: some View {
HStack {
ForEach(items.indices) { index in
ZStack {
if isSelected(index) {
Color.gray.clipShape(Capsule())
.matchedGeometryEffect(id: "selector", in: animation)
.animation(.easeInOut)
.zIndex(0)
}
itemView(for: index)
.padding(7)
.zIndex(1)
}
.fixedSize()
}
}
.padding(7)
}
func itemView(for index: Int) -> some View {
Text(items[index])
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(isSelected(index) ? .black : .gray)
.font(.caption)
.onTapGesture { selection = index }
}
func isSelected(_ index: Int) -> Bool { selection == index }
}
And in ContentView
:
struct ContentView: View {
@State private var selection = 0
let pickerItems = [ "Item 1", "Long item 2", "Item 3", "Item 4", "Long item 5"]
var body: some View {
MatchedGeometryPicker(selection: $selection, items: pickerItems)
.background(Color.gray.opacity(0.10).clipShape(Capsule()))
.padding(.horizontal, 5)
}
}
Any ideas how to fix this?
Upvotes: 2
Views: 902
Reputation: 459
I managed to solve all the animation issues I had with the picker implementation that uses PreferenceKey
s when the items have different frame sizes. This does not solve the issue I have with the zIndex
and the .matchedGeometryEffect()
, so I will not accept my own answer, but I'll post it as a reference in case anyone needs it in the future.
The code:
public struct PKPicker: View {
@Binding var selection: Int
@State private var frames: [CGRect] = []
let items: [String]
public init(
selection: Binding<Int>,
items: [String])
{
self._selection = selection
self._frames = State(wrappedValue: Array<CGRect>(repeating: CGRect(),
count: items.count))
self.items = items
}
public var body: some View {
ZStack(alignment: .topLeading) {
selector
HStack {
ForEach(items.indices) { index in
itemView(for: index)
}
}
}
.onPreferenceChange(PKPickerItemPreferenceKey.self) { preferences in
preferences.forEach { frames[$0.id] = $0.frame }
}
.coordinateSpace(name: "picker2")
}
var selector: some View {
Color.gray.opacity(0.25).clipShape(Capsule())
.frame(width: frames[selection].size.width,
height: frames[selection].size.height)
.offset(x: frames[selection].minX, y: frames[selection].minY)
}
func itemView(for index: Int) -> some View {
Text(items[index])
.fixedSize()
.padding(7)
.foregroundColor(isSelected(index) ? .black : .gray)
.font( .caption)
.onTapGesture { selection = index }
.background(PKPickerItemPreferenceSetter(id: index))
}
func isSelected(_ index: Int) -> Bool {
index == selection
}
}
struct PKPickerItemPreferenceData: Equatable {
let id: Int
let frame: CGRect
}
struct PKPickerItemPreferenceKey: PreferenceKey {
typealias Value = [PKPickerItemPreferenceData]
static var defaultValue: [PKPickerItemPreferenceData] = []
static func reduce(
value: inout [PKPickerItemPreferenceData],
nextValue: () -> [PKPickerItemPreferenceData])
{
value.append(contentsOf: nextValue())
}
}
struct PKPickerItemPreferenceSetter: View {
let id: Int
let coordinateSpace = CoordinateSpace.named("picker2")
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: PKPickerItemPreferenceKey.self,
value: [PKPickerItemPreferenceData(
id: id, frame: geometry.frame(in: coordinateSpace))])
}
}
}
And in ContentView
struct ContentView: View { @State private var selection = 0
let pickerItems = [ "Item 1", "Long item 2", "Item 3", "Item 4", "Long Item 5"]
var body: some View {
PKPicker(selection: $selection.animation(.easeInOut), items: pickerItems)
.padding(7)
.background(Color.gray.opacity(0.10).clipShape(Capsule()))
.padding(5)
}
Result:
Upvotes: 0