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

iOS Swift实现Label文本随滑块上下弯曲(无需添加子层)

问题描述

我有一个Label,希望通过滑块控制其文本向上或向下弯曲。目前我已通过以下代码实现该功能:

func drawCurvedString(on layer: CALayer, text: NSAttributedString, angle: CGFloat, radius: CGFloat) {
    var radAngle = angle.radians
    let textSize = text.boundingRect(
        with: CGSize(width: .max, height: .max),
        options: [.usesLineFragmentOrigin, .usesFontLeading],
        context: nil)
        .integral
        .size
    let perimeter: CGFloat = 2 * .pi * radius
    let textAngle: CGFloat = textSize.width / perimeter * 2 * .pi
    var textRotation: CGFloat = 0
    var textDirection: CGFloat = 0
    if angle > CGFloat(10).radians, angle < CGFloat(170).radians {
        // bottom string
        textRotation = 0.5 * .pi
        textDirection = -2 * .pi
        radAngle += textAngle / 2
    } else {
        // top string
        textRotation = 1.5 * .pi
        textDirection = 2 * .pi
        radAngle -= textAngle / 2
    }
    for c in 0..<text.length {
        let letter = text.attributedSubstring(from: NSRange(c..<c+1))
        let charSize = letter.boundingRect(
            with: CGSize(width: .max, height: .max),
            options: [.usesLineFragmentOrigin, .usesFontLeading],
            context: nil)
            .integral
            .size
        let letterAngle = (charSize.width / perimeter) * textDirection
        let x = radius * cos(radAngle + (letterAngle / 2))
        let y = radius * sin(radAngle + (letterAngle / 2))
        let singleChar = drawText(
            on: layer,
            text: letter,
            frame: CGRect(
                x: (layer.frame.size.width / 2) - (charSize.width / 2) + x,
                y: (layer.frame.size.height / 2) - (charSize.height / 2) + y,
                width: charSize.width,
                height: charSize.height))
        layer.addSublayer(singleChar)
        singleChar.transform = CATransform3DMakeAffineTransform(CGAffineTransform(rotationAngle: radAngle - textRotation))
        radAngle += letterAngle
    }
}
func drawText(on layer: CALayer, text: NSAttributedString, frame: CGRect) -> CATextLayer {
    let textLayer = CATextLayer()
    textLayer.frame = frame
    textLayer.string = text
    textLayer.alignmentMode = kCAAlignmentCenter
    textLayer.contentsScale = UIScreen.main.scale
    return textLayer
}

但问题在于这段代码会添加文本子层,我想要在Swift中实现无需添加子层的方案,恳请帮助。

解决方案

可以通过自定义UIView,在draw(_ rect:)方法中直接使用Core Graphics绘制弯曲文本,不需要添加任何子层。以下是实现代码:

import UIKit

class CurvedLabel: UIView {
    var attributedText: NSAttributedString = NSAttributedString(string: "") {
        didSet { setNeedsDisplay() }
    }
    var curveRadius: CGFloat = 100 {
        didSet { setNeedsDisplay() }
    }
    // 控制弯曲方向:true为向上弯(顶部弧形),false为向下弯(底部弧形)
    var curveUpwards: Bool = false {
        didSet { setNeedsDisplay() }
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let context = UIGraphicsGetCurrentContext(), attributedText.length > 0 else { return }
        
        let textSize = attributedText.boundingRect(
            with: CGSize(width: .max, height: .max),
            options: [.usesLineFragmentOrigin, .usesFontLeading],
            context: nil
        ).integral.size
        
        let perimeter = 2 * .pi * curveRadius
        let totalTextAngle = textSize.width / perimeter * 2 * .pi
        
        var currentAngle: CGFloat
        var angleDirection: CGFloat
        let rotationOffset: CGFloat
        
        if curveUpwards {
            // 顶部弧形:文本沿上半圆排列,方向顺时针
            rotationOffset = 1.5 * .pi
            angleDirection = 2 * .pi
            currentAngle = (.pi - totalTextAngle / 2)
        } else {
            // 底部弧形:文本沿下半圆排列,方向逆时针
            rotationOffset = 0.5 * .pi
            angleDirection = -2 * .pi
            currentAngle = totalTextAngle / 2
        }
        
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        
        for charIndex in 0..<attributedText.length {
            let charRange = NSRange(location: charIndex, length: 1)
            let charAttr = attributedText.attributedSubstring(from: charRange)
            
            let charSize = charAttr.boundingRect(
                with: CGSize(width: .max, height: .max),
                options: [.usesLineFragmentOrigin, .usesFontLeading],
                context: nil
            ).integral.size
            
            let charAngle = (charSize.width / perimeter) * angleDirection
            
            // 计算当前字符的圆心角位置
            let charCenterAngle = currentAngle + charAngle / 2
            // 计算字符绘制的位置(基于圆心偏移)
            let charPosition = CGPoint(
                x: center.x + curveRadius * cos(charCenterAngle),
                y: center.y + curveRadius * sin(charCenterAngle)
            )
            
            // 保存当前上下文状态
            context.saveGState()
            
            // 移动上下文到字符位置,旋转对应角度
            context.translateBy(x: charPosition.x, y: charPosition.y)
            context.rotate(by: charCenterAngle - rotationOffset)
            
            // 绘制字符,锚点设为中心
            charAttr.draw(at: CGPoint(x: -charSize.width/2, y: -charSize.height/2))
            
            // 恢复上下文状态
            context.restoreGState()
            
            currentAngle += charAngle
        }
    }
}

使用方式

  1. 在代码中创建或在Storyboard中添加CurvedLabel实例
  2. 设置attributedText、curveRadius和curveUpwards属性,配合滑块控制参数:
// 示例:滑块控制弯曲半径
radiusSlider.addTarget(self, action: #selector(radiusChanged(_:)), for: .valueChanged)
// 示例:切换弯曲方向
directionSwitch.addTarget(self, action: #selector(directionChanged(_:)), for: .valueChanged)

@objc func radiusChanged(_ slider: UISlider) {
    curvedLabel.curveRadius = CGFloat(slider.value)
}

@objc func directionChanged(_ `switch`: UISwitch) {
    curvedLabel.curveUpwards = `switch`.isOn
}

说明

  • 该方案直接在视图的绘制上下文里渲染每个字符,不会创建任何子层
  • 通过setNeedsDisplay()在参数变化时触发重绘,保证实时更新
  • 保留了原代码的弯曲逻辑,同时优化了角度计算和上下文操作

内容的提问来源于stack exchange,提问作者Asim Iftikhar Abbasi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 03:15:32