I have two views and am trying to Matched Geometry animate from one view where the geometry is deep to an overlay view. However, I keep getting an error in the console, claiming there are two geometries with the same id and isSource: true
. I'm purposely not setting source because I want the animation to work forward and backward. Code is below and attempts are further down. (See code comments where the conditional rendering occurs)
Omitted a bunch of code because the view is fairly complex. The error is for all matched geometries in the view, here are two examples. Optional Matched Geometry is a helper that maps to
matchedGeometryEffect
if id and ns are both defined, otherwise returns the view.
Parent View:
struct ExploreView: View {
let ns: Namespace.ID
var navPath: Binding<ExploreNavigationPath>
@StateObject var sliderModel = ExplorePackageModel()
var body: some View {
GeometryReader { geom in
ZStack {
VStack(...) {
...
PackageSlider(ns: ns) { pkg in
withAnimation(.spring) {
sliderModel.displayPkgDetails = pkg
}
}
}
...
.frame(height: geom.size.height)
// CONDITIONAL RENDER FOR ANIMATING TO
if let pkg = sliderModel.displayPkgDetails {
PackageView(
ns: ns,
optimisticData: pkg
) {
withAnimation(.spring) {
sliderModel.displayPkgDetails = nil
}
}
.zIndex(100)
}
}
}
.ignoresSafeArea(.keyboard, edges: [.top])
.environmentObject(sliderModel)
}
}
Child Default View, Drawn Underneath Overlay (animating from)
struct PackageSlider: View {
var ns: Namespace.ID? = nil
var onSlidePress: ((MPackageSlide) -> Void)? = nil
@EnvironmentObject var model: ExplorePackageModel
var body: some View {
GeometryReader { proxy in
ScrollView(...) {
LazyVStack(
spacing: proxy.safeAreaInsets.bottom / 2
) {
let loadingSlide = PackageSlide()
.frame(height: proxy.size.height + proxy.safeAreaInsets.top)
switch self.model.state {
case .Start, .Loading:
loadingSlide // correctly prints onAppear, onDisappear
case .LoadingNewData(oldData: let data), .Success(data: let data):
ForEach(data) { pkg in
Group { // Group was an attempt that doesn't seem to affect anything
// CONDITIONAL RENDER FOR ANIMATING FROM
if model.displayPkgDetails?.id == pkg.id {
// A print here DOES occur on transition
loadingSlide
.zIndex(0)
.id(pkg.id)
} else {
PackageSlide(
ns: ns,
onSlidePress: onSlidePress,
pkg: pkg
)
.frame(height: proxy.size.height + proxy.safeAreaInsets.top)
.id(pkg.id)
}
}
}
if case .LoadingNewData(oldData: _) = self.model.state {
loadingSlide
}
case .Error(error: _):
...
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
...
}
}
}
struct PackageSlide: View {
var ns: Namespace.ID? = nil
...
var pkg: MPackageSlide? = nil
var id: String? { pkg?.id }
... // other properties unwrapped similarly
var nsType: PackageNS { PackageNS(id) }
func safePressHandler() {
if let pkg = pkg {
onSlidePress?(pkg)
}
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
...
// Example matched geometry, others omitted for concision
PackageFooter(prices: prices) {
safePressHandler()
}
.optionalMatchedGeometry(id: nsType.bookFooter, in: ns)
}
.onAppear {
print("pkg view \(String(describing: id)) loading")
}
.onDisappear {
print("pkg view \(String(describing: id)) unloading")
}
}
}
Child Overlay Snippet (animating to)
PackageDescription(description: optimisticData?.description)
.frame(maxWidth: .infinity)
.optionalMatchedGeometry(id: nsType.packageDesc, in: ns)
PackageProperties(properties: optimisticData?.properties)
.optionalMatchedGeometry(id: nsType.packageProps, in: ns)
}
Attempts and Suspicions
Weirdly enough, there doesn't seem to be an unloading of the target view when transitioning. onDisappear
is not printed at all when I trigger the transition. Perhaps this is something to do with the ForEach? onDisappear
does trigger for the other loadingView usages. Interestingly, putting a print in the ForEach render away branch does seem trigger. I suspect the new branch is running but the old branch's view is not unloading.
I'm also hesitant to remove the package with the id from the model because the model manages loading the data; concurrency issues can make a mess if I simply store and reload the package with the id.
Today I learned a valuable lesson on
id
's in SwiftUI.Because the id was not changed, SwiftUI did not see the need to rerender the two branches in the ForEach.