Invoke code in extension on object deinit?
Asked Answered
S

1

6

In Swift (I'm using 4.1), is there a way to do some clean-up in an extension when an object is being destructed? I have in mind the kind of code that would go in deinit(), but an extension can't declare deinit(). (Besides which, if several extensions needed to do this, there would be multiple deinit() declarations.)

Series answered 13/4, 2018 at 18:17 Comment(2)
No, you cannot override deinit in an extension, only in a subclass. Can you elaborate on the possible use-case? – A possible workaround might be to “associate“ another object (compare https://mcmap.net/q/175831/-is-there-a-way-to-set-associated-objects-in-swift/1187415) which is then deallocated together with the main object.Rely
@MartinR - An associated object is a nice way to introduce state data in an extension. What I had in mind was more along the lines of the example of a Bank and a Player provided by Apple's description of deinit(), but where the Player behavior was introduced as an extension to some other class.Series
E
10

I did not find a way to exactly get what you want, but maybe this code will help. I have never tried it, so maybe use it more as an inspiration. In a nutshell, it allows you to add bits of code that will excute on deinitialization.

/// This is a simple object whose job is to execute
/// some closure when it deinitializes
class DeinitializationObserver {

    let execute: () -> ()

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

    deinit {
        execute()
    }
}

/// We're using objc associated objects to have this `DeinitializationObserver`
/// stored inside the protocol extension
private struct AssociatedKeys {
    static var DeinitializationObserver = "DeinitializationObserver"
}

/// Protocol for any object that implements this logic
protocol ObservableDeinitialization: AnyObject {

    func onDeinit(_ execute: @escaping () -> ())

}

extension ObservableDeinitialization {

    /// This stores the `DeinitializationObserver`. It's fileprivate so you
    /// cannot interfere with this outside. Also we're using a strong retain
    /// which will ensure that the `DeinitializationObserver` is deinitialized
    /// at the same time as your object.
    fileprivate var deinitializationObserver: DeinitializationObserver {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.DeinitializationObserver) as! DeinitializationObserver
        }
        set {
            objc_setAssociatedObject(
                self,
                &AssociatedKeys.DeinitializationObserver,
                newValue,
                objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
            )    
        }
    }

    /// This is what you call to add a block that should execute on `deinit`
    func onDeinit(_ execute: @escaping () -> ()) {
        deinitializationObserver = DeinitializationObserver(execute: execute)
    }
}

How it works

Now let's assume you have your object, let's call it A and you want to create some code inside an extension, you can use onDeinit to add your clean-up code like this:

extension A {

    func someSetup() {

        // Do your thing...


        onDeinit {
            /// This will be executed when A is deinitialized
            print("Execute this")
        }
    }
}

You can also add it from outside the extension,

let a = A()
a.onDeinit {
    print("Deinitialized")
}

Discussion

  • This is different than what you want in that instead of definition a function (deinit) you need to call one. But anyway in protocol extensions, you can never really use the underlying object's life cycle, so it should be okay for most cases.
  • I am not sure about the order of execution between A's deinit and the DeinitializationObserver's deinit. You might need to drop the assumption that A is still in memory when you write the code inside the closure.
  • If you need more than one onDeinit you can update the associated object to retain an array of DeinitializationObserver
  • In my code, I usually use ReactiveCocoa's Lifetime for this kind of things. However, it is more complicated than what I wrote here. Instead, it looks like they swizzle the dealloc selector. If you're interested you can take a look inside - https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/ReactiveCocoa/NSObject+Lifetime.swift
Exum answered 13/4, 2018 at 20:13 Comment(4)
This is really interesting. It's a lot slicker than what I was thinking of, which was to define some sort of "deinit registry" in the base class of functions that should be called from deinit(), and implement deinit() to run through the registry, calling each function. However, the fact that A may not be in memory is an issue, particularly if the extension uses state data (stored in an associated object) as part of its own clean-up. I'll have to play around with this a bit to see how it works.Series
Glad to see that it might help you. If you need to ensure that the blocks are all executed prior to the de-init, I would encourage you look inside the ReactiveCocoa code because that's what they do. If it's just because you need state data, then the closure itself should be able to retain whatever you need to clean-up.Exum
It's absolutely awesome, working and genious. On one hand try not to abuse using it everywhere not to mess your code, but somewhere it's very great in place. For example if you subscribe for notification in extension you obviously want to unsubscribe on deinit; otherwise you have bad access. Five stars to you friend!Supportable
updated link from post: github.com/ReactiveCocoa/ReactiveCocoa/blob/master/…Jame

© 2022 - 2024 — McMap. All rights reserved.