swiftmacoscore-graphicscore-animationnsanimationcontext

NSAnimationContext crashing when called by an Observer


I have two WebViews: webView and customizerWebView. Both of these WKWebViews are attached by a trailing constraint. Essentially, when I go to the menu and click "Show Customizer" showCustomizer() or "Hide Customizer" hideCustomizer(), it calls the respective function and either shows or hides all the things related to customizerWebView.

To clarify, everything works and animates as expected when calling these functions from their attached NSMenuItems. However, when show/hideCustomizer() gets called from an Observer that essentially detects a URL - ie. url.contains("#close") - the app crashes on the first line of animator() code with the error: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

ViewController.swift

import Cocoa
import WebKit

class ViewController: NSViewController, WKUIDelegate, WKNavigationDelegate {
    var customizerURLObserver: NSKeyValueObservation?

    @IBOutlet var webView: WKWebView!
    @IBOutlet var customizerWebView: WKWebView!
    @IBOutlet var rightConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad
        ...
        customizerURLObserver = customizerWebView.observe(\.url, options: .new) { webView, change in
            let url = "\(String(describing: change.newValue))"
            ViewController().urlDidChange(urlString: url) }
    }

    func urlDidChange(urlString: String) {
        let url = cleanURL(urlString)
        if url.contains("#close") { hideCustomizer() }  // Observer call to hide function
    }

    @IBAction func showCustomizerMenu(_ sender: Any) { showCustomizer() }  // These work flawlessly
    @IBAction func hideCustomizerMenu(_ sender: Any) { hideCustomizer() }  // These work flawlessly

    func showCustomizer() {
        let customTimeFunction = CAMediaTimingFunction(controlPoints: 5/6, 0.2, 2/6, 0.9)
        NSAnimationContext.runAnimationGroup({(_ context: NSAnimationContext) -> Void in
            context.timingFunction = customTimeFunction
            context.duration = 0.3
            rightConstraint.animator().constant = 280
            customizerWebView.animator().isHidden = false
            webView.animator().alphaValue = 0.6
        }, completionHandler: {() -> Void in
        })
    }

    func hideCustomizer() {
        let customTimeFunction = CAMediaTimingFunction(controlPoints: 5/6, 0.2, 2/6, 0.9)
        NSAnimationContext.runAnimationGroup({(_ context: NSAnimationContext) -> Void in
            context.timingFunction = customTimeFunction
            context.duration = 0.3
            webView.animator().alphaValue = 1     // Found nil crash highlights this line
            rightConstraint.animator().constant = 0
        }, completionHandler: {() -> Void in
            self.customizerWebView.isHidden = true
        })
    }
}

Could someone please enlighten me as to why this animation looks and works flawlessly 100 times when called from the NSMenu, but crashes when hideCustomizer() gets called once from an Observer function?

I have also tried calling the NSMenu object function hideCustomizerMenu(self), but to no avail.


Solution

  • On the line:

    ViewController().urlDidChange(urlString: url)
    

    you are mistakenly creating a new instance of your view controller class and calling urlDidChange on that instance. Since this new instance is not created from a storyboard/xib, all of its outlets are nil, and thus when you try to call the animator method on its webView in hideCustomizer, it crashes because it's nil.

    Instead, call urlDidChange on self (actually a weakified self so that you don't create a retain cycle):

    customizerURLObserver = customizerWebView.observe(\.url, options: .new) { [weak self] webView, change in
        let url = "\(String(describing: change.newValue))"
        self?.urlDidChange(urlString: url)
    }