Create a Resolution Independent iOS8 App and Set Your Images Free
Since iOS launched, png’s have been used for icons. It’s been so simple, why would anyone consider any other solution? The workflow looks like: “we want to add a button, it needs an icon for this action, email the png to me and we’re done.” So the designer queues it up in his workflow, and when he can get around to it you get an email with 2 .png files (for retina and non-retina screens)
Wait a minute... the icon needs to change to this color and fill in when it’s in the highlighted state. So you repeat the process and get 2 more pngs. Actually, we’re going to disable this button for certain situations so we need a disabled state. Oh yeah, and iOS devices now support @3x, so I need all of the icons, and their states exported again… Wait the designer is busy right now and can’t get those to me for a few days?! How can I get my app ready for new screen sizes if I can’t get my resources today?
Anyone who’s worked on a team towards a mobile goal has most likely been through at least one or all of these situations. It can be incredibly frustrating and cause a lot of lost time for your team. Here at Craftsy we took a step back and look at what we were really trying to accomplish and see if there was possibly a better approach we we’re missing. And when I say take a step back, I mean take a BIG step back.
What are we trying to accomplish?
Goal: Draw an icon on the screen that is pixel perfect for that screen’s resolution.
At its most rudimentary level, this is what we are doing. The question would then be, are we addressing that in the most efficient and pragmatic way? Let’s take a look at our options. We can continue to build and update a library of fairly simple images and incur the workload of keeping said library up to date from both a development and design overhead. Or we can take a dynamic approach, get down to only addressing the changes and work that are absolutely necessary, and focus that time on solving other problems.
Enter PaintCode, an OSX application that’s name does exactly what you would assume. It interprets vector assets created or imported from Photoshop, and then “paints” it into a library of code. I won’t spend too much time trying to sell you on PaintCode, I suggest you investigate their solution online. However going forward for us this proved to be necessary in accomplishing the dynamic aspect in removing our dependence on images. A good way to think of PaintCode and its price is that it’s like hiring a developer for your team. The question to ask yourself is “would you hire a developer who could write perfectly managed code for a one time price of the cost of the app?” Yeah, that’s what I thought.
Designers have tons of amazing data hiding inside of their photoshop files. The data, specifically, is called vectors. Why are vectors important? Put simply, the problems the iOS world is currently encountering around resolution and state have long been solved in the design world. When the design team creates an icon, they most likely take a vector based approach to it using lines and bezier curves. This is done to create “Resolution Independence”. That way if we need the icon the size of the screen at 1000 dpi or the size of a thumbnail at 20 dpi, it can be produced, and it looks pixel-perfect!
So we asked, what if developers could simply harness that already existing technology? That would make us resolution independent. Although this has always been an available approach, until PaintCode the cost outweighed the benefits as it would take more time to programmatically draw the vectors in code than it does to just keep making and exporting images. Also, most programmers don’t have the best eye for design details, and the final version probably wouldn’t come out as intended. The design team can push the exported PaintCode Objective-C files.
But PaintCode eliminated all of those problems in one fell swoop by managing all of the code between developer and designer! The designer imports his vectors directly from Photoshop into PaintCode, does a little management to conform to the interface for the developers, and voila, the developer can now simply draw that resource in whatever capacity is necessary.
This fundamentally started changing our communication between designer and developer. Once standards were developed, the discussion quickly changed from “Can you email me the assets?” to “Let me know when you check that icon into git”. Sometimes no communication is necessary; design adjusts an icon and the app draws the new version.
So I can just buy PaintCode and everything will just get easier?
Sadly, no. It’s not that simple, PaintCode is merely a tool, and how you utilize that tool will ultimately determine your success. But in the long run we were able to save ourselves a lot of time and money by utilizing PaintCode and Xcode properly together. I’m going to go over how we got started using PaintCode to cover our bases on one of the most simple implementations of it: icons and states.
Step One - Developing a protocol for the designer to conform to
The developer should be the first one to jump in and set things up for how it will work for them. PaintCode allows for variables to be added that the developer can programmatically plug into at the end. The first one we’ll deal with is the size of the icon.
Icons are easily scalable if you work with squares, and squares don’t need two dimensions - they only need one. So we added a public (code assignable) number variable to PaintCode simply called “size”. We can’t work with the number directly from a design perspective, so we add an expression to PaintCode to calculate an area to draw in based on that size:
We only need to make a size because the frame will always origin at 0,0. Now, the protocol is the following:
- Every icon canvas must contain a root frame
- The root frame origin must be 0,0
- The root frame size must be tied to the calculated size from the parameter passed in from the code
The design will need to conform to the frame, but for now lets assume that going forward everything is drawn relative to that root frame, and is therefore infinitely scalable.
Now we need to programmatically change the color of the drawn icon. Unfortunately, PaintCode currently does not support passing in a color directly. However, like I said earlier, PaintCode is a tool and everything depends on how you utilize it. To handle color, we publicly make available four numbers, Red, Green, Blue, and Alpha. Then in PaintCode we simply make that color in an expression:
makeColor(red, green, blue, alpha)
This makes that passed in color available in PaintCode, and our protocol further evolves:
The drawn icon must be tied to the color variable generated from the codebase.
Okay, so we’ve got the size of the icon and its color now ultimately coming from the code. This is great for general icons drawn around the app, but what about buttons?
Buttons, as we all know and love, conform to four states in iOS. Normal, Highlighted, Selected, and Disabled. From a PaintCode perspective we want design and UX to communicate those different states to us so the appearance can be modified based on the state. To solve this, we add an additional public parameter called “drawType”. This essentially gives the icon the option to have several types to be drawn. We then add four expressions to determine the state to draw:
drawType < 1 || drawType > 3
drawType == 1
drawType == 2
drawType == 3
Each one of theses expressions changes a boolean attached to it, we then tie that boolean to the visibility of an icon path. That was when the drawType is 0, only the normal icon is shown. drawType 1 hides the normal icon and shows the highlighted icon, etc…
Our protocol now has an optional parameter as drawType:
If the icon needs a different appearance depending on its state, tie the visibility of each icon to the state required. However, Normal is always assumed.
From a code perspective now, we end up with the following:
- (void) draw(IconName)WithSize: (CGFloat)size red:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha drawState:(CGFloat)drawState;
So going forward the developer has control of the size of the icon to draw, the overall color, and the state to draw it in. There is no need to pass in device scale, this is all handled automatically. Now the developer passes this interface off to the design team where they conform to the protocol.
Step Two - Designing within the scope of the protocol
The design team now imports the assets from Photoshop into PaintCode, and conforms the assets to the protocol designed above.
- A canvas is added to house the icon
- A root frame is added to the canvas attached to the calculated size with an origin at 0,0
- The icon is drawn within the bounds of the root frame as a group, with the appropriate attachments to the frame alignment as needed.
(For example this icon has struts on the left and right and springs for the top and bottom)
- The color is attached to the icon
- If needed, this process is repeated for different states, and the visibility of these groups is attached to the states available (Normal, Highlighted, etc.)
Now, without any actual code written, PaintCode has helped generate something similar to the following for the developer to code around:
+ (void)drawRecoStateIconWithSize: (CGFloat)size red: (CGFloat)red green: (CGFloat)green blue: (CGFloat)blue alpha: (CGFloat)alpha drawType: (CGFloat)drawType;
//// Variable Declarations
CGSize sizeCalc = CGSizeMake(size, size);
UIColor* fillColor = [UIColor colorWithRed: red green: green blue: blue alpha:alpha];
BOOL normal = drawType < 1 || drawType > 3;
BOOL selected = drawType == 2;
CGRect frame = CGRectMake(0, 0, sizeCalc.width, sizeCalc.height);
CGRect recoOrange = CGRectMake(CGRectGetMinX(frame) - 0.01, CGRectGetMinY(frame) + floor(CGRectGetHeight(frame) * 0.11995 - 0.48) + 0.98, CGRectGetWidth(frame) + 0.02, floor(CGRectGetHeight(frame) * 0.88022 + 0.5) - floor(CGRectGetHeight(frame) * 0.11995 - 0.48) - 0.97);
//// Reco Orange
//// defaultType Drawing
UIBezierPath* defaultTypePath = UIBezierPath.bezierPath;
[defaultTypePath moveToPoint: CGPointMake(CGRectGetMinX(recoOrange) + 0.00000 * CGRectGetWidth(recoOrange), CGRectGetMinY(recoOrange) + 0.99740 * CGRectGetHeight(recoOrange))];
[defaultTypePath addLineToPoint: CGPointMake(CGRectGetMinX(recoOrange) + 0.00000 * CGRectGetWidth(recoOrange), CGRectGetMinY(recoOrange) + 0.16623 * CGRectGetHeight(recoOrange))];
//// type2 Drawing
UIBezierPath* type2Path = UIBezierPath.bezierPath;
[type2Path moveToPoint: CGPointMake(CGRectGetMinX(recoOrange) + 0.00000 * CGRectGetWidth(recoOrange), CGRectGetMinY(recoOrange) + 1.00000 * CGRectGetHeight(recoOrange))];
[type2Path addLineToPoint: CGPointMake(CGRectGetMinX(recoOrange) + 0.00000 * CGRectGetWidth(recoOrange), CGRectGetMinY(recoOrange) + 0.16623 * CGRectGetHeight(recoOrange))];
Step Three - Adapting to code generated by PaintCode
Once you have your generated PaintCode implementation, it’s really up to you how you want to bridge that into your codebase. One option is to execute the methods directly with parameters. We went with option 2, a wrapper approach that simplifies some of the requirements from PaintCode.
For example, when passing in the color, it’s much more convenient to use a UIColor than to type out RGBA values one by one. So a simple UIColor to RGBA converter is a nice to have. We also included the ability to render out these methods into images if we need them. Using a wrapper to bridge certain fundamental gaps gives us more options to extend the code that is generated by PaintCode, and I would highly recommend taking this approach.
Other advantages PaintCode provides
PaintCode is also a great library for references other than image paths. We also use it to manage and organize the following:
- Colors - PaintCode provides a terrific interface for a palette of colors. Instead of using [UIColor colorWithRed/Blue/Green/Alpha] we use [PaintCodeLibrary colorCustomOrange].
- Gradients - Communicating design elements like gradients and overlays are much easier with PaintCode. You can edit alpha values programmatically and make gradients more exact and modifiable.
- Drop Shadows - Drop Shadows can be stored and accessed publicly as well, this allows design control over universal drop shadows.
- Swift: Since PaintCode is just generating the code you use, transforming it into a new language (like Swift) is as easy as a click of a button.
Is all of this worth the effort?
Absolutely. Freeing yourself from resolution “dependence” is a huge step towards making iOS apps work across an ecosystem and it will save your team time, effort, and headaches now. There was a time prior to PaintCode when this would not have been practical but now, you should do it today. This approach gives your teams more time to focus on the problems they should be solving so that instead of rebuilding the assets to adjust to a new screen resolution they can focus on building a more stable and performant app!