0

I have a bluetooth class which passes when a char value is updated to a closure in a view controller (as well as the same closure in a singleton class). when the VC deinit is called, the closure in the VC is still being executed when the char value is updated. I am using [weak self] for the closure in the VC. I'd like to be able to stop this VC closure from being called when the view is deinitialised. But I also don't understand why the other callback in the singleton is not being executed after the VC is presented!

Included below is the syntax for the closure inside the VC

bluetooth.updatedCharacteristicsValue { [weak self] char in
Phil
  • 151
  • 9

1 Answers1

2

[weak self] does not mean that the closure can be discarded, it only prevents the closure from retaining the VC (and therefore preventing the VC from being deinited).

Simply begin your closure with:

guard let self = self else { return }

... to exit early if the VC no longer exists.

As for why the closure supplied by the VC is being called but the one in the singleton isn't, it sounds like your bluetooth class doesn't understand the concept of multiple 'users'. Whoever registers their callback last is the one that is called.

An approach to handling your own observer registration with convenient self-unregistering tokens:

class ObserverToken {
    let id = UUID()
    private let onDeinit: (UUID) -> ()

    init(onDeinit: @escaping (UUID) -> ()) {
        self.onDeinit = onDeinit
    }

    deinit {
        onDeinit(id)
    }
}

class BluetoothThing {
    // Associate observers with the .id of the corresponding token
    private var observers = [UUID: (Int) -> ()]()

    func addObserver(using closure: @escaping (Int) -> ()) -> ObserverToken {
        // Create a token which sets the corresponding observer to nil
        // when it is deinit'd
        let token = ObserverToken { [weak self] in self?.observers[$0] = nil }
        observers[token.id] = closure
        return token
    }

    func tellObserversThatSomethingHappened(newValue: Int) {
        // However many observers we currently have, tell them all
        observers.values.forEach { $0(newValue) }
    }

    deinit {
        print("")
    }
}

// I've only made this var optional so that it can later be set to nil
// to prove there's no retain cycle with the tokens
var bluetooth: BluetoothThing? = BluetoothThing()

// For as long as this token exists, updates will cause this closure
// to be called. As soon as this token is set to nil, it's deinit
// will automatically deregister the closure
var observerA: ObserverToken? = bluetooth?.addObserver { newValue in
    print("Observer A saw: \(newValue)")
}

// Results in:
// Observer A saw: 42
bluetooth?.tellObserversThatSomethingHappened(newValue: 42)

// A second observer
var observerB: ObserverToken? = bluetooth?.addObserver { newValue in
    print("Observer B saw: \(newValue)")
}

// Results in:
// Observer A saw: 123
// Observer B saw: 123
bluetooth?.tellObserversThatSomethingHappened(newValue: 123)

// The first observer goes away.
observerA = nil

// Results in:
// Observer B saw: 99
bluetooth?.tellObserversThatSomethingHappened(newValue: 99)

// There is still one 'live' token. If it is retaining the
// Bluetooth object then this assignment won't allow the
// Bluetooth to deinit (no wavey hand)
bluetooth = nil

So if your VC stores it's token as a property, when the VC goes away, the token goes away and the closure is deregistered.

Chris
  • 3,445
  • 3
  • 22
  • 28
  • I have implemented that guard statement so what you said makes sense (thanks!). my issue is the closure is still called only in the VC. whereas I'd like it to be called into my singleton instead where I also have a callback (which is working fine before I enter that VC) – Phil Aug 07 '19 at 09:44
  • @Phil See the addition to my answer for a possible explanation. Is the bluetooth code your own or something like SwiftyBluetooth? – Chris Aug 07 '19 at 09:47
  • The code is my own yes. Yeah that makes sense, so basically I guess my question is- am I able to have these 'multiple users'? or if not, can I remove that closure in the VC and somehow reenable the one in my singleton? – Phil Aug 07 '19 at 09:49
  • You could keep a list of observers and have them register their closures via a function in your bluetooth class (but then ideally you need to provide a way to unregister them for when your VC goes away, so then you need to be able to identify them individually to unregister the right one). Or you could change your Bluetooth class to emit notifications and then your 'users' just register and deregister via the usual NotificationCenter mechanisms. – Chris Aug 07 '19 at 09:52
  • Ok I think I understand the problem now. I thought that I was able to have multiple closures that would be executed at once. Ill check out keeping a list of observers. Thanks for your help mate! – Phil Aug 07 '19 at 09:55
  • What's the current implementation of `bluetooth.updatedCharacteristicsValue` look like? – Chris Aug 07 '19 at 09:56
  • Very simple as it just passes the characteristic to either the singleton or VC and logic is performed there bluetooth.valueUpdated = { char in completed(char) } – Phil Aug 07 '19 at 09:59