1

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.

JDev
  • 5,168
  • 6
  • 40
  • 61

1 Answers1

1

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)
}
TylerP
  • 9,600
  • 4
  • 39
  • 43
  • 1
    Now I feel foolish. Thank you very much – for both the corrected code **and** explanation! Now I know not to make the same mistake in the future. Just a quick followup if you have the time: Why choose a `weak self` in instances like these? How would I go about knowing when to choose a `weak self` upon coding a new project or function? – JDev Mar 27 '20 at 00:35
  • No prob! In this particular case, I knew to use `[weak self]` because `self` owns a strong reference to `customizerURLObserver`, which owns a strong reference to the change handler closure specified in the call to `observe(_:options:)`, which would own a strong reference to `self` if I had not made `self` weak – so there would have been a retain cycle going from self->customizerURLObserver->closure->self, which would create a memory leak because none of those things would ever deallocate. In general, though, you pretty much just have to be aware of what holds strong references to what. – TylerP Mar 27 '20 at 02:23