我在快速用户界面中有一个复杂的 struct ,可以检测字符串中的链接和标签.该 struct 修改它们的 colored颜色 ,并检测对特定标签和链接的点击.我想添加一个背景 colored颜色 的标签,特别是一些填充.目前,标签是紫色和粗体的.我希望最后的外观是一个紫色背景的白色标签.当我try 将填充和背景修饰符应用于文本元素时,我得到以下错误.

无法将类型为‘Some View’的返回表达式转换为返回类型‘Text’

我将提供完整的代码,以使这个问题可重现.但请注意,相关代码在LinkColoredText以内.这是第一个 struct ,因此您不必滚动所有内容.

问题在于,Body中的这些视图是Components数组中的元素,添加填充或背景 colored颜色 将导致类型不匹配.

import SwiftUI

private let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)

struct LinkColoredText: View {
    @Environment(\.colorScheme) var colorScheme
    enum Component {
        case text(String)
        case link(String, URL)
        case hashtag(String, String)
    }

    let text: String
    let components: [Component]

    init(text: String, links: [NSTextCheckingResult], hashtags: [String]) {
        self.text = text
        let nsText = text as NSString

        var components: [Component] = []
        var index = 0
        for result in links {
            if result.range.location > index {
                
                let sub_str = nsText.substring(with: NSRange(location: index, length: result.range.location - index))
                
                if findHashtags(in: sub_str).isEmpty {
                    components.append(.text(nsText.substring(with: NSRange(location: index, length: result.range.location - index))))
                } else {
                    let words = extractWordsAndSpaces(from: sub_str)
                    var offset = 0
                    for word in words {
                        if !findHashtags(in: String(word)).isEmpty && containsWhiteSpace(at: offset - 1, in: sub_str) {
                            components.append(.hashtag(nsText.substring(with: NSRange(location: index + offset, length: word.count)), String(word)))
                        } else {
                            components.append(.text(nsText.substring(with: NSRange(location: index + offset, length: word.count))))
                        }
                        offset += word.count
                    }
                }
            }
            components.append(.link(nsText.substring(with: result.range), result.url!))
            index = result.range.location + result.range.length
        }
        if index < nsText.length {
            let sub_str = nsText.substring(from: index)
            var offset = 0
            
            if findHashtags(in: sub_str).isEmpty {
                components.append(.text(sub_str))
            } else {
                let words = extractWordsAndSpaces(from: sub_str)

                for word in words {
                    if !findHashtags(in: String(word)).isEmpty && containsWhiteSpace(at: offset - 1, in: sub_str){
                        components.append(.hashtag(nsText.substring(with: NSRange(location: index + offset, length: word.count)), String(word)))
                        
                    } else {
                        components.append(.text(nsText.substring(with: NSRange(location: index + offset, length: word.count))))
                    }
                    offset += word.count
                }
            }
        }
        self.components = components
    }
    
    var body: some View {
        components.map { component in
            switch component {
            case .text(let text):
                return Text(verbatim: text)
            case .link(let text, _):
                return Text(verbatim: text)
                    .foregroundColor(.blue)
            case .hashtag(let text, _):
                return Text(verbatim: text)
                    .foregroundColor(.indigo).bold()
            }
        }.reduce(Text(""), +)
    }
}

struct LinkedText: View {
    let text: String
    let istip: Bool
    let isMessage: Bool?
    let links: [NSTextCheckingResult]
    let hashtags: [String]
    
    init (_ text: String, tip: Bool, isMess: Bool?) {
        self.text = text
        self.istip = tip
        self.isMessage = isMess
        let nsText = text as NSString
        let wholeString = NSRange(location: 0, length: nsText.length)
        links = linkDetector.matches(in: text, options: [], range: wholeString)
        hashtags = findHashtags(in: text)
    }
    
    var body: some View {
        LinkColoredText(text: text, links: links, hashtags: hashtags)
            .overlay(LinkTapOverlay(text: text, isTip: istip, isMessage: isMessage, links: links, hashtags: hashtags))
    }
}

