Mac Catalyst 上包含“VStack”的“ScrollView”的分页无限滚动

问题描述 投票:0回答:1

我正在尝试找到一种方法来实现包含来自核心数据获取请求的

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)
        }
    }
}
swiftui core-data
1个回答
0
投票

我不知道这是否有任何帮助,但我确实在 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)
行即可。 让我知道是否有任何帮助。

© www.soinside.com 2019 - 2024. All rights reserved.