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

如何实现UILabel文本外围的粗描边效果

推荐解决方案

你可以通过CoreText手动合并文本路径的自定义绘制方案实现需求,该方案可同时解决NSAttributedString在iOS14+的路径渲染bug、以及普通drawText方案的描边不融合问题,全系统版本兼容,效果完全可控。

以下是自定义UILabel的实现代码:

class StrokeTextLabel: UILabel {
    // 对外暴露可配置属性
    var strokeWidth: CGFloat = 8
    var strokeColor: UIColor = .white
    var showShadow: Bool = true
    
    override func drawText(in rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext(), let text = self.text, let font = self.font else {
            super.drawText(in: rect)
            return
        }
        
        // 保存上下文状态
        context.saveGState()
        // 翻转CoreText坐标系 匹配UIKit布局
        context.textMatrix = .identity
        context.translateBy(x: 0, y: rect.size.height)
        context.scaleBy(x: 1, y: -1)
        // 设置线样式 保证描边连接处圆润
        context.setLineJoin(.round)
        context.setLineCap(.round)
        context.setLineWidth(strokeWidth)
        
        // 初始化文本属性
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: textColor ?? .black
        ]
        let attributedString = NSAttributedString(string: text, attributes: attributes)
        let path = CGMutablePath()
        path.addRect(rect)
        
        // 生成CoreText排版帧
        let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), path, nil)
        guard let lines = CTFrameGetLines(frame) as? [CTLine] else {
            context.restoreGState()
            super.drawText(in: rect)
            return
        }
        
        // 读取行原点坐标
        var lineOrigins = [CGPoint](repeating: .zero, count: lines.count)
        CTFrameGetLineOrigins(frame, CFRangeMake(0, lines.count), &lineOrigins)
        
        // 合并所有字符的路径为整体路径
        let textPath = CGMutablePath()
        for (index, line) in lines.enumerated() {
            let lineOrigin = lineOrigins[index]
            guard let runs = CTLineGetGlyphRuns(line) as? [CTRun] else { continue }
            for run in runs {
                let glyphCount = CTRunGetGlyphCount(run)
                var glyphs = [CGGlyph](repeating: 0, count: glyphCount)
                var positions = [CGPoint](repeating: .zero, count: glyphCount)
                CTRunGetGlyphs(run, CFRangeMake(0, glyphCount), &glyphs)
                CTRunGetPositions(run, CFRangeMake(0, glyphCount), &positions)
                
                for (glyphIndex, glyph) in glyphs.enumerated() {
                    guard let glyphPath = CTFontCreatePathForGlyph(font, glyph, nil) else { continue }
                    let position = positions[glyphIndex]
                    let transform = CGAffineTransform(translationX: lineOrigin.x + position.x, y: lineOrigin.y + position.y)
                    textPath.addPath(glyphPath, transform: transform)
                }
            }
        }
        
        // 先整体描边:保证相邻字符描边自然融合
        context.setStrokeColor(strokeColor.cgColor)
        context.addPath(textPath)
        context.strokePath()
        
        // 再填充文本内部
        context.setFillColor((textColor ?? .black).cgColor)
        context.addPath(textPath)
        context.fillPath()
        
        // 按需添加阴影
        if showShadow {
            context.setShadow(offset: CGSize(width: 0, height: 2), blur: 2, color: UIColor.black.withAlphaComponent(0.08).cgColor)
        }
        
        // 恢复上下文状态
        context.restoreGState()
    }
}

该方案的优势:

  • 不存在系统版本适配问题,所有iOS版本渲染效果一致
  • 基于合并后的整体路径描边,天然实现相邻字符描边的融合效果,不会出现单字符描边重叠的缝隙
  • 描边厚度、线连接样式、阴影参数都可以自由调整,完全匹配你的设计需求
  • 用法和系统UILabel完全一致,只需要替换类名后配置对应属性即可使用

内容的提问来源于stack exchange,提问作者Xaxxus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 10:57:03