SwiftUI 文本 - 如何创建超链接并在字符串中为网络链接下划线

SwiftUI Text - how can I create a hyperlink and underline a weblink in a string

在 SwiftUI 中,如果我在字符串之间有一个网络链接,我该如何创建一个超链接并在我的视图中为其添加下划线。

请注意,“messageContent”字符串不会始终相同。

例如

struct MessageModel {
    var messageContent: String = "Test of hyperlink www.google.co.uk within a text message"
}


struct Content: View {
    
    var message: MessageModel
        
    var body: some View {
        
        VStack {
            Text(message.messageContent)
        }
        
    }
}

这张图片显示了我想要实现的目标。 “www.google.co.uk”是一个可点击的超链接并带有下划线

Example of what I want to achieve



更新我正在努力实现的目标

我创建了下面的一段测试代码来展示我想要实现的目标,因为如上所述,“messageContent”并不总是相同的字符串。

虽然下面的内容并不能完美地处理所有情况和处理错误等,但希望这能让我更好地理解我想要实现的目标。唯一的麻烦是这似乎不起作用。

它为超链接生成下划线,但文本不以 Markdown 格式显示 - 见附图。

Result

import SwiftUI

struct HyperlinkAndUnderlineText: View {
    
    var message: MessagesModel = MessagesModel(messageContent: "Test of hyperlink www.google.co.uk within a text message")
    
    @State var messageContentAfterSplitting: [SplitMessage] = []
    
    var body: some View {
            
            CustomText(inputText: messageContentAfterSplitting)
        
        .onAppear() {
            messageContentAfterSplitting = splitMessage(message: message)
        }
    }
}





struct MessagesModel {
    var messageContent: String = ""
}


struct SplitMessage {
    var content: String = ""
    var type: contentType = .text
}

enum contentType {
    case text
    case url
}





func splitMessage(message: MessagesModel) -> [SplitMessage] {
    
    func detectIfMessageContainsUrl(message: String) -> [String]? {
        
        let urlDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
        let matches = urlDetector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
        
        var urls: [String] = []
        
        for (index, match) in matches.enumerated() {
            guard let range = Range(match.range, in: message) else { continue }
            let url = message[range]
            
            urls.append(String(url))
            
            if index == matches.count - 1 {
                return urls
            }
        }
        return []
        
    }
    
    let urlsFoundInMessage = detectIfMessageContainsUrl(message: message.messageContent)
    
    
    
    
    func getComponents(urlsFoundInMessage: [String]) -> [String] {
        
        var componentsEitherSideOfUrl: [String] = []
        
        for (index,url) in urlsFoundInMessage.enumerated() {
            componentsEitherSideOfUrl = message.messageContent.components(separatedBy: url)
            
            if index == urlsFoundInMessage.count - 1 {
                return componentsEitherSideOfUrl
            }
        }
        
        return []
    }
    
    let componentsEitherSideOfUrl = getComponents(urlsFoundInMessage: urlsFoundInMessage!)
    
    
    
    
    func markdown(urlsFoundInMessage: [String]) -> [String] {
        
        var markdownUrlsArray: [String] = []
        
        for (index, url) in urlsFoundInMessage.enumerated() {
            
            let placeholderText = "[\(url)]"
            
            var url2: String
            if url.hasPrefix("https://www.") {
                url2 = "(\(url.replacingOccurrences(of: "https://www.", with: "https://")))"
            } else if url.hasPrefix("www.") {
                url2 = "(\(url.replacingOccurrences(of: "www.", with: "https://")))"
            } else {
                url2 = "(\(url))"
            }
            
            let markdownUrl = placeholderText + url2
            
            markdownUrlsArray.append(markdownUrl)
            
            if index == urlsFoundInMessage.count - 1 {
                return markdownUrlsArray
            }
        }
        
        return []
        
    }
    
    let markdownUrls = markdown(urlsFoundInMessage: urlsFoundInMessage!)
    
    
    
    
    func recombineStrings(componentsEitherSideOfUrl: [String], markdownUrls: [String]) -> [SplitMessage] {
        
        var text = SplitMessage()
        var textAsArray: [SplitMessage] = []
        
        
        for i in 0...2 {
            if i.isMultiple(of: 2) {
                if i == 0 {
                    text.content = componentsEitherSideOfUrl[i]
                    text.type = .text
                    textAsArray.append(text)
                } else {
                    text.content = componentsEitherSideOfUrl[i-1]
                    text.type = .text
                    textAsArray.append(text)
                }
            } else {
                text.content = markdownUrls[i-1]
                text.type = .url
                textAsArray.append(text)
            }
        }
        
        return textAsArray
    }
    
    
    let recombinedStringArray = recombineStrings(componentsEitherSideOfUrl: componentsEitherSideOfUrl, markdownUrls: markdownUrls)
    
    return recombinedStringArray
    
}




