This is a follow-up to my question How to fill a Path in a Canvas using a Material style where the comment was made, why use a Canvas
when a Shape
would be easier?
One difference between a Canvas
and a custom Shape
is that you get a GraphicsContext
with a Canvas
and this can be used for resolving text. Here is an example of a case where this is useful. This is a simplified version of the answer to How to add rounded borders that cling to a Text in SwiftUI:
struct ContentView: View {
private let textLine1 = "The quick brown fox"
private let textLine2 = "jumps over the lazy dog"
private let paddingSize: CGFloat = 4
private var textBackground: some View {
Canvas { ctx, size in
let resolvedText1 = ctx.resolve(Text(textLine1))
let resolvedText2 = ctx.resolve(Text(textLine2))
let sizeText1 = resolvedText1.measure(in: size)
let sizeText2 = resolvedText2.measure(in: size)
let outlineText1 = Path(
roundedRect: CGRect(
x: ((size.width - sizeText1.width) / 2) - paddingSize,
y: ((size.height / 2) - sizeText1.height) - paddingSize,
width: sizeText1.width + (2 * paddingSize),
height: sizeText1.height + (2 * paddingSize)
),
cornerRadius: 4
)
let outlineText2 = Path(
roundedRect: CGRect(
x: ((size.width - sizeText2.width) / 2) - paddingSize,
y: (size.height / 2) - paddingSize,
width: sizeText2.width + (2 * paddingSize),
height: sizeText2.height + (2 * paddingSize)
),
cornerRadius: 4
)
let combinedOutline = outlineText1.union(outlineText2)
ctx.fill(combinedOutline, with: .color(white: 0.4, opacity: 0.4))
ctx.stroke(combinedOutline, with: .foreground, lineWidth: 2)
}
}
var body: some View {
ZStack {
textBackground
VStack(spacing: 0) {
Text(textLine1)
Text(textLine2)
}
}
.font(.title2)
}
}
What would be preferable, would be to construct the background as a custom shape:
struct MyTextBackground: Shape {
let text1: String
let text2: String
func path(in rect: CGRect) -> Path {
var path = Path()
//let ctx = GraphicsContext(path) // Error: 'GraphicsContext' cannot be constructed because it has no accessible initializers
// Help - how do I find the size of the text??
return path
}
}
A shape like this could be filled and stroked easily. A Material
fill would be easy too, instead of having to use the (rather convoluted) workarounds that were suggested as answers to the post mentioned above:
ZStack {
MyTextBackground(text1: textLine1, text2: textLine2)
.fill(.ultraThinMaterial)
.stroke(.primary, lineWidth: 2)
VStack(spacing: 0) {
// ...
}
}
.font(.title2)
So, how can I get get the size of text inside the path
function of a custom Shape
, when there is no GraphicsContext
available?
In iOS 18, TextRenderer
makes available a GraphicsContext
, allowing drawing based on actual per-line sizes.
import SwiftUI
struct MultiTextLineBackground: View {
var body: some View {
if #available(iOS 18.0, *) {
Text("The quick brown fox jumps over the lazy dog")
.frame(width: 150)
.multilineTextAlignment(.center)
.textRenderer(ContourTextRenderer())
} else {
// Fallback on earlier versions
}
}
}
struct ContourTextRenderer: TextRenderer {
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
var combinedPath = Path()
for line in layout {
let rect = line.typographicBounds.rect.insetBy(dx: -6, dy: -4) //adds padding
let path = Path(roundedRect: rect, cornerRadius: 4)
combinedPath = combinedPath.union(path)
}
// Fill the background of the combined path
ctx.fill(combinedPath, with: .color(.yellow))
// Stroke the contour of the combined path
ctx.stroke(combinedPath, with: .color(.orange), lineWidth: 1)
// Draw text
for line in layout {
ctx.draw(line)
}
}
}
#Preview {
MultiTextLineBackground()
}