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

如何实现类似苹果笔记的UITextView多行标题功能?

实现思路:仿苹果笔记的UITextView标题行为

核心逻辑

以第一个换行符为分界点,将换行前的内容作为标题(应用大字号粗体样式),换行后的内容作为正文(应用普通样式)。通过监听文本变化动态调整样式,同时处理回车事件确保首次回车后光标自动定位到正文区域。

代码实现细节

  1. 绑定UITextView代理
    让你的ViewController遵守UITextViewDelegate协议,并给目标UITextView设置delegate = self。

  2. 文本变化时的样式区分
    在textViewDidChange方法中分割标题与正文,分别应用不同样式:

func textViewDidChange(_ textView: UITextView) {
    guard let fullText = textView.text, !fullText.isEmpty else {
        // 空文本时可添加占位符逻辑
        return
    }
    
    let attributedText = NSMutableAttributedString(string: fullText)
    // 标题样式配置
    let titleAttrs: [NSAttributedString.Key: Any] = [
        .font: UIFont.systemFont(ofSize: 24, weight: .bold),
        .foregroundColor: UIColor.label
    ]
    // 正文字体配置
    let bodyAttrs: [NSAttributedString.Key: Any] = [
        .font: UIFont.systemFont(ofSize: 16),
        .foregroundColor: UIColor.label
    ]
    
    if let newlineRange = fullText.range(of: "\n") {
        let titleLength = fullText.distance(from: fullText.startIndex, to: newlineRange.lowerBound)
        // 为标题区域设置样式
        attributedText.addAttributes(titleAttrs, range: NSRange(location: 0, length: titleLength))
        // 为正文区域设置样式(跳过换行符)
        let bodyStart = titleLength + 1
        let bodyLength = fullText.count - bodyStart
        if bodyLength > 0 {
            attributedText.addAttributes(bodyAttrs, range: NSRange(location: bodyStart, length: bodyLength))
        }
    } else {
        // 无换行时,全部内容作为标题
        attributedText.addAttributes(titleAttrs, range: NSRange(location: 0, length: fullText.count))
    }
    
    // 避免重复赋值影响性能,仅当样式或文本变化时更新
    if textView.attributedText?.string != attributedText.string ||
       textView.attributedText?.attributes(at: 0, effectiveRange: nil) != titleAttrs {
        textView.attributedText = attributedText
    }
}
  1. 回车事件的光标处理
    在shouldChangeTextIn方法中处理首次回车后的光标定位:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    guard text == "\n" else { return true }
    
    let fullText = textView.text ?? ""
    // 首次回车时,光标跳到正文开头
    if fullText.range(of: "\n") == nil {
        DispatchQueue.main.async {
            let cursorOffset = fullText.count + 1
            if let newPosition = textView.position(from: textView.beginningOfDocument, offset: cursorOffset) {
                textView.selectedTextRange = textView.textRange(from: newPosition, to: newPosition)
            }
        }
    }
    return true
}

额外优化点

  • 占位符定制:重写UITextView的draw(_ rect: CGRect)方法,为标题和正文区域分别设置不同的提示文本(比如标题区显示"输入标题",正文区显示"输入内容")。
  • 性能优化:缓存标题与正文的范围,仅当范围发生变化时再更新富文本样式,减少不必要的计算。
  • 编辑体验优化:当用户删除所有换行符时,自动将正文内容合并到标题区域并恢复标题样式。

替代库推荐

如果不想自定义实现,可以尝试以下库:

  • RichEditorView:支持富文本编辑,可通过配置实现标题与正文的样式区分,需额外适配换行触发规则。
  • MarkdownTextView:基于Markdown语法,可调整默认规则,将无#标记的首段识别为标题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 14:00:59