I was working on an app store mock up project and I was trying to implement the "Hero" Animation.
At first the matchedGeometryEffect was working fine for the home page.
However on another tab view, when the same picture was show in another style, the swiftUI started confusing the pictures. Including the transition to the detail page. swiftUI can not track the source correctly.
Here is the relevant codes.
TabView:
struct TabBar: View {
@Namespace var animation
@StateObject var detailObject = DetailPageViewModel()
var body: some View {
ZStack {
TabView {
//Home Page
HomePage(animation: animation)
.environmentObject(detailObject)
.tabItem({
Image(systemName: "house")
Text("Home")
})
//Saved Page
SavedPage(animation: animation)
.environmentObject(detailObject)
.tabItem({
Image(systemName: "square.stack.3d.down.right")
Text("Saved")
})
}
// hiding TabView when show detail page
.opacity(detailObject.show ? 0 : 1)
if detailObject.show {
DetailPage(detailPageViewModel: detailObject, animation: animation)
}
}
}
}
HomePage:
struct HomePage: View {
var animation: Namespace.ID //For Homepage animation
@EnvironmentObject var detailPageViewModel : DetailPageViewModel
var body: some View {
ScrollView(.vertical, showsIndicators: true){
Spacer()
HStack {
VStack(alignment: .leading) {
//Banner info
}
.padding(.leading, 15)
Spacer()
}
LazyVStack (alignment: .leading) {
ForEach (detailPageViewModel.HomepageList){ recipeDets in
HomePageCard(recipeDetail: recipeDets, animation: animation)
.onTapGesture {
withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.8)){
detailPageViewModel.selectedRecipe = recipeDets
detailPageViewModel.show.toggle()
}
}
}
}
}
.onAppear {
detailPageViewModel.getData()
}
}
}
HomePage Card:
struct HomePageCard: View {
var recipeDetail : RecipeDetail
// getting environment scheme color
@Environment(\.colorScheme) var color
var animation: Namespace.ID
var body: some View {
VStack{
LazyImage(url: URL(string: recipeDetail.thumbnailPath))
.aspectRatio(contentMode: .fill)
.matchedGeometryEffect(id: recipeDetail.thumbnailPath, in: animation)
HStack{
VStack(alignment: .leading) {
Text(recipeDetail.kcals) // should be determined by the sorting conditions in the settings.
.font(.headline)
.foregroundColor(.secondary)
Text(recipeDetail.name)
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text(shortDescriptionGenerator(recipeDetail.description))
.foregroundColor(.secondary)
}
.layoutPriority(100)
Spacer()
}
.matchedGeometryEffect(id: recipeDetail.id, in: animation)
.padding()
}
.background(color == .dark ? Color(red: 152/255, green: 152/255, blue: 157/255, opacity: 0.1) : Color(red: 243/255, green: 241/255, blue: 241/255, opacity: 1.0))
.cornerRadius(20)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color(.sRGB, red: 150/255, green: 150/255, blue: 150/255, opacity: color == .dark ? 0.6 : 0.2), lineWidth: 1)
)
.padding([.top, .horizontal])
}
}
Saved Page:
struct SavedPage: View {
//@ObservedObject var detailPageViewModel = DetailPageViewModel()
var animation: Namespace.ID
@EnvironmentObject var detailPageViewModel: DetailPageViewModel
var body: some View {
ScrollView(.vertical, showsIndicators: true){
Spacer()
HStack {
VStack(alignment: .leading) {
// Banner Info
}
.padding(.leading, 15)
Spacer()
}
LazyVStack (alignment: .leading) {
ForEach (detailPageViewModel.HomepageList){ recipeDets in
RegularCard(recipeDetail: recipeDets, animation: animation)
.onTapGesture {
withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.8)){
detailPageViewModel.selectedRecipe = recipeDets
detailPageViewModel.show.toggle()
print(detailPageViewModel.show)
}
}
}
}
}.onAppear{
detailPageViewModel.getData()
}
}
}
Saved Page Card
struct RegularCard: View {
var recipeDetail : RecipeDetail
var animation: Namespace.ID
@Environment(\.colorScheme) var color
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill()
.foregroundColor(color == .dark ? Color(red: 152/255, green: 152/255, blue: 157/255, opacity: 0.2) : Color(red: 243/255, green: 241/255, blue: 241/255, opacity: 1.0))
.aspectRatio(2.2/1, contentMode: .fit)
HStack {
LazyImage(url: URL(string: recipeDetail.thumbnailPath))
.aspectRatio(contentMode: .fill)
.matchedGeometryEffect(id: recipeDetail.thumbnailPath, in: animation)
.frame(width: 120, height: 120)
.cornerRadius(10.0)
.padding([.top, .leading, .bottom])
VStack (alignment: .leading){
Text(recipeDetail.name)
.font(.system(.title2))
.padding(.top)
Text(recipeDetail.methodSummary)
.padding([.bottom, .trailing])
.font(.system(.headline))
Spacer()
HStack(alignment: .center){
Image(systemName: recipeDetail.isLiked ? "hand.thumbsup.fill" : "hand.thumbsup")
.imageScale(.small)
Text("\(recipeDetail.likes)")
.font(.callout)
Image(systemName: "flame")
.imageScale(.small)
Text(recipeDetail.kcals)
.font(.callout)
Text("\(recipeDetail.ratings, specifier: "%.1f")")
.font(.callout)
Image(systemName: recipeDetail.isSaved ? "bookmark.fill" : "bookmark")
.imageScale(.small)
}.layoutPriority(100)
Spacer()
}
Spacer()
}
}
.padding(.horizontal, 9.0)
}
}
Detail Page:
struct DetailPage: View {
@ObservedObject var detailPageViewModel : DetailPageViewModel
var animation: Namespace.ID
//var imageType: String
var body: some View {
ScrollView {
ZStack {
LazyImage(url: URL(string: detailPageViewModel.selectedRecipe.thumbnailPath), resizingMode: .aspectFill)
.aspectRatio(contentMode: .fit)
.matchedGeometryEffect(id: detailPageViewModel.selectedRecipe.thumbnailPath, in: animation)
.ignoresSafeArea()
VStack{
HStack {
Spacer()
Button(action: {
withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.8)){
detailPageViewModel.show.toggle()
}
}){
Image(systemName: "xmark")
.imageScale(.medium)
.foregroundColor(Color.black.opacity(0.7))
.padding()
.background(Color.white.opacity(0.7))
.clipShape(Circle())
}
}.padding()
Spacer()
}
}
}
.ignoresSafeArea(.all, edges: .top)
.statusBar(hidden: true)
}
}
Then I add isSource: false
to HomePageCard and SavedPageCard's matchedGeometryEffect
to "separate" them from each other. Now swiftUI will not confuse them in TabView. However, when quitting from the DetailPage to HomePage or SavedPage, the image cannot scale itself correctly.