[英]How can I avoid this SwiftUI + Combine Timer Publisher reference cycle / memory leak?
I have the following SwiftUI view which contains a subview that fades away after five seconds.我有以下 SwiftUI 视图,其中包含一个在五秒钟后消失的子视图。 The fade is triggered by receiving the result of a Combine TimePublisher, but changing the value of showRedView
in the sink
publisher's sink block is causing a memory leak.淡入淡出是通过接收 Combine TimePublisher 的结果来触发的,但是在sink
发布者的 sink 块中更改showRedView
的值会导致 memory 泄漏。
import Combine
import SwiftUI
struct ContentView: View {
@State var showRedView = true
@State var subscriptions: Set<AnyCancellable> = []
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onAppear {
fadeRedView()
}
}
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.sink { _ in
withAnimation {
showRedView = false
}
}
.store(in: &subscriptions)
}
}
I thought this was somehow managed behind the scenes with the AnyCancellable
collection.我认为这是通过AnyCancellable
集合在幕后以某种方式管理的。 I'm relatively new to SwiftUI and Combine, so sure I'm either messing something up here or not thinking about it correctly.我对 SwiftUI 和 Combine 比较陌生,所以肯定我要么在这里搞砸了一些东西,要么没有正确考虑它。 What's the best way to avoid this leak?避免这种泄漏的最佳方法是什么?
Edit: Adding some pictures showing the leak.编辑:添加一些显示泄漏的图片。
Views should be thought of as describing the structure of the view, and how it reacts to data.视图应该被认为是描述视图的结构,以及它如何对数据做出反应。 They ought to be small, single-purpose, easy-to-init structures.它们应该是小型、单一用途、易于初始化的结构。 They shouldn't hold instances with their own life-cycles (like keeping publisher subscriptions) - those belong to the view model.他们不应该拥有具有自己生命周期的实例(例如保留发布者订阅)——这些实例属于视图 model。
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Timer.publish(every: 2.0, on: .main, in: .default).autoconnect()
.prefix(1)
.map { _ in }
.eraseToAnyPublisher()
}
}
And use .onReceive
to react to published events in the View:并使用.onReceive
对视图中发布的事件做出反应:
struct ContentView: View {
@State var showRedView = true
@ObservedObject vm = ViewModel()
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onReceive(self.vm.pub, perform: {
withAnimation {
self.showRedView = false
}
})
}
}
So, it seems that with the above arrangement, the TimerPublisher
with prefix
publisher chain is causing the leak.因此,似乎通过上述安排,带有prefix
发布者链的TimerPublisher
导致了泄漏。 It's also not the right publisher to use for your use case.它也不是用于您的用例的正确发布者。
The following achieves the same result, without the leak:以下实现了相同的结果,没有泄漏:
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Just(())
.delay(for: .seconds(2.0), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
My guess is that you're leaking because you store an AnyCancellable
in subscriptions
and you never remove it.我的猜测是您正在泄漏,因为您将AnyCancellable
存储在subscriptions
中并且您从未将其删除。
The sink
operator creates the AnyCancellable
.接收sink
操作员创建AnyCancellable
。 Unless you store it somewhere, the subscription will be cancelled prematurely.除非您将其存储在某处,否则订阅将被提前取消。 But if we use the Subscribers.Sink
subscriber directly, instead of using the sink
operator, there will be no AnyCancellable
for us to manage.但是如果我们直接使用Subscribers.Sink
者,而不是使用sink
操作符,就没有AnyCancellable
供我们管理。
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: { _ in
withAnimation {
showRedView = false
}
}
))
}
But this is still overkill.但这仍然是矫枉过正。 You don't need Combine for this.为此,您不需要合并。 You can schedule the event directly:您可以直接安排活动:
func fadeRedView() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
withAnimation {
showRedView = false
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.