How we made dynamically changed layout for iOS

What is dynamically changed layout?

This is a layout that differs due to different device sizes. I faced this problem working on my last project. The main idea was to scale everything that we have on the screen, as bigger screen demands more adjustments.

How can it be solved?

Solve the dynamic UI First idea that you may get is “Size Classes”. Actualy we also used this at first, but that is not worth it. When a project grows, controllers grows as well. That leads to using more different constraints for each size class. It makes your life as difficult as hell, when you try to change anything. These are not all the problems that we faced using it. Some controllers have so complex UI that it differs almost for any device, so we were forced to use almost all possible size classes. Imagine when you need to change something for iPad, probably you will waste lots of time searching the place where it is. A bigger problem comes out when we want to change not constraint constant, but its multiplier or priority. That’s not possible within the size classes. Instead of it you need to make different constraints for each size class with required values inside, otherwise it will be dissabled for other size classes of this constraint .

What do we get in the end?

Evantually, such approach is really hard to support, it’s not flexible, storyboards become realy complex, we got a lot of constraints which are trouble makers. In the end, it’s impossible to differ devices such as 4s, 5, 6 etc…(in future the list may become bigger).

What about devices with new resolution?

We need to create new UI for them, that means that we need to update our code when new devices may be released and upload new versions to Appstore. That is probably not the best solution for a project, as this update may be performed by other developers that aren’t familiar with previous project architecture. The only benefit I can find is that all UI staff is performed in UI file and almost is not connected with code. That makes our code much cleaner.
Even with all its cons we can accept such approach, as reason we switched it on is the possibility to scale UI for ALL devices, which is imposible with Size Classes.
The conclusion we can make is to neglect the size classes.

What’s next?

Next step we make is to change constraint values from code. Due to different devices we use different values. How good is this approach? Actually, it may be strange, but it’s definitely better than Size classes. We easily separate this code from all the rest. Yeh, it’s not the clean separation and amount of code is rised enormously.
In the end, the amount of code increases sharply. It takes a lot of time to add new views or to change existing one. Still it becomes much clearer for devlopers and the weight of storyboard files decreases. Such approach allowes us to have possibility to cover all devices. The problem with future devices is still present, no one wants to go back to his previous project and add new values for ALL views. It will be hell for developers especially if the project was big.

I would like to emphasise that all writen above can be applied to fonts as for constraints as well.

Next two solutions are probably the best, with its advantages and disadvantages. In the end, you just need to make your decission which is better for you.

What was the main problem of previous approach?

how to make code simplier There was huge amount of code that we needed to add to new views. It takes a lot of time and code becomes realy complicated and ugly. The question was where to remove this code or how to do it much simplier for us? The solution was found in IBInspectable properties. We added to contraints property for each device that was changing constraint constants. Here is an example of code how it was implemented.

@interface NSLayoutConstraint(BBBDevices)

@property (nonatomic, assign) IBInspectable CGFloat iPhone4Constants;
@property (nonatomic, assign) IBInspectable CGFloat iPhone5Constants;
@property (nonatomic, assign) IBInspectable CGFloat iPhone6Constants;
@property (nonatomic, assign) IBInspectable CGFloat iPhone6PlusConstants;
@property (nonatomic, assign) IBInspectable CGFloat iPadConstants;
@property (nonatomic, assign) IBInspectable CGFloat iPadProConstants;

@end

@implementation NSLayoutConstraint(BBBDevices)

#pragma mark - Properties

- (void)setIPhone4Constants:(CGFloat)iPhone4Constants {
    objc_setAssociatedObject(self,   @selector(iPhone4Constants), @(iPhone4Constants), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if ([DeviceOperations deviceModel] == DeviceModelIPhone4) {
        self.constant = iPhone4Constants;
    }
}

- (void)setIPhone5Constants:(CGFloat)iPhone5Constants {
    objc_setAssociatedObject(self,   @selector(iPhone5Constants), @(iPhone5Constants), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if ([DeviceOperations deviceModel] == DeviceModelIPhone5) {
        self.constant = iPhone5Constants;
    }
}

- (void)setIPhone6Constants:(CGFloat)iPhone6Constants {
    objc_setAssociatedObject(self,   @selector(iPhone6Constants), @(iPhone6Constants), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if ([DeviceOperations deviceModel] == DeviceModelIPhone6) {
        self.constant = iPhone6Constants;
    }
}

- (void)setIPhone6PlusConstants:(CGFloat)iPhone6PlusConstants {
    objc_setAssociatedObject(self,   @selector(iPhone6PlusConstants), @(iPhone6PlusConstants), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if ([DeviceOperations deviceModel] == DeviceModelIPhone6plus) {
        self.constant = iPhone6PlusConstants;
    }
}

- (void)setIPadConstants:(CGFloat)iPadConstants {
    objc_setAssociatedObject(self,   @selector(iPadConstants), @(iPadConstants), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if ([DeviceOperations deviceModel] == DeviceModelIPad) {
        self.constant = iPadConstants;
    }
}

- (void)setIPadProConstants:(CGFloat)iPadProConstants {
    objc_setAssociatedObject(self,   @selector(iPadProConstants), @(iPadProConstants), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if ([DeviceOperations deviceModel] == DeviceModelIPadPro) {
        self.constant = iPadProConstants;
    }
}

- (CGFloat)iPhone4Constants {
    return [objc_getAssociatedObject(self, @selector(iPhone4Constants)) floatValue];
}

- (CGFloat)iPhone5Constants {
    return [objc_getAssociatedObject(self, @selector(iPhone5Constants)) floatValue];
}

- (CGFloat)iPhone6Constants {
    return [objc_getAssociatedObject(self, @selector(iPhone6Constants)) floatValue];
}

- (CGFloat)iPhone6PlusConstants {
    return [objc_getAssociatedObject(self, @selector(iPhone6PlusConstants)) floatValue];
}

- (CGFloat)iPadConstants {
    return [objc_getAssociatedObject(self, @selector(iPadConstants)) floatValue];
}

- (CGFloat)iPadProConstants {
    return [objc_getAssociatedObject(self, @selector(iPadProConstants)) floatValue];
}
@end

As you can see it’s pretty simple in implementation and thanks to it we get rid of our main pain in neck. We remove all UI layout building code to UI files. As now we just need to input necessary values for each device for specific constraints. It saves a lot of time. We were really happy that such a simple solution solves such a complex problem. The same code was written for labels and buttons fonts changing. I think this part does not need description. Though, I wasn’t satisfied enough. What shall we do with new devices? Is it fine to give away product that needs to be reimplemented each time when a new device comes out? As for me it was just a good solution, but not a perfect one.

I started searching other solutions that would fully satisfy me .

Finally, I found my silver bullet. It’s dynamicaly changable layout. It’s my naming so do not worry if you don’t understand it at first. The main idea of such approach is “everything should be implemented as a proportion”. Just set proportional height, connecting your height to another one, in most cases it was controllers height as it was static value. Let’s check it out how all constraints are represented now. From time to time I will give you some usefull tips that I learned using such approach.
solution for iOS issue

First tip says “If you ever used constraint constant not equal 0, probably you are doing something wrong”. Probably it’s not a proportion but a static value and this constant should be changed for other screen resolution to complete proportion.

Let’s get back to our constraints.

Aspect Ratio - it transforms into aspect ratio, because it actualy is a proportion.
Heigh and Width - you set heigh equally to some other object height and setting multiplier to fit in due to requirements. Usually it is screen height or width. When device size changes the size changes as well.
Another tip says to try connecting the heigh or width to some object that will be rarely changed. For example screen is a pretty good choise, or some parent view.
Center Y or Center X - you can use multipliers if the object is not exactly in the center. Then it will move further from the center within a bigger screen.
Easy, isn’t it? That’s not everything.What about Leading and Trailing space? How can we make them proportional? Constraint multiplier won’t help us, so we found another solution. You need to create a view with a zero height, proportional width and place it somewhere vertically in order to avoid warnings. Next you need to connect this view with views that need to have space between, one to left and one to right side with leading and trealing constants equal zero. As the resullt, views width will increase within bigger device size. As you are connected to this view, space between connected views will increase as well. That is exactly the thing we are looking for.
Bottom and top space are similar to Leading and Trealling space, main difference is that we will have zero width, proportional height and horizontal placement. Connection will be made to top and bottom of our help view.
There is another helpful thing. Building such an UI will cause a lot of previously described help views. To structurise it, place them inside of other views where you can find them easily and give them common names to understand what you have connected with.
Huh, it was hard but we did it.
iOS app case study Or not? What about fonts? It was another problem I had to solve, there is no multipliers that will help us. Unfortunately, it’s not the situation that I can call “it’s the same as with constraints” .

But don’t worry guys, I have some magic dust in my poket and will make up the solution.
What if Labels or buttons font will scale to fit its Rect? If your button Use dynamic layout, their sizes will depend on screen size, and its font will depend on it as well.
Sounds easy, what about code implementation?
You can find it here https://github.com/VolodymyrKhmil/BBBLibs/tree/master/BBBAutoresizedFontLabel
Here is screen for lasy readers

@interface BBBAutoresizedFontLabel : UILabel

@property (nonatomic) IBInspectable NSInteger horizontalMargin;
@property (nonatomic) IBInspectable NSInteger verticalMargin;

@property (nonatomic) IBInspectable BOOL notCheckVericaly;
@property (nonatomic) IBInspectable BOOL notCheckHorizontaly;

@end
@implementation BBBAutoresizedFontLabel

#pragma mark - Life Cycle

- (void)layoutSubviews {
    [super layoutSubviews];

        if (self.text.length > 0) {
            __block CGFloat largestFontSize = 1;
            NSInteger numberOfLines = self.numberOfLines == 0 ? 1 : self.numberOfLines;

            NSInteger (^linesNumber)(void) = ^NSInteger {
                CTFontRef myFont = CTFontCreateWithName((__bridge CFStringRef)([self.font fontName]), largestFontSize, NULL);
                NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:self.text];
                [attStr addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)myFont range:NSMakeRange(0, attStr.length)];

                CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attStr);

                CGMutablePathRef path = CGPathCreateMutable();
                CGPathAddRect(path, NULL, CGRectMake(0,0,self.bounds.size.width - self.horizontalMargin * 2,100000));

                CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);
                NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);

                return lines.count;
            };

            BOOL (^widthIsOverLimit)(void) = ^BOOL {
                return numberOfLines < linesNumber();
            };

            BOOL (^heightIsOverLimit)(void) = ^BOOL {
                CGSize countedSize = [self.text sizeWithAttributes:@{NSFontAttributeName : [self.font fontWithSize:largestFontSize]}];
                CGFloat neededHeight = countedSize.height * numberOfLines;
                return neededHeight > self.bounds.size.height - self.verticalMargin * 2;
            };

            while ((self.notCheckHorizontaly || !widthIsOverLimit()) &&
                   (self.notCheckVericaly || !heightIsOverLimit())) {
                ++largestFontSize;
            }
            self.font = [self.font fontWithSize:largestFontSize - 1];
        }
}

@end

It may be hard to understand, but I believe in you. All you need is to tell that your class is of this label.
What we get in the end is that our layout will dynamicaly update if device screen size changes. Fonts will be automaticaly updated as well. There were simple cases described, you might face more complicated ones, but if you’ve grasped general understanding of the topic, you can easily solve them. As you can see all problems that were described above are fixed within this approach.

More complex doesn’t mean better. It would be the best solution unless we got new problems. First and main one is bad app performance. As we know system recalculates all constraints using system of linear equations. The more veriables it has, the harder it becomes to solve it. With all this additional views we make it hard for system to calculate needed layout. It creates performance issues on low devices and may cause even crashes if your UI is really complex, as calculating font sizes is more complicated for system than getting static values.
Second problem that appears is complex weight UI files. It causes the fact that new devolopers will not able to build good and flexible UI.
The last problem is inheritance. It is caused because of automaticaly calculated font. If I want to have this possibility and some others in one class, I can’t reach it because of multiple inheritance. Of course you can omit this problem with Swift 2.0 and its protocol extensions. As our project was initially written with Objective-C and it caught us out off guard.
All these problems may be nothing for your project or may create reall hell, it depends only on the project and how it shall fit.
iOS app tutorial To finish with, I just want to say, I’m not waiting for a medal for the best solution that ever exists, I’m just sharing the way we solved this problem. As for me two last solutions are the best and have their pros and cons. All you need to do is pick what fits better in your project and requirements.

Contact us