写了一个通用的无限滚动组件

参考了这个博主的实现:

https://juejin.cn/post/6898258968775245837


import SwiftUI

struct InfiniteScrollView<Content: View>: ContainerView {
    @EnvironmentObject var viewModel: InfiniteScrollViewModel

    // if viewmodel's active viewId is not equal to this viewId, then this view is passive scroll view
    var viewId: String? = nil
    var containerWidth: CGFloat
    var content: () -> Content
    
    var viewOffset: CGFloat {
        return viewModel.dragOffset - containerWidth / CGFloat(viewModel.sectionAmountPerPage) * CGFloat(viewModel.currentSectionIndex)
    }
    
    var body: some View {
        HStack(spacing: 0, content: self.content)
            .contentShape(Rectangle())
            .offset(x: viewOffset)
            .animation(viewModel.enableAnimation ? .easeIn(duration: 0.3) : .none, value: viewOffset)
            .gesture(dragGesture)
            .frame(width: containerWidth, alignment: .leading)
            .onChange(of: viewModel.currentSectionIndex, perform: { newIndex in
                if viewModel.activeViewID == self.viewId {
                    viewModel.enableAnimation = true
                    if newIndex < (2 * viewModel.sectionAmountPerPage - 1) {
                        viewModel.enableAnimation = false
                        viewModel.currentSectionIndex = viewModel.sectionAmountPerPage * 3
                    }
                    if newIndex > 3 * viewModel.sectionAmountPerPage {
                        viewModel.enableAnimation = false
                        viewModel.currentSectionIndex = 2 * viewModel.sectionAmountPerPage - 1
                    }
                }
            })
    }
}

extension InfiniteScrollView {
    private var dragGesture: some Gesture {
        DragGesture()
            .onChanged { changedValue in
                let dragOffsetX = changedValue.translation.width
                viewModel.isDragging = true
                viewModel.activeViewID = self.viewId
                viewModel.enableAnimation = true
                viewModel.dragOffset = dragOffsetX
            }
            .onEnded { endValue in
                viewModel.dragOffset = .zero
                viewModel.isDragging = false
                viewModel.enableAnimation = true
                let sectionWidth = (containerWidth / CGFloat(viewModel.sectionAmountPerPage))
                let translateWidth = endValue.translation.width
                let times = abs(translateWidth) / sectionWidth
                let remain = abs(translateWidth).truncatingRemainder(dividingBy: sectionWidth)
                var steps = translateWidth > 0 ? -Int(times) : Int(times)
                
                if translateWidth > 0 {
                    // -
                    if remain > sectionWidth / 3 || translateWidth > 30 {
                        steps -= 1
                    }
                } else {
                    // +
                    if remain > sectionWidth / 3 || translateWidth < -30 {
                        steps += 1
                    }
                }

                viewModel.currentSectionIndex += steps
                viewModel.steps += steps
            }
    }
}

@MainActor
class InfiniteScrollViewModel: ObservableObject {
    @Published var dragOffset: CGFloat = 0
    @Published var steps: Int = 0
    @Published var sectionAmountPerPage: Int//每页的数量,通常是 1 页
    @Published var currentSectionIndex: Int// 当前的index
    @Published var enableAnimation: Bool = true
    @Published var activeViewID: String? = nil// 这个一般在多个无限滚动相互影响的场景需要
    @Published var isDragging: Bool = false
    
    init(sectionAmount: Int = 1) {
        self.sectionAmountPerPage = sectionAmount
        self.currentSectionIndex = 2 * sectionAmount
    }
    
    // 用来获取实际的index
    var _currentSectionActualIndex: Int {
        if currentSectionIndex >= 2 * sectionAmountPerPage - 1
            && currentSectionIndex <= 3 * sectionAmountPerPage {
            return currentSectionIndex
        } else {
            if currentSectionIndex < 2 * sectionAmountPerPage - 1 {
                return 3 * sectionAmountPerPage
            } else {
                return 2 * sectionAmountPerPage - 1
            }
        }
    }
    
    // 判断某个section是否在当前section的后面
    public func _isInNextSection(index: Int) -> Bool {
        return index > _currentSectionActualIndex //&& index <= (currentSectionIndex + 2 * sectionAmountPerPage - 1)
    }
    // 判断某个section是否在当前section的前面
    public func _isInPrevSection(index: Int) -> Bool {
        return  index < _currentSectionActualIndex //&& index >= (currentSectionIndex - sectionAmountPerPage )
    }
    
    public func _getActualIndex() -> Int {
        if currentSectionIndex >= 2 * sectionAmountPerPage - 1
            && currentSectionIndex <= 3 * sectionAmountPerPage {
            return currentSectionIndex
        } else {
            if currentSectionIndex < 2 * sectionAmountPerPage - 1 {
                return 3 * sectionAmountPerPage
            } else {
                return 2 * sectionAmountPerPage - 1
            }
        }
    }
}

protocol ContainerView: View {
    associatedtype Content
    init(viewId: String?, containerWidth: CGFloat, @ViewBuilder _ content: @escaping () -> Content)
}

extension ContainerView {
    init(viewId: String? = nil, containerWidth: CGFloat, @ViewBuilder _ content: @escaping () -> Content) {
        self.init(viewId: viewId, containerWidth: containerWidth, content)
    }
}

使用方法

@StateObject var infiniteScrollViewModel = InfiniteScrollViewModel()
var body: some View {
    InfiniteScrollView(viewId: "exampleID", containerWidth: containerWidth) {
        ForEach(sections) { section in
           // section view
           // SectionView()
           // .frame(width: sectionWidth)// section width 根据section 的数量和container width 计算所得
        }
    }
    .frame(width: containerWidth)
    .environmentObject(infiniteScrollViewModel)
    //containerWidth 一般通过GeometryReader 来获取
}

对于简单的滚动页面,几乎不会遇到卡顿问题,但如果实现一个无限滚动的日历,拖拽时能够感觉到明显的卡顿。

如果你有更好的实现,欢迎指教。

Categorized in:

Tagged in:

,