【问题标题】:Receiving SwiftUI observed changes on the main thread在主线程上接收 SwiftUI 观察到的变化
【发布时间】:2021-08-12 10:31:22
【问题描述】:

有什么方法可以告诉 SwiftUI 接收对主线程上 @ObservedObject 属性的更改?

我有一个模型对象,用于管理聊天频道中的聊天消息和用户列表。该通道由一些 websocket 代码提供,这些代码在我的模型上定期调用 addMessages()usersJoinedAndLeft() 等方法。然后我的模型对用户和消息列表进行一些内务管理。

class ChatStream
    @Published var users: [Users]
    @Published var messages: [Message]
}

我的 SwiftUI 看起来像

struct OverlayChatView : View {
    @ObservedObject public var stream: ChatStream
    var body: some View {
        …
            ForEach(self.stream.messages) { inMsg in
            }
        …
    }
}

这一切都很好。当我的网络代码更新以在不同的调度队列上进行调用时,问题就出现了。现在.users.messages 的更改发生在此队列上,SwiftUI 尝试在此队列上更新。

显而易见的解决方法是将我对.users.messages 的所有更改封装在DispatchQueue.main.async 调用中。但这有点混乱,因为所有的内务处理都是就地完成的,可能会在完成处理一组传入消息或用户状态更改之前更新 .users.messages 几次(最后它会保留一个有序列表消息以及当前和离开用户的集合,每条消息都引用其作者,因为用户可以回来,他们的属性可以改变。)

因此,我可以更改我的代码以处理这些集合的临时副本,并且仅在调度异步调用中的处理结束时设置已发布的属性。这将具有合并许多临时更改的额外好处。

我可以添加第二个 PassthroughSubject 并在最后调用 .send(),但这看起来很笨拙,并且会破坏 SwiftUI 的精简特性,因为我可能必须保留一份单独的状态副本才能更新主线程。

我喜欢做的事情是这样的

    @ObservedObject(receiveOn: DispatchQueue.main) var stream: ChatStream

或者在我的模型中,类似

class ChatStream {
    func add(messages: [Message]) {
        self.$users.suspend()
        self.$messages.suspend()
        …process messages…
        DispatchQueue.main.async {
            self.$users.resume()
            self.$messages.resume()
        }
}

但 SwiftUI 似乎应该只为我在主线程上重建 UI,因为它需要它。

【问题讨论】:

  • 如果您使用组合发布者来更新您的消息/用户名,您可以使用此修饰符切换到主 Runloop developer.apple.com/documentation/combine/fail/…
  • 我不确定我是否理解您关于使用 DispatchQueue.main.async 会变得混乱的原因
  • 因为对模型的每次修改都会生成通知。我的模型负责更新列表中的每个用户,如果他们离开了,将他们移动到单独的列表中,更新消息列表并对它们进行排序,等等。我都在原地完成了这些。我必须在主线程上进行整个处理,这是我想避免的。
  • @Rick,“因为对模型的每次修改都会生成通知”——这是不正确的。 Published 属性的编辑不会每次都导致重新呈现。重新渲染被合并。
  • 这不是我所看到的。特别是,无论是否合并,它们都不会进入主线程。

标签: swift swiftui combine


【解决方案1】:

你有没有尝试过这样的事情,因为你有出版商?

class ChatStream
    @Published var users: [Users]
    @Published var messages: [Message]
}

struct OverlayChatView : View {
    @ObservedObject public var stream: ChatStream
    var body: some View {
        …
            ForEach(self.stream.$messages.receive((on: DispatchQueue.main)) { inMsg in
            }
        …
    }
}

我不知道这是否适用于ForEach,但如果您可以修改代码,则适用于.onReceive

.onReceive(self.observedObject.$publishedProperty.receive(on: DispatchQueue.main), perform: { newValue in
    ....
})

【讨论】:

    【解决方案2】:

    查看解决方案

    如果你不介意在你的视图中添加一些额外的代码,你可以观察你的ObservableObjectobjectWillChangePublisher,然后在主队列的视图上更新@State 属性,例如类似:

    struct ChatView: View {
        @ObservedObject var stream: ChatStream
    
        @State private var _refreshState: Bool = false 
        
        var body: some View {
            YourChatViewContent()
                .onReceive(stream.objectWillChange) {
                    DispatchQueue.main.async {
                        _refreshState.toggle()
                    }
                }
        }
    }
    

    由于主队列上的_refreshState 发生更改,它应该触发视图的重新渲染,应该使用ChatStream 对象的新值,尽管此解决方案可能会导致_refreshState 的多次切换,因为objectWillChange 将在您的ObservableObject 中的每个状态更改时触发一次,这可能会导致不必要的渲染。

    如果您在其他视图中重复使用相同的模式,则可以将 onReceive 部分包装在扩展函数中。

    ObservableObject 解决方案

    或者,在您的ObservableObject 中,在主队列上调用self.objectWillChange.send()应该也会触发视图的更新,例如:

    class ChatStream: ObservableObject {
        func add(messages: [Message]) {
            …process messages…
            
            DispatchQueue.main.async {
                self.objectWillChange.send()
            }
        }
    }
    

    但是,您不妨首先在主队列中设置 @Published 属性的值。

    【讨论】:

    • 这比添加PassthroughSubjects 稍微好一点,但仍然以一种我觉得没有吸引力的方式使视图混乱。我向 Apple 提交了增强请求以简化此操作。
    • “ObservableObject 解决方案”将不起作用,因为……处理消息中的更改……将触发来自错误队列的视图更新。
    猜你喜欢
    • 2020-03-21
    • 2020-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-16
    • 1970-01-01
    • 1970-01-01
    • 2017-11-05
    相关资源
    最近更新 更多