轮播组件出现非对称动画卡顿,原因何在?
无限循环轮播组件向前滚动卡顿问题
我正在做一个支持无限循环的轮播组件——滑到最后一张图能切到第一张,反过来也一样,但只有向前滚动时,动画会在显示正确的新图前出现卡顿,向后滚动完全正常。前后滑动逻辑是对称的,大部分代码也是共用的,这让我搞不懂问题出在哪。
编辑说明:示例使用5张1920x1080的图片,命名为"PH1"、"PH2"……"PH5"。
CarouselModel.swift
import Foundation class CarouselModel: ObservableObject { var uuid: UUID = UUID() @Published internal var onScreenBuffer: [BufferedImage] = [] private var offScreenBuffer: [BufferedImage] = [] internal var currentImage: Int = 0 internal var offsetImageSelector: Int = 0 @Published internal var imagesIDs: [String] = [] { didSet { guard imagesIDs.count > 0 else { return } self.makeBuffer(for: self.currentImage, buffer: &self.onScreenBuffer) self.makeBuffer(for: self.currentImage, buffer: &self.offScreenBuffer) } } internal var bufferedImagesIDs: [BufferedImage] { return self.imagesIDs.enumerated().map { i, image in return BufferedImage(assetImageName: image, position: i) } } init(imagesIDs: [String]) { self.imagesIDs = imagesIDs } internal func makeBuffer(for centralIdx: Int, buffer: inout [BufferedImage]) { precondition(centralIdx >= 0 && centralIdx < self.imagesIDs.count) var tempBuffer: [BufferedImage] = [BufferedImage].init(repeating: .zero, count: 3) for i in -1...1 { let next = (centralIdx + i + imagesIDs.count) % imagesIDs.count tempBuffer[i+1] = BufferedImage(assetImageName: imagesIDs[next], position: i+1) } buffer = tempBuffer } func prepareForNext(forward: Bool = true) { let nextIdx = forward ? (currentImage+1)%imagesIDs.count : (currentImage-1+imagesIDs.count) % imagesIDs.count if forward { self.offsetImageSelector -= 1 } else { self.offsetImageSelector += 1 } assert(nextIdx >= 0 && nextIdx <= imagesIDs.count) self.makeBuffer(for: nextIdx, buffer: &self.offScreenBuffer) currentImage = nextIdx } func swapOnscreenBuffer() { self.onScreenBuffer = Array(self.offScreenBuffer) self.offsetImageSelector = 0 } internal struct BufferedImage: Identifiable { var assetImageName: String var position: Int var id: String init(assetImageName: String, position: Int) { self.assetImageName = assetImageName self.position = position self.id = "\(assetImageName)\(position)" } public static let zero = BufferedImage(assetImageName: "placeholder", position: .zero) } }
ContentView.swift
import SwiftUI struct ContentView: View { @StateObject private var carouselModel = CarouselModel( imagesIDs: (1..<6).map { i in return "PH\(i)" } ) @State private var dragOffset: CGFloat = .zero var body: some View { GeometryReader { geo in VStack { HStack(spacing: 0) { ForEach(carouselModel.onScreenBuffer, id: \.id) { bufferedImage in Image(bufferedImage.assetImageName) .resizable() .aspectRatio(16.0/9.0, contentMode: .fit) .frame(width: geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing, alignment: .leading) } } .offset( x: -(geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing - dragOffset)*CGFloat((1-carouselModel.offsetImageSelector)) ) .frame(maxWidth: geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing, alignment: .leading) .clipped() .overlay { ChevronButtonsOverlay(galleryWidth: geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing) } } } } @ViewBuilder func ChevronButton() -> some View { ZStack { Circle() .fill(.ultraThinMaterial) .frame(width: 35, height: 35) .background { Color(UIColor.label).opacity(0.8).clipShape(Circle()) } Image(systemName: "chevron.left") .font(.system(size: 15).weight(.black)) .blendMode(.destinationOut) .offset(y: -1) } .compositingGroup() } @ViewBuilder func ChevronButtonsOverlay(galleryWidth: CGFloat) -> some View { HStack(alignment: .center, spacing: 0) { Button(action: { if abs(carouselModel.offsetImageSelector) < 1 { withAnimation(.easeOut(duration: 0.25)) { carouselModel.prepareForNext(forward: false) self.dragOffset = galleryWidth } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.dragOffset = 0 carouselModel.swapOnscreenBuffer() } } }) { ChevronButton() } Spacer() Button(action: { if abs(carouselModel.offsetImageSelector) < 1 { withAnimation(.easeOut(duration: 0.25)) { carouselModel.prepareForNext(forward: true) self.dragOffset = -galleryWidth } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.dragOffset = 0 carouselModel.swapOnscreenBuffer() } } }) { ChevronButton() } .rotationEffect(.degrees(180)) } .padding(5) .frame(maxWidth: .infinity, maxHeight: .infinity) } }
问题原因及修复方案
问题根源
- 偏移量计算逻辑不对称:向前滚动时
offsetImageSelector为-1,代入偏移公式后会产生两倍宽度的偏移,和向后滚动的偏移逻辑完全不一致,导致动画过程中视图布局异常。 - 视图ID冲突:
BufferedImage的id仅用图片名+位置生成,当循环滚动时,不同缓冲池中的图片会出现重复ID,SwiftUI无法正确识别视图身份,引发视图复用错误,出现短暂卡顿。
修复步骤
1. 修正偏移量计算逻辑
把offset公式改成对称逻辑,确保前后滚动的偏移变化一致:
.offset( x: (dragOffset - (geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing)) * CGFloat(carouselModel.offsetImageSelector) )
2. 修复视图ID冲突
给BufferedImage的id加入模型的唯一UUID,避免不同缓冲池的视图ID重复:
// 修改BufferedImage的初始化方法 init(assetImageName: String, position: Int, parentUUID: UUID) { self.assetImageName = assetImageName self.position = position self.id = "\(parentUUID)-\(assetImageName)-\(position)" } // 修改makeBuffer方法中的创建逻辑 tempBuffer[i+1] = BufferedImage(assetImageName: imagesIDs[next], position: i+1, parentUUID: self.uuid)
3. 对齐动画与缓冲切换时机
把缓冲切换的延迟时间改成和动画时长一致,同时关闭切换缓冲时的动画,避免额外的视图波动:
// 前后按钮的异步回调都改成这样 DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { self.dragOffset = 0 withAnimation(nil) { carouselModel.swapOnscreenBuffer() } }
内容的提问来源于stack exchange,提问作者Baffo rasta
相关产品推荐
相关产品推荐

