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

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

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.


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

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

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

Tags:

Swift

Combine