Difficulty trying to use a struct to replace a cluster of stored properties in a class

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.
  • 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?

Hmmm, here's an idea:

protocol BlurableViewController where Self: NSViewController {
    var blurOverlayVC: BlurOverlayVC { get set }
    // and some other things
}

extension BlurableViewController {
    func activate() { ... }
    func deactivate() { ... }
}

I can use a protocol, which makes the responsibility of storing the stored properties belong on the view controller, just like before I extracted this logic.

However, this still requires me to have a bunch of stored properties I need to repeat in each VC.

Terms of Service

Privacy Policy

Cookie Policy