I have a square that is embedded within a scroll view inside a subview. When the square is clicked I want to animate it to the top of the view hierarchy and present it above everything else, including .sheets, using a matched geometry effect. I cannot get the animation to work. When the square presented at the top is dismissed it should animate back down to the original view.
Problem I present the square at the top of the view hierarchy through a window which makes the animation fail. How can I correctly animate with a window? How can I add basic match geometry effects, transitions, and other animations with windows?
struct SomeView: View {
@EnvironmentObject var popRoot: PopToRoot
@Namespace private var animation
var body: some View {
ScrollView {
RoundedRectangle(cornerRadius: 10)
.matchedGeometryEffect(id: "squareAnim", in: animation)
.foregroundStyle(.blue).frame(width: 50, height: 50)
.onTapGesture {
withAnimation {
popRoot.showOverlay = true
}
}
//other sub views
}
}
}
struct ContentView: View { //top of view hierarchy
@EnvironmentObject var popRoot: PopToRoot
@Namespace private var animation
var body: some View {
SomeView()
.overlay {
if showOverlay {
TopViewWindow()
.matchedGeometryEffect(id: "squareAnim", in: animation)
// ----- HERE
}
}
}
}
class PopToRoot: ObservableObject { //envirnment object to be accessed anywhere
@Published var showOverlay = false
}
video player that goes above everything
struct TopView: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.blue).frame(width: 100, height: 100)
//other views
}.ignoresSafeArea()
}
}
struct TopViewWindow: View {
@State private var hostingController: UIHostingController<TopView>? = nil
func showImage() {
let swiftUIView = TopView()
hostingController = UIHostingController(rootView: swiftUIView)
hostingController?.view.backgroundColor = .clear
hostingController?.view.frame = CGRect(
x: 0,
y: 0,
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
// withAnimation { doesnt work here either
window.addSubview(hostingController!.view)
hostingController?.view.center.x = window.center.x
}
}
func dismissImage() {
hostingController?.view.removeFromSuperview()
hostingController = nil
}
var body: some View {
VStack {}.onAppear { showImage() }.onDisappear { dismissImage() }
}
}

matchedGeometryEffects if they're wrapped inside of a UIHostingControllerSorry, but you're trying to inject a SwiftUI view (wrapped in a UIHostingController) into your UIApplication.shared.connectedScenes.first. There's just no way SwiftUI will be able to match up your matchedGeometryEffects. If for some reason you really need to have a UIHostingController layer, I would recommend doing the matchedGeometryEffect with a placeholder and then once the animation is finished, quickly swapping the placeholder out with a UIHostingController. If you do this instantly with no animations, the user won't know the difference.
.frame(width: 50, height: 50) lines after the matchedGeometryEffectThis will create a smooth animation between the sizes. This is because you're telling SwiftUI the core element that needs to stay the same. When you put the frame after, you're saying "Transition this blue rectangle through these size changes"
let namespace: Namespace.IDThis makes sure the view doesn't get lost from SwiftUI's perspective
if statement (optional)matchedGeometryEffect relies on one view being removed while another is inserted at the exact same time. I like to have the if statement at the top level so it's clear where the transition happens. It makes it easier to see instead of burying the if statement inside low level views.
class PopToRoot: ObservableObject {
static let shared = PopToRoot()
private init() {}
@Published var showOverlay = false
}
struct TopView: View {
@ObservedObject var popToRoot = PopToRoot.shared
let namespace: Namespace.ID // Pass namespace like this
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.blue)
.matchedGeometryEffect(id: "squareAnim", in: namespace)
.frame(width: 100, height: 100) // Move frame here
.onTapGesture {
withAnimation {
popToRoot.showOverlay = false
}
}
//other views
}
.ignoresSafeArea()
}
}
struct SomeView: View {
@ObservedObject var popToRoot = PopToRoot.shared
let namespace: Namespace.ID // Pass namespace like this
var body: some View {
ScrollView {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.blue)
.matchedGeometryEffect(id: "squareAnim", in: namespace)
.frame(width: 50, height: 50) // Move frame here
.onTapGesture {
withAnimation {
popToRoot.showOverlay = true
}
}
}
}
}
struct ContentView: View {
@ObservedObject var popToRoot = PopToRoot.shared
@Namespace private var namespace
var body: some View {
if popToRoot.showOverlay { // Top level if statement (optional)
TopView(namespace: namespace)
} else {
SomeView(namespace: namespace)
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With