0

我正在尝试在 SwiftUI 中列出核心数据项,其中添加新项也会触发滚动到最后一项。

这是我的代码。它基于 Xcode 中的示例 Core Data 应用程序。一个实体:具有一个属性的项目:时间戳。

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
    
    var body: some View {
        NavigationView {
            ScrollViewReader { proxy in
                ScrollView {
                    ForEach(items, id: \.self) { item in
                        Text("Some item")
                            .padding()
                            .id(item.objectID)
                    }
                    .onDelete(perform: deleteItems)
                }
                .onChange(of: items.count) { _ in
//                  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                        withAnimation {
                            proxy.scrollTo(items.last?.objectID)
                        }
//                  }
                }
                .toolbar {
                    ToolbarItem {
                        Button(action: addItem) {
                            Label("Add Item", systemImage: "plus")
                        }
                    }
                }
            }
            Text("Select an item")
        }
    }
    
    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
            
            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    
    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)
            
            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

它可以工作,但是我在 Xcode 中遇到了这样的错误:

2022-02-06 16:28:47.280984+0700 滚动 [901:2894134] [错误] 前置条件失败:无效图更新(从多个线程访问?)

如何解决这个问题?我的猜测是它与并发有关。创建新项目时,它会获得新的 ID。项目更改的数量。Core Data 保存数据。UI 滚动动画。但是也许当触发滚动时,swift仍然不确定列表中的最后一项?

所以,我尝试过的事情:

如果你取消注释

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {

以及右括号 - 当我一次添加一个项目时,我没有收到错误消息。但是,如果我点击 + 几次,我会再次遇到错误。

另一个令人惊讶的事情是,如果我将ScrollView更改为List,我根本不会遇到错误。即使没有DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {

4

2 回答 2

0

似乎需要ScrollView一个不喜欢可选的 AND 你需要VStack一个ForEach实际延迟的 AND 。我找到了一些修复它的参考资料,但没有解释它为什么起作用。但是,我认为这些是导致相同错误的独立问题,因此必须进行所有修复。.scrollTo()DispatchQueue.main.asyncAfter(deadline:)VStack

struct ContentView: View {

    // Nothing changed here
    
    var body: some View {
        NavigationView {
            ScrollViewReader { proxy in
                ScrollView {
                    VStack {
                        ForEach(items, id: \.self) { item in
                            Text("Some item")
                                .padding()
                                .id(item.objectID)
                        }
                        .onDelete(perform: deleteItems)
                    }
                }
                .onChange(of: items.count) { _ in
                    if let last = items.last {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                            withAnimation {
                                proxy.scrollTo(last.objectID)
                            }
                        }
                    }
                }

    // Nothing changed here
    
}
于 2022-02-07T15:36:27.837 回答
0

我认为使用带 debounce(for:scheduler:options:) 运算符的组合框架应该可以解决您的问题。

我的代码遇到了同样的问题,以下解决方案效果很好。

import SwiftUI
import Combine

struct ChatView: View {
@StateObject var messagesManager: MessagesManager
@State private var cancellables: Set<AnyCancellable> = []

var body: some View {
    VStack {
        VStack {
            TitleRow()
            
            ScrollViewReader { proxy in
                ScrollView {
                VStack {
                    ForEach(messagesManager.messages) { message in
                        MessageBubble(message: message)
                    }
                }
                .padding(.top, 10)
                .background(.white)
                .cornerRadius(30, corners: [.topLeft, .topRight]) // Custom cornerRadius modifier added in Extensions file
                .onChange(of: messagesManager.lastMessageId) { id in
                    // When the lastMessageId changes, scroll to the bottom of the conversation
                    messagesManager.$lastMessageId.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
                        .sink { _ in
                            withAnimation {
                                proxy.scrollTo(id, anchor: .bottom)
                            }
                        }.store(in: &cancellables)
                    
                    }
                }
            }
        }

        .background(Color("blue-curious"))
        
        MessageField()
            .environmentObject(messagesManager)
    }.onTapGesture {
        dissmissKeybord()
    }
}

func dissmissKeybord() {
    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}

}

于 2022-02-08T10:04:10.970 回答