0

Let's say I have a Swift class that stores a completion block, and does a few asynchronous tasks.

I want that block to be called by whichever of the tasks finishes first, but only that one - I don't want it to be called again when the second task finishes.

How can I implement this in a clean way?

bkbeachlabs
  • 2,121
  • 1
  • 22
  • 33

2 Answers2

0

As long as you don't need this to be thread safe, you can solve this problem with a fairly straightforward @propertyWrapper.

@propertyWrapper
struct ReadableOnce<T> {
    var wrappedValue: T? {
        mutating get {
            defer { self._value = nil }
            return self._value
        }
        set {
            self._value = newValue
        }
    }
    private var _value: T? = nil
}

Mark the completion block var with @ReadableOnce, and it will be destroyed after the first time it's value is read.

Something like this:

class MyClass {
    @ReadableOnce private var completion: ((Error?) -> Void)?

    init(completion: @escaping ((Error?) -> Void)) {
        self.completion = completion
    }

    public func doSomething() {
        // These could all be invoked from different places, like your separate tasks' asynchronous callbacks
        self.completion?(error) // This triggers the callback, then the property wrapper sets it to nil.
        self.completion?(error) // This does nothing
        self.completion?(error) // This does nothing
    }
}

I wrote up more of a detailed discussion of this here but the key thing to be aware of is that reading the value sets it to nil, even if you don't invoke the closure! This might be surprising to someone who isn't familiar with the clever property wrapper you've written.

bkbeachlabs
  • 2,121
  • 1
  • 22
  • 33
  • 1
    I would just be careful with this. In your case "does nothing" might be fine, but in general you probably want to log, fail a test, etc. when the later calls occur. Otherwise you might end up with strange tasks going on in the background that you never notice and which eat up memory/battery. – Alexander Feb 28 '20 at 02:13
  • Totally agree - this may not be appropriate for all cases (or any depending on how you feel about this :D) – bkbeachlabs Feb 28 '20 at 02:15
  • I don't quite see why the property wrapper is needed. There is already a standard built-in way of expressing onceness; why not just use it? – matt Feb 28 '20 at 02:19
  • @matt Oh is there a better way I've missed? Thats's certainly possible - what are you referring to? – bkbeachlabs Feb 28 '20 at 02:22
  • See https://stackoverflow.com/questions/37886994/dispatch-once-after-the-swift-3-gcd-api-changes, https://stackoverflow.com/questions/37801407/whither-dispatch-once-in-swift-3 and others. – matt Feb 28 '20 at 03:19
0

There is already a standard expression of onceness. Unfortunately the standard Objective-C is unavailable in Swift (GCD dispatch_once), but the standard Swift technique works fine, namely a property with a lazy define-and-call initializer.

Exactly how you do this depends on the level at which you want onceness to be enforced. In this example it's at the level of the class instance:

class MyClass {
    // private part
    private let completion : (() -> ())
    private lazy var once : Void = {
        self.completion()
    }()
    private func doCompletionOnce() {
        _ = self.once
    }
    // public-facing part
    init(completion:@escaping () -> ()) {
        self.completion = completion
    }
    func doCompletion() {
        self.doCompletionOnce()
    }
}

And here we'll test it:

    let c = MyClass() {
        print("howdy")
    }
    c.doCompletion() // howdy
    c.doCompletion()
    let cc = MyClass() {
        print("howdy2")
    }
    cc.doCompletion() // howdy2
    cc.doCompletion()

If you promote the private stuff to the level of the class (using a static once property), the completion can be performed only once in the lifetime of the entire program.

matt
  • 515,959
  • 87
  • 875
  • 1,141