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 assign
s:
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
Print() to Console Log with Color
Bold Part of String in Uitextview Swift
Uicollectionview Compositionallayout Not Calling Uiscrolldelegate
Keyboard Overlaying Action Sheet in iOS 13.1 on Cncontactviewcontroller
What Is the Advantage of a Lazy Var in Swift
Confusion Due to Swift Lacking Implicit Conversion of Cgfloat
How to Remove Spaces from a String in Swift
How to Line Break Long Large Title in iOS 11
Swift - Seeding Arc4Random_Uniform? or Alternative
Rotate an Object in Its Direction of Motion
Iso8601 Date JSON Decoding Using Swift4
Swift Convert String to Unsafemutablepointer<Int8>
How to Hide Labels in iOS-Charts
How to Open Settings App Programmatically
How to Clear Alamofireimage Setimagewithurl Cache
Xcode 8.3 Can't Support Swift 2.3
Multiplying Variables and Doubles in Swift
How to Set the Alpha of an Uiimage in Swift Programmatically