SwiftUI中自定义Shape时如何获取Text的尺寸?
在SwiftUI自定义Shape中获取文本尺寸的解决方案
问题背景
我此前在《How to fill a Path in a Canvas using a Material style》的问题中,用Canvas实现了贴合两行文本的圆角背景,代码示例如下:
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) } }
效果如下:

但评论指出用自定义Shape实现会更简洁,因为Shape可以直接用Material填充,无需复杂变通。不过自定义Shape的path(in:)方法中没有GraphicsContext,无法像Canvas那样解析并测量文本尺寸,因此需要解决如何在Shape中获取文本大小的问题。
解决方案
方案一:通过PreferenceKey提前测量文本尺寸并传递给Shape
这种方式通过GeometryReader测量文本的实际渲染尺寸,再用PreferenceKey将尺寸传递给Shape,能精准匹配SwiftUI文本的显示效果。
- 定义PreferenceKey用于存储文本尺寸:
struct TextSizePreferenceKey: PreferenceKey { static var defaultValue: [CGSize] = [] static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) { value.append(contentsOf: nextValue()) } }
- 实现自定义Shape,接收提前测量好的文本尺寸:
struct MyTextBackground: Shape { let textSizes: [CGSize] let paddingSize: CGFloat = 4 func path(in rect: CGRect) -> Path { guard textSizes.count == 2 else { return Path() } let sizeText1 = textSizes[0] let sizeText2 = textSizes[1] let outlineText1 = Path( roundedRect: CGRect( x: ((rect.width - sizeText1.width) / 2) - paddingSize, y: ((rect.height / 2) - sizeText1.height) - paddingSize, width: sizeText1.width + (2 * paddingSize), height: sizeText1.height + (2 * paddingSize) ), cornerRadius: 4 ) let outlineText2 = Path( roundedRect: CGRect( x: ((rect.width - sizeText2.width) / 2) - paddingSize, y: (rect.height / 2) - paddingSize, width: sizeText2.width + (2 * paddingSize), height: sizeText2.height + (2 * paddingSize) ), cornerRadius: 4 ) return outlineText1.union(outlineText2) } }
- 在主视图中测量文本尺寸并传递给Shape:
struct ContentView: View { private let textLine1 = "The quick brown fox" private let textLine2 = "jumps over the lazy dog" @State private var textSizes: [CGSize] = [] var body: some View { ZStack { MyTextBackground(textSizes: textSizes) .fill(.ultraThinMaterial) .stroke(.primary, lineWidth: 2) VStack(spacing: 0) { Text(textLine1) .background( GeometryReader { geo in Color.clear.preference( key: TextSizePreferenceKey.self, value: [geo.size] ) } ) Text(textLine2) .background( GeometryReader { geo in Color.clear.preference( key: TextSizePreferenceKey.self, value: [geo.size] ) } ) } } .font(.title2) .onPreferenceChange(TextSizePreferenceKey.self) { newValue in textSizes = newValue } } }
方案二:在Shape内部用UIKit API计算文本尺寸
如果不需要完全匹配SwiftUI的文本渲染细节,可以直接在Shape内部用UIKit的UIFont和NSString计算文本尺寸,无需提前测量。
- 实现自定义Shape:
struct MyTextBackground: Shape { let text1: String let text2: String let font: UIFont let paddingSize: CGFloat = 4 func path(in rect: CGRect) -> Path { let attributes: [NSAttributedString.Key: Any] = [.font: font] let sizeText1 = text1.size(withAttributes: attributes) let sizeText2 = text2.size(withAttributes: attributes) let outlineText1 = Path( roundedRect: CGRect( x: ((rect.width - sizeText1.width) / 2) - paddingSize, y: ((rect.height / 2) - sizeText1.height) - paddingSize, width: sizeText1.width + (2 * paddingSize), height: sizeText1.height + (2 * paddingSize) ), cornerRadius: 4 ) let outlineText2 = Path( roundedRect: CGRect( x: ((rect.width - sizeText2.width) / 2) - paddingSize, y: (rect.height / 2) - paddingSize, width: sizeText2.width + (2 * paddingSize), height: sizeText2.height + (2 * paddingSize) ), cornerRadius: 4 ) return outlineText1.union(outlineText2) } }
- 在主视图中使用Shape,注意匹配SwiftUI和UIKit的字体:
struct ContentView: View { private let textLine1 = "The quick brown fox" private let textLine2 = "jumps over the lazy dog" // 对应SwiftUI的.title2字体 private let uiFont = UIFont.systemFont(ofSize: 22, weight: .medium) var body: some View { ZStack { MyTextBackground(text1: textLine1, text2: textLine2, font: uiFont) .fill(.ultraThinMaterial) .stroke(.primary, lineWidth: 2) VStack(spacing: 0) { Text(textLine1) Text(textLine2) } } .font(.title2) } }
方案对比
- 方案一:精准匹配SwiftUI文本的实际渲染尺寸,包括行高、字间距等样式,适合对精度要求高的场景。
- 方案二:实现更简洁,无需额外的PreferenceKey和状态管理,但需要确保UIKit字体与SwiftUI字体完全对应,否则可能出现尺寸偏差。
内容的提问来源于stack exchange,提问作者Benzy Neez
相关产品推荐
相关产品推荐