private struct LinkTapOverlay: UIViewRepresentable {
    let text: String
    let isTip: Bool
    let isMessage: Bool?
    let links: [NSTextCheckingResult]
    let hashtags: [String]
    
    func makeUIView(context: Context) -> LinkTapOverlayView {
        let view = LinkTapOverlayView(frame: .zero, text: text, overlay: self)
        view.textContainer = context.coordinator.textContainer

        view.isUserInteractionEnabled = true

        let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.didTapLabel(_:)))
        tapGesture.delegate = context.coordinator
        view.addGestureRecognizer(tapGesture)

        let longPressGesture = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.didLongPressLabel(_:)))
        longPressGesture.delegate = context.coordinator
        view.addGestureRecognizer(longPressGesture)

        return view
    }
    
    func updateUIView(_ uiView: LinkTapOverlayView, context: Context) {
        let attributedString = NSAttributedString(string: text, attributes: [.font: UIFont.preferredFont(forTextStyle: .body)])
        context.coordinator.textStorage = NSTextStorage(attributedString: attributedString)
        context.coordinator.textStorage!.addLayoutManager(context.coordinator.layoutManager)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UIGestureRecognizerDelegate {
        let overlay: LinkTapOverlay

        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: .zero)
        var textStorage: NSTextStorage?
        
        init(_ overlay: LinkTapOverlay) {
            self.overlay = overlay
            
            textContainer.lineFragmentPadding = 0
            textContainer.lineBreakMode = .byWordWrapping
            textContainer.maximumNumberOfLines = 0
            layoutManager.addTextContainer(textContainer)
        }

        @objc func didTapLabel(_ gesture: UITapGestureRecognizer) {
            let location = gesture.location(in: gesture.view!)
            if let result = link(at: location) {
                guard let url = result.url else {
                    return
                }
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            } else if let result = hashtag(at: location) {
                
            }
        }
        
        @objc func didLongPressLabel(_ gesture: UILongPressGestureRecognizer) {
            if gesture.state == .began {

            }
        }

        private func link(at point: CGPoint) -> NSTextCheckingResult? {
            guard !overlay.links.isEmpty else {
                return nil
            }
            let indexOfCharacter = layoutManager.characterIndex(
                for: point,
                in: textContainer,
                fractionOfDistanceBetweenInsertionPoints: nil
            )

            for i in 0..<overlay.links.count {
                if indexOfCharacter >= overlay.links[i].range.location && indexOfCharacter <= (overlay.links[i].range.location + overlay.links[i].range.length) {
                    return overlay.links[i]
                }
            }
            
            return nil
        }
        
        private func hashtag(at point: CGPoint) -> String? {
            guard !overlay.hashtags.isEmpty else {
                return nil
            }
            let indexOfCharacter = layoutManager.characterIndex(
                for: point,
                in: textContainer,
                fractionOfDistanceBetweenInsertionPoints: nil
            )
            let words = extractWordsAndSpaces(from: overlay.text)
            var offset = 0

            for i in 0..<words.count {
                if indexOfCharacter >= offset && indexOfCharacter <= (offset + words[i].count){
                    if !findHashtags(in: words[i]).isEmpty {
                        if i == 0 {
                            return words[i]
                        } else if words[i - 1].hasSuffix(" "){
                            return words[i]
                        }
                    }
                    return nil
                }
                offset += words[i].count
            }
            return nil
        }
    }
}

private class LinkTapOverlayView: UIView {
    private var overlay: LinkTapOverlay?
    var textContainer: NSTextContainer!
    var text: String?
    
    override init(frame: CGRect) {
        super.init(frame: frame)

        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPressLabel(_:)))
        addGestureRecognizer(longPressGesture)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    convenience init(frame: CGRect, text: String, overlay: LinkTapOverlay) {
        self.init(frame: frame)
        self.text = text
        self.overlay = overlay
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()

        var newSize = bounds.size
        newSize.height += 20
        textContainer.size = newSize
    }

    @objc func didLongPressLabel(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .began {

        }
    }
}

