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

SwiftUI中可点击子串实现问题:@用户标签点击功能优化

SwiftUI实现可点击@用户标签的最优方案

你的现有代码存在两个核心问题:

  1. attributedText函数仅提取了@标签文本,完全丢弃了非标签内容,导致帖子只显示标签;
  2. 使用HStack布局会强制所有文本块横向排列,无法自动处理换行,不符合常规文本排版逻辑。

下面提供两种更优的SwiftUI实现方案,既能保留完整文本,又能实现@标签的点击交互:


方案一:原生SwiftUI + AttributedString(推荐)

利用SwiftUI的AttributedString标记可点击的@标签,结合点击位置检测实现交互,完全贴合原生SwiftUI生态:

import SwiftUI

// 自定义属性,用于标记@用户标签
struct UserTagAttribute: AttributedStringKey {
    static var name: String = "UserTag"
}

extension AttributedString {
    var userTag: Bool? {
        get { self[UserTagAttribute.self] }
        set { self[UserTagAttribute.self] = newValue }
    }
}

struct ClickableTagTextView: View {
    let content: String
    
    var body: some View {
        Text(processedAttributedString)
            .onTapGesture { location in
                handleTagTap(at: location)
            }
            .padding()
    }
    
    // 生成带标记的AttributedString
    private var processedAttributedString: AttributedString {
        var attrString = AttributedString(content)
        let tagRegex = try! NSRegularExpression(pattern: "@\\w+")
        let fullRange = NSRange(content.startIndex..<content.endIndex, in: content)
        
        for match in tagRegex.matches(in: content, range: fullRange) {
            guard let swiftRange = Range(match.range, in: attrString) else { continue }
            // 标记为可点击标签
            attrString[swiftRange].userTag = true
            // 设置标签样式(可选)
            attrString[swiftRange].foregroundColor = .blue
            attrString[swiftRange].underlineStyle = .single
        }
        
        return attrString
    }
    
    // 检测点击位置是否命中@标签
    private func handleTagTap(at location: CGPoint) {
        // 获取底层UITextView(SwiftUI的Text在iOS中会包装为UITextView)
        guard let textView = UIApplication.shared.windows.first?
            .rootViewController?
            .view
            .subviews
            .first(where: { $0 is UITextView }) as? UITextView else {
            return
        }
        
        let layoutManager = textView.layoutManager
        let charIndex = layoutManager.characterIndex(
            for: location,
            in: textView.textContainer,
            fractionOfDistanceBetweenInsertionPoints: nil
        )
        
        guard charIndex < textView.textStorage.length else { return }
        
        // 检查点击位置是否带有UserTag属性
        let attributes = textView.textStorage.attributes(at: charIndex, effectiveRange: nil)
        guard attributes[UserTagAttribute.name] != nil else { return }
        
        // 提取完整的@标签文本
        if let tagRange = textView.text.range(of: "@\\w+", options: .regularExpression, range: text.startIndex..<text.endIndex) {
            let tagText = String(text[tagRange])
            print("点击了标签:\(tagText)")
            // 这里添加跳转用户主页等业务逻辑
        }
    }
}

// 预览
struct ClickableTagTextView_Previews: PreviewProvider {
    static var previews: some View {
        ClickableTagTextView(content: "hello @someUser and @otherUser, have a nice day!")
    }
}

方案二:UIViewRepresentable包装UITextView

如果需要更精细的交互控制(比如多个标签的精准识别),可以用UITextView的代理实现:

import SwiftUI
import UIKit

struct TagTextView: UIViewRepresentable {
    let content: String
    var onTagTap: ((String) -> Void)?
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.isEditable = false
        textView.isSelectable = true
        textView.delegate = context.coordinator
        textView.attributedText = generateAttributedText()
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.attributedText = generateAttributedText()
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // 生成带URL属性的AttributedString(用URL模拟标签点击)
    private func generateAttributedText() -> NSAttributedString {
        let attrString = NSMutableAttributedString(string: content)
        let tagRegex = try! NSRegularExpression(pattern: "@\\w+")
        let fullRange = NSRange(content.startIndex..<content.endIndex, in: content)
        
        for match in tagRegex.matches(in: content, range: fullRange) {
            guard let swiftRange = Range(match.range, in: content) else { continue }
            let tagText = String(content[swiftRange])
            // 给标签添加自定义URL,用于代理识别
            attrString.addAttribute(.link, value: "user://\(tagText)", range: match.range)
            attrString.addAttribute(.foregroundColor, value: UIColor.blue, range: match.range)
            attrString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: match.range)
        }
        
        return attrString
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: TagTextView
        
        init(_ parent: TagTextView) {
            self.parent = parent
        }
        
        func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
            if URL.scheme == "user" {
                let tagText = URL.lastPathComponent
                parent.onTagTap?(tagText)
                print("点击了标签:\(tagText)")
                return false // 阻止系统默认的URL跳转
            }
            return true
        }
    }
}

// 使用示例
struct TagTextView_Previews: PreviewProvider {
    static var previews: some View {
        TagTextView(content: "hello @someUser and @otherUser") { tag in
            print("触发标签点击:\(tag)")
        }
        .padding()
    }
}

方案对比:

  • 方案一更贴近SwiftUI原生写法,无需额外包装UIKit组件,但需要处理底层视图的获取;
  • 方案二利用UITextView的成熟代理机制,标签识别更精准,适合复杂文本场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 08:22:06