swift 仅当列表未滚动到顶部时,NavigationLink才会在列表更新时弹出

2nbm6dog  于 2023-02-28  发布在  Swift
关注(0)|答案(2)|浏览(103)

如果标题令人困惑,我很抱歉。因此,我正在实现一个聊天应用程序,其中有一个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())
    }
}
exdqitrt

exdqitrt1#

这与List延迟加载元素的事实有关--一旦NavigationLink离开屏幕,如果Chat元素发生变化,View最终将从堆栈中弹出。
标准的解决方案是在层次结构中添加一个隐藏的NavigationLink,该NavigationLink具有isActive属性,该属性控制NavigationLink是否处于活动状态。不幸的是,与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,尽管在演示代码中它看起来只是一个单向连接,假设在您的真实的代码中,您需要双向通信 *
4xrmg8kj

4xrmg8kj2#

尽管@jnpdc的答案有效,但是您失去了所有NavigationLink特性,比如选定状态等。
解决这个问题的一个简单方法是将你的List Package 在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)
                })
            }
        }
    }
}

P.S.这个问题在任何情况下都可以通过iOS 16 API解决。

相关问题