ios – Scroll to item with animation issue


In a scrollview with LazyHStack, I’m running into an issue with smoothly scrolling a tapped item into view. This is the animation Im trying to achieve:

enter image description here

Note that the item hovered on above smoothly expands into view with items shifting on either side.

My scrollview item is structured such that it expands with animation on tap. However for a scroll view item that is partially visible, I’m unable to have it expand as well as scroll completely into view.

My code is as follows:

struct CarouselItem: Identifiable, Equatable {
    private(set) var id: UUID = .init()
    var title: String
    var year: String
    var runtime: String
    var certification: String
    var posterImage: String
    var stillImage: String
}

struct CarousalItemView: View {
    @State var item: CarouselItem
    @Binding var selectedItem: CarouselItem?
    @Namespace private var namespace
    private var expanded: Bool {
        item.id == selectedItem?.id
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            ZStack {
                Image(item.posterImage)
                    .resizable()
                    .scaledToFill()
                    .scaleEffect(expanded ?  1.5 : 1, anchor: .topLeading)
                    .frame(width: expanded ? 250 : 150, height: 200)
                    .matchedGeometryEffect(id: "image", in: namespace, anchor: .topLeading, isSource: true)
                    .opacity(expanded ? 0 : 1)
                    .animation(.easeOut(duration: 0.3), value: expanded)
                
                Image(item.stillImage)
                    .resizable()
                    .scaledToFill()
                    .frame(width: expanded ? 250 : 150, height: 200)
                    .matchedGeometryEffect(id: "image", in: namespace, anchor: .topLeading, isSource: false)
                    .opacity(expanded ? 1 : 0)
                    .animation(.easeInOut(duration: 0.2).delay(expanded ? 0.1 : 0), value: expanded)
                    .overlay(alignment: .leading) {
                        if expanded {
                            itemDetailsView()
                        }
                    }
            }
            .clipShape(RoundedRectangle(cornerRadius: 10))

            Text(item.title)
                .frame(height: 50)
        }
    }
    
    @ViewBuilder func itemDetailsView() -> some View {
        VStack(alignment: .leading, spacing: 15) {
            Spacer()
            Text(item.title)
                .font(.title2)
            HStack {
                Text("CBFC: " + item.certification)
                    .padding(.horizontal, 5)
                    .overlay {
                        Rectangle()
                            .stroke(.white, lineWidth: 1)
                    }
                Text(item.runtime)
                Text(item.year)
            }
            .shadow(color: .gray, radius: 5, y: 5)
            Button {

            } label: {
                Text("Trailer")
            }
            .buttonStyle(.borderedProminent)
            .tint(.gray)
            .padding(.bottom, 15)
        }
        .padding(.horizontal, 10)
    }
}

struct ContentView: View {
    var items: [CarouselItem] = [
        .init(title: "Oppenheimer", year: "2023", runtime: "3h 1m", certification: "UA", posterImage: "poster1", stillImage: "still1"),
        .init(title: "Barbie", year: "2023", runtime: "1h 54m", certification: "PG", posterImage: "poster2", stillImage: "still2"),
        .init(title: "The Martian", year: "2015", runtime: "2h 24m", certification: "UA", posterImage: "poster3", stillImage: "still3"),
        .init(title: "The Shawshank Redemption", year: "1994", runtime: "2h 22m", certification: "U", posterImage: "poster4", stillImage: "still4"),
        .init(title: "The Godfather", year: "1972", runtime: "2h 55m", certification: "UA", posterImage: "poster5", stillImage: "still5"),
        .init(title: "The Dark Knight", year: "2008", runtime: "2h 32m", certification: "UA", posterImage: "poster6", stillImage: "still6")
    ]
    @State var selectedItem: CarouselItem?
    var body: some View {
        VStack {
            HStack {
                Text("Popular films")
                    .font(.title2.bold())
                Spacer()
            }
            
            // MARK: Carousal
            ScrollViewReader { proxy in
                ScrollView(.horizontal) {
                    LazyHStack(spacing: 20) {
                        ForEach(items, id: \.id) { item in
                            CarousalItemView(item: item, selectedItem: $selectedItem)
                                .frame(width: selectedItem?.id != item.id ? 150 : 250, height: 250)
                                .clipShape(RoundedRectangle(cornerRadius: 10))
                                .contentShape(Rectangle())
                                .onTapGesture {
                                    DispatchQueue.main.async {
                                        withAnimation {
                                            if selectedItem?.id == item.id {
                                                selectedItem = nil
                                            } else {
                                                selectedItem = item
                                                guard let item = selectedItem else { return }
                                                proxy.scrollTo(item.id, anchor: .center)
                                            }
                                        }
                                    }
                                }
                        }
                    }
                }
                .frame(height: 250)
                .scrollIndicators(.hidden)
                .clipped()
            }
            Spacer()
        }
        .padding()
        .background {
            Color(.darkGray).ignoresSafeArea()
        }
        .foregroundStyle(.white)
    }
}

In the code above, the animation is kind of jerky especially if an item is expanded and is partially visible at the left & right edges.
Any help is appreciated.

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img