I have a pattern that repeats in quite a few places across my code base, which I'm trying to extract out. This happens to be in the macOS/AppKit space, but I've reduced my code example down to be platform-agnostic.
The repeating code in question is responsible covering up or uncovering a view controller (well, it's view), with a "Blur Overlay".
There's two parts:
- The
activate()
function, which:- for instantiates a
BlurOverlayViewController
from a storyboard - saves the reference to the VC to a stored property for layer use
- setting up constraints on it (so that it covers the full width/height of the "target" that it's overlaying)
- Adds the view to the hierarchy and animates in its appearance.
- for instantiates a
- The
deactivate()
function, which:- Accesses the stored property to get a reference to the overlay VCV
- Animates out the disappearance of the overaly
- Once the overlay is totally transparent, it removes the overlay from the view hierarchy
- It destroys the overlay, and nils-out the overlayVC stored property. This is where the issue lies.
Here's the concrete code. The error is right near the bottom.
// Some stubs to remove the dependancy on AppKit
class NSViewController {}
class NSAnimationContext {
class func runAnimationGroup(_ changes: (NSAnimationContext) -> Void, completionHandler: (() -> Void)? = nil) {}
var allowsImplicitAnimation: Bool = false
var duration: Double = 1.23
}
/// The VC that will be shown atop another VC to blur it out and show a message to the user
class BlurOverlayViewController: NSViewController {
static func loadFromStoryboard() -> Self { fatalError("stub") }
func configureToCoverUp(_ targetVC: NSViewController) {}
func removeFromViewHeirarchy() {}
func startUnblur() {}
func startBlur() {}
}
// My BlurOverlay abstraction
struct BlurOverlay {
private var overlayVC: BlurOverlayViewController? = nil
private let targetVC: NSViewController
init(targetVC: NSViewController) {
self.targetVC = targetVC
}
/// Activate the blur overlay
public mutating func activatate() {
guard self.overlayVC == nil else { return } // Already active
let _overlayVC = BlurOverlayViewController.loadFromStoryboard()
overlayVC = _overlayVC
_overlayVC.configureToCoverUp(self.targetVC)
NSAnimationContext.runAnimationGroup { animationContext in
animationContext.allowsImplicitAnimation = true
animationContext.duration = 1
_overlayVC.startBlur()
}
}
/// Deactivate the blur overlay
public mutating func deactivate() {
guard let overlayVC = self.overlayVC else { return } // Already inactive
NSAnimationContext.runAnimationGroup { animationContext in
animationContext.allowsImplicitAnimation = true
animationContext.duration = 1
overlayVC.startUnblur()
} completionHandler: { // ❌ error: escaping closure captures mutating 'self' parameter
overlayVC.removeFromViewHeirarchy()
// Then the overlayVC stored property is nil-ed out, which leads to the destruciton of the VC.
self.overlayVC = nil // 📝 note: captured here
}
}
}
When this code used to be "embedded" into the view controllers that used it, it worked fine, because the NSAnimationContext
completion handler could capture a mutating reference to self (the view controller, which was an instance of a class).
Of course, structs don't allow you to make escaping mutable captures of self
, because that would introduce aliasing/reference semantics. So when I extracted out this logic into a struct, that mutable capture is no longer possible.
In reality, the lifetime of this struct is one-to-one with the NSViewController subclass that will be holding it as a stored property, but I don't think there's a way to express that in Swift. Is that correct?
Is this an instance where I'm forced to use a class
for my BlurOverlay, or is there some other technique I'm missing?