Div*_*Div 18 html markdown text swift swiftui
如何设置 SwiftUIText以显示呈现的 HTML 或 Markdown?
像这样的东西:
Text(HtmlRenderedString(fromString: "<b>Hi!</b>"))
或对于 MD:
Text(MarkdownRenderedString(fromString: "**Bold**"))
也许我需要一个不同的视图?
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)
    }
}
在你的身体中简单地调用这个对象,如下所示:
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()
    }
}
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()
    }
}
代码呈现如下:
ahe*_*eze 10
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`***")
        }
    }
}
结果:
但是,如果您String在属性中存储包含 Markdown 的 ,则它不会呈现。我很确定这是一个错误。
struct ContentView: View {
    @State var textWithMarkdown = "***[They](https://apple.com) ~are~ `combinable`***"
    var body: some View {
        Text(textWithMarkdown)
    }
}
结果:
您可以通过转换textWithMarkdown为AttributedString,使用来解决此问题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)")
        }
    }
}
结果:
由于我找到了另一个解决方案,我想与您分享。
创建一个新的可表示视图
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) {}
}
然后像这样使用它:
HTMLText(html: "<h1>Your html string</h1>")
您可以尝试使用包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))
    }
}
我在这方面取得了一定的成功,但无法正确获得标签子类的框架。也许我需要使用 GeometryReader 来实现这一点。
有些人建议使用WKWebView或UILabel,但这些解决方案非常慢或不方便。我找不到原生 SwiftUI 解决方案,因此我实现了自己的解决方案(AttributedText)。它非常简单,功能有限,但运行速度很快并且满足了我的需求。您可以在 README.md 文件中看到所有功能。如果现有功能不足以满足您的需要,请随时做出贡献。
代码示例
AttributedText("This is <b>bold</b> and <i>italic</i> text.")
结果
Swift 5.7 带来了与正则表达式相关的新功能。RegexBuilder除了现有的正则表达式支持之外,还实现了新的支持,这使得推断 HTML 标记中的字符串变得更加容易。
只需很少的工作,我们就可以构建一个从“基本”HTML 代码到 Markdown 的转换器。我所说的“基本”是指:
当然,只要付出更多努力,任何事情都可以实现,但我将坚持使用基本示例。
扩展名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
    }
}
用法:
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>
"""
降价转换:
let markdown = html.htmlToMarkDown()
print(markdown)
// Result:
// You need to **follow *this* link** here: [sample site](https://example.org/en)
在 SwiftUI 中:
Text(.init(markdown))
你看到什么了:
Text只能显示Strings。您可以将 aUIViewRepresentable与UILabeland一起使用attributedText。
可能稍后会为SwiftUI.Text.
iOS 15 支持基本 Markdown,但不包括标题或图像。如果您想在文本中包含基本标题和图像,这里有一个答案:
Text("Body of text here with **bold** text") // This will work as expected
但:
let markdownText = "Body of text here with **bold** text".
Text(markdownText) // This will not render the markdown styling
但您可以通过执行以下操作来解决此问题:
Text(.init(markdownText)) // This will work as expected, but you won't see the headings formatted
但是 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)
                                    }
                                }
}
这将创建一个格式良好的 Markdown 文本,包括标题。
如果我们还想添加图像,首先我们可以在 URL 属性上创建一个扩展:
extension URL {
func isImage() -> Bool {
    let imageExtensions = ["jpg", "jpeg", "png", "gif"]
    return imageExtensions.contains(self.pathExtension.lowercased())
}
}
此方法检查 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)
}
}
在此更新的代码中,我们尝试使用 URL(string: line) 从该行创建 URL 对象,然后在生成的 URL 上调用自定义扩展方法 isImage() 来检查该行是否包含有效的图像 URL。如果它指向一个图像。
如果该行包含有效的图像 URL,我们将使用 AsyncImage 视图从 URL 异步加载图像。AsyncImage 视图自动处理图像的加载和缓存,并在加载图像时提供占位符 ProgressView。加载图像后,我们使用图像视图以及 ressized() 和aspectRatio(contentMode: .fit) 修饰符来显示它,以适当地调整图像大小和缩放图像。如果图像由于某种原因无法加载,我们会显示一条错误消息。
| 归档时间: | 
 | 
| 查看次数: | 11853 次 | 
| 最近记录: |