You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI Carousel的scrollPosition无法正确居中及初始化定位问题

自定义ScrollView Carousel的初始化定位问题

我用ScrollView实现了一个Carousel组件,希望通过父视图控制并初始化它的.scrollPosition(id:)属性,但遇到以下问题:

  • 使用HStack时,初始化定位完全失效
  • 改用LazyHStack后,初始化无法将选中项正确居中,选择第三个项目时偏移特别明显

数据模型代码

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.")
    ]
}

CustomPagingSlider组件代码

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) { // 使用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)
    }
}

DetailedView代码

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)
    }
}

ContentView代码

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)
            }
        }
    }
}

问题表现

  • 使用HStack时,初始化定位完全无效,组件始终停留在第一个项目
  • 改用LazyHStack后,初始化时选中项无法居中,选择第三个项目时偏移现象明显

内容的提问来源于stack exchange,提问作者Robert Basamac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 09:02:03