Thursday, April 9, 2015

Using UILayoutPriority in Swift

Swift is supposed to be a new magical language that greatly reduces the barrier to iOS and OS X development. Well, sort of...  I used to go to in-house tech talks where a computer language expert presented bits of code he had refined over the years to be more concise. This guy loved (and I mean Loved) Ruby. He went on and on about why Ruby was such a great language. But he was generally unsatisfied with all modern programming languages and often wanted to create his own. I'd love to chat with him now and see what he thought of Swift. I think he'd love it. Swift seems to incorporate most of what any mature developer wants.

The reality for many experienced Objective-C developers is that taking the existing Cocoa and Cocoa Touch libraries and making them work with Swift has been less than wonderful. Perhaps if Apple could start fresh, and re-write all the great libraries we know and love to "work with" Swift, we'd be much happier with Swift as a language. But Swift doesn't really seem to be fitting into places where Objective-C was actually working very well (for us that know Objective-C already).

Let's take a specific example: setting priority for layout constraints in code using Swift. The old way was pretty straight forward. If we wanted to set the content hugging priority for a view we would do so like this:

UIView * myView = [[UIView alloc]init];
[myView setContentHuggingPriority:UILayoutPriorityDefaultHigh 
                          forAxis:UILayoutConstraintAxisHorizontal];


Note that the use of UILayoutPriorityDefaultHigh makes it easier for us to do what we want, without having to know that the number 1000 would have the same effect, while the number 25 would not have the desired effect.

The documentation for this method looks like this:

Declaration

SWIFT
func setContentHuggingPriority(_ priority: UILayoutPriority,
                             forAxis axis: UILayoutConstraintAxis)
OBJECTIVE-C
- (void)setContentHuggingPriority:(UILayoutPriority)priority

                          forAxis:(UILayoutConstraintAxis)axis

We don't really know what a UILayoutPriority is, only that it's not an object (it's not an NSString for example). UILayoutPriority could be an integer or a float or an enum. If we look at the documentation for UILayoutPriority, this is what we find:

Declaration

SWIFT
typealias UILayoutPriority = Float
OBJECTIVE-C
enum {
  UILayoutPriorityRequired = 1000,

  UILayoutPriorityDefaultHigh = 750,

  UILayoutPriorityDefaultLow = 250,

  UILayoutPriorityFittingSizeLevel = 50,

};

typedef float UILayoutPriority;

O.K. UILayoutPriority is a float. In Objective-C, a couple of values have been pre-defined for us in an enum. We might expect that the same enums would be available to us in Swift. Certainly these two questions in Stack Overflow thought this to be a reasonble assumption: http://stackoverflow.com/questions/25881872/strange-exception-in-layoutshttp://stackoverflow.com/questions/27210527/swift-set-content-compression-resistance

But in Xcode, if you ask to see the defintion of one of these enum values (UILayoutPriorityRequired, for example), you will see that they are actually defined in the header file as constant floats.

typedef float UILayoutPriority;
static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000; // A required constraint.  Do not exceed this.
static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750; // This is the priority level with which a button resists compressing its content.
static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250; // This is the priority level at which a button hugs its contents horizontally.

Very interesting. So although we may like to think of the pre-defined layout priorities as enum values (as the documentation suggests or actually blatantly states) the layout priorities are not really defined as enums; they are defined as constant floats.

The iBook says

Swift imports as a Swift enumeration any C-style enumeration marked with the NS_ENUM macro. This means that the prefixes to enumeration value names are truncated when they are imported into Swift, whether they’re defined in system frameworks or in custom code.

Excerpt From: Apple Inc. “Using Swift with Cocoa and Objective-C.” iBooks. https://itun.es/us/1u3-0.l

That means that if UILayoutPriority had been defined as an integer using the NS_ENUM macro, it would have been imported into Swift as Swift enum
For example, see this Objective-C enumeration:
OBJECTIVE-C
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
   UITableViewCellStyleDefault,
   UITableViewCellStyleValue1,
   UITableViewCellStyleValue2,
   UITableViewCellStyleSubtitle
};

In Swift, it’s imported like this:
SWIFT
enum UITableViewCellStyle: Int {
    case Default
    case Value1
    case Value2
    case Subtitle
}

Excerpt From: Apple Inc. “Using Swift with Cocoa and Objective-C.” iBooks. https://itun.es/us/1u3-0.l

So back to what we want to do, which is to set content hugging priority. Remember that the function / method takes two parameters, the first of type UILayoutPriority and the second of type UILayoutConstraintAxis.
In Objective-C, we can either use a raw value (such as 564.75) or a pre-defined value such as UILayoutPriorityDefaultHigh. But in Swift - as of the time of this writing - there has been no porting over of the conventient pre-defined values, so you have to know and use a raw value.
What about the second value, the UILayoutConstraintAxis? The documentation states

Declaration

SWIFT

enum UILayoutConstraintAxis : Int {
    case Horizontal
    case Vertical
}
OBJECTIVE-C
enum {
   UILayoutConstraintAxisHorizontal = 0,
   UILayoutConstraintAxisVertical = 1
};
typedef NSInteger UILayoutConstraintAxis;
If you're thinking ahead, you already know what's coming next. The type of UILayoutConstraintAxis is NSInteger. It is listed in the documentation as an enum for both languages, Objective-C and Swift. So the header file in Objective-C likely defines UILayoutConstraintAxis using the NS_ENUM macro. Let's see if that's true.

//
// UIView Constraint-based Layout Support
//

typedef NS_ENUM(NSInteger, UILayoutConstraintAxis) {
    UILayoutConstraintAxisHorizontal = 0,
    UILayoutConstraintAxisVertical = 1
};

Yes, indeed, as expeted UILayoutConstraintAxis is defined as an NSInteger using the NS_ENUM macro. So that's why we can use the enum value UILayoutConstraintAxisHorizontal in either language. The two enum values have been imported from Objective-C to Swift.
So we have discussed two ways to know if a pre-defined value you are used to using in Objective-C is available in Swift:
  1. check the documentation
  2. check the header file in Objective-C (found by right-clicking the value and then selecting "Jump to Definition")
There is one more way to see if a typedef you are used to using is a constant or an enum. In code, test to see if the address of the constant exists. Constants have a memory address, while enums do not. See the code below.

// this line will compile and run just fine. 
// UILayoutPriorityDefaultHigh is a constant and has a memory address
// the value will be true if the device is running iOS 6.0 or later
// and false otherwise
BOOL predefinedValueIsAvailable = (NULL != &UILayoutPriorityDefaultHigh);

// this line will not compile
// UILayoutConstraintAxisHorizontal is an enum (NOT a constant) 
// and does not have a memory address
predefinedValueIsAvailable = (NULL != &UILayoutConstraintAxisHorizontal);

So Swift may not be the Pancea we expect if we have a lot of Objective-C knowledge and habits. Perhaps eventually we will displace this knowledge with the Swift way of doing thigs. In the meantime, remember not to assume that every single nuance of what you're used to doing in Objective-C is immediately available and identical in Swift. Check the documentation; it often helps!

3 comments: