Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 491 Vote(s) - 3.49 Average
  • 1
  • 2
  • 3
  • 4
  • 5
How to prevent strong reference cycles when using Apple's new Combine framework (.assign is causing problems)

#1
I don't quite understand how to properly store subscribers inside a class so that they persist but don't prevent the object from being deinitialized. Here's an example where the object won't deinit:

```swift
import UIKit
import Combine

class Test {
public var name: String = ""

private var disposeBag: Set<AnyCancellable> = Set()

deinit {
print("deinit")
}

init(publisher: CurrentValueSubject<String, Never>) {
publisher.assign(to: \.name, on: self).store(in: &disposeBag)
}
}

let publisher = CurrentValueSubject<String, Never>("Test")

var test: Test? = Test(publisher: publisher)
test = nil

```

When I replace the `assign` with a `sink` (in which I properly declare `[weak self]`) it actually does deinit properly (probably because the `assign` accesses `self` in a way that causes problems).

How can I prevent strong reference cycles when using `.assign` for instance?

Thanks
Reply

#2
import protocol Combine.Publisher
import class Combine.AnyCancellable

fileprivate struct RootUnownedWrapper<R: AnyObject> {
unowned var root: R
init(_ root: R) { self.root = root }
}

fileprivate struct RootWeakWrapper<R: AnyObject> {
weak var root: R!
init(_ root: R) { self.root = root }
}

extension Publisher where Self.Failure == Never {
typealias TRootKeyPath<T> = ReferenceWritableKeyPath<T, Self.Output>
fileprivate typealias TWeakKeyPath<T: AnyObject> = WritableKeyPath<RootWeakWrapper<T>, T>
func assignWeakly<Root: AnyObject>(to keyPath: TRootKeyPath<Root>, on object: Root) -> AnyCancellable {

let wrapped = RootWeakWrapper(object)
let wkp: TWeakKeyPath<Root> = \.root
return assign(to: wkp.appending(path: keyPath), on: wrapped)
}

fileprivate typealias TUnownedKeyPath<T: AnyObject> = WritableKeyPath<RootUnownedWrapper<T>, T>

func assignUnowned<Root: AnyObject>(to keyPath: TRootKeyPath<Root>,on object: Root) -> AnyCancellable {
let wrapped = RootUnownedWrapper(object)
let wkp: TUnownedKeyPath<Root> = \.root
return assign(to: wkp.appending(path: keyPath), on: wrapped)
}
}
Reply

#3
In addition to previously recommended way of using @Published property or .sink() there is another way to break strong reference cycle.

From documentation:

> The ``Subscribers/Assign`` instance created by this operator maintains
> a strong reference to `object`, and sets it to `nil` when the upstream
> publisher completes (either normally or with an error).

So sending **completion** event when you don't need updates from **publisher** will break reference cycle.

In this case:

publisher.send(completion: .finished)

Reply

#4
How about:

class Test {
@Published var name: String = ""

deinit {
print("deinit")
}

init(publisher: CurrentValueSubject<String, Never>) {
publisher.assign(to: &$name)
}
}


This version of the `assign` operation manages memory internally (does not return `AnyCancellable`), as it dies together with the object. Note you need to convert your property to `@Published`.
Reply

#5
You should remove stored `AnyCancellable` from `disposeBag` to release `Test` instance.


```swift
import UIKit
import Combine

private var disposeBag: Set<AnyCancellable> = Set()

class Test {
public var name: String = ""


deinit {
print("deinit")
}

init(publisher: CurrentValueSubject<String, Never>) {
publisher.assign(to: \.name, on: self).store(in: &disposeBag)
}
}

let publisher = CurrentValueSubject<String, Never>("Test")

var test: Test? = Test(publisher: publisher)
disposeBag.removeAll()
test = nil
```

or use optional `disposeBag`

```swift
import UIKit
import Combine

class Test {
public var name: String = ""
private var disposeBag: Set<AnyCancellable>? = Set()

deinit {
print("deinit")
}

init(publisher: CurrentValueSubject<String, Never>) {
guard var disposeBag = disposeBag else { return }
publisher.assign(to: \.name, on: self).store(in: &disposeBag)
}
}

let publisher = CurrentValueSubject<String, Never>("Test")

var test: Test? = Test(publisher: publisher)
test = nil
```
Reply

#6
you can replace .asign(to:) with sink where [weak self] in its closure brake the memory cycle. Try it in Playground to see the difference

final class Bar: ObservableObject {
@Published var input: String = ""
@Published var output: String = ""

private var subscription: AnyCancellable?

init() {
subscription = $input
.filter { $0.count > 0 }
.map { "\($0) World!" }
//.assignNoRetain(to: \.output, on: self)
.sink { [weak self] (value) in
self?.output = value
}

}

deinit {
subscription?.cancel()
print("\(self): \(#function)")
}
}

// test it!!
var bar: Bar? = Bar()
let foo = bar?.$output.sink { print($0) }
bar?.input = "Hello"
bar?.input = "Goodby,"
bar = nil

it prints

Hello World!
Goodby, World!
__lldb_expr_4.Bar: deinit

so we don't have the memory leak !

finally at forums.swift.org someone make a nice little

extension Publisher where Self.Failure == Never {
public func assignNoRetain<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable where Root: AnyObject {
sink { [weak object] (value) in
object?[keyPath: keyPath] = value
}
}
}
Reply

#7
I don't know what you have against closures but the solution is to not use self in the assign:

``` swift
import Combine
import SwiftUI

class NameStore {
var name: String
init() { name = "" }
deinit { print("deinit NameStore") }
}

class Test {
private var nameStore = NameStore()
public var name: String { get { return nameStore.name } }

var subscriber: AnyCancellable? = nil

deinit { print("deinit Test") }

init(publisher: CurrentValueSubject<String, Never>) {
subscriber = publisher.print().assign(to: \NameStore.name, on: nameStore)
}
}

let publisher = CurrentValueSubject<String, Never>("Test")
var test: Test? = Test(publisher: publisher)

struct ContentView : View {
var body: some View {
Button(
action: { test = nil },
label: {Text("test = nil")}
)
}
}
```

As far as I can see weak references are only allowed in closures so that wasn't the answer. Putting the reference into another object meant that both could be released.

I added a ContentView because it makes it easier to play with and I added a print to the pipeline to see what was happening. The computed name is probably not necessary, it just made it look the same as you had. I also removed the Set, it's probably useful but I haven't worked out when.
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through