Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Xcode Forcing Swift Optional Unwraps Twice (!!)

I am subclassing a UIStoryboardSegue and every time I try to use one of the two UIViews, Xcode is making me add two optional unwraps (!!) such as:

let sourceView = self.sourceViewController.view
sourceView!!.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight

or

let sourceView = self.sourceViewController.view!
sourceView!.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight

or

self.sourceViewController.view!!.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight

I'm wondering if someone could explain why this is.

like image 312
Doug Mead Avatar asked Apr 29 '15 00:04

Doug Mead


2 Answers

The sourceViewController property of UIStoryboardSegue is typed as AnyObject, and as an Objective-C compatibility feature, once you import Foundation, things get a little weird with AnyObject.

Instead of looking for methods on the AnyObject type, Swift looks for Objective-C selectors instead, exactly as Objective-C does with the id type. Any selector from any class is fair game: if you wanted, you could try to invoke activeProcessorCount on your object, even though that's a NSProcessInfo selector, and the compiler would let you do it. (It would fail at runtime for obvious reasons.) This is called dynamic dispatch, in opposition to static dispatch (the normal calling mechanism in Swift).

One thing about dynamic dispatch, though, is that it always adds a layer of implicit wrapping. If you have an Objective-C property that returns a String, dynamic dispatch will have it return a String!.

Things get hairy when multiple classes declare selectors with the same name but different return types (or with different parameters, but we're not interested in this case here). I don't know how the compiler chooses which selector out of the many identically-named ones it knows about. Either way, it picks one, and you're stuck with it unless you cast the object to a more precise type, in which case the Swift compiler will only let you use static dispatch.

Using swiftc's -dump-ast argument, we can see a lisp-like representation of how the compiler parsed the expression:

(pattern_binding_decl
  (pattern_named type='UIView?!' 'sourceView')
  (dynamic_member_ref_expr type='UIView?!' location=MySegue.swift:15:46 range=[MySegue.swift:15:25 - line:15:46] decl=UIKit.(file).UIGestureRecognizer.view
    (member_ref_expr type='AnyObject' location=MySegue.swift:15:25 range=[MySegue.swift:15:25 - line:15:25] decl=UIKit.(file).UIStoryboardSegue.sourceViewController
      (derived_to_base_expr implicit type='UIStoryboardSegue' location=MySegue.swift:15:20 range=[MySegue.swift:15:20 - line:15:20]
        (declref_expr type='Segue' location=MySegue.swift:15:20 range=[MySegue.swift:15:20 - line:15:20] decl=xxx.(file).Segue.func [email protected]:14:7 specialized=no)))))

There's a lot of cruft, but you can see that it generated a dynamic_member_ref_expr instead of a member_ref_expr. If you scroll all the way to the right to the decl "attribute", you'll see that it is using UIGestureRecognizer's view property (declared as UIView?), so that the expression returns a UIView?!.

In contrast, UIViewController's view property is declared as UIView!. If the compiler picked this selector instead, you would have ended up with a UIView!!, and you would need only one level of explicit unwrapping.

You can (and must, in instances like this one) explicitly unwrap implicitly-unwrapped values. With sourceView as a UIView?!, the first ! unwraps the UIView?! into UIView?, and the second one unwraps the UIView? into a finally usable UIView. This is why you need two exclamation marks.

The class that declares the selector used for dynamic dispatch is irrelevant, as long as the target object implements it, accepts compatible arguments, and returns a compatible type. UIView? and UIView! are compatible at the binary level, so in the end, your program still runs. However, if you were somehow expecting a String or another unrelated type, you could be in for a surprise. In my opinion, you should avoid dynamic dispatch as much as you can.

tl;dr: if you cast sourceViewController to UIViewController, you get the correct view property definition and won't need to unwrap it at all.

like image 181
zneak Avatar answered Oct 19 '22 05:10

zneak


self.sourceViewController.view is a rare case of a "double-wrapped Optional":

  • once because UIStoryboardSegue's sourceViewController is typed as an AnyObject - to which the view message can be sent, because type-checking is suppressed, but at the cost of getting back an Optional (as I explain my book: http://www.apeth.com/swiftBook/ch04.html#SECsuppressing)

  • and again because a UIViewController might or might not have a view - indeed, a UIViewController's view is initially nil, until the view loads and viewDidLoad is called - so the view property is itself typed as an Optional

Thus we wind up with an Optional wrapped in an Optional. What I do, in this situation, is write it like this in Swift 1.1 or before:

let sourceView = (self.sourceViewController as UIViewController).view

Or with as! in Swift 1.2:

let sourceView = (self.sourceViewController as! UIViewController).view

The downcast settles all doubts at once. Now, setting sourceView.frame just works.

like image 21
matt Avatar answered Oct 19 '22 04:10

matt