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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With