I've been trying for sometime now to figure out why the .matchGeometryEffect is not transitioning smoothly in my use case. The state change speed is not consistent, growing quicker and going back slower. Similarly, the transition is also broken for going back and clipping. Also I would like to avoid the fading effect in the end.
I set up an example that represents the issue. Any advice would be much appreciated.
struct PeopleView: View {
struct Person: Identifiable {
let id: UUID = UUID()
let first: String
let last: String
}
@Namespace var animationNamespace
@State private var isDetailPresented = false
@State private var selectedPerson: Person? = nil
let people: [Person] = [
Person(first: "John", last: "Doe"),
Person(first: "Jane", last: "Doe")
]
var body: some View {
homeView
.overlay {
if isDetailPresented, let selectedPerson {
detailView(person: selectedPerson)
.transition(.asymmetric(insertion: .identity, removal: .offset(y: 5)))
}
}
}
var homeView: some View {
ScrollView {
VStack {
cardScrollView
}
}
}
var cardScrollView: some View {
ScrollView(.horizontal) {
HStack {
ForEach(people) { person in
if !isDetailPresented {
personView(person: person, size: 100)
.onTapGesture {
withAnimation(.interactiveSpring(response: 0.3, dampingFraction: 0.8, blendDuration: 0.8)){
self.selectedPerson = person
self.isDetailPresented = true
}
}
}
else {
Rectangle()
.frame(width: 50, height: 100)
}
}
}
}
}
func personView(person: Person, size: CGFloat) -> some View {
Group {
Text(person.first)
.padding()
.frame(height: size)
.background(Color.gray)
.cornerRadius(5)
.shadow(radius: 5)
}
.matchedGeometryEffect(id: person.id, in: animationNamespace)
}
func detailView(person: Person) -> some View {
VStack {
personView(person: person, size: 300)
Text(person.first + " " + person.last)
}
.onTapGesture {
withAnimation {
self.isDetailPresented = false
self.selectedPerson = nil
}
}
}
}
The animation works a lot better if you make two small tweaks:
.transition
modifier on thedetailView
. This is what is causing the sudden "chop off" at the end of your animation..top
for thematchedGeometryEffect
:Making it better still
When a card is selected, you can see how the card fades out and the detail fades in. Then when the detail is de-selected, the detail fades out and the card fades back in. This is because the views are distinct separate views, so an opactiy transition is happening as one view is replaced by the other.
The animation can be made much smoother by having one single view moving from position A to position B. To do it this way, you could have a base position for the card and a base position for the detail and the person view could move between them.
The key to getting this to work is for the moving view to be outside of the
ScrollView
s, otherwise these perform clipping and the moving view disappears out of sight.So here is a re-factored example that works this way:
You will see that
matchedGeometryEffect
is now applied in several places. The card bases and the detail base all haveisSource
set to true. The person views haveisSource
set to false, so their positions are determined by the source views with matching ids. Opacity is used to hide content that is not moving but shouldn't be visible.You will also notice that I am using computed properties and functions to build views, just as you were doing. I find this a good way to break down a big view into small views, so that you don't end up with one huge
body
. It seems that the author of another answer doesn't agree, but we all have our own personal preferences and opinions.