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

如何为UITextView的UIMenuController添加更多文本样式选项?

嘿,我懂你想给UITextView的文本样式菜单加删除线、高亮这类选项的需求——就像原生备忘录那样对吧?之前尝试重写私有方法_showTextStyleOptions:没成功太正常了,毕竟苹果对私有API的限制越来越严,而且这种方法在不同iOS版本里极不稳定,完全不推荐用。下面给你两种靠谱的实现方案,亲测在iOS 13+上有效:


方案一:完全自定义文本样式菜单

这个方案可以让你完全掌控菜单的内容和样式,替换掉系统默认的BIU菜单,按需添加任意选项:

实现步骤

  1. 禁用系统默认的BIU动作,避免和自定义菜单冲突
  2. 用UIMenuBuilder创建包含所有需要样式的自定义菜单
  3. 实现每个样式的切换逻辑,处理富文本属性的添加/移除

代码示例(Swift)

class CustomStyledTextView: UITextView {
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        self.allowsEditingTextAttributes = true // 必须开启富文本编辑支持
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.allowsEditingTextAttributes = true
    }
    
    // 禁用系统默认的BIU动作
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        if action == #selector(toggleBoldface(_:)) || 
           action == #selector(toggleItalics(_:)) || 
           action == #selector(toggleUnderline(_:)) {
            return false
        }
        return super.canPerformAction(action, withSender: sender)
    }
    
    // 构建自定义菜单
    override func buildMenu(with builder: UIMenuBuilder) {
        super.buildMenu(with: builder)
        
        // 移除系统默认的文本样式菜单组
        builder.remove(menu: .textStyling)
        
        // 创建所有样式动作
        let boldAction = UIAction(title: "粗体") { [weak self] _ in
            self?.toggleFontStyle(isBold: true)
        }
        
        let italicAction = UIAction(title: "斜体") { [weak self] _ in
            self?.toggleFontStyle(isItalic: true)
        }
        
        let underlineAction = UIAction(title: "下划线") { [weak self] _ in
            self?.toggleTextAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue)
        }
        
        let strikethroughAction = UIAction(title: "删除线") { [weak self] _ in
            self?.toggleTextAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue)
        }
        
        let highlightAction = UIAction(title: "高亮") { [weak self] _ in
            self?.toggleTextAttribute(.backgroundColor, value: UIColor.yellow.withAlphaComponent(0.5))
        }
        
        // 组装成自定义样式菜单
        let styleMenu = UIMenu(title: "样式", children: [
            boldAction, italicAction, underlineAction, strikethroughAction, highlightAction
        ])
        
        // 将自定义菜单插入到编辑菜单最前面
        builder.insertChild(styleMenu, atStartOfMenu: .edit)
    }
    
    // 切换粗体/斜体字体样式
    private func toggleFontStyle(isBold: Bool = false, isItalic: Bool = false) {
        guard let selectedRange = selectedTextRange, selectedRange.length > 0, let baseFont = font else { return }
        let nsRange = range(from: selectedRange)
        let mutableText = attributedText.mutableCopy() as! NSMutableAttributedString
        
        let currentFont = mutableText.attribute(.font, at: nsRange.location, effectiveRange: nil) as? UIFont
        var newFont: UIFont
        
        if isBold {
            newFont = currentFont?.isBold == true ? baseFont : baseFont.bold()
        } else {
            newFont = currentFont?.isItalic == true ? baseFont : baseFont.italic()
        }
        
        mutableText.addAttribute(.font, value: newFont, range: nsRange)
        attributedText = mutableText
        selectedTextRange = selectedRange // 保持选中状态
    }
    
    // 通用的文本属性切换方法(下划线、删除线、高亮等)
    private func toggleTextAttribute(_ key: NSAttributedString.Key, value: Any) {
        guard let selectedRange = selectedTextRange, selectedRange.length > 0 else { return }
        let nsRange = range(from: selectedRange)
        let mutableText = attributedText.mutableCopy() as! NSMutableAttributedString
        
        let currentValue = mutableText.attribute(key, at: nsRange.location, effectiveRange: nil)
        if currentValue != nil && (currentValue as? AnyHashable) == (value as? AnyHashable) {
            mutableText.removeAttribute(key, range: nsRange)
        } else {
            mutableText.addAttribute(key, value: value, range: nsRange)
        }
        
        attributedText = mutableText
        selectedTextRange = selectedRange
    }
    
    // 辅助方法:将UITextRange转换为NSRange
    private func range(from textRange: UITextRange) -> NSRange {
        let start = offset(from: beginningOfDocument, to: textRange.start)
        let length = offset(from: textRange.start, to: textRange.end)
        return NSRange(location: start, length: length)
    }
}

