Custom Bordered UIButtonBarItems

Code and wisdom in this article have not been kept up-to-date. Use them at your own peril.

I recently needed a bordered UIButtonBarItem with custom content. To my surprise, no such thing was available directly in UIKit. To my even bigger surprise, there was no good answer on the Internet.

The UIKit limitation appears to be that once customView is set on a UIButtonBarItem, the item stops paying attention to its style, and always draws without a border. (This seems to have always been the case since beginnings of iOS.)

The only solution I saw (that does not rely on private UIKit classes) is to actually draw the item myself, inside an item with a custom view. Item + custom view = no border; no border + my own copy of the UIKit border = happiness.

You’d think that at this point I could just have UIKit draw me a blank item, take a screenshot of the border, and call it a day. But, UIKit composites the border with the tint color or the item’s bar. If I take a screenshot of an item in the default gray bar, I can’t use it with a black bar (and vice-versa). Unsatisfying.

Instead, I decided to reverse-engineer the compositing that UIKit uses for the border, so that I can get a border with the correct transparency, which I can then apply to any color bar. Yes, I would still have to mess around with taking screenshots to get the transparency data out, but I only need to do it once.

To reverse-engineer border compositing, I turned to math. Ahh, math.

I made the assumption that UIKit uses simple compositing mode, in which composited pixels = background pixels * (1 - alpha) + overlaid pixels * alpha. In my case, this means: bar with border = bar without border * (1 - border transparency) + border * border transparency.

If all I have is a bar without the border and the same bar with the border, at each pixel I have one equation with two unknowns: the color of the border, and the transparency of the border. However, if I have two bars, with two distinct tint colors, then between them they produce a system of two equations with two unknowns, which I can solve for both border color and border transparency.

So, first I wrote a Python script that takes in four images and, assuming that those images represent two distinct backgrounds onto which the same transparent images has been overlaid, it calculates the overlay. (The script isn’t particularly exciting — it just does some basic linear algebra to solve the system of two equations. It does require the Python Image Library, which is not part of built-in Mac OS X Python.)

Then I ran my app twice, once with its navigation bar set to 100% red, then with it set to 100% blue, and I took the screenshots of the two bars. I cut out the relevant regions of the two bars, producing the following 4 input images:

I ran the script and it produced the following output:

Done.

(I had to repeat this process twice — once for Retina, one for non-Retina graphics. The results for Retina are what you would expect.)

This technique can be used any time you want to extract a transparent image from iOS, as long as you can get it to render on two distinct backgrounds. (The script will fail if any two matching background pixels are identical, as it is impossible for it to solve the two equations for such pixels.) For example, if you want to draw a UIKit back button outside of a UINavigationItem, you can now extract its pentagonal border with transparency, and go wild.