1

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.

Ideal Effect

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.

Messed Up Effect

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.

Partially fixed the probelm

GeekNomore
  • 13
  • 4

0 Answers0