func findHashtags(in text: String) -> [String] {
   do {
       let regex = try NSRegularExpression(pattern: "#\\w+", options: [])
       let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
       return matches.map {
           String(text[Range($0.range, in: text)!])
       }
   } catch {
       return []
   }
}

func extractWordsAndSpaces(from sentence: String) -> [String] {
    let pattern = try! NSRegularExpression(pattern: "\\S+|\\s+", options: [])

    let matches = pattern.matches(in: sentence, options: [], range: NSRange(location: 0, length: sentence.utf16.count))

    let resultArray = matches.map { (match) -> String in
        let range = Range(match.range, in: sentence)!
        return String(sentence[range])
    }
    return resultArray
}

func containsWhiteSpace(at index: Int, in text: String) -> Bool {
    if index == -1 {
        return true
    }
    guard index >= 0 && index < text.count else {
        return false
    }

    let charAtIndex = text[text.index(text.startIndex, offsetBy: index)]
    return charAtIndex.isWhitespace
}

推荐答案

您的问题应该是因为您试图将Text个视图与修饰符混合在一起,这些修饰符将它们的返回类型更改为some View,这与预期的Text元素的返回类型不再匹配.

与使用@State字典存储动态计算的标签背景的度量和偏移量(MatBuompy‘S answer)不同,您可以try 直接修改LinkColoredText struct ,以使用ForEach迭代组件并在SwiftUI视图层次 struct 中有条件地应用样式.

这将在SwiftUI的声明性性质的范围内保留样式,确保每个组件根据其类型without来设置样式,这需要在SwiftUI视图更新之外进行额外的状态管理或计算.

struct LinkColoredText: View {
    // Existing properties

    var body: some View {
        HStack(alignment: .top, spacing: 0) {
            ForEach(Array(components.enumerated()), id: \.offset) { index, component in
                Group {
                    switch component {
                    case .text(let text):
                        Text(verbatim: text)
                    case .link(let text, _):
                        Text(verbatim: text)
                            .foregroundColor(.blue)
                    case .hashtag(let text, _):
                        Text(verbatim: text)
                            .bold()
                            .padding(4) // Adjust padding as needed
                            .background(Color.purple) // Background color for hashtag
                            .foregroundColor(.white) // Text color for hashtag
                            .cornerRadius(5) // Optional: Adjust corner radius as needed
                    }
                }
            }
        }.frame(maxWidth: .infinity, alignment: .leading)
    }
}

By wrapping the Text elements inside a Group within a HStack, you can maintain inline display of the text components while applying specific modifications to each component.
That allows you to iterate over the components with their indices, which is necessary for using them in a ForEach loop for SwiftUI to maintain state properly.
The .hashtag case applies padding, background color, and text color directly to the Text view. That should give you the desired look for hashtags with a purple background and white text.

Swift相关问答推荐

按Esc键时执行操作(工作表)

OBJC代码中Swift 演员的伊瓦尔:原子还是非原子?

Swift-Can无法在AudioKit中找出简单的麦克风效果-文件链

是否在核心数据中使用Uint?

插入/删除视图时的Swiftui动态过渡

SwiftUI 中的同一个 ForEach 中是否可以有 2 个数组?

在表单中对齐文本框

解码 JSON 时 Swift Struct Decoder 初始化程序错误

从文字创建数组时的 Swift 宏

如何避免从模块导入函数

关于变量的视图的 SwiftUI 生命周期

Swift 非参与者隔离闭包

如何在 Swift 中返回 Task 中定义的变量

为什么 `map(foo)` 和 `map{ foo($0) }` 返回不同的结果?

在视图中设置所有变量

无法从自定义动态框架(Swift)访问类

仅在 Swift 中创建 Setter

在swift 3中将文件保存在文档目录中?

UIButton 标题左对齐问题 (iOS/Swift)

iOS 视图可见性消失了