如何实现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
相关产品推荐
相关产品推荐

