magnuskahr

writing code

Blendmode trick: SwiftUI source overlay

SwiftUI has the .overlay(alignment: Alignment, content: () -> View) modifier to place a view above another. However, it can fill the entire bounding rectangle of the applied view, which may not be what we desire - see the following code:

Circle()
  .fill(Color.red)
  .overlay {
    LinearGradient(
      colors: [.black, .white],
      startPoint: .top,
      endPoint: .bottom
    )
    .opacity(0.5)
  }

Running this code, we see a red circle with a gradient overlay that fills the bounding rectangle. What if we just wanted to overlay the gradient on the circle itself?

We can do so, with just two lines of code.

  1. Add a blendmode to the gradient. Blendmode.sourceAtop draws the source (gradient) only above the destination pixels.
  2. Flatten the view to lock the blendmode to just that view, using .drawingGroup(opaque: false).

Modifying our example above, we now have:

Circle()
  .fill(Color.red)
  .overlay {
    LinearGradient(
      colors: [.black, .white],
      startPoint: .top,
      endPoint: .bottom
    )
    .opacity(0.5)
    .blendMode(.sourceAtop)
  }
  .drawingGroup(opaque: false)

Which successfully applies the gradient as needed.

Given this, we can isolate the functionality into a reusable modifier:

extension View {
  func sourceOverlay<Overlay: View>(@ViewBuilder overlay: () -> Overlay) -> some View {
    self.overlay {
      overlay().blendMode(.sourceAtop)
    }
    .drawingGroup(opaque: false)
  }
}

And now we have a simple modifier to overlay content above a view, but only on the shape of that view.