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

SwiftUI文本编辑器中嵌入可交互UIButton实现方案咨询

可行,以下是几种实现思路

思路1:基于UITextView的富文本附件方案(适配复杂排版)

既然你已经在用UIViewRepresentable封装编辑器,这种方案能完美兼容原生UITextView的排版能力,同时插入真正的UIButton:

  1. 解析文本片段:用正则提取所有<button id="x">...</button>标签,记录每个按钮的id、包裹文本以及它们在原文本中的位置。
  2. 构建带附件的富文本:
    • 创建NSTextAttachment,将自定义UIButton添加到附件的contentView中,设置按钮标题和点击事件(可根据id触发对应操作)。
    • 把原文本拆分成普通文本段和按钮附件段,拼接成NSAttributedString。
  3. 封装到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组件的数组,再用容器视图拼接:

  1. 拆分文本:用正则将原文本拆分成多个片段,标记普通文本和按钮(含id与文本内容)。
  2. 动态生成视图:遍历片段数组,普通文本用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的自定义链接实现:

  1. 给
  2. 在视图上添加点击手势,识别点击的文本范围,解析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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 20:27:26