func CustomText(inputText: [SplitMessage]) -> Text {
    
    var output = Text("")
    
    for input in inputText {
                
        let text: Text
        
        text = Text(input.content)
            .underline(input.type == .url ? true : false, color: .blue)
        
        output = output + text
        
    }
    
    return output
    
    
}

在Swift 5.5 (iOS 15+)

你可以使用降价:

 Text("This is a link [Google](https://google.com.com)")

要仅在 link 上添加下划线,您可以这样做:

  Text("This is a link ") + Text("[google.com](https://google.com.com)").underline()

这是我使用的最终解决方案。应该适用于各种字符串输入。

import SwiftUI


struct HyperlinkAndUnderlineTextView: View {
    
    var body: some View {
        
        ScrollView {
            
            VStack (alignment: .leading, spacing: 30) {
                
                Group {
                    CustomTextWithHyperlinkAndUnderline("Test of a hyperlink www.google.co.uk within a text message", .blue)
                    CustomTextWithHyperlinkAndUnderline("www.google.co.uk hyperlink at the start of a text message", .blue)
                    CustomTextWithHyperlinkAndUnderline("Test of hyperlink at the end of a text message www.google.co.uk", .blue)
                    CustomTextWithHyperlinkAndUnderline("www.google.co.uk", .blue)
                    CustomTextWithHyperlinkAndUnderline("This is 1 hyperlink www.google.co.uk.  This is a 2nd hyperlink www.apple.com", .blue)
                    CustomTextWithHyperlinkAndUnderline("This is 1 hyperlink www.google.co.uk.  This is a 2nd hyperlink www.apple.com.  This is text after it.", .blue)
                    CustomTextWithHyperlinkAndUnderline("This is 1 hyperlink www.google.co.uk.  This is a 2nd hyperlink www.apple.com.  This is a 3rd hyperlink www.microsoft.com",  .blue)
                    CustomTextWithHyperlinkAndUnderline("This is 1 hyperlink www.google.co.uk.  This is a 2nd hyperlink www.apple.com.  This is a 3rd hyperlink www.microsoft.com.  This is text after it.", .blue)
                    CustomTextWithHyperlinkAndUnderline("www.google.co.uk is a hyperlink at the start of a text message.  www.apple.com is the 2nd hyperlink within the same text message.", .blue)
                    CustomTextWithHyperlinkAndUnderline("This is a test of another type of url which will get processed google.co.uk", .blue)
                }
                
                Group {
                    CustomTextWithHyperlinkAndUnderline("google.co.uk", .blue)
                    CustomTextWithHyperlinkAndUnderline("Pure text with no hyperlink", .blue)
                    CustomTextWithHyperlinkAndUnderline("Emoji test ", .blue)
                }
            }
        }
    }
}



struct SplitMessageContentWithType {
    var content: String = ""
    var type: contentType = .text
}

enum contentType {
    case text
    case url
}



//Function to produce a text view where all urls and clickable and underlined
func CustomTextWithHyperlinkAndUnderline(_ inputString: String, _ underlineColor: Color) -> Text {
    
    let inputText: [SplitMessageContentWithType] = splitMessage(inputString)
    

    var output = Text("")
    
    for input in inputText {
        let text: Text
        
        text = Text(.init(input.content))
            .underline(input.type == .url ? true : false, color: underlineColor)
        
        output = output + text
    }
    
    return output
    
}



