I am using following code, This works perfectly fine but its full screen, however in case of App Store animation, its not full screen view. I have tried popover and custom view as well but didnt work.
struct ContentView: View {
let icons = [
Icon(id: "figure.badminton", color: .red),
Icon(id: "figure.fencing", color: .orange),
Icon(id: "figure.gymnastics", color: .green),
Icon(id: "figure.indoor.cycle", color: .blue),
Icon(id: "figure.outdoor.cycle", color: .purple),
Icon(id: "figure.rower", color: .indigo),
]
@Namespace var animation
@State private var selected: Icon?
var body: some View {
ZStack {
LazyVGrid(columns: [.init(.adaptive(minimum: 100, maximum: 300))]) {
ForEach(icons) { icon in
Button {
selected = icon
} label: {
Image(systemName: icon.id)
}
.foregroundStyle(icon.color.gradient)
.font(.system(size: 100))
.background(Color.yellow.opacity(0.5))
.matchedTransitionSource(id: icon.id, in: animation)
}
}
.sheet(item: $selected, content: { icon in
VStack(content: {
DestinationView(icon: icon, animation: animation)
.background(Color.yellow.opacity(0.5))
.presentationDetents([.medium])
})
})
.background(Color.yellow.opacity(0.5))
}
.background(.yellow)
.ignoresSafeArea(.all)
}
}
The view which will be presented is
struct DestinationView: View {
var icon: Icon
var animation: Namespace.ID
var body: some View {
Image(systemName: icon.id)
.font(.system(size: 200))
.frame(width: 250, height: 250)
.foregroundStyle(icon.color.gradient)
.background(Color.yellow.opacity(0.5))
.navigationTransition(.zoom(sourceID: icon.id, in: animation))
}
}
Instead of using a sheet, you could consider showing the selected view as the top layer of the ZStack
and using .matchedGeometryEffect
for the animation. This might give you more control over the appearance and animation.
The technique is illustrated in the second part of this answer (it was my answer). To adapt it to your case:
Add a second state variable isShowingSelection
for driving the animation.
Only use .matchedGeometryEffect
to determine the .position
of the DestinationView
, not its size too.
There is no need for a separate placeholder for the enlarged view. The container (the ZStack
) can be used for determining the position instead.
Apply a scale effect to DestinationView
as it is being shown. Since the font size changes from 100 to 200, it works well to scale from 0.5 to 1.0.
You might want to add a shadow effect to the enlarged view too.
Use an intermediate layer to dim the background when a selection is showing. A tap gesture can be added to this layer, to reset the selection.
By using .matchedGeometryEffect
to determine only the position for DestinationView
, not its size, the fixed font size that DestinationView
is using does not cause animation issues. This should also work better if the view contains some text too, because the size of the destination view doesn't change during animation (it is scaled instead).
struct ContentView: View {
let icons = [
Icon(id: "figure.badminton", color: .red),
Icon(id: "figure.fencing", color: .orange),
Icon(id: "figure.gymnastics", color: .green),
Icon(id: "figure.indoor.cycle", color: .blue),
Icon(id: "figure.outdoor.cycle", color: .purple),
Icon(id: "figure.rower", color: .indigo),
]
@Namespace private var animation
@State private var selected: Icon?
@State private var isShowingSelection = false
let containerId = "container"
var body: some View {
ZStack {
LazyVGrid(columns: [.init(.adaptive(minimum: 100, maximum: 300))]) {
ForEach(icons) { icon in
Button {
selected = icon
} label: {
Image(systemName: icon.id)
}
.foregroundStyle(icon.color.gradient)
.font(.system(size: 100))
.matchedGeometryEffect(id: icon.id, in: animation)
}
}
if let icon = selected {
// Add a dimming layer and use to intercept taps
Color.black
.opacity(isShowingSelection ? 0.25 : 0)
.animation(.easeInOut(duration: 0.2), value: isShowingSelection)
.onTapGesture {
withAnimation(.easeInOut) {
isShowingSelection = false
} completion: {
selected = nil
}
}
.ignoresSafeArea()
// Show the selected view
DestinationView(icon: icon)
.compositingGroup()
.shadow(radius: isShowingSelection ? 10 : 0)
.allowsHitTesting(false)
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
.scaleEffect(isShowingSelection ? 1 : 0.5)
.matchedGeometryEffect(
id: isShowingSelection ? containerId : icon.id,
in: animation,
properties: .position,
isSource: false
)
.onAppear {
withAnimation(.bouncy(extraBounce: 0.1)) {
isShowingSelection = true
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.matchedGeometryEffect(id: containerId, in: animation)
.background(.yellow.opacity(0.5))
}
}
struct DestinationView: View {
let icon: Icon
var body: some View {
Image(systemName: icon.id)
.font(.system(size: 200))
.foregroundStyle(icon.color.gradient)
.padding()
.background(.yellow, in: .rect(cornerRadius: 20))
}
}
struct Icon: Identifiable {
let id: String
let color: Color
}