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

SwiftUI SnapCarousel无法吸附到图片问题排查求助

问题:SnapCarousel轮播组件向右滚动无法吸附到对应图片

我实现了SnapCarousel轮播组件,图片可正常显示,但向右滚动时无法吸附到对应图片,预期滚动后会自动吸附但未生效。

SnapCarousel.swift 代码

import SwiftUI

struct SnapCarousel<Content : View,T: Identifiable>: View{
    var content: (T) -> Content
    var list: [T]
    
    // Properties...
    var spacing: CGFloat
    var trailingSpace: CGFloat
    @Binding var index: Int
    
    init(spacing: CGFloat = 15,trailingSpace: CGFloat = 100,index: Binding<Int>,items: [T],@ViewBuilder content: @escaping (T)->Content)
    {
        self.list = items
        self.spacing = spacing
        self.trailingSpace = trailingSpace
        self._index = index
        self.content = content
    }
    //offset
    @GestureState var offset: CGFloat = 0
    @State var currentIndex: Int = 0
    
    var body: some View {
        
        GeometryReader{ proxy in
            
            let width = proxy.size.width - (trailingSpace - spacing)
            let adjustMentWidth = (trailingSpace / 2) - spacing
            
            HStack(spacing: spacing){
            
            
            ForEach(list){item in
                content(item)
                    .frame(width: proxy.size.width - trailingSpace)
            }
            .padding(.horizontal,spacing)
            .offset(x: (CGFloat(currentIndex) * -width) + (currentIndex != 0 ? adjustMentWidth : 0) + offset)
            .gesture(
                DragGesture()
                    .updating($offset, body: { value, out, _ in
                        
                        out = value.translation.width
                    })
                    .onEnded({ value in
                        
                        //Update current index for scroll
                        let offsetX = value.translation.width
                        
                        //convert the translation into progress (0-1)
                        //round the value based on the currentIndex
                        
                        let progress = -offsetX / width
                        
                        let roundIndex = progress.rounded()
                        
                        //setting max and min
                        currentIndex = max(min(currentIndex + Int(roundIndex), list.count - 1), 0)
                        
                        //updating index
                        currentIndex = index
                    })
                )
        }
            
        }
        //animation when offset = 0
        .animation(.easeInOut, value: offset == 0)
    }
    }

struct SnapCarousel_Previews: PreviewProvider {
    static var previews: some View {
        MotorDetail(landmark: landmarks[0])
    }
}

MotorDetail视图调用代码

VStack (alignment: .leading, spacing: 12) {} .frame(maxWidth: .infinity,alignment: .leading) .padding()
// Snap Carousel....
SnapCarousel (trailingSpace: 27,index: $currentIndex, items: posts)
{post in
    GeometryReader{proxy in
        let size = proxy.size
        
        Image(post.postImage)
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(width: size.width)
            .cornerRadius(12)
            .padding(.top, -200)
    }
}
.padding(.vertical,80)
}
.frame(maxHeight: .infinity, alignment: .top)
.onAppear{ for index in 1...5{
    posts.append(Post(postImage:  "\(String(landmark.name))" + "_overview\(index)"))}

问题定位与修复方案

核心问题

  1. 索引赋值逻辑错误:在DragGesture的onEnded回调中,最后一行currentIndex = index完全覆盖了之前计算好的滚动目标索引,导致滚动后的位置无法正确更新,这是吸附失效的关键原因。
  2. 动画触发条件不准确:原代码仅在offset == 0时触发动画,无法覆盖滚动结束后索引更新的场景,导致吸附动画不生效。

修复后的SnapCarousel代码

import SwiftUI

struct SnapCarousel<Content : View,T: Identifiable>: View{
    var content: (T) -> Content
    var list: [T]
    
    var spacing: CGFloat
    var trailingSpace: CGFloat
    @Binding var index: Int
    
    init(spacing: CGFloat = 15,trailingSpace: CGFloat = 100,index: Binding<Int>,items: [T],@ViewBuilder content: @escaping (T)->Content)
    {
        self.list = items
        self.spacing = spacing
        self.trailingSpace = trailingSpace
        self._index = index
        self.content = content
    }
    
    @GestureState var offset: CGFloat = 0
    @State var currentIndex: Int = 0
    
    var body: some View {
        GeometryReader{ proxy in
            let width = proxy.size.width - (trailingSpace - spacing)
            let adjustMentWidth = (trailingSpace / 2) - spacing
            
            HStack(spacing: spacing){
                ForEach(list){item in
                    content(item)
                        .frame(width: proxy.size.width - trailingSpace)
                }
            }
            .padding(.horizontal,spacing)
            .offset(x: (CGFloat(currentIndex) * -width) + (currentIndex != 0 ? adjustMentWidth : 0) + offset)
            .gesture(
                DragGesture()
                    .updating($offset, body: { value, out, _ in
                        out = value.translation.width
                    })
                    .onEnded({ value in
                        let offsetX = value.translation.width
                        let progress = -offsetX / width
                        let roundIndex = progress.rounded()
                        
                        // 更新内部维护的索引
                        currentIndex = max(min(currentIndex + Int(roundIndex), list.count - 1), 0)
                        // 同步到外部绑定的索引值
                        index = currentIndex
                    })
            )
        }
        // 监听索引和偏移量变化,确保滚动结束后触发吸附动画
        .animation(.easeInOut, value: [currentIndex, offset])
    }
}

struct SnapCarousel_Previews: PreviewProvider {
    static var previews: some View {
        MotorDetail(landmark: landmarks[0])
    }
}

额外注意事项

  • 确保调用组件时,currentIndex的初始值为0,避免初始位置偏移。
  • 检查posts数组在onAppear中的初始化逻辑,确保数组元素数量正确且图片资源名称无误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 00:15:47