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
}
}
}
}
2
Answers
You’ve got a lot going on here! The solution is to clean it up a bit.
I’ve put your original code and my solution on GitHub for you here. You’ll want to adjust it a bit because I didn’t center the detailed view on the screen like you did, but everything else works.
First, you shouldn’t define new views as variables or functions. Make a whole new struct that conforms to View and has its own body instead, and pass in any values needed from the parent. This is going to allow SwiftUI to keep track of everything better, and it will automatically redraw all subviews if any state changes in the parent view.
Once you’ve done that, you want to put the
.matchedGeometryEffect
modifier in two spots that you know are going to be for the same view. In your original code, you only have it once on thepersonView
, which is sometimes rendered inside of adetailView
and sometimes not. Just bring it down to one spot where the personView is small and another where it’s large.As you can see in the code sample and in the repo I’ve attached, I’ve used it once inside the conditional and again inside the overlay’s conditional. Both of them are directly on
PersonView
.You’ve made a really cool effect! Just keep working with SwiftUI and try to learn "the Apple way" to think about how views are composed, and… well, you’ll still run into these problems lol. But hopefully they’ll be a little easier to diagnose.
ETA: I took out the
.transition
as well, but that’s not reflected in the screenshot. Whoops. It removes the "jump" from 300 to 100 height at the end of the animation.Also removed an image and replaced with text.
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
:You might also want to make sure that the rectangles which are shown as placeholders for the cards when a selection is active have the same size and rounded corners as the cards themselves.
Also as a suggestion, you don’t need a separate state variable for when a selection is active. A computed property can be used instead:
As for the other comments made in another answer, your use of computed properties and functions for returning Views seems fine to me. I use this technique a lot for breaking down large view sections into smaller sections. But we all have our own personal preferences and opinions.