Search code examples
swiftuimatchedgeometryeffect

SwiftUI: Creating a custom segmented control using .matchedGeometry()


I am currently trying to build some kind of custom segmented control element. My code currently looks like this:

@AppStorage("selectedcountry") private var selectedCountry: Country = .france
@Namespace private var animation

HStack {
    ForEach(Country.allCases, id: \.self) { country in
        Button {
            withAnimation {
                selectedCountry = country
            }
        } label: {
            Text(country.flag)
                .padding()
        }
        .frame(maxWidth: .infinity)
        .background {
            if selectedCountry == country {
                RoundedRectangle(cornerRadius: 10)
                    .fill(Color.accentColor)
                    .matchedGeometryEffect(id: "country", in: animation)
            }
        }
    }
}

The "RoundedRectangle(cornerRadius: 10)" element is supposed to slide around like in the regular iOS segmented controls. But somehow I just don't get the matched geometry effect to work.

Any advice that you could give me?


Solution

  • Here's how to get it working:

    • There only needs to be one marker, which can be set as the background behind the HStack.
    • The id of the background marker needs to match to the id of the selected item.
    • The source for matching the geometry effect needs to be identified. Only one item may be the source at any time.
    HStack {
        ForEach(Country.allCases, id: \.self) { country in
            Button {
                withAnimation {
                    selectedCountry = country
                }
            } label: {
                Text(country.flag)
                    .padding()
            }
            .matchedGeometryEffect(id: country, in: animation, isSource: selectedCountry == country)
            .frame(maxWidth: .infinity)
        }
    }
    .background {
        RoundedRectangle(cornerRadius: 10)
            .fill(Color.accentColor)
            .matchedGeometryEffect(id: selectedCountry, in: animation, isSource: false)
    }
    

    I couldn't understand why the marker was jumping from one position to another, instead of moving in an animated way. But it's because, the variable that gets updated is @AppStorage instead of @State. When you change it to @State, animations work fine:

    @State private var selectedCountry: Country = .france
    

    Animation

    See SwiftUI: save the state of toggle and keep the animation for some workarounds.