ios – SwiftUI Carousel scrollPosition not properly centered


I have a carousel made with ScrollView and I am trying to control/init the .scrollPosition(id:) from a parent view.

Here is the Custom Slider:

struct Item: Identifiable, Hashable {
    private(set) var id: UUID = .init()
    var color: Color
    var title: String
    var subTitle: String
    
    static var previews: [Item] = [
        .init(color: .red, title: "World Clock", subTitle: "View the time in multiple cities around the world."),
        .init(color: .blue, title: "City Digital", subTitle: "Add a clock for a city to check the time at that location."),
        .init(color: .green, title: "City Analouge", subTitle: "Add a clock for a city to check the time at that location."),
        .init(color: .yellow, title: "Next Alarm", subTitle: "Display upcomiong alarm.")
    ]
}

struct CustomPagingSlider<Content: View, TitleContent: View, Item: RandomAccessCollection>: View where Item: MutableCollection, Item.Element: Identifiable {
    
    /// View Properties
    @Binding var activeID: UUID?
    @State var data: Item
    
    /// Customization Properties
    var showsIndicator: ScrollIndicatorVisibility = .hidden
    var showPagingControl: Bool = true
    var disablePagingInteraction: Bool = false
    var titleScrollSpeed: CGFloat = 0.6
    var pagingControlSpacing: CGFloat = 20
    var spacing: CGFloat = 10
    
    @ViewBuilder var content: (Binding<Item.Element>) -> Content
    @ViewBuilder var titleContent: (Binding<Item.Element>) -> TitleContent
    
    var body: some View {
        VStack(spacing: pagingControlSpacing) {
            ScrollView(.horizontal) {
                HStack(spacing: spacing) { // not working when using HStack
                    ForEach($data) { item in
                        VStack(spacing: 0) {
                            titleContent(item)
                                .frame(maxWidth: .infinity)
                                .visualEffect { content, geometryProxy in
                                    content
                                        .offset(x: scrollOffset(geometryProxy))
                                }
                            
                            content(item)
                        }
                        .containerRelativeFrame(.horizontal)
                    }
                }
                .scrollTargetLayout()
            }
            .scrollIndicators(showsIndicator)
            .scrollTargetBehavior(.viewAligned)
            .scrollPosition(id: $activeID)
        }
    }
    
    func scrollOffset(_ proxy: GeometryProxy) -> CGFloat {
        let minX = proxy.bounds(of: .scrollView)?.minX ?? 0
        
        return -minX * min(titleScrollSpeed, 1.0)
    }
}

In this DetailedView I will use onChange(of: ) to do something when the activeID changes from the CustomPagingSlider.

struct DetailedView: View {
    
    /// View Properties
    @State var items: [Item]
    @State var activeID: UUID?
        
    /// Customization Properties
    @State private var showPagingControl: Bool = false
    @State private var disablePagingInteraction: Bool = false
    @State private var pagingSpacing: CGFloat = 20
    @State private var titleScrollSpeed: CGFloat = 0.75
    @State private var stretchContent: Bool = true
    
    init(items: [Item], activeID: UUID?) {
        self._items = State(wrappedValue: items)
        self._activeID = State(wrappedValue: activeID)
        
        if let item = self.items.first(where: { $0.id == activeID }) {
            print("DetailedView init: \(item.title)")
        }
    }
    
    var body: some View {
        VStack {
            CustomPagingSlider(activeID: $activeID,
                               data: items,
                               showPagingControl: showPagingControl,
                               disablePagingInteraction: disablePagingInteraction,
                               titleScrollSpeed: titleScrollSpeed,
                               pagingControlSpacing: pagingSpacing
            ) { $item in
                RoundedRectangle(cornerRadius: 15)
                    .fill(item.color.gradient)
                    .frame(width: stretchContent ? nil : 150, height: stretchContent ? 220 : 120)
            } titleContent: { $item in
                VStack(spacing: 5) {
                    Text(item.title)
                        .font(.largeTitle.bold())
                        .frame(height: 45)
                        .background(.cyan)
                    
                    Text(item.subTitle)
                        .foregroundStyle(.gray)
                        .multilineTextAlignment(.center)
                        .frame(height: 45)
                }
            }
            .safeAreaPadding([.horizontal], 35)
            
            List {
                Toggle("Show Paging Control", isOn: $showPagingControl)
                
                Toggle("Disable Page Interaction", isOn: $disablePagingInteraction)
                
                Toggle("Stretch Content", isOn: $stretchContent)
                
                Section("Title Scroll Speed") {
                    Slider(value: $titleScrollSpeed)
                }
                
                Section("Paging Spacing") {
                    Slider(value: $pagingSpacing, in: 20...40)
                }
            }
            .clipShape(.rect(cornerRadius: 15))
            .padding(15)
        }
        .navigationTitle("Detailed View")
        .navigationBarTitleDisplayMode(.inline)
    }
}

But I want to make the CustomPagingSlider to position on a specific item (selected from the ContentView when displaying the DetailedView, not always on the 1st item.

struct ContentView: View {
    @State private var items: [Item] = Item.previews
    
    @State private var showDetailedView: Bool = false
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    NavigationLink(value: item) {
                        VStack(alignment: .leading, spacing: 5) {
                            Text(item.title)
                                .font(.largeTitle.bold())
                            
                            Text(item.subTitle)
                                .foregroundStyle(.gray)
                                .multilineTextAlignment(.leading)
                                .frame(height: 45)
                        }
                    }
                    
                }
            }
            .navigationTitle("Items")
            .navigationDestination(for: Item.self) { item in
                DetailedView(items: items, activeID: item.id)
            }
        }
    }
}

It does not work at all when using a HStack within the CustomPagingSlider (see comment). It works when using LazyHStack, but the positioning is not correctly aligned (check video). When selecting the 3rd item, the positioning is not aligned.
enter link description here

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img