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.
- Add a blendmode to the gradient. Blendmode.sourceAtop draws the source (gradient) only above the destination pixels.
- 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.