如何在 SwiftUI 文本中显示 HTML 或 Markdown?

Div*_*Div 18 html markdown text swift swiftui

如何设置 SwiftUIText以显示呈现的 HTML 或 Markdown?

像这样的东西:

Text(HtmlRenderedString(fromString: "<b>Hi!</b>"))
Run Code Online (Sandbox Code Playgroud)

或对于 MD:

Text(MarkdownRenderedString(fromString: "**Bold**"))
Run Code Online (Sandbox Code Playgroud)

也许我需要一个不同的视图?

Tom*_*mas 16

如果您不需要专门使用文本视图。您可以创建一个显示 WKWebView 和简单调用 loadHTMLString() 的 UIViewRepresentable。

import WebKit
import SwiftUI

struct HTMLStringView: UIViewRepresentable {
    let htmlContent: String

    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.loadHTMLString(htmlContent, baseURL: nil)
    }
}
Run Code Online (Sandbox Code Playgroud)

在你的身体中简单地调用这个对象,如下所示:

import SwiftUI

struct Test: View {
    var body: some View {
        VStack {
            Text("Testing HTML Content")
            Spacer()
            HTMLStringView(htmlContent: "<h1>This is HTML String</h1>")
            Spacer()
        }
    }
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 感谢您的回复,它也适用,但不在列表中。我相信这可能是列表中尺寸的问题。我将尝试进一步调查它。 (2认同)

Ger*_*tan 11

iOS 15开始,Text可以有一个AttributedString参数。

没有UIViewRepresentable必要

由于NSAttributedString可以从 HTML 创建,因此该过程很简单:

import SwiftUI

@available(iOS 15, *)
struct TestHTMLText: View {
    var body: some View {
        let html = "<h1>Heading</h1> <p>paragraph.</p>"

        if let nsAttributedString = try? NSAttributedString(data: Data(html.utf8), options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil),
           let attributedString = try? AttributedString(nsAttributedString, including: \.uiKit) {
            Text(attributedString)
        } else {
            // fallback...
            Text(html)
        }
    }
}

@available(iOS 15, *)
struct TestHTMLText_Previews: PreviewProvider {
    static var previews: some View {
        TestHTMLText()
    }
}
Run Code Online (Sandbox Code Playgroud)

代码呈现如下:

渲染的 HTML 示例


ahe*_*eze 10

iOS 15(测试版)

Text 现在支持基本的 Markdown!

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Regular")
            Text("*Italics*")
            Text("**Bold**")
            Text("~Strikethrough~")
            Text("`Code`")
            Text("[Link](https://apple.com)")
            Text("***[They](https://apple.com) ~are~ `combinable`***")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

降价结果


但是,如果您String在属性中存储包含 Markdown 的 ,则它不会呈现。我很确定这是一个错误。

struct ContentView: View {
    @State var textWithMarkdown = "***[They](https://apple.com) ~are~ `combinable`***"
    var body: some View {
        Text(textWithMarkdown)
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

文本不呈现 Markdown 而是呈现原始字符串

您可以通过转换textWithMarkdownAttributedString,使用来解决此问题init(markdown:options:baseURL:)

struct ContentView: View {
    @State var textWithMarkdown = "***[They](https://apple.com) ~are~ `combinable`***"
    var body: some View {
        Text(textWithMarkdown.markdownToAttributed()) /// pass in AttributedString to Text
    }
}

extension String {
    func markdownToAttributed() -> AttributedString {
        do {
            return try AttributedString(markdown: self) /// convert to AttributedString
        } catch {
            return AttributedString("Error parsing markdown: \(error)")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

降价渲染

  • 要解决存储的字符串未转换为 Markdown 的问题,您可以简单地从字符串值创建一个“LocalizedStringKey”,并使用该“LocalizedStringKey”初始化“Text”视图,而不是转换为“AttributedString”。即“文本(LocalizedStringKey(textWithMarkdown))” (12认同)
  • 我通过使用“Text(.init(yourTextVariable))”解决了这个问题。不需要“markdownToAttributed”函数。查看答案:/sf/answers/4892908261/ (6认同)
  • @aheze Markdown 仅适用于字符串文字,请参阅[此推文](https://twitter.com/natpanferova/status/1426082374052286470)。 (3认同)

Tom*_*mas 8

由于我找到了另一个解决方案,我想与您分享。

创建一个新的可表示视图

struct HTMLText: UIViewRepresentable {

   let html: String
    
   func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
        let label = UILabel()
        DispatchQueue.main.async {
            let data = Data(self.html.utf8)
            if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
                label.attributedText = attributedString
            }
        }

        return label
    }
    
    func updateUIView(_ uiView: UILabel, context: Context) {}
}
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它:

HTMLText(html: "<h1>Your html string</h1>")
Run Code Online (Sandbox Code Playgroud)


bla*_*acx 7

您可以尝试使用包https://github.com/iwasrobbed/Down,从 Markdown 字符串生成 HTML 或 MD,然后创建一个自定义 UILabel 子类并使其可用于 SwiftUI,如下例所示:

struct TextWithAttributedString: UIViewRepresentable {

    var attributedString: NSAttributedString

    func makeUIView(context: Context) -> ViewWithLabel {
        let view = ViewWithLabel(frame: .zero)
        return view
    }

    func updateUIView(_ uiView: ViewWithLabel, context: Context) {
        uiView.setString(attributedString)
    }
}

class ViewWithLabel : UIView {
    private var label = UILabel()

    override init(frame: CGRect) {
        super.init(frame:frame)
        self.addSubview(label)
        label.numberOfLines = 0
        label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setString(_ attributedString:NSAttributedString) {
        self.label.attributedText = attributedString
    }

    override var intrinsicContentSize: CGSize {
        label.sizeThatFits(CGSize(width: UIScreen.main.bounds.width - 50, height: 9999))
    }
}
Run Code Online (Sandbox Code Playgroud)

我在这方面取得了一定的成功,但无法正确获得标签子类的框架。也许我需要使用 GeometryReader 来实现这一点。


Iae*_*all 7

有些人建议使用WKWebViewUILabel,但这些解决方案非常慢或不方便。我找不到原生 SwiftUI 解决方案,因此我实现了自己的解决方案(AttributedText)。它非常简单,功能有限,但运行速度很快并且满足了我的需求。您可以在 README.md 文件中看到所有功能。如果现有功能不足以满足您的需要,请随时做出贡献。

代码示例

AttributedText("This is <b>bold</b> and <i>italic</i> text.")
Run Code Online (Sandbox Code Playgroud)

结果

例子


Hun*_*ion 7

Swift 5.7 以来的新功能 - 从“基本”HTML 转换

Swift 5.7 带来了与正则表达式相关的新功能。RegexBuilder除了现有的正则表达式支持之外,还实现了新的支持,这使得推断 HTML 标记中的字符串变得更加容易。

只需很少的工作,我们就可以构建一个从“基本”HTML 代码到 Markdown 的转换器。我所说的“基本”是指:

  • 它们包含换行符、粗体、斜体(无属性)
  • 它们可以包含超链接,这是下面转换器的复杂部分
  • 它们不包含标头、脚本、id 属性...

当然,只要付出更多努力,任何事情都可以实现,但我将坚持使用基本示例。

扩展名String


extension String {
    func htmlToMarkDown() -> String {
        
        var text = self
        
        var loop = true

        // Replace HTML comments, in the format <!-- ... comment ... -->
        // Stop looking for comments when none is found
        while loop {
            
            // Retrieve hyperlink
            let searchComment = Regex {
                Capture {
                    
                    // A comment in HTML starts with:
                    "<!--"
                    
                    ZeroOrMore(.any, .reluctant)
                    
                    // A comment in HTML ends with:
                    "-->"
                }
            }
            if let match = text.firstMatch(of: searchComment) {
                let (_, comment) = match.output
                text = text.replacing(comment, with: "")
            } else {
                loop = false
            }
        }

        // Replace line feeds with nothing, which is how HTML notation is read in the browsers
        var text = self.replacing("\n", with: "")
        
        // Line breaks
        text = text.replacing("<div>", with: "\n")
        text = text.replacing("</div>", with: "")
        text = text.replacing("<p>", with: "\n")
        text = text.replacing("<br>", with: "\n")

        // Text formatting
        text = text.replacing("<strong>", with: "**")
        text = text.replacing("</strong>", with: "**")
        text = text.replacing("<b>", with: "**")
        text = text.replacing("</b>", with: "**")
        text = text.replacing("<em>", with: "*")
        text = text.replacing("</em>", with: "*")
        text = text.replacing("<i>", with: "*")
        text = text.replacing("</i>", with: "*")
        
        // Replace hyperlinks block
        
        loop = true
        
        // Stop looking for hyperlinks when none is found
        while loop {
            
            // Retrieve hyperlink
            let searchHyperlink = Regex {

                // A hyperlink that is embedded in an HTML tag in this format: <a... href="<hyperlink>"....>
                "<a"

                // There could be other attributes between <a... and href=...
                // .reluctant parameter: to stop matching after the first occurrence
                ZeroOrMore(.any)
                
                // We could have href="..., href ="..., href= "..., href = "...
                "href"
                ZeroOrMore(.any)
                "="
                ZeroOrMore(.any)
                "\""
                
                // Here is where the hyperlink (href) is captured
                Capture {
                    ZeroOrMore(.any)
                }
                
                "\""

                // After href="<hyperlink>", there could be a ">" sign or other attributes
                ZeroOrMore(.any)
                ">"
                
                // Here is where the linked text is captured
                Capture {
                    ZeroOrMore(.any, .reluctant)
                }
                One("</a>")
            }
                .repetitionBehavior(.reluctant)
            
            if let match = text.firstMatch(of: searchHyperlink) {
                let (hyperlinkTag, href, content) = match.output
                let markDownLink = "[" + content + "](" + href + ")"
                text = text.replacing(hyperlinkTag, with: markDownLink)
            } else {
                loop = false
            }
        }

        return text
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

HTML 文本:

let html = """
<div>You need to <b>follow <i>this</i> link</b> here: <a href="https://example.org/en">sample site</a></div>
"""
Run Code Online (Sandbox Code Playgroud)

降价转换:

let markdown = html.htmlToMarkDown()
print(markdown)

// Result:
// You need to **follow *this* link** here: [sample site](https://example.org/en)
Run Code Online (Sandbox Code Playgroud)

在 SwiftUI 中:

Text(.init(markdown))
Run Code Online (Sandbox Code Playgroud)

你看到什么了:

在此输入图像描述


Ugo*_*ino 5

Text只能显示Strings。您可以将 aUIViewRepresentableUILabeland一起使用attributedText

可能稍后会为SwiftUI.Text.


Pet*_*ert 5

iOS 15 支持基本 Markdown,但不包括标题或图像。如果您想在文本中包含基本标题和图像,这里有一个答案:

Text("Body of text here with **bold** text") // This will work as expected
Run Code Online (Sandbox Code Playgroud)

但:

let markdownText = "Body of text here with **bold** text".
Text(markdownText) // This will not render the markdown styling
Run Code Online (Sandbox Code Playgroud)

但您可以通过执行以下操作来解决此问题:

Text(.init(markdownText)) // This will work as expected, but you won't see the headings formatted
Run Code Online (Sandbox Code Playgroud)

但是 SwiftUI markdown 不支持标题(#、##、### 等),因此如果您希望"# heading \nBody of text here with **bold** text"所有内容都能正确呈现,减去标题,您仍然会看到“# header”。

因此,一种解决方案是将字符串分成几行,并实现一个ForEach循环来检查标题前缀 (#),删除#, 并创建一个Text()具有适当样式的元素,如下所示:

let lines = blogPost.blogpost.components(separatedBy: .newlines)

VStack(alignment: .leading) {
                    ForEach(lines, id: \.self) { line in
                                    if line.hasPrefix("# ") {
                                        Text(line.dropFirst(2))
                                            .font(.largeTitle)
                                            .fontWeight(.heavy)
                                    } else if line.hasPrefix("## ") {
                                        Text(line.dropFirst(3))
                                            .font(.title)
                                            .fontWeight(.heavy)
                                    } else if line.hasPrefix("### ") {
                                        Text(line.dropFirst(4))
                                            .font(.headline)
                                            .fontWeight(.heavy)
                                    } else {
                                        Text(.init(line))
                                            .font(.body)
                                    }
                                }
}
Run Code Online (Sandbox Code Playgroud)

这将创建一个格式良好的 Markdown 文本,包括标题。

如果我们还想添加图像,首先我们可以在 URL 属性上创建一个扩展:

extension URL {
func isImage() -> Bool {
    let imageExtensions = ["jpg", "jpeg", "png", "gif"]
    return imageExtensions.contains(self.pathExtension.lowercased())
}
}
Run Code Online (Sandbox Code Playgroud)

此方法检查 URL 的路径扩展名是否是常见图像文件扩展名(jpg、jpeg、png 或 gif)之一,如果是,则返回 true。

然后,我们可以像这样改变 ForEach 循环:

let lines = blogPost.blogpost.components(separatedBy: .newlines)
ForEach(lines, id: \.self) { line in
if line.hasPrefix("# ") {
    Text(line.dropFirst(2))
        .font(.largeTitle)
        .fontWeight(.heavy)
} else if line.hasPrefix("## ") {
    Text(line.dropFirst(3))
        .font(.title)
        .fontWeight(.heavy)
} else if line.hasPrefix("### ") {
    Text(line.dropFirst(4))
        .font(.headline)
        .fontWeight(.heavy)
} else if let imageUrl = URL(string: line), imageUrl.isImage() {
    // If the line contains a valid image URL, display the image
    AsyncImage(url: imageUrl) { phase in
        switch phase {
        case .empty:
            ProgressView()
        case .success(let image):
            image
                .resizable()
                .aspectRatio(contentMode: .fit)
        case .failure:
            Text("Failed to load image")
        @unknown default:
            fatalError()
        }
    }
} else {
    Text(line)
        .font(.body)
}
}
Run Code Online (Sandbox Code Playgroud)

在此更新的代码中,我们尝试使用 URL(string: line) 从该行创建 URL 对象,然后在生成的 URL 上调用自定义扩展方法 isImage() 来检查该行是否包含有效的图像 URL。如果它指向一个图像。

如果该行包含有效的图像 URL,我们将使用 AsyncImage 视图从 URL 异步加载图像。AsyncImage 视图自动处理图像的加载和缓存,并在加载图像时提供占位符 ProgressView。加载图像后,我们使用图像视图以及 ressized() 和aspectRatio(contentMode: .fit) 修饰符来显示它,以适当地调整图像大小和缩放图像。如果图像由于某种原因无法加载,我们会显示一条错误消息。