Recreate The 3D Aqua Buttons From OS X Leopard In SwiftUI

Daily Coding Tip 036

The original Mac OS X (version 10.0) introduced a new User Interface called Aqua.

As you can see in the screenshot above, Steve Jobs was confident that it would look good enough to lick. This was probably due to the way that the lighting on the button bubbles made them seem like candy. This is a tutorial for recreating the dialog box above, which more closely resembles OS X 10.5, otherwise known as Leopard. The main differences between this version and previous versions are the lack of pinstripes behind the content or brushed metal effect on the top bar.

We’ll start with the most difficult part, which is replicating the Aqua button.

There’s a lot going on here, but I’ve commented it which should help. At the top we’ve created a structure that conforms to the ButtonStyle protocol, which requires the makeBody(configuration:) function. The function is passed a ButtonStyleConfiguration, which only gives us the Label and the isPressed Boolean. While we won’t be doing anything with the isPressed state, the Label provides a generic View that was created as part of a Button.

In my examples the Label will take the form of a Text.

In order to allow the foregroundColor modifier to work, there is a Capsule shape at the back of the ZStack with no explicit colour of its own. On top of the colour we have the main gradient, which goes from almost opaque white to completely transparent white. On top of that is a highlight that itself resembles a Capsule that is less wide than the button.

A GeometryReader has been used to set the height of the highlight, as any hard-coded value would not scale with the rest of the button.

At the front of the button there is the Label and an outline. The button is clipped to the shape of a Capsule, meaning that the contents will not spill outside of the bounds, no matter what size they are.

Now we’re ready to create our buttons.

The buttons have explicit foregroundColor and frame. In this case the buttons are the same size, but hard-coding the size in AquaButtonView would have made it impossible to customise it for any situation. The same goes for the colours, which I could fixed and by making separate grey and blue button styles. Instead I’ve extended SwiftUI’s Color and placed them as static constants there. This allows me to use them in much the same way as I would use the standard system Color constants.

The actions I’ve taken only print to the console, but it’s easy to see how this could be made more complicated. The HStack containing the buttons has an infinite maximum width and height, meaning that it will stretch to the entire size of the dialog box. Some additional padding is needed, otherwise the buttons will be touching the bottom right edge of the window.

Here’s the content for the window, which includes an outline around the edge.

I used the emoji ⚠️ for the left image mostly so that I wouldn’t need to import an image to my project. Other than the outline around the edges, this code is mostly designed to make sure that the text has sufficient padding from the edges. The windows in Aqua had curved top left and top right corners, but the botom corners were not curved. There’s no easy way to do this in SwiftUI, so I’ve made my own shape to use as a mask. This shape draws the curve for the top two corners, and then draws straight lines to the bottom two corners.

Now we can combine everything to make the final view. It’s important to specify that the VStack has zero spacing, otherwise there will be a gap between the top bar and the main content of the window. The top bar is simply a grey gradient with a height of 20, while the background is a light grey colour with a shadow. The Aqua buttons are on top of the contents, which includes the icon and text. Both of these views stretch to the full size of the window and position their content using padding, so they shouldn’t overlap one another.

Using a ZStack in this way makes it easier to position views independently, as using a VStack would cause changes to the layout when the size of the views or their padding is modified.


Get more Daily Coding Tips in your inbox!