Use Your Loaf

Change the Width of the Master View in a Split View Controller

How do you change the width of the master view in a Split View Controller?

The side-by-side mode of a UISplitViewController shows both master and detail view controllers onscreen at the same time. You can see an example of this below taken from my WorldFacts example code running in portrait on an iPad Air.

If you look carefully you will notice that the master view is too narrow and truncates the country name for the “Democratic Republic of the Congo”. The UISplitViewController sets the width of the master view but by default has a maximum width of 320 points.

Primary Column Width

The UISplitViewController class has three new properties available from iOS 8 that allow you to set the width of the master view:

preferredPrimaryColumnWidthFraction: A CGFloat between 0.0 and 1.0 that sets the preferred width of the primary (master) column as a percentage of the width of the split view controller. The values of minimumPrimaryColumnWidth and maximumPrimaryColumnWidth limit the actual width of the master view.

minimumPrimaryColumnWidth: A CGFloat for the minimum width in points of the master view content. Default value of UISplitViewControllerAutomaticDimension sets a minimum width of 0 points.

maximumPrimaryColumnWidth: A CGFloat for the maximum width in points of the master view content. Default value of UISplitViewControllerAutomaticDimension sets a maximum width of 320 points.

The above properties are also animatable if you need to change them at runtime. If you need the actual width of the master view there is a read-only property primaryColumnWidth.

Setting the Width of the Master View

My first thoughts were to set the width of the master view to be 50% of the split view controller width. In my example project I can do this when configuring the split view controller in the Application Delegate:

splitViewController.preferredPrimaryColumnWidthFraction = 0.5;

Remember that the value of maximumPrimaryColumnWidth limits the actual width of the view and has a default value of 320 points. An iPad has a width of 768 points in portrait and 1024 points in landscape. Either way 50% of our screen width exceeds the maximum primary column width so we need to increase the maximum value. In this case I set it to the full width of the split view which is large enough for either orientation.

splitViewController.maximumPrimaryColumnWidth = splitViewController.view.bounds.size.width;

The screenshot below shows the appearance of the split view controller in portrait with the master and detail views now each using 50% of the screen width:

Unfortunately a setting of 50% makes the master view look a little too large in landscape mode:

A better approach to deal with the original problem with a master view that was too narrow is to leave the fraction property alone and adjust the minimum and maximum values. I want the minimum width of the master view to be half the width of the device in portrait mode which will be the minimum of the view width and height regardless of the launch orientation.

CGFloat minimumWidth = MIN(CGRectGetWidth(splitViewController.view.bounds),CGRectGetHeight(splitViewController.view.bounds));
splitViewController.minimumPrimaryColumnWidth = minimumWidth / 2;
splitViewController.maximumPrimaryColumnWidth = minimumWidth;

The maximum value is less important as long as it does not constrain the minimum width. The appearance in portrait mode is unchanged but the landscape mode is now better proportioned:

As always you can find the full code example in my GitHub repository

Animating Autolayout Constraints

Updated 23 May 2015: I reworked this code a little from the original posting based on feedback to avoid adding and removing constraints at runtime. Instead I now animate changing the relative priorities of the yellow and blue view constraints which is a lot simpler as well as being more efficient.

Before autolayout if you wanted to move a view on screen you would manipulate the frame of the view to change either the origin or size. The frame along with the bounds and center are among the properties of UIView that you can animate so that the user sees the changes happen over a short period of time.

If you start using autolayout you quickly learn that you should not directly change the frame (or bounds or center) of a view. Instead you need to change the autolayout constraints. This post walks through a simple example showing how you can achieve the same view animation effects using autolayout.

The Challenge

To keep it simple we will use just two views - a yellow view and a blue view. In “normal” mode only the yellow view should be visible. In “fancy” mode both the yellow and blue views should be visible. The views should fill the screen apart from the standard margins at the edge of the device and space for a switch that toggles between modes. The animated gif below shows what we want to achieve.

The blue view should slide into and out of the view from the right and the yellow view should resize to fit.

Setting up the base constraints

To get started I will setup the views and autolayout constraints in Interface Builder for the situation when both views are onscreen.

The yellow view has five constraints: a leading space constraint to the superview, a trailing space constraint to the blue view, top and bottom spacing to the switch and bottom layout guide and finally equal width to the blue view.

The blue view has a similar setup with five constraints except that it has a trailing space constraint to the superview:

Nonrequired Priorities

For the situation where only the yellow view is visible it also needs a trailing space constraint to the superview. If I add that constraint it will conflict with the blue trailing constraint as they will both have a priority of 1000. To avoid the conflict and to move the blue view on and offscreen we can change the relative priorities of the trailing space constraints for the yellow and blue views.

Required constraints have a priority of UILayoutPriorityRequired (1000). You cannot change the priority of a required constraint at runtime. A priority less than UILayoutPriorityRequired is an optional or nonrequired constraint. You can change the priority of a nonrequired constraint at runtime as long as you do not set it to UILayoutPriorityRequired.

So first we can adjust the priority of the blue trailing space constraint to set the priority to High (750).

We can then also add a trailing space constraint from the yellow view to the superview and also set its priority to High (750).

Creating Outlets for the Constraints

To be able to change the priority of the blue trailing space constraint at runtime we need to set up an outlet in our view controller connected to the constraint in the storyboard. You can create an outlet to a constraint the same way you create outlets for any other object in Interface Builder by control dragging from the constraint into the view controller.

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *blueViewConstraint;

To be sure we push the blue view offscreen we also need to be able to adjust the spacing between the views. So we will also create an outlet in our view controller for the yellow-blue horizontal spacing constraint:

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *viewSpacingContraint;

Updating the constraints

It is now easy to write a method to set the correct priority for the blue constraint depending on the mode switch:

- (void)updateConstraintsForMode {
  if (self.modeSwitch.isOn) {
    self.viewSpacingContraint.constant = 8.0;
    self.blueViewConstraint.priority = UILayoutPriorityDefaultHigh+1;
  } else {
    self.viewSpacingContraint.constant = self.view.frame.size.width;
    self.blueViewConstraint.priority = UILayoutPriorityDefaultHigh-1;
  }
}

We set the priority of the yellow trailing space constraint to UILayoutPriorityDefaultHigh in the storyboard. To make the blue view visible we need the priority of its trailing space constraint to be some value higher than UILayoutPriorityDefaultHigh. To hide the blue view we set the priority to a lower value.

Note that we also set the width between the two views to some arbitrary large value (in this case I use the width of the superview) when we want to be sure that the blue view is off screen to push it beyond the right margin.

We should also configure the constraints when the view first loads:

- (void)viewDidLoad {
  // ...
  [self updateConstraintsForMode];
}

Animating the changes

We now have everything we need to animate the changes when the user switches mode. The Apple Auto Layout Guide describes the basic approach to animating changes made by auto layout. The recommended code snippet for iOS is as follows:

[containerView layoutIfNeeded];
[UIView animateWithDuration:1.0 animations:^{
  // Make all constraint changes here
  [containerView layoutIfNeeded];
}];

The two calls to layoutIfNeeded on the container view force any pending operations to finish and then in the animation block capture the frame changes. Applying this approach to our example when the mode switch changes we have:

- (IBAction)enableMode:(UISwitch *)sender {
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  [defaults setBool:sender.isOn forKey:modeUserDefaultKey];
  [defaults synchronize];

  [self.view layoutIfNeeded];
  [UIView animateWithDuration:1.0 animations:^{
    [self updateConstraintsForMode];
    [self.view layoutIfNeeded];
  }];
}

Wrapping Up

You can find the full AnimatedConstraints example Xcode project from this article in my Coding Examples GitHub repository.

Sorting an Array of Dictionaries

How would sort an array of dictionaries in Objective-C? Now how would you do it in Swift?

Let’s suppose we have a dictionary object with three keys (“surname”, “given” and “title”) to represent the contact details for a person. The question is how would we sort an array of such dictionary items. For illustration, consider the following array of dictionaries. We need to sort first on the value of the “surname” key and then on the value of the “given” key:

NSArray *people = @[
        @{@"surname":@"Simpson", @"given":@"Homer", @"title":@"Mr"},
        @{@"surname":@"Simpson", @"given":@"Marge", @"title":@"Mrs"},
        @{@"surname":@"Simpson", @"given":@"Bart", @"title":@"Mr"},
        @{@"surname":@"Simpson", @"given":@"Lisa", @"title":@"Miss"},
        @{@"surname":@"Simpson", @"given":@"Maggie", @"title":@"Miss"},
        @{@"surname":@"Flanders", @"given":@"Ned", @"title":@"Mr"}
        ];

That is easily achieved with two sort descriptors as follows:

NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name"  ascending:YES];
NSSortDescriptor *givenDescriptor = [[NSSortDescriptor alloc] initWithKey:@"given"  ascending:YES];
NSArray *sortDescriptors = @[nameDescriptor, givenDescriptor];

Sorting the array of dictionaries is then trivial:

NSArray *ordered = [people sortedArrayUsingDescriptors:sortDescriptors];

So What About Swift?

I still don’t find it easy to move between Objective-C and Swift code. To get some practise I wondered how the above would be implemented in Swift?

First the easy part of constructing the array of dictionaries which is, I think, a little cleaner than the Objective-C syntax:

let family = [
    ["surname":"Simpson", "given":"Homer", "title": "Mr"],
    ["surname":"Simpson", "given":"Marge", "title": "Mrs"],
    ["surname":"Simpson", "given":"Bart", "title": "Mr"],
    ["surname":"Simpson", "given":"Lisa", "title": "Miss"],
    ["surname":"Simpson", "given":"Maggie", "title": "Miss"],
    ["surname":"Flanders", "given":"Ned", "title": "Mr"]
]

(I should say here that for the purposes of this discussion I am ignoring the fact that there are better ways to represent this data)

To sort a Swift array I want to use the Swift standard library rather than falling back on the more familiar foundation classes. Sorting an array of strings in Swift is very simple and obvious:

let names = ["Homer","Marge","Bart","Lisa","Maggie"]
let ordered = sorted(names,<)
// [Bart, Homer, Lisa, Maggie, Marge]

The power of the Swift type system and the ability to infer so much from types leads to some very terse code. To sort an array of String values we need a comparison closure that takes two String arguments and returns a Bool:

(String, String) -> Bool

In the most verbose form you can even write this as a standalone function as follows:

func compareNames(s1:String, s2:String) -> Bool {
    return s1 < s2
}
let ordered = sorted(names,compareNames)

We can move that function to an inline closure expression and infer the types of the two arguments and the return value and use $0, $1 as shorthand for the s1, s1 arguments:

let ordered = sorted(names, {$0 < $1} )

Since the String type implements the “<” operator which is just a function that takes two String values and returns a Bool you end up at the more readable statement:

let ordered = sorted(names,<)

So to sort our array of dictionaries we need to start with a function that compares two dictionaries containing our person keys and values (both of type String):

func personSort(p1:[String:String], p2:[String:String]) -> Bool {
    if (p1["surname"] == p2["surname"]) {
        return p1["given"] < p2["given"]
    } else {
        return p1["surname"] < p2["surname"]
    }
}

Comparing to the Objective-C code this is equivalent to creating the sort descriptors. We first compare the surnames and if equal also compare the given names. The verbose way to sort our array of dictionaries is then as follows:

let ordered = sorted(family) { personSort($0,$1) }

After inlining the function and removing what can be inferred I ended up with the following:

let ordered = sorted(family) {
    if ($0["surname"] == $1["surname"]) {
        return $0["given"] < $1["given"]
    } else {
        return $0["surname"] < $1["surname"]
    }
}

Let us know if you have a better way.