// 给UIFont添加便捷扩展
extension UIFont {
    func bold() -> UIFont {
        return UIFont(descriptor: fontDescriptor.withSymbolicTraits(.traitBold)!, size: pointSize)
    }
    
    func italic() -> UIFont {
        return UIFont(descriptor: fontDescriptor.withSymbolicTraits(.traitItalic)!, size: pointSize)
    }
    
    var isBold: Bool {
        return fontDescriptor.symbolicTraits.contains(.traitBold)
    }
    
    var isItalic: Bool {
        return fontDescriptor.symbolicTraits.contains(.traitItalic)
    }
}

方案二:在系统BIU菜单基础上新增选项

如果你想保留系统原生的BIU菜单,只需要添加删除线、高亮等新选项,这个方案更省心:

代码示例(Swift)

class ExtendedStyleTextView: UITextView {
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        self.allowsEditingTextAttributes = true
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.allowsEditingTextAttributes = true
    }
    
    override func buildMenu(with builder: UIMenuBuilder) {
        super.buildMenu(with: builder)
        
        // 获取系统原生的文本样式菜单
        guard let systemStyleMenu = builder.menu(for: .textStyling) else { return }
        
        // 创建自定义动作
        let strikethroughAction = UIAction(title: "删除线") { [weak self] _ in
            self?.toggleStrikethrough()
        }
        
        let highlightAction = UIAction(title: "高亮") { [weak self] _ in
            self?.toggleHighlight()
        }
        
        // 组装新的菜单:系统原有选项 + 自定义选项
        let newStyleMenu = UIMenu(title: systemStyleMenu.title, 
                                 children: systemStyleMenu.children + [strikethroughAction, highlightAction])
        
        // 替换系统默认菜单
        builder.replace(menu: .textStyling, with: newStyleMenu)
    }
    
    // 切换删除线
    private func toggleStrikethrough() {
        guard let selectedRange = selectedTextRange, selectedRange.length > 0 else { return }
        let nsRange = range(from: selectedRange)
        let mutableText = attributedText.mutableCopy() as! NSMutableAttributedString
        
        let currentStyle = mutableText.attribute(.strikethroughStyle, at: nsRange.location, effectiveRange: nil) as? Int
        if currentStyle == NSUnderlineStyle.single.rawValue {
            mutableText.removeAttribute(.strikethroughStyle, range: nsRange)
            mutableText.removeAttribute(.strikethroughColor, range: nsRange)
        } else {
            mutableText.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
            mutableText.addAttribute(.strikethroughColor, value: UIColor.black, range: nsRange)
        }
        
        attributedText = mutableText
        selectedTextRange = selectedRange
    }
    
    // 切换高亮
    private func toggleHighlight() {
        guard let selectedRange = selectedTextRange, selectedRange.length > 0 else { return }
        let nsRange = range(from: selectedRange)
        let mutableText = attributedText.mutableCopy() as! NSMutableAttributedString
        
        let currentColor = mutableText.attribute(.backgroundColor, at: nsRange.location, effectiveRange: nil) as? UIColor
        let targetColor = UIColor.yellow.withAlphaComponent(0.5)
        if currentColor?.isEqual(targetColor) == true {
            mutableText.removeAttribute(.backgroundColor, range: nsRange)
        } else {
            mutableText.addAttribute(.backgroundColor, value: targetColor, range: nsRange)
        }
        
        attributedText = mutableText
        selectedTextRange = selectedRange
    }
    
    // 辅助方法:UITextRange转NSRange
    private func range(from textRange: UITextRange) -> NSRange {
        let start = offset(from: beginningOfDocument, to: textRange.start)
        let length = offset(from: textRange.start, to: textRange.end)
        return NSRange(location: start, length: length)
    }
}

注意事项

  1. 必须确保allowsEditingTextAttributes = true,否则文本视图不支持富文本编辑
  2. 以上方案基于iOS 13+的UIMenuBuilder实现,这是苹果推荐的自定义上下文菜单方式,替代了旧的UIMenuController
  3. 如果需要处理选中文本部分有样式、部分无样式的情况,可以调整逻辑(比如统一设置属性,而非切换)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:31:27