27 Jan 20205 min read

SwiftUI masks and overlays

Finally, I had some spare time to get my hands dirty with SwiftUI and that was a pleasant few hours. I started doing a new project - macOS application which soon I might open-source but for now, I have prepared an article about masks and overlays.

Overlay

What does it do? It layers a secondary view in front of the view. Sounds just like well known ZStack although it's a little bit different, so let's dive into details.

func overlay<Overlay>(_ overlay: Overlay, alignment: Alignment = .center) -> some View where OverlayView

This function takes an overlay view which is just another object that conforms to View protocol, as well as an alignment that defaults to center. Since in SwiftUI pretty much every user interface element is a View we can use this pretty much on anything we would like to.

What's the advantage of using it over ZStack you might be wondering? When you apply an overlay to a view, the parent view continues to provide the layout characteristics for its child view. That means our overlay view won't change a frame parent view in any way. Thanks to that we can easily place a view on top of another view and keep it within its frame while with ZStack we would have to do some extra work to achieve the same result.

Let's say we want to place an image on top of another image while keeping the top image inside the parent image's frame. With overlay, it's just a matter of a few lines of code.

Image("background-image")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .overlay(
                    Image("top-image")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .scaledToFit()
            )

And here is the result:

Overlay example

We could make it with ZStack as well but that would require from us a little bit more of work and with SwiftUI there is always a several way to achieve the same thing - you can pick what you like the most, ie. we could use a background instead of overlay although in some cases the result might have been a little bit different as it would reverse the parent-child hierarchy and the other image would be responsible for the layout characteristics.
Anyway, let's move to another topic - masks which I think are more interesting.

Mask

func mask<Mask>(_ mask: Mask) -> some View where MaskView

It's a simple function that only takes a mask (another view) as its parameter but the effect is way more interesting 🤩 For example, we can easily make an interesting mask and apply it to our image:

Image("background-image")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .mask(
                    ZStack {
                        Circle()
                            .frame(width: 128, height: 128)
                        Circle()
                            .frame(width: 64, height: 64)
                            .offset(x: 64, y: 32)
                    }
            )
                .frame(width: 480, height: 480)

which will give us the result below

Mask example

And that's just a 5 minutes example... the possibilities are endless, only your imagination limits you what you can create.

Mixing it up

It's time for some examples that actually might be used in our real application (I do it in the app I am working now at). Let's say we want to make a text with some nice effect - not just a solid color as that would be quite easy. I will show you how to use those two methods we just learned to get a gradient text like the one below

Text with a gradient color

To do that, we will need a gradient which can be easily created with SwiftUI

let gradient = LinearGradient(gradient: Gradient(colors: [.red, .orange]),
                               startPoint: .topLeading,
                               endPoint: .bottomTrailing)

and then we can use Text as a mask for the gradient view like this

gradient
    .mask(Text("SwiftUI is awesome!").font(.largeTitle))

If you give it a try you will notice a small issue though. As you might remember, the layout characteristics are taken from the parent view - a gradient in this case - so it doesn’t look as we would like to. How to fix that?

We can use overlay and mask together to make it fit the actual text size.

let text = Text("SwiftUI is awesome!").font(.largeTitle)

let gradient = LinearGradient(gradient: Gradient(colors: [.red, .orange]),
                              startPoint: .topLeading,
                              endPoint: .bottomTrailing)

text
    .foregroundColor(.clear)
    .overlay(
        gradient.mask(
            text
                .scaledToFill()
        )
)

We create a transparent text here which has gradient text overlay from the above. Thanks to that the frame size fits the text.

There is a lot of awesome effects you can do with those 2 simple methods and combining it with other views like images or anything else you want.

Conclusion

I just started digging into SwiftUI and started with something simple, I hope you will find it useful. More advanced articles coming soon.

Even though SwiftUI is quite a fresh framework and it might be confusing sometimes, you can already make great user interfaces with it and the best thing is - most of the time it’s way faster to do it than in UIKit.

If you got any feedback, follow me on Twitter and send me a message there.

Thank you for reading!