【问题标题】:NavigationLink pops out upon List update ONLY when List is not scrolled to the top仅当列表未滚动到顶部时,NavigationLink 才会在列表更新时弹出
【发布时间】:2023-02-23 02:21:21
【问题描述】:

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

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

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 在手动弹出时正确更新) .

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

我从网上搜索得知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())
    }
}

【问题讨论】:

  • 如果没有minimal reproducible example,这将很难帮助调试
  • @jnpdx 谢谢。我正在努力,并将更新帖子。
  • @jnpdx 已更新。

标签: swift swiftui


【解决方案1】:

你是对的,这与 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 即使它看起来只是演示代码中的单向连接,假设在你的真实代码中,你需要双向通信

【讨论】:

  • 谢谢你工作得很好!是的,您的假设是正确的——我的真实代码是影响视图呈现方式的双向通信。
  • 你能简单解释一下你的解决方案的逻辑吗?
  • 其逻辑是,如果您将 NavigationLink 作为您的 View 的背景,它将始终位于层次结构中,并且不会像 List 元素那样加载/卸载。要以编程方式与其交互,您必须使用由 Binding&lt;Bool&gt; 控制的 isActive 属性。 activeChatBindingid : String 转换为 isActive 期望的 Binding&lt;Bool&gt;bindingForChat 提供了一个基于项目的 id 返回到原始数组的绑定。
  • 啊,这很有道理而且很棒!十分感谢你的帮助。
【解决方案2】:

尽管@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 解决。

【讨论】:

    猜你喜欢
    • 2022-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    • 1970-01-01
    • 1970-01-01
    • 2011-11-08
    相关资源
    最近更新 更多