SwiftUI Text - 如何在字符串中创建超链接并添加下划线

4
在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" 是一个可点击的超链接,并且有下划线。

所要实现的效果示例



关于我要实现的更新

我创建了以下代码片段以展示我想要实现的效果,因为如上所述,"messageContent" 字符串不一定相同。虽然以下代码不能处理所有情况并处理错误等内容,但这更好地展示了我尝试实现的内容。唯一的问题是这似乎无法正常工作。

它将为超链接添加下划线,但是文本不以 Markdown 格式显示 - 请参见附加的图像。

结果

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
    
    
}
4个回答

6
在Swift 5.5 (iOS 15+)中,您可以使用Markdown:
 Text("This is a link [Google](https://google.com.com)")

为链接添加下划线,您可以像这样进行操作:

只需将下划线添加到链接即可:

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

1
感谢@Guillermo Jiménez。这有些帮助-请参见我问题的上面更新。如上所述,messageContent不是固定的,它会改变,因此我需要更具动态性的东西。 - Vaz
很高兴我能帮助解决你的最后一个问题,只需像这样初始化字符串 Text(.init(input.content))。 - Guillermo Jiménez
感谢您的小而强大的贡献!我已经发布了我需要的解决方案的代码。 - Vaz

3
这是我使用的最终解决方案。适用于各种字符串输入。
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
    
}

0

我为这个问题提供了一个非常简单的解决方案。 您可以使用此引用enter link description here创建任何视图,只需提供您想要显示的文本,该文本可以包含任何URL、电子邮件地址。


我也尝试过同样的事情。我可以显示超链接,但是不知道为什么无法单独给超链接添加下划线。你有什么办法可以解决这个问题吗? - Bhanuteja

0
这对我来说是有效的,链接文本有自定义颜色;
HStack {
    Text("No login yet, create one first at ") + Text("[7redbits.com](https://www.7redbits.com)")
    .underline()
}
.font(.system(.body))
.foregroundColor(.black)
.tint(.orange)
.frame(maxWidth: .infinity, alignment: .leading)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接