如何为UITextView的UIMenuController添加更多文本样式选项?
嘿,我懂你想给UITextView的文本样式菜单加删除线、高亮这类选项的需求——就像原生备忘录那样对吧?之前尝试重写私有方法_showTextStyleOptions:没成功太正常了,毕竟苹果对私有API的限制越来越严,而且这种方法在不同iOS版本里极不稳定,完全不推荐用。下面给你两种靠谱的实现方案,亲测在iOS 13+上有效:
方案一:完全自定义文本样式菜单
这个方案可以让你完全掌控菜单的内容和样式,替换掉系统默认的BIU菜单,按需添加任意选项:
实现步骤
- 禁用系统默认的BIU动作,避免和自定义菜单冲突
- 用
UIMenuBuilder创建包含所有需要样式的自定义菜单 - 实现每个样式的切换逻辑,处理富文本属性的添加/移除
代码示例(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) } }
注意事项
- 必须确保
allowsEditingTextAttributes = true,否则文本视图不支持富文本编辑 - 以上方案基于iOS 13+的
UIMenuBuilder实现,这是苹果推荐的自定义上下文菜单方式,替代了旧的UIMenuController - 如果需要处理选中文本部分有样式、部分无样式的情况,可以调整逻辑(比如统一设置属性,而非切换)
内容的提问来源于stack exchange,提问作者Jaehoon Lee
相关产品推荐
相关产品推荐