func splitMessage(_ inputString: String) -> [SplitMessageContentWithType] {
    
    
    //1) Function to detect if the input string contains any urls and returns the ones found as an array of strings
    func detectIfInputStringContainsUrl(inputString: String) -> [String] {
        
        let urlDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
        let matches = urlDetector.matches(in: inputString, options: [], range: NSRange(location: 0, length: inputString.utf16.count))
        
        var urls: [String] = []
        
        for match in matches {
            guard let range = Range(match.range, in: inputString) else { continue }
            let url = inputString[range]
            
            urls.append(String(url))
        }
        return urls
        
    }
    let urlsFoundInInputString = detectIfInputStringContainsUrl(inputString: inputString)
    print("\n \nurlsFoundInInputString are: \(urlsFoundInInputString)")
    
    
    
    //2) Function to get the string components either side of a url from the inputString.  Returns these components as an array of strings
    func getStringComponentsSurroundingUrls(urlsFoundInInputString: [String]) -> [String] {
        
        var stringComponentsSurroundingUrls: [String] = []
        
        for (index, url) in urlsFoundInInputString.enumerated() {
            
            let splitInputString = inputString.components(separatedBy: url)
            
            //This code handles the case of an input string with 2 hyperlinks inside it (e.g. This is 1 hyperlink www.google.co.uk.  This is a 2nd hyperlink www.apple.com.  This is text after it.)
            //In the 1st pass of the for loop, this will return splitInputString = ["This is 1 hyperlink ", ".  This is a 2nd hyperlink www.apple.com.  This is text after it."]
            //Because the last element in the array contains either "www" or "http", we only append the contents of the first (prefix(1)) to stringComponentsSurroundingUrls (i.e "This is 1 hyperlink ")
            //In the 2nd pass of the for loop, this will return splitInputString = ["This is 1 hyperlink www.google.co.uk.  This is a 2nd hyperlink ", ".  This is text after it."]
            //Beacuse the last element in the array does not contain a hyperlink, we append both elements to stringComponentsSurroundingUrls
            if splitInputString.last!.contains("www") || splitInputString.last!.contains("http") {
                stringComponentsSurroundingUrls.append(contentsOf: inputString.components(separatedBy: url).prefix(1))
            } else {
                stringComponentsSurroundingUrls.append(contentsOf: inputString.components(separatedBy: url))
            }
            
            
            //At this point in the code, in the above example, stringComponentsSurroundingUrls = ["This is 1 hyperlink ",
            //                                                                                    "This is 1 hyperlink www.google.co.uk.  This is a 2nd hyperlink ",
            //                                                                                    ".  This is text after it."]
            //We now iterate through this array of string, to complete another split.  This time we separate out by any elements by urlsFoundInInputString[index-1]
            //At the end of this for loop, stringComponentsSurroundingUrls = ["This is 1 hyperlink ",
            //                                                                ".  This is a 2nd hyperlink ",
            //                                                                ".  This is text after it."]
            if index == urlsFoundInInputString.count - 1 {
                for (index, stringComponent) in stringComponentsSurroundingUrls.enumerated() {
                    if index != 0 {
                        let stringComponentFurtherSeparated = stringComponent.components(separatedBy: urlsFoundInInputString[index-1])
                        stringComponentsSurroundingUrls.remove(at: index)
                        stringComponentsSurroundingUrls.insert(stringComponentFurtherSeparated.last!, at: index)
                    }
                }
            }
        }
        
        return stringComponentsSurroundingUrls
    }
    
    var stringComponentsSurroundingUrls: [String]
    //If there no no urls found in the inputString, simply set stringComponentsSurroundingUrls equal to the input string as an array, else call the function to find the string comoponents surrounding the Urls found
    if urlsFoundInInputString == [] {
        stringComponentsSurroundingUrls = [inputString]
    } else {
        stringComponentsSurroundingUrls = getStringComponentsSurroundingUrls(urlsFoundInInputString: urlsFoundInInputString)
    }
    print("\n \nstringComponentsSurroundingUrls are: \(stringComponentsSurroundingUrls)")
    
    
    
    
    //3)Function to markdown the urls found to follow a format of [placeholderText](hyperlink) such as [Google](https://google.com) so SwiftUI markdown can render it as a hyperlink
    func markdown(urlsFoundInInputString: [String]) -> [String] {
        
        var markdownUrlsArray: [String] = []
        
        for url in urlsFoundInInputString {
            let placeholderText = "[\(url)]"
            
            var hyperlink: String
            if url.hasPrefix("https://www.") {
                hyperlink = "(\(url.replacingOccurrences(of: "https://www.", with: "https://")))"
            } else if url.hasPrefix("www.") {
                hyperlink = "(\(url.replacingOccurrences(of: "www.", with: "https://")))"
            } else {
                hyperlink = "(http://\(url))"
            }
            
            let markdownUrl = placeholderText + hyperlink
            
            markdownUrlsArray.append(markdownUrl)
        }
        
        return markdownUrlsArray
        
    }
    
    let markdownUrls = markdown(urlsFoundInInputString: urlsFoundInInputString)
    print("\n \nmarkdownUrls is: \(markdownUrls)")
    
    
    
    //4) Function to combine stringComponentsSurroundingUrls and markdownUrls back together
    func recombineStringComponentsAndMarkdownUrls(stringComponentsSurroundingUrls: [String], markdownUrls: [String]) -> [SplitMessageContentWithType] {
        
        var text = SplitMessageContentWithType()
        var text2 = SplitMessageContentWithType()
        var splitMessageContentWithTypeAsArray: [SplitMessageContentWithType] = []
        
        //Saves each string component and url as either .text or .url type so in the CustomTextWithHyperlinkAndUnderline() function, we can underline all .url types
        for (index, stringComponents) in stringComponentsSurroundingUrls.enumerated() {
            text.content = stringComponents
            text.type = .text
            splitMessageContentWithTypeAsArray.append(text)
            
            if index <= (markdownUrls.count - 1) {
                text2.content = markdownUrls[index]
                text2.type = .url
                splitMessageContentWithTypeAsArray.append(text2)
            }
        }
        
        return splitMessageContentWithTypeAsArray
    }
    
    
    let recombineStringComponentsAndMarkdownUrls = recombineStringComponentsAndMarkdownUrls(stringComponentsSurroundingUrls: stringComponentsSurroundingUrls, markdownUrls: markdownUrls)
    print("\n \nrecombineStringComponentsAndMarkdownUrls is: \(recombineStringComponentsAndMarkdownUrls)")
    
    return recombineStringComponentsAndMarkdownUrls
    
}