In this segment, you’ll use the matchedGeometryEffect
method to synchronize multiple views’ animations. Open AwardsView.swift under the AwardsView group. When you tap an award, it transitions to a view displaying its details. You’ll change it to pop up the award details over the grid.
Add the following code to the top of the view after the flightNavigation
:
@State var selectedAward: AwardInformation?
When the user taps an award, you’ll store it in this optional state variable. Otherwise, the property will be nil
. Because that tap action occurs in a subview, you need to pass this into that subview.
Open AwardGrid.swift. Add the following binding after the awards
property:
@Binding var selected: AwardInformation?
Using this binding, you’ll pass the state from the AwardsView
to the AwardGrid
. Replace the NavigationLink
in the ForEach
loop with:
AwardCardView(award: award)
.foregroundColor(.black)
.aspectRatio(0.67, contentMode: .fit)
.onTapGesture {
selected = award
}
You’ve removed the navigation link and added an onTapGesture(count:perform:)
modifier to set the binding to the tapped award.
You also must update the preview to add the new binding parameter. Change it to:
AwardGrid(
title: "Test",
awards: AppEnvironment().awardList,
selected: .constant(nil)
)
Now, go back to AwardsView.swift and change the view’s body to:
ZStack {
// 1
if let award = selectedAward {
// 2
AwardDetails(award: award)
.background(Color.white)
.shadow(radius: 5.0)
.clipShape(RoundedRectangle(cornerRadius: 20.0))
// 3
.onTapGesture {
selectedAward = nil
}
// 4
.navigationTitle(award.title)
} else {
ScrollView {
LazyVGrid(columns: awardColumns) {
AwardGrid(
title: "Awarded",
awards: activeAwards,
selected: $selectedAward
)
AwardGrid(
title: "Not Awarded",
awards: inactiveAwards,
selected: $selectedAward
)
}
}
.navigationTitle("Your Awards")
}
}
.background(
Image("background-view")
.resizable()
.frame(maxWidth: .infinity, maxHeight: .infinity)
)
You now have a ZStack
that shows one of two views depending on the if
statement results. The code in the else
condition didn’t change other than passing the binding to the selectedAward
state variable. Some changes are worth noting:
-
The first change is that you attempt to unwrap the
selectedAward
state variable. If that fails, you show the grid as before in theelse
part of the statement. -
If the unwrapping succeeds, you display the
AwardDetails
view that was previously theNavigationLink
target. -
When the user taps the view, you set the
selectedAward
state variable back tonil
. This change removes theAwardDetails
view and displays the grid. - You set the title to the current award’s name.
Run the app. Tap Your Awards and then tap any award in the grid. You see the view changes to the large details display for the award. Tap once more and the grid reappears.
The transition is abrupt. You already know you can fix that by adding a view transition. Find the onTapGesture
modifier in AwardsView
, under comment three, and change it to:
.onTapGesture {
withAnimation {
selectedAward = nil
}
}
Hopefully, you remember from the last section that this tells SwiftUI to animate events caused by the state change in the closure. For the other end of the transition in AwardGrid.swift, find the onTapGesture
modifier in AwardGrid
and change it to:
.onTapGesture {
withAnimation {
selected = award
}
}
Run the app, and you’ll find the change works better. Now, you have the default fade-out/fade-in effect that smooths the previously harsh transitions between the views. But there’s still no sense of connection between the views. That’s where matchedGeometryEffect(id:in:properties:anchor:isSource:)
comes in. It lets you connect the two view transitions.
You must specify the first two parameters. The id
works much like other IDs you’ve encountered in SwiftUI. It uniquely identifies a connection, so giving two items the same id
links their animations. You pass a Namespace
to the in
property. The namespace groups related items, and the two together define unique links between views.
Creating a namespace is simple. At the top of AwardsView
, add the following code after the selectedAward
state variable.
@Namespace var cardNamespace
Now, you have a unique namespace for the method.
After the onTapGesture
modifier is attached to AwardDetails
(before comment 4), add the following:
.matchedGeometryEffect(
id: award.hashValue,
in: cardNamespace,
anchor: .topLeading
)
You use the existing hashValue
property as the identifier along with your created namespace. You can use any identifier as long as it’s unique in the namespace and consistent. You also specify the anchor
parameter to identify a location in the view used to produce the shared values. It’s not always needed, but in this case, it improves the animation.
Now, you have one side set up but need to link the state change in the subview. To do so, you must pass the namespace into that view. Change the AwardGrid
s in the LazyVGrid
to add the namespace as a parameter:
AwardGrid(
title: "Awarded",
awards: activeAwards,
selected: $selectedAward,
namespace: cardNamespace
)
AwardGrid(
title: "Not Awarded",
awards: inactiveAwards,
selected: $selectedAward,
namespace: cardNamespace
)
Now, open AwardGrid. First, you must add a property to capture the passed namespace. After the selected
binding, add the following code:
var namespace: Namespace.ID
When you pass a namespace into a view, it comes in as a Namespace.ID
type. Add the following code after the onTapGesture(count:perform:)
call:
.matchedGeometryEffect(
id: award.hashValue,
in: namespace,
anchor: .topLeading
)
Notice this uses the namespace you passed in and therefore is the same namespace as the parent view. Again, you also use the hashValue
property on the award, the same as you used in the parent view. SwiftUI knows to link the transitions when you pass in the same namespace.
You also need to update the preview to pass this new parameter:
#Preview {
@Namespace var namespace
return AwardGrid(
title: "Test",
awards: AppEnvironment().awardList,
selected: .constant(nil),
namespace: namespace
)
}
Run the app now. When you tap an award in the small grid, it shifts and expands while changing to the AwardDetails
view. Similarly, when you tap AwardDetails
view, it appears to shrink and move back to the smaller view in the grid.
Adding matchedGeometryEffect()
only arranges for the views’ geometry to be linked. The usual transition mechanisms applied to the views still occur during the transition.