SwiftUI文本编辑器中嵌入可交互UIButton实现方案咨询
可行,以下是几种实现思路
思路1:基于UITextView的富文本附件方案(适配复杂排版)
既然你已经在用UIViewRepresentable封装编辑器,这种方案能完美兼容原生UITextView的排版能力,同时插入真正的UIButton:
- 解析文本片段:用正则提取所有
<button id="x">...</button>标签,记录每个按钮的id、包裹文本以及它们在原文本中的位置。 - 构建带附件的富文本:
- 创建
NSTextAttachment,将自定义UIButton添加到附件的contentView中,设置按钮标题和点击事件(可根据id触发对应操作)。 - 把原文本拆分成普通文本段和按钮附件段,拼接成
NSAttributedString。
- 创建
- 封装到UIViewRepresentable:初始化
UITextView并设置attributedText,同时开启附件交互权限。
示例代码片段:
struct ButtonTextAttachment: NSTextAttachment { let buttonId: String let action: () -> Void override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { // 让按钮和文本行高对齐 return CGRect(x: 0, y: -4, width: 120, height: lineFrag.height) } func makeButton(with title: String) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) return button } @objc private func buttonTapped() { action() } } // 在UIViewRepresentable中使用 func makeUIView(context: Context) -> UITextView { let textView = UITextView() textView.isEditable = true // 保持编辑器可编辑特性 textView.isSelectable = true textView.allowsEditingTextAttributes = true let originalText = "Lorem ipsum dolor sit amet, <button id = \"4\"> consectetur adipiscing elit. Maecenas ornare </button> eget sapien vel gravida." let attributedString = NSMutableAttributedString(string: "") // 正则匹配按钮标签(实际可优化正则规则) let regex = try! NSRegularExpression(pattern: "<button id = \"(\\d+)\">(.*?)</button>", options: .dotMatchesLineSeparators) let matches = regex.matches(in: originalText, range: NSRange(originalText.startIndex..., in: originalText)) var currentPosition = 0 for match in matches { // 添加匹配前的普通文本 let prefixRange = NSRange(location: currentPosition, length: match.range.location - currentPosition) let prefixText = (originalText as NSString).substring(with: prefixRange) attributedString.append(NSAttributedString(string: prefixText)) // 创建按钮附件 let buttonId = (originalText as NSString).substring(with: match.range(at: 1)) let buttonText = (originalText as NSString).substring(with: match.range(at: 2)) let attachment = ButtonTextAttachment(buttonId: buttonId) { // 自定义按钮点击逻辑 print("点击按钮ID: \(buttonId)") // 示例:切换模式 // self.mode = "normal" } let button = attachment.makeButton(with: buttonText) attachment.contentView.addSubview(button) button.frame = attachment.bounds(for: nil, proposedLineFragment: CGRect(x:0,y:0,width:200,height:20), glyphPosition: .zero, characterIndex: 0) // 将附件加入富文本 let attachmentString = NSAttributedString(attachment: attachment) attributedString.append(attachmentString) currentPosition = match.range.location + match.range.length } // 添加剩余文本 let suffixText = (originalText as NSString).substring(from: currentPosition) attributedString.append(NSAttributedString(string: suffixText)) textView.attributedText = attributedString return textView }
思路2:拆分为SwiftUI混合视图(轻量方案,适合简单排版)
如果不需要复杂的文本换行排版,可以把原文本拆分成普通文本段和SwiftUI Button组件的数组,再用容器视图拼接:
- 拆分文本:用正则将原文本拆分成多个片段,标记普通文本和按钮(含id与文本内容)。
- 动态生成视图:遍历片段数组,普通文本用
Text渲染,按钮片段用SwiftUI的Button组件绑定自定义操作。
示例代码片段:
struct EditorView: View { @State private var mode = "edit" // 定义文本片段类型 enum TextFragment { case plain(String) case button(id: String, text: String) } private func parseText(_ text: String) -> [TextFragment] { var fragments: [TextFragment] = [] let regex = try! NSRegularExpression(pattern: "<button id = \"(\\d+)\">(.*?)</button>", options: .dotMatchesLineSeparators) let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text)) var currentPosition = 0 for match in matches { let prefixRange = NSRange(location: currentPosition, length: match.range.location - currentPosition) let prefixText = (text as NSString).substring(with: prefixRange) if !prefixText.isEmpty { fragments.append(.plain(prefixText)) } let buttonId = (text as NSString).substring(with: match.range(at: 1)) let buttonText = (text as NSString).substring(with: match.range(at: 2)) fragments.append(.button(id: buttonId, text: buttonText)) currentPosition = match.range.location + match.range.length } let suffixText = (text as NSString).substring(from: currentPosition) if !suffixText.isEmpty { fragments.append(.plain(suffixText)) } return fragments } var body: some View { let originalText = "Lorem ipsum dolor sit amet, <button id = \"4\"> consectetur adipiscing elit. Maecenas ornare </button> eget sapien vel gravida." let fragments = parseText(originalText) VStack(alignment: .leading, spacing: 0) { ForEach(Array(fragments.enumerated()), id: \.offset) { _, fragment in switch fragment { case .plain(let text): Text(text) .font(.body) case .button(let id, let text): Button(action: { self.mode = "normal" print("点击按钮ID: \(id)") }) { Text(text) .foregroundColor(.blue) .underline() } } } } .padding() } }
思路3:用AttributedString自定义链接(模拟按钮,iOS15+)
如果可以接受用可点击文本替代原生UIButton,iOS15+可借助AttributedString的自定义链接实现:
- 给
- 在视图上添加点击手势,识别点击的文本范围,解析URL触发对应操作。
示例代码片段:
struct LinkButtonView: View { @State private var mode = "edit" private func makeAttributedText() -> AttributedString { let originalText = "Lorem ipsum dolor sit amet, <button id = \"4\"> consectetur adipiscing elit. Maecenas ornare </button> eget sapien vel gravida." // 先移除标签,再标记可点击范围 var cleanedText = originalText.replacingOccurrences(of: "<button id = \"4\">", with: "") cleanedText = cleanedText.replacingOccurrences(of: "</button>", with: "") var attributedString = AttributedString(cleanedText) // 定位按钮文本范围(实际可通过正则精准匹配) if let buttonRange = attributedString.range(of: "consectetur adipiscing elit. Maecenas ornare") { attributedString[buttonRange].link = URL(string: "button://4") attributedString[buttonRange].foregroundColor = .blue attributedString[buttonRange].underlineStyle = .single } return attributedString } var body: some View { Text(makeAttributedText()) .font(.body) .onTapGesture { location in guard let textView = UIApplication.shared.windows.first?.rootViewController?.view.subviews.first(where: { $0 is UITextView }) as? UITextView else { return } let position = textView.closestPosition(to: location) guard let range = textView.tokenizer.rangeEnclosingPosition(position!, with: .character, inDirection: .layout(.left)) else { return } let index = textView.offset(from: textView.beginningOfDocument, to: range.start) let attributes = textView.attributedText.attributes(at: index, effectiveRange: nil) if let link = attributes[.link] as? URL, link.scheme == "button" { let buttonId = link.host ?? "" self.mode = "normal" print("点击按钮ID: \(buttonId)") } } } }
内容的提问来源于stack exchange,提问作者Stefano Vet
相关产品推荐
相关产品推荐

