写了一个通用的无限滚动组件
参考了这个博主的实现:
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 来获取
}
对于简单的滚动页面,几乎不会遇到卡顿问题,但如果实现一个无限滚动的日历,拖拽时能够感觉到明显的卡顿。
如果你有更好的实现,欢迎指教。