Borders

It seems a little odd to write a post about borders, since every post so far has already used them without calling them out. In all of the examples I’ve added borders to views and used the result in the previews, rather than drawing them in by hand.

But I haven’t actually shown the code to do that, and it turns out that they’re slightly more interesting than you might expect.

There is a single method for specifying the border for a view:

func border<S>(_ content: S, width: CGFloat = 1) -> some View where S : ShapeStyle

The first parameter is required and specifies a shape style, there’s a quite a few options for that, but fortunately Color confirms to the ShapeStyle protocol so for the simplest cases all we need to do is specify a color.

The second parameter is optional and specifies the width of the border, defaulting to a single pixel.

So it wouldn’t be a surprise that to draw a single pixel yellow border around a Text we would use code like this:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .border(Color.yellow)
    }
}

And this produces the same example I’ve used before, except with the border that’s always been in the preview actually stated in code now:

text with a border

But now time for the first surprise.

You might expect that a border works a lot like a frame or padding, adding a view around the Text with enough space to draw the border, and positioning the child inside it.

Except they don’t, we can demonstrate this by increasing the width of the border:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .border(Color.yellow, width: 4)
    }
}

If this worked like padding, the border would increase in width around the text; instead we see something quite different:

text with a thick border

Instead of surrounding the text, the border has overlaid it. In fact, .border creates a secondary view on its child, and draws the border overlaid on top of it.

.border creates a secondary view on its child, and draws the border overlaid on top of it

If we wanted the border around the view instead, we have to combine it with .padding:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .padding(4)
            .border(Color.yellow, width: 4)
    }
}

This creates the Text view, and then .padding creates another view around that with additional padding added, and then .border adds a secondary view to the padding view, and draws overlaid on that:

text with thick border and padding

It’s important to note the distinction that the border is on the padding view; combined effects can be performed by carefully placing the overlays in the correct place:

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .border(Color.red)
            .padding(4)
            .border(Color.yellow, width: 4)
            .border(Color.red)
    }
}

Here we create a red border overlaid on the Text, and then use padding to draw a thicker yellow border around the Text, and finally overlaid another red border onto the padding:

text with three borders

The total border width is 5px since it includes the additional pixel-wide border overlaid on the Text, or put another way, the yellow part of the border is 3px wide since the outer pixel is overlaid by the red border added to it.