Responsive iOS UI without AutoLayout

Note: All of the concepts in this post are about creating views programmatically. I do not use Storyboards at all in my projects and therefore do not create views or layout constraints from the interface builder.

Intro

Hello, dear reader!

The other day as I was perusing reddit, I came across a post that was something along the lines of “The case against AutoLayout”. The post linked to an issue on the IGList Github repo, where Ryan Nystrom, a developer for Instagram, said “Instagram doesn’t use self-sizing-cells or auto layout at all”. This had me thinking. I don’t love or hate AutoLayout but I’ve also never considered there was a different way to create views, and I certainly didn’t think that a company as big as Instagram would shy away from the industry standard of AutoLayout.

After researching and testing a few ways to create UI without AutoLayout, I believe I have found a solid starting point. Today we’ll go over the basics and I’ll show you how I got from a design in Sketch straight to Xcode using math and not a single AutoLayout constraint.

We’ll go over some of my issues with AutoLayout, explain some of the math and custom view classes we’ll be using, cover our Sketch design, and finally start writing the code in our project. If you’d like to skip to a specific section, please see the table of contents above. You can also just go straight to the Github repo for the full project.

Issues with AutoLayout

I only have 2 real issues with AutoLayout; syntax and scalability. But I understand there are workarounds and/or better ways to use AutoLayout to fix them. We’ll cover my go-to’s to get around these.

1). The original AutoLayout API syntax isn’t as clear as I’d like and takes too long to write.

When you have quite a few views, this syntax just becomes unreadable.

I use Snapkit in most of my projects, which fixes a lot of my frustration with this issue.

But the code isn’t perfect and it leads me to my second issue:

2). Insets/Constants are not scaled for each screen size. So, going from a design on an iPhone to an iPad can be a mess.

All of my apps are for iPhone and iPad because why not? My apps are not made for just one screen size and they don’t use anything specific to a particular device. So, the only barrier is my UI. AutoLayout is supposed to bridge this gap, but there are a few problems. For example, when setting up my layout I set an inset of 10 points for the left side of my view:

Then, when I launch my app on the iPad simulator, the inset will still be 10 points but the iPad has a much larger screen size. This leads to odd looking views that have been stretched instead of scaled proportionally.

These issues seem small but they start to snowball when you’re working on a lengthy project, working on multiple projects, or trying to get a UI design from Sketch just right, to balance your designer’s temperament. [make designer happy?]

If you have no issue with AutoLayout, feel free to read this simply as an alternative way to create UI. This isn’t a statement about the right or wrong way to create UI, because there is none.

The make up of a view frame

Let’s quickly talk about view frames. We create views like this:

The way I think about views in iOS development is as if we are placing the views on grids. The points on a grid are represented by a point on the x axis and a point on the y axis or (x, y). With an iPhone 7 screen as an example the grid starts at the top left of our screen at (0, 0) and goes all the way to the bottom right which is (375, 667). This is because an iPhone 7 screen has a width of 375 points and a height of 667 points. Of course the points scale out the bigger our screen is.

Diagram

Frames also have a min, mid, and max property for the x and y values.

Imgur

We can use these properties to get the top left point, top middle point, and top right point of any view. The same goes for the y axis. If we use these properties conjointly, we can easily snap views to each other.

Extensions, Math

Extensions/Math

Before we start creating views, we need two functions in an extension to help calculate the height, width, and position for our views when they are being scaled for different devices. These two functions:

These functions follow a simple formula:

CurrentScreenHeight / (iPhone7ScreenHeight / NumberToCalculate) = CalculatedHeight

So, for a view with a height of 50 on an iPhone 7 the calculations would be:

iPhone 7: 50
iPhone 7+: 55.1724137931035
iPad Pro(9.7 Inch): 76.7616191904048
iPad Pro(12.9 Inch): 102.3988005997

Now, our view height stays proportional to the screen height with a two simple functions.

In the functions I calculate the return value in relation to an iPhone 7 screen because that’s where the majority of my designs start. But feel free to tweak this and start from whatever screen size fits your needs.

More UIView Extensions

I also made up some UIView extensions that allow you to quickly get the points for the frame of a specific view. This will clean up and shorten a lot of our code. Think of this as “UIView.frame.center”.

Here’s the full extensions file:

Custom UIView Initialization

Alright, so while writing this article, I’ve gone back and forth on creating custom classes, inheriting a protocol, and copy pasting the same init method with a few tweaks. But honestly, my code just looked ugly. Like really ugly. That’s where you need to start, though. Looking over my code, I figured out a way to build a protocol, extend that protocol, and then extend the UIView to conform to my protocol. In doing that, I rendered all my custom classes obsolete and I turned 227 lines of code to 55 lines of code.

So, to show you a bit of my process, here is some of the code before:

Oh man, that looks good.

So, let’s break down what we’re doing here. First, we create a protocol with an init function and a function called “performLayout”. This makes sure that we implement both in our extension. We’ll be using the init method to initialize our view and calculate a new frame for it. Extending UIView means that any class that inherits from UIView will also use this custom init method.

After that, we call “performLayout” in the init method so we can use that method in the UIView extension and our custom classes. The “performLayout” method is where we’ll do anything specific to whichever view you’re using. This will always be called during initialization, so we don’t have to override our init method. You’ll see how we use this in a class later.

Note: We had to add “@objc” to our performLayout function in the UIView extension because in Swift 4 there is either a bug or a new syntax for the Swift 3 inference for @objc and I kept getting warnings about it.

I put in this bit with my old code to highlight that everyone writes ugly code and that you are supposed to write ugly code. My ugly code worked and then when I started writing this article it was confusing to ME. So I rewrote everything, even most of the article. I’ve done this about 4 times just so far. Just an interesting tidbit. Never be ashamed of your code. Please resume.

Sketch design

For the actual view we will recreate in Xcode, I have chosen the Post view from Instagram. This is a post I made of my puppy, Bandit, growing up way too fast. The design is simple and with this, we can see how Instagram would avoid using AutoLayout in their app.

I won’t go too in-depth into Sketch or our design. You can examine the image below and see that I have used a plugin called SketchMeasure to measure the height, width, and distance between views for every part of our design. You will be able to see pretty clearly how knowing all these measurements will help us in coding the views.

Full Design

Coding the views

Finally! Let’s stop talking about concepts and get down to the code.

Coding the Top Bar

Top Bar View

Create a new Swift file or create a UIView and call it “TopBarView”. Make sure this view inherits from UIView. Then, let’s create variables for all of our views.

And instead of overriding the UIView’s init methods, we will override the “performLayout” method.

The “performLayout” method works like an init method because we call “performLayout” during the UIView’s init method. So, inside the performLayout method we’ll start with the userImageView. This contains our user’s image (go figure). We will go ahead and initialize a new UIImageView, using our custom init method. We’ll also make the view circular and add an image.

First of the userImageView will be a subview of TopBarView so they begin with the same origin point. Right now without setting the origin point of TopBarView the origin is (0, 0). LeftInset represents the x value of userImageView’s origin in relation to it’s immediate superview which is TopBarView. TopInset is the same but for the y axis. So if we set leftInset to 10 and topInset to 10 then userImageView’s origin point will be (10, 10) because TopBarView’s origin is (0, 0). If TopBarView’s origin was (20, 20) then userImageView’s origin would be (30, 30).

Next we set just the height in our init method to 32 and set “keepEqual” to true so the width will equal the height at all times (If you set just the width and “keepEqual” to true, the height will always equal the width).

We use the “keepEqual” parameter here because the height and width for every other view is scaled by different formulas. If we didn’t set “keepEqual” to true, the circular view will become more of an oval when we scale it.

It’s important to note here that the values in our init method are optional and have default values so you can leave out parts like height and width and the view will still initialize. Though if we don’t set “keepEqual” then our height or width will default to 0 and our view will not be seen.

Then we set our corner radius to be the height of the userImageView divided by 2. That will round the view.

We have to set .clipsToBounds to true so that when we add an image it will be masked out and not extend beyond our circular view.

Then we add our image. I’m using an “Image Literal” because I have the asset inside of my project and you can just call the asset by name inside your project.

Finally we have to add our completed view as a subview to our TopBarView class.

Moving on to the userLabel which will contain the username of our user. Here’s the code:

We create a new UILabel with the same topInset as our userImageView and with a leftInset of 52 so our userLabel origin point will be (10, 52) in relation to the superview, TopBarView.

We also would like to scale our font in relation to screen size as well, so we’ll use the same getValueScaledByScreenWidthFor function we use for our views. The function will work the same way with any value you put in but I’ll create an extension for fonts some other time.

Then of course we add the userLabel as a subview to TopBarView.

Side note: We could easily calculate the inset from the right side of the userImageView like so:

The inset is 10 from the right side of the userImageView which we can find using userImageView.topRightPoint().

Last we have the ellipsesImageView. The code for this imageView is almost exactly the same as the userImageView.

And we’re finished with the TopBarView.

Coding the Button View

Button View

For the Button View we don’t do anything too crazy, so I’m just going to throw the code for the class up here.

Coding the Comments View

Comments View

The comments view isn’t super complicated but we do a few new concepts that I’ll cover. But of course every view starts the same.

We use the performLayout method again

In our performLayout method we will start with our first label:

We create a variable called “labelsWidth” because all our labels will have the same width. If you look at the Sketch design you can see that there is a 16 point inset on both sides of each label so we just multiply 16 with a calculated width by 2 and subtract the total frame width by the outcome. We’re using 375 because that is the width of an iPhone 7 screen.

Next we create our description label:

We snap the description labels origin point to the bottom left of the likeLabel by using “likeLabel.bottomLeftPoint()”, use the same width, and a height of 42. We also want the username to be bold for this label so we do a bit of fiddling around with attributed strings. There are much better ways to work with attributed strings here but I just threw this together and it works for our use.

Our second to last label is the viewAllCommentsLabel. In the actual Instagram app, you can see the last comment that was made on that specific post but we’re going to avoid that for now since we’d need to use a stackView and hide/show labels when there are a certain number of comments. That’s a bit much for this tutorial but i’ll fit that into it’s own smaller tutorial.

Here is the viewAllCommentsLabel code:

Our last label is the timeLabel. Nothing complicated here:

Lastly for this view, I’m going to show you a way for a view like this to calculate it’s own height depending on the sum of it’s subviews. We can quickly accomplish this like so:

I’m adding this because not only will we want to add another label or two but labels can have multiple lines and descriptions can get very long in Instagram. There are a few tricks we’ll need to use to correctly calculate the height for each label and at the end this snippet will calculate the sum of every subviews height.

And that is it for the comments view.

Putting it all together

Now that we have all of our individual views, let’s put all of them together in another view that would be our view to use in a collectionViewCell with some setup functions, but we created some default data for every view so we won’t do any of that today. We’re just going to call this new view in a view controller.

Start out like we do with our other views:

You see that we skipped over the postImageView and that’s only because it is simply an image and doesn’t need to be a separate view.

Let’s initialize all our views in the performLayout method:

Hopefully by now you can tell what we’re doing here by snapping every view to the bottom of the view above it.

And if we’d like to use this PostView in a view controller all we have to do is use it like this:

Also I hope you caught that I am not adding the subviews height’s together and setting the PostView’s height based on that. That is something that I’ll leave for another post to cover all together or for you to figure out on the side.

After we create the PostView and initialize it in a view controller, we’ll see that our UI scales for every screen:

All Screens

And we’re done! Let’s talk about drawbacks.

Drawbacks

The biggest drawback for this workflow is that when in Xcode you won’t be able to see the changes you make to your views until you run your app. Fixing that issue specifically is a whole other can of worms. Though the future of this project isn’t to work with UI inside of Xcode. The future of this project is to export your designs directly from Sketch, or a similar design program. That way we keep our design and our code separate. This of course has it’s own positives and negatives but for my projects, that is the workflow I am working towards.

Another drawback you may be thinking about is rotating an iPhone screen. Though for this project, a lot of the apps I have created, and even most the apps I use on a daily basis don’t rotate at all. This is an issue I’m experimenting with but it’s not a huge deal for me at the moment.

The third biggest drawback would be animation. I’ve been experimenting with this more recently and I can see it not being to hard to implement a quality animation library. We’ll save that for another post though.

Despite these drawbacks, I believe there are ways to get around them and I believe they aren’t a critical issue at this time.

Conclusion

To conclude, this is a new way of creating UI that I have been experimenting with. The next steps are refining the classes and giving a bit more flexibility with what you can do with them. I’m very happy with where things are now though and these concepts are at a base state where I can use them sparingly in my projects where performance is key.

Speaking of performance, I never spoke about performance. Mainly because I don’t have a good way to test the speed of UI but there are obvious performance benefits to not using AutoLayout. If you have a way, please let me know!

If you would like to see this workflow in an actual app check out my apps Kibbl and the Software Engineering Daily Podcast App.

That’s about it though! Thank you so much for reading. Please let me know what you think by commenting on this post or sending me a message through social media(see below). If you have something to contribute to this project, please visit the Github repo.

Also since writing this post, I’ve made this entire responsive UI concept into a CocoaPod library you can use in your projects. Here is the KTResponsiveUI Github repo. Go ahead and star and follow the repo because I will be updating this library more and more as I find out new and better ways to implement this type of UI.

The KoalaTea Github: https://github.com/KoalaTeaCode

My Twitter: https://twitter.com/TheMrHolliday

My Reddit: https://www.reddit.com/user/Misterholliday/

If you liked this post, sign up for the KoalaTea Newsletter. You’ll get notifications when we have new blog posts and we’ll be working on even more content soon!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s