How to Prevent Strong Reference Cycles When Using Apple's New Combine Framework (.Assign Is Causing Problems)

How to prevent strong reference cycles when using Apple's new Combine framework (.assign is causing problems)

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
}
}
}

Combine: can't use `.assign` with structs - why?

The answer from Asperi is correct in so far as it explains the framework's design. The conceptual reason is that since passengers is a value type, passing it to assign(to:on:) would cause the copy of passengers passed to assign to be modified, which wouldn't update the value in your class instance. That's why the API prevents that. What you want to do is update the passengers.women property of self, which is what your closure example does:

minusButtonTapPublisher
.map { self.passengers.women - 1 }
// WARNING: Leaks memory!
.assign(to: \.passengers.women, on: self)
.store(in: &cancellables)
}

Unfortunately this version will create a retain cycle because assign(to:on:) holds a strong reference to the object passed, and the cancellables collection holds a strong reference back. See How to prevent strong reference cycles when using Apple's new Combine framework (.assign is causing problems) for further discussion, but tl;dr: use the weak self block based version if the object being assigned to is also the owner of the cancellable.

Combine assign(to: on:) another publisher

Because CurrentValueSubject is a class, you can use it as the object argument of assign(to:on:). This way, there is no memory leak:

class Download {

public let progress: CurrentValueSubject<Double, Never> = .init(0)

var subscriptions: [AnyCancellable] = []

func start(task: URLSessionTask) {
task.resume()
task.progress
.publisher(for: \.fractionCompleted)
.assign(to: \.value, on: progress)
.store(in: &subscriptions)
}

}

Is there a way to call assign() more than once in Swift Combine?

UPDATE

If your deployment target is a 2020 system (like iOS 14+ or macOS 11+), you can use a different version of the assign operator to avoid retain cycles and to avoid storing cancellables:

init() {
external.XPublisher.assign(to: $PublishedX)
external.YPublisher.assign(to: $PublishedY)
external.XPublisher
.combineLatest(external.YPublisher) { $0 * $1 }
.assign(to: $PublishedProduct)
}

ORIGINAL

There is no built-in variant of assign(to:on:) that returns another Publisher instead of a Cancellable.

Just use multiple assigns:

class ProductViewModel: ObservableObject {
@Published var PublishedX: Int = 0
@Published var PublishedY: Int = 0
@Published var PublishedProduct: Int = 0

init() {
external.XPublisher
.assign(to: \.PublishedX, on: self)
.store(in: &tickets)
internal.YPublisher
.assign(to: \.PublishedY, on: self)
.store(in: &tickets)
external.XPublisher
.combineLatest(internal.YPublisher) { $0 * $1 }
.assign(to: \.PublishedProduct, on: self)
.store(in: &tickets)
}

private var tickets: [AnyCancellable] = []
}

Note that these subscriptions create retain cycles. Swift will not be able to destroy an instance of ProductViewModel until the tickets array is cleared. (This is not a property of my suggestion. Your original code also needs to store its subscription somewhere, else it will be cancelled immediately.)

Also, the existence of PublishedProduct is questionable. Why not just a computed property?

var product: Int { PublishedX * PublishedY }

SwiftUI Combine: Nested Observed-Objects

When a Ball in the balls array changes, you can call objectWillChange.send() to update the ObservableObject.

The follow should work for you:

class BallManager: ObservableObject {
@Published var balls: [Ball] {
didSet { setCancellables() }
}
let ballPublisher = PassthroughSubject<Ball, Never>()
private var cancellables = [AnyCancellable]()

init() {
self.balls = []
}

private func setCancellables() {
cancellables = balls.map { ball in
ball.objectWillChange.sink { [weak self] in
guard let self = self else { return }
self.objectWillChange.send()
self.ballPublisher.send(ball)
}
}
}
}

And get changes with:

.onReceive(bm.ballPublisher) { ball in
print("ball update:", ball.id, ball.color)
}

Note: If the initial value of balls was passed in and not always an empty array, you should also call setCancellables() in the init.



Related Topics



Leave a reply



Submit