我正在尝试找到一种方法来实现包含来自核心数据获取请求的
ScrollView
的VStack
的分页无限滚动。下面的解决方案似乎适用于 iOS 17 和 16.4,但不适用于 Mac Catalyst 16。不幸的是,我无法使用列表我想我这里有一个适用于列表的可行解决方案。下面的代码基于一个很棒的StackOverflow在这里回答。谁能帮助我在 Mac Catalyst 上实现此功能?谢谢!
此代码依赖于一个名为 Item 的核心数据实体,该实体具有 2 个名为 date 和 timestamp 的日期属性以及一个名为 value 的第三个 Integer 32 属性。
import SwiftUI
import CoreData
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
struct ScrollGeomSectionedFetchQueryView: View {
@Environment(\.managedObjectContext) private var viewContext
@State var fetchLimit = 10
var body: some View {
NavigationView {
SectionedFetchQueryScrollVStackGeomView(initialFetchLimit: fetchLimit, fetchLimitBinding: $fetchLimit)
.toolbar {
ToolbarItem {
Button(action: { addItem(viewContext) }) {
Label("Add Item", systemImage: "plus")
}
}
ToolbarItem {
Button(action: { add50Items(viewContext) } ) {
Label("Add 50 Items", systemImage: "plus.square.on.square")
}
}
}
}
}
}
struct SectionedFetchQueryScrollVStackGeomView: View {
@Environment(\.managedObjectContext) private var viewContext
@SectionedFetchRequest
private var items: SectionedFetchResults<Date, Item>
@Binding var fetchLimitBinding : Int
@State var flag = false
init(initialFetchLimit: Int, fetchLimitBinding: Binding<Int>) {
self._fetchLimitBinding = fetchLimitBinding
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(keyPath: \Item.timestamp, ascending: false)
]
request.fetchLimit = initialFetchLimit
_items = SectionedFetchRequest<Date, Item>(fetchRequest: request, sectionIdentifier: \.date!)
}
func getPosition(proxy: GeometryProxy, tag: Int, preferences: [PositionData])->CGPoint {
let p = preferences.filter({ (p) -> Bool in
p.id == tag
})
if p.isEmpty { return .zero }
if proxy.size.height - proxy[p[0].center].y > 0 && flag == false {
self.flag.toggle()
fetchLimitBinding = fetchLimitBinding + 10
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.flag.toggle()
}
print("fetch")
}
return .zero
}
var body: some View {
ScrollViewReader { proxy in
ScrollView([.vertical]) {
HStack(alignment: .top, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
EmptyView().id("top")
ForEach(items) { section in
ForEach(section) { item in
NavigationLink(destination: EditItemView(item: item)) {
Text("\(item.timestamp!, formatter: timeFormatter) - \(item.value)")
}
}
}
Rectangle().tag(items.count).frame(height: 0).anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.items.count, center: anchor)]
}.id(items.count)
}
}
}
.backgroundPreferenceValue(Positions.self) { (preferences) in
GeometryReader { proxy in
Rectangle().frame(width: 0, height: 0).position(self.getPosition(proxy: proxy, tag: self.items.count, preferences: preferences))
}
}
}
}
}
private func stripTime(_ timestamp: Date?) -> Date {
let components = Calendar.current.dateComponents([.year, .month, .day], from: timestamp!)
let date = Calendar.current.date(from: components)
return date!
}
private func addItem(_ viewContext: NSManagedObjectContext) {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.date = stripTime(newItem.timestamp)
newItem.value = Int32(Int.random(in: 1..<1000))
saveContext(viewContext)
}
}
private func add50Items(_ viewContext: NSManagedObjectContext) {
withAnimation {
for _ in 0..<50 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.date = stripTime(newItem.timestamp)
newItem.value = Int32.random(in: 0..<1000)
}
saveContext(viewContext)
}
}
private func saveContext(_ viewContext: NSManagedObjectContext) {
if viewContext.hasChanges {
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
let datetimeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter
}()
let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
struct EditItemView: View {
@Environment(\.managedObjectContext) private var viewContext
@ObservedObject var item: Item
@State private var selectedDate: Date
init(item: Item) {
self.item = item
self._selectedDate = State(initialValue: item.timestamp!)
}
var body: some View {
Form {
DatePicker("Date", selection: $selectedDate, displayedComponents: .date)
DatePicker("Time", selection: $selectedDate, displayedComponents: .hourAndMinute)
LabeledContent("Value") {
TextField("Value", value: $item.value, formatter: NumberFormatter())
}
}
.navigationTitle("Edit")
.onDisappear {
item.timestamp = selectedDate
item.date = stripTime(item.timestamp)
try? viewContext.save()
}
}
}
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "TestPagination")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
@main
struct TestPaginationApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TabView {
ScrollGeomSectionedFetchQueryView()
.tabItem {
Text("ScrollGeom SectionedFetch")
}
}
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
我不知道这是否有任何帮助,但我确实在 Mac Catalyst 16 上有一个可用的无限轮播。它可能需要根据您的需求进行一些调整,但我希望能为您指明正确的方向或给你一个提示。
import SwiftUI
struct LoopingScrollView<Content: View, Items: RandomAccessCollection>: View where Items.Element: Identifiable {
/// Customization properties
var width: CGFloat
var spacing: CGFloat
//MARK: - PROPERTIES
var items: Items
@ViewBuilder var content: (Items.Element) -> Content
var body: some View {
GeometryReader { geometry in
let size = geometry.size
/// Safety check
let repeatingCount = width > 0 ? Int((size.width / width).rounded()) + 1 : 1
ScrollView(.horizontal) {
LazyHStack(spacing: spacing) {
ForEach(items) { item in
content(item)
.frame(width: width)
} //: LOOP
ForEach(0..<repeatingCount, id: \.self) { index in
let item = Array(items)[index % items.count]
content(item)
.frame(width: width)
} //: LOOP
} //: LazyHStack
.background(
ScrollViewHelper(width: width,
spacing: spacing,
itemCount: items.count,
repeatingCount: repeatingCount
)
)
} //: SCROLL
.scrollIndicators(.hidden)
} //: GEOMETRY
}
}
fileprivate struct ScrollViewHelper: UIViewRepresentable {
var width: CGFloat
var spacing: CGFloat
var itemCount: Int
var repeatingCount: Int
func makeCoordinator() -> Coordinator {
return Coordinator(width: width,
spacing: spacing,
itemCount: itemCount,
repeatingCount: repeatingCount
)
}
func makeUIView(context: Context) -> some UIView {
return .init()
}
func updateUIView(_ uiView: UIViewType, context: Context) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
if let scrollview = uiView.superview?.superview?.superview as? UIScrollView,
!context.coordinator.isAdded {
scrollview.delegate = context.coordinator
context.coordinator.isAdded = true
}
}
context.coordinator.width = width
context.coordinator.spacing = spacing
context.coordinator.itemCount = itemCount
context.coordinator.repeatingCount = repeatingCount
}
class Coordinator: NSObject, UIScrollViewDelegate {
var width: CGFloat
var spacing: CGFloat
var itemCount: Int
var repeatingCount: Int
///Tells us whether the delegate is added or not
var isAdded: Bool = false
init(width: CGFloat, spacing: CGFloat, itemCount: Int, repeatingCount: Int) {
self.width = width
self.spacing = spacing
self.itemCount = itemCount
self.repeatingCount = repeatingCount
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard itemCount > 0 else { return }
let minX = scrollView.contentOffset.x
let mainContentSize = CGFloat(itemCount) * width
let spacingSize = CGFloat(itemCount) * spacing
if minX > (mainContentSize + spacingSize) {
scrollView.contentOffset.x -= (mainContentSize + spacingSize)
}
if minX < 0 {
scrollView.contentOffset.x += (mainContentSize + spacingSize)
}
}
}
}
它缺乏分页行为,因为它首先是使用新的 iOS 17 API 编写的,但只需注释它就可以在 Mac Catalyst 上运行。 您可以像这样使用这个无限旋转木马:
let width: CGFloat = 150
ScrollView(.vertical) {
VStack {
GeometryReader { geom in
let size = geom.size
LoopingScrollView(width: size.width, spacing: 0, items: items) { item in
RoundedRectangle(cornerRadius: 15)
.fill(item.color.gradient)
.padding(.horizontal, 15)
}
//.contentMargins(.horizontal, 15, for: .scrollContent)
//.scrollTargetBehavior(.paging) // <-- Only works on iOS 17+
}
.frame(height: width)
} //: VSTACK
.padding(.vertical, 15)
} //: ScrollView
.scrollIndicators(.hidden)
Item 结构只包含一个 ID 和一个颜色,您可以使用任何您想要的 Identifier 对象。对于任何想要在 iOS 17+ 上进行分页行为的人,只需取消注释
.scrollTargetBehavior(.paging)
行即可。
让我知道是否有任何帮助。