只有当列表没有滚动到顶部时,导航链接才会在列表更新时弹出



如果标题令人困惑,请道歉。因此,我正在实现一个聊天应用程序,其中有一个ChatRow的列表,点击后将进入MessageView。当用户发送消息时,ChatRow的列表可能会重新排序,因为我对它们的排序方式是将包含最新消息的列表放在顶部。

代码大致如下(如果需要更多细节,请告诉我(:

struct ContentView: View {
@EnvironmentObject var chatsManager: ChatsManager
@EnvironmentObject var messagesManager: MessagesManager
var body: some View {
NavigationView{
VStack{
// Some Views
VStack{
if chatsManager.chats.isEmpty{
Text("you have no chats for now").frame(maxHeight:.infinity, alignment: .top)
}
else {
List() {
ForEach($chatsManager.chats, id: .id){ $chat in
NavigationLink (destination:
MessageView(chat: chat)
.onAppear{messagesManager.fetchMessages(from: chat.id)}
){ ChatRow(chat: $chat) }
}
}.listStyle(.plain)
}
}
}.navigationBarTitle("").navigationBarHidden(true)

}.navigationViewStyle(.stack)

}
}

一件非常奇怪的事情是,当List滚动到顶部时,如果我点击视口中的聊天,一切都会完美地进行(没有自动弹出,手动弹出时List会正确更新(。

但是,如果我在前几个ChatRow从屏幕上滚动时向下滚动列表,如果我发送任何消息,我就会被弹出。

我从搜索网页中了解到List懒惰加载元素,所以这可能是问题的原因。但我想不出解决问题的方法

重现问题的代码

只需将以下内容复制到一个文件中即可运行。

当你点击第一次聊天并点击按钮时,观察事情的表现与点击最后一次聊天并单击按钮时的不同。

import SwiftUI
import Combine
struct DebugView: View {

@StateObject var chatsManager = ChatsManager()

var body: some View {
NavigationView{
VStack{
HStack {
Text("Chats")
}.padding()
VStack{
List() {
ForEach($chatsManager.chats, id: .id){ $chat in
NavigationLink (destination:
ChatDetailView(chat: chat)
){ DemoChatRow(chat: $chat) }}
}.listStyle(.plain)
}
}.navigationBarTitle("").navigationBarHidden(true)

}.navigationViewStyle(.stack)
.environmentObject(chatsManager)
}
}

struct DemoChatRow: View {
@Binding var chat: Chat
var body: some View {
VStack{
Text(chat.name)
Text(chat.lastMessageTimeStamp, style: .time)
}
.frame(height: 50)
}
}

struct ChatDetailView: View {
var chat: Chat
@EnvironmentObject var chatsManager: ChatsManager
var body: some View {
Button(action: {
chatsManager.updateDate(for: chat.id)
} ) {
Text("Click to update the current chat to now")
}
}
}

class ChatsManager: ObservableObject {
@Published var chats = [
Chat(id: "GroupChat 1", name: "GroupChat 1", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 2", name: "GroupChat 2", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 3", name: "GroupChat 3", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 4", name: "GroupChat 4", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 5", name: "GroupChat 5", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 6", name: "GroupChat 6", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 7", name: "GroupChat 7", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 8", name: "GroupChat 8", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 9", name: "GroupChat 9", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat 10", name: "GroupChat 10", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat2 5", name: "GroupChat2 5", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat2 6", name: "GroupChat2 6", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat2 7", name: "GroupChat2 7", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat2 8", name: "GroupChat2 8", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat2 9", name: "GroupChat2 9", lastMessageTimeStamp: Date()),
Chat(id: "GroupChat2 10", name: "GroupChat2 10", lastMessageTimeStamp: Date())].sorted(by: {$0.lastMessageTimeStamp.compare($1.lastMessageTimeStamp) == .orderedDescending})

func updateDate(for chatID: String) {
if let idx = chats.firstIndex(where: {$0.id == chatID}) {
self.chats[idx] = Chat(id: chatID, name: self.chats[idx].name, lastMessageTimeStamp: Date())
}
self.chats.sort(by: {$0.lastMessageTimeStamp.compare($1.lastMessageTimeStamp) == .orderedDescending})
}


}


struct Chat: Identifiable, Hashable {
var id: String
var name: String
var lastMessageTimeStamp: Date

static func == (lhs: Chat, rhs: Chat) -> Bool {
return lhs.id == rhs.id
}

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

struct DebugView_Previews: PreviewProvider {
static var previews: some View {
DebugView().environmentObject(ChatsManager())
}
}

这与List延迟加载元素的事实有关,这是正确的——一旦NavigationLink离开屏幕,如果Chat元素发生变化,View最终会从堆栈中弹出。

对此的标准解决方案是将隐藏的NavigationLink添加到层次结构中,该层次结构具有控制其是否处于活动状态的isActive属性。不幸的是,与Swift 5.5中引入的方便的列表元素绑定相比,它需要更多的样板代码。

你的代码可能看起来像这样:

struct DebugView: View {

@StateObject var chatsManager = ChatsManager()
@State private var activeChat : String?

private func activeChatBinding(id: String?) -> Binding<Bool> {
.init {
activeChat != nil && activeChat == id
} set: { newValue in
activeChat = newValue ? id : nil
}
}

private func bindingForChat(id: String) -> Binding<Chat> {
.init {
chatsManager.chats.first { $0.id == id }!
} set: { newValue in
chatsManager.chats = chatsManager.chats.map { $0.id == id ? newValue : $0 }
}
}

var body: some View {
NavigationView{
VStack{
HStack {
Text("Chats")
}.padding()
VStack{
List() {
ForEach($chatsManager.chats, id: .id) { $chat in
Button(action: {
activeChat = chat.id
}) {
DemoChatRow(chat: $chat)
}
}
}.listStyle(.plain)
}
.background {
NavigationLink("", isActive: activeChatBinding(id: activeChat)) {
if let activeChat = activeChat {
ChatDetailView(chat: bindingForChat(id: activeChat).wrappedValue)
} else {
EmptyView()
}
}
}
}.navigationBarTitle("").navigationBarHidden(true)

}.navigationViewStyle(.stack)
.environmentObject(chatsManager)
}
}

注意:我保留了Binding,您必须使用DemoChatRow,尽管在演示代码中它看起来只是单向连接,并假设在您的真实代码中,您需要在那里进行双向通信

尽管@jnpdc的答案有效,但您会丢失所有NavigationLink功能,如所选状态等。

对此,一个简单的修复方法是将List包装在ScrollViewReader中,并在后台保持滚动位置。只需观察活动对象,当它发生变化时,滚动到对象的id

struct ContentView: View {
@EnvironmentObject var chatsManager: ChatsManager
var body: some View {
NavigationView {
ScrollViewReader { proxy in
List {
// list content
}
.onChange(of: chatsManager.activeChat, perform: { _ in
proxy.scrollTo(loc.internalName, anchor: .center)
})
}
}
}
}

附言:这个问题在任何情况下都可以通过iOS 16 API解决。

最新更新