How to make a SwiftUI component that draws a Shape with light

While I was experimenting with SwiftUI, one of the effects I wanted to re-create was taking a shape and making “stroke effects” on it. The goal was to create a SwiftUI component that could take an arbitrary path, apply this effect to the shape, and render it within the context of a larger SwiftUI view.

The effect I wanted to create was a “laser light” like drawing. You see this a lot in science fiction film user interfaces or backgrounds, and it is just kind of fun and neat. And yeah, I want it to be decent in both light and dark modes, although “dark mode” is where it will shine the most.

And the code to represent this is:

VStack {
    LaserLightShape(color: Color.orange, lineWidth: 1) {
        Rectangle()
    }

    LaserLightShape(color: Color.red, lineWidth: 2) {
        Circle()
    }

    LaserLightShape(color: Color.blue, lineWidth: 0.5) {
        Path { path in
            path.move(to: CGPoint(x: 0, y: 0))
            path.addLine(to: CGPoint(x: 50, y: 50))
        }
    }
}

Solving this challenge underscored the change in mindset from imperative code to declarative code. When I was looking through the SwiftUI methods, I kept looking for a methods with a means to “add on” to the existing view – or perhaps to replace an element within the view. In both of those cases, this highlighted my pattern of thinking – me telling the framework (or library) what to do. SwiftUI’s biggest win (and challenge) is inverting that thinking.

The methods to work with SwiftUI Views don’t “take a view, modify it, and hand it back”. Instead they take in some information, make a whole new View, and return it. There’s no “tweaking” or “changing” – the closest you get to that (imperative) paradigm is wholesale replacement. My natural instinct was to reach for something that had an explicit side effect, I suspect because that’s how a lot of languages I’ve used for years got something done. It’s a pattern I’m familiar with, and the first tool I reach towards.

This change in mindset is also why you’ll see a lot of the same people who “get” the new paradigm talking about how it overlaps with functional programming, using phrases like “pure functions”, and perhaps even “functors” and “monads”. I quickly get lost in many of these abstract concepts. For me, the biggest similarity is like when I understand the inversion of control that was the mental leap in moving from using a library to writing for a framework. This feels very much akin to that ‘Ah ha’ moment. And for the record, I’m not asserting that I fully understand it – only that I recognize I need to change how I’m framing the problem in my head in order to use these new tools.

To solve this particular challenge, I originally started looking at SwiftUI ViewModifiers. I’d been reading about them and thought maybe that’s what I want. Unfortunately, that didn’t work – ViewModifiers are great when you want to layer additional effects on a View – which means constraining what you do to the methods available and defined on the View protocol – but I wanted to work on a Shape, specifically leveraging the stroke method, which is a different critter.

The solution that I came up with uses a ViewBuilder. I wrote a bit about these before talking about making PreviewBackground. The mental framing that helped provide this solution was thinking about what I wanted to achieve as taking some information (a Shape) and returning some kind of View.

A ViewBuilder is a fairly significantly generic-heavy function. So to make it accept something conforming to the Shape protocol, I specified that the generic type it accepted was constrained to the protocol. I am still stumbling through the specifics of how to effectively use generics and protocols with swift, and thought this was a pretty nice way to show some of its strength.

Without further ado, here’s the code that produces the effect:

struct LaserLightShape<Content>: View where Content: Shape {
    let content: () -> Content
    let color: Color
    let lineWidth: CGFloat

    @Environment(\.colorScheme) var colorSchemeMode
    var blendMode: BlendMode {
        if colorSchemeMode == .dark {
            // lightens content within a dark
            // color scheme
            return BlendMode.colorDodge
        } else {
            // darkens content within a light
            // color scheme
            return BlendMode.colorBurn
        }
    }

    init(color: Color, lineWidth: CGFloat, @ViewBuilder content: @escaping () -> Content) {
        self.content = content
        self.color = color
        self.lineWidth = lineWidth
    }

    var body: some View {
        ZStack {
            // top layer, intended only to reinforce the color
            // narrowest, and not blurred or blended
            if colorSchemeMode == .dark {
                content()
                    .stroke(Color.primary, lineWidth: lineWidth / 4)
            } else {
                content()
                    .stroke(color, lineWidth: lineWidth / 4)
            }

            if colorSchemeMode == .dark {
                // pushes in a bit of additional lightness 
                // when in dark mode
                content()
                    .stroke(Color.primary, lineWidth: lineWidth)
                    .blendMode(.softLight)
            }
            // middle layer, half-width of the stroke and blended
            // with reduced opacity. re-inforces the underlying
            // color - blended to impact the color, but not blurred
            content()
                .stroke(color, lineWidth: lineWidth / 2)
                .blendMode(blendMode)

            // bottom layer - broad, blurred out, semi-transparent
            // this is the "glow" around the shape
            if colorSchemeMode == .dark {
                content()
                    .stroke(color, lineWidth: lineWidth)
                    .blur(radius: lineWidth)
                    .opacity(0.9)
            } else {
                // knock back the blur/background effects on
                // light mode vs. dark mode
                content()
                    .stroke(color, lineWidth: lineWidth / 2)
                    .blur(radius: lineWidth / 1.5)
                    .opacity(0.8)
            }
        }
    }
}

The instructions for what to do are embedded in body of the view we are returning. The pattern is one I looked up from online references of how to make this same effect in photoshop. The gist is:

  • You take the base shape, make a wide stroke of it, and blur it out a bit. This will end up being the “widest” portion of the effect.
  • Over that, you put another layer – roughly half the width of the bottom layer, and stroke it with the color you want to show.
  • And then you add a final top layer, narrowest of the set, intended to put the “highlight” or shine onto the result.

You’ll notice in the code that it’s also dynamic in regards to light and dark backgrounds – in a light background, I tried to reinforce the color without making the effect look like an unholy darkness shadow trying to swallow the result, and in the dark background I wanted to have a “laser light” light shine show through. I also found through trial-and-error experiments that it helped to have a fourth layer sandwiched in there in dark mode specifically to brighten up the stack, adding in more “white” to the end effect.

A lot of this ends up leveraging the blend modes from SwiftUI that composite layers. I’m far from a master of blend modes, and had to look up a number of primers to figure out what I wanted from the fairly large set of possibilities.

I suspect there is a lot more that can be accomplished by leveraging the ViewBuilder pattern.

Introducing and explaining the PreviewBackground package

While learning and experimenting with SwiftUI, I use the canvas assistant editor to preview SwiftUI views extensively. It is an amazing feature of Xcode 11 and I love it. There is a quirk that gets difficult for me though – the default behavior of the preview provider uses a gray background. I frequently use multiple previews while making SwiftUI elements, wanting to see my creation on a background supporting both light and dark modes.

The following little stanza is a lovely way to iterate through the modes and displaying them as previews:

#if DEBUG
struct ExampleView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ForEach(ColorScheme.allCases,
                    id: \.self) { scheme in

                Text("preview")
                    .environment(\.colorScheme, scheme)
                    .frame(width: 100,
                           height: 100,
                           alignment: .center)
                    .previewDisplayName("\(scheme)")
            }
        }
    }
}
#endif

Results in the following preview:

The gray background doesn’t help all that much here. It is perfect when you are viewing a fairly composed element set, as you are often working over an existing background. When you are creating an element to stand alone, or moving an element. In those cases, I really want a background for the element.

And this is exactly what PreviewBackground provides. I made PreviewBackground into a SwiftPM package. While I could have created this effect with a ViewModifier, I tried it out as a ViewBuilder instead, thinking it would be nice to wrap the elements I want to preview explicitly.

The same example, using PreviewBackground:

import PreviewBackground

#if DEBUG
struct ExampleView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ForEach(ColorScheme.allCases,
                    id: \.self) { scheme in
                PreviewBackground {
                    Text("preview")
                }
                .environment(\.colorScheme, scheme)
                .frame(width: 100,
                       height: 100,
                       alignment: .center)
                .previewDisplayName("\(scheme)")
            }
        }
    }
}
#endif

The code is available on Github, and you may include it within your own projects by adding a swift package with the URL: https://github.com/heckj/PreviewBackground

Remember to import PreviewBackground in the views where you want to use it, and work away!

Explaining the code

There are not many examples of using ViewBuilder to construct a view, and this is a simple use case. Here is how it works:

import SwiftUI

public struct PreviewBackground<Content>: View where Content: View {
    @Environment(\.colorScheme) public var colorSchemeMode

    public let content: () -> Content

    public init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    public var body: some View {
        ZStack {
            if colorSchemeMode == .dark {
                Color.black
            } else {
                Color.white
            }
            content()
        }
    }
}

The heart of using ViewBuilder is using it within a View initializer to return a (specific but) generic instance of View, and using the returned closure as a property that you execute when composing a view.

There is a lot of complexity in that statement. Allow me to try and explain it:

Normally when creating a SwiftUI view, you create a struct that conforms to the View protocol. This is written in code as struct SomeView: View. You may use the default initializer that swift creates for you, or you can write your own – often to set properties on your view. ViewBuilder allows you to take a function in that initializer that returns an arbitrary View. But since the kind of view is arbitrary, we need to make the struct generic – since we can’t assert exactly what type it will be until the closure is compiled. To tell the compiler it’ll need to do the work to figure out the types, we label the struct as a being generic, using the <SomeType> syntax:

struct SomeView<Content>: View where Content: View

This says there is a generic type that we’re calling Content, and that generic type is expected to conform to the View protocol. There is a more compact way to represent this that you may prefer:

struct SomeView<Content: View>: View

Within the view itself, we have a property – which we name content. The type of this content isn’t known up front – it is the arbitrary type that the compiler gets to infer from the closure that will provided in the future. This declaration is saying the content property will be a closure – taking no parameters – that returns some an arbitrary type we are calling Content.

public let content: () -> Content

Then in the initializer, we use ViewBuilder:

public init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
}

In case it wasn’t obvious, ViewBuilder is a function builder, the swift feature that is enabling this declarative structure with SwiftUI. This is what allows us to ultimately use it with in that declarative syntax form.

The final bit of code to describe is using the @Environment property wrapper.

@Environment(\.colorScheme) public var colorSchemeMode

The property wrapper is not in common use, but perfect for this need. The property wrapper uses exposes a specific part of the existing environment as a local property for this view. This is what enables PreviewBackground to choose the color for the background appropriate to the mode. By reading the environment it chooses an appropriately colored background. It then uses that property to assemble a view by invoking the property named content (which was provided by the function builder) within a ZStack.

By using ViewBuilder, we can use the PreviewBackground struct like any other composed view within SwiftUI:

var body: some View {
    PreviewBackground {
        Text("Hello there!")
    }
}

If we had created this code as a ViewModifier, then using it would look different – instead of the curly-bracket syntax, we would be chaining on a method. The default set up for something like that looks like:

var body: some View {
    Text("Hello there!")
    .modify(PreviewBackground())
}

I wanted to enable the curly-bracket syntax for this, hence the choice of using a ViewBuilder.

A side note about moving code into a Swift package

When I created this code, I did so within the context of another project. I wanted to use it across a second project, and the code was simple enough (a single file) to copy/paste – but instead I went ahead and made it a Swift package. Partially to make it easier for anyone else to use, but also just to get a bit more experience with what it takes to set up and use this kind of thing.

The mistake that I made immediately on moving the code was not explicitly making all the structs and properties public. It moved over, compiled fine, and everything was looking great as a package, but then when I went to use it – I got some really odd errors:

Cannot call value of non-function type 'module<PreviewBackground>'

In other instances (yes, I admit this wasn’t the first time I made this mistake – and it likely won’t be the last) the swift compiler would complain about the scope of a function, letting me know that it was using the default internal scope, and was not available. But SwiftUI and this lovely function builder mechanism is making the compiler work quite a bit more, and it is not nearly as good at identifying why this mistake might have happened, only that it was failing.

If you hit the error Cannot call value of non-function type when moving code into a package, you may have forgotten to make the struct (and relevant properties) explicitly public.