Live Editing Layout Constants Using Classy, Masonry and ClassyLiveLayout

The source code and example project presented in this post are available at https://github.com/olegam/ClassyLiveLayout and using the CocoaPod ClassyLiveLayout.

Gif showing editing layout constants results in real time layout updates

Implementing user interfaces often requires many iterations where you compile your app, launch it in the simulator, navigate to the screen you are working on, check the result and then go back to code and make a change. Then repeat. In many cases this can be a tiresome process. If it takes a long time to compile the app or navigate to the relevant situation I find this particularly frustrating.

I never use nibs or storyboards (for several reasons, but Sam Soffes sums it up pretty well). In contrast I’m a huge fan of DCIntrospect, a tool to inspect and tweak view frames directly in the simulator. I use that all the time and only fall back to Reveal app when there is a more complex case I need to inspect. Using DCIntrospect is convenient because it lets you move and resize views directly in the simulator using the arrow keys. But once you figure out how many pixels you want to move your view you still need to remember that value, make the necessary code change and run the app again to check the result. Wouldn’t it be nice if this process could be made easier? I’ll show a new approach later in this post after mentioning two essential components that makes this possible.

Masonry

I have been using Jonas Budelmann’s excellent Masonry DSL for AutoLayout for the last few months. It makes setting up layout constraints very easy and declarative compared to the more verbose APIs provided by Apple. Constraining a view to its superview with some padding could look like this using Masonry:

1
2
3
4
5
6
7
8
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top);
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

or even this:

1
2
3
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(superview).with.insets(padding);
}];

Basically, if you don’t know Masonry you should check it out right now. One very nice feature recently added to Masonry is the mas_updateConstraints: method. It let’s you easily change the layout of a view by automatically updating existing constraints, instead of manually saving pointers to the constraint you may need to update in ivars. We will see how this comes in extremely handy when combined with the next awesome library and a little extra magic.

Classy

Another excellent tool from Jonas Budelmann (he is called @cloudkite on twitter and github) that I just recently started using is Classy. Classy is a little like CSS (or actually more like Stylus, but for native iOS apps. Classy let’s you define stylesheets where you can match your views using different kinds of selectors and set styling properties on them. In this way it’s easy to define all your styling in a central place that’s much more convenient and flexible than Apple’s appearance proxy API. A stylesheet could look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$main-color = #e1e1e1;

MYCustomView {
  background-color: $main-color;
  title-insets: 5, 10, 5, 10;
  > UIProgressView.tinted {
    progress-tint-color: black;
    track-tint-color: yellow;
  }
}

^UIButton.warning, UIView.warning ^UIButton {
  title-color[state:highlighted]: #e3e3e3;
}

We can define variables and enjoy shortcuts for declaring colors, fonts, size, etc. You should really check out the features and the syntax on the Classy website.

Another very nice thing about Classy is it’s live reloading feature. When running the app in the simulator we can simply edit the stylesheet file and and as soon as we save the file, the app’s appearance will update immediately. I think this is so cool! But also extremely useful. Now the iteration time for tweaking things like colors, font sizes, scrollview insets etc. is pretty much down to zero.

ClassyLiveLayout

I have quickly become addicted to this live editing thing and I now enjoy compiling way fewer times during the day. But all adjustments not made using Classy still require a fresh compile and restart of the app. One such adjustment is tweaking the constant numbers used to define the view layout. Like the top margin of a view or the distance between two views. If I were to define a view with two rectangles with the same height and top margin I might do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
[_blueBoxView mas_updateConstraints:^(MASConstraintMaker *make) {
  make.width.equalTo(@80.f);
  make.height.equalTo(@100.f);
  make.top.equalTo(@60.f);
  make.left.equalTo(@50.f);
}];

[_redBoxView mas_updateConstraints:^(MASConstraintMaker *make) {
  make.width.equalTo(@120.f);
  make.height.equalTo(_blueBoxView);
  make.top.equalTo(_blueBoxView);
  make.left.equalTo(_blueBoxView.mas_right).with.offset(30.f);
}];

This is how the layout looks when rendered in the simulator: Screenshot of app with blue and red box views

In a real app I would probably extract the magic numbers into constants. I thought it would be nice if we could edit those constants at runtime and see the effect immediately like the example with Classy above. So I asked Jonas Budelmann if he had any ideas on how that could be done and he luckily had! So here goes a solution based on Jonas’ idea and a category I wrote on UIView to get rid of the boilerplate.

The UIView+ClassyLayoutProperties category defines the following properties on UIView:

1
2
3
4
5
6
7
8
9
10
11
@property(nonatomic, assign) UIEdgeInsets cas_margin;
@property(nonatomic, assign) CGSize cas_size;

// shorthand properties for setting only a single constant value
@property(nonatomic, assign) CGFloat cas_sizeWidth;
@property(nonatomic, assign) CGFloat cas_sizeHeight;

@property(nonatomic, assign) CGFloat cas_marginTop;
@property(nonatomic, assign) CGFloat cas_marginLeft;
@property(nonatomic, assign) CGFloat cas_marginBottom;
@property(nonatomic, assign) CGFloat cas_marginRight;

The first two properties cas_size and cas_margin are the interesting ones, the others are just shorthands to set individual values of the CGSize and UIEdgeInsets structs. We can access these properties from a stylesheet to define our constants:

1
2
3
4
5
6
7
8
9
10
UIView.blue-box {
    cas_size: 80 100
    cas_margin-top: 60
    cas_margin-left: 50
}

UIView.red-box {
    cas_size-width: 120
    cas_margin-left: 20
}

We will also refer to them when we define the layout in -updateConstraints (or -updateViewConstrains if we are lazy and setup the view from the ViewController):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)updateViewConstraints {
  [super updateViewConstraints];

  [_blueBoxView mas_updateConstraints:^(MASConstraintMaker *make) {
      make.width.equalTo(@(_blueBoxView.cas_size.width));
      make.height.equalTo(@(_blueBoxView.cas_size.height));
      make.top.equalTo(@(_blueBoxView.cas_margin.top));
      make.left.equalTo(@(_blueBoxView.cas_margin.left));
  }];

  [_redBoxView mas_updateConstraints:^(MASConstraintMaker *make) {
      make.width.equalTo(@(_redBoxView.cas_size.width));
      make.height.equalTo(_blueBoxView);
      make.top.equalTo(_blueBoxView);
      make.left.equalTo(_blueBoxView.mas_right).with.offset(_redBoxView.cas_margin.left);
  }];
}

That’s basically everything needed to be able to live edit margin and size constants and see the results in real time in the simulator. If you want to try this yourself go get the ClassyLiveLayout demo app or follow the extra steps listed in the next section.

Note that we do not have to use all the margin and size values. Also we can use the margin values to specify either the distance to the superview or to a neighbor view.

The cas_size and cas_margin property setter implementations call -setNeedsUpdateConstraints on the superview after storing the new value in an associated object. This causes -updateConstraints to be called if it is overridden or -updateViewConstrains on the ViewController. In either of these methods mas_updateConstraints: can update the constraints with the new constants.

Configuring your project

Your podfile should look something like this:

1
2
3
pod 'Masonry'
pod 'Classy'
pod 'ClassyLiveLayout'

Your Prefix.pch file should include

1
2
3
#import "Masonry.h"
#import "Classy.h"
#import "ClassyLiveLayout.h"

And the -application:didFinishLaunchingWithOptions: method in your app delegate should include these lines to activate Classy live reload:

1
2
3
4
#if TARGET_IPHONE_SIMULATOR
    NSString *absoluteFilePath = CASAbsoluteFilePath(@"stylesheet.cas");
    [CASStyler defaultStyler].watchFilePath = absoluteFilePath;
#endif

This assumes your stylesheet is called stylesheet.cas. If you want to call it something else then follow the Classy setup instructions.

So by using Classy, Masonry and a little extra salt we were able to edit and tweak layout constants in real time without adding extra boilerplate to our app.

Do you think this is too much ‘automagic’? Let me know what you think in the comments and also please submit any ideas for improvements.

Comments