You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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文本的显示效果。

  1. 定义PreferenceKey用于存储文本尺寸:
struct TextSizePreferenceKey: PreferenceKey {
    static var defaultValue: [CGSize] = []
    
    static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) {
        value.append(contentsOf: nextValue())
    }
}
  1. 实现自定义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)
    }
}
  1. 在主视图中测量文本尺寸并传递给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计算文本尺寸,无需提前测量。

  1. 实现自定义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)
    }
}
  1. 在主视图中使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 20:04:55