我该如何设置SwiftUI的Text
以显示渲染后的HTML或Markdown?
类似这样:
Text(HtmlRenderedString(fromString: "<b>Hi!</b>"))
或者对于MD:Text(MarkdownRenderedString(fromString: "**Bold**"))
也许我需要一个不同的视角?
现在支持基本的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`***")
}
}
}
结果:
更新: 如果您将Markdown存储为String
,它不会呈现 - 相反,请将类型设置为LocalizedStringKey
。
struct ContentView: View {
@State var textWithMarkdown: LocalizedStringKey = "***[They](https://apple.com) ~are~ `combinable`***"
var body: some View {
Text(textWithMarkdown)
}
}
结果:
如果您不需要特别使用Text视图,您可以创建一个UIViewRepresentable来显示WKWebView并简单地调用loadHTMLString()。
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()
}
}
ScrollView
中遇到了问题。而且还存在加载延迟(我使用的是本地文件)。 - Chris Prince我找到了另一个解决方案,我想与您分享。
创建一个新的视图表示器
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>")
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()
}
}
Text
。例如.font
。 - meowmeowmeowstruct 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 来解决。
TextWithAttributedString(attributedString:"# Hello SwiftUI")
来调用它,但与此同时,我转而采用了另一种方法,虽然可以显示一些内容,但仍不够理想。如果我有真正的进展,我会在这里发布新答案。 - blackjacx有些人建议使用WKWebView或UILabel,但这些解决方案非常缓慢或不方便。我找不到原生的SwiftUI解决方案,所以我实现了自己的(AttributedText)。它相当简单且功能有限,但它可以快速工作并满足我的需求。您可以在README.md文件中查看所有功能。如果现有功能不足,请随时进行贡献。
代码示例
AttributedText("This is <b>bold</b> and <i>italic</i> text.")
结果
Text
可以仅显示 String
。
您可以使用 UIViewRepresentable
与 UILabel
和 attributedText
。
可能会在以后为 SwiftUI.Text
添加 attributedText 支持。
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 不支持标题(#、##、###等),因此,如果您想要 "# 标题 \n这里是正文,带有 **粗体** 文本"
,除了标题以外,一切都会正确呈现,你仍然会看到 "# 标题"。
因此,一个解决方案是将字符串分成几行,并实现一个 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)
}
}
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>
"""
Markdown 转换:
let markdown = html.htmlToMarkDown()
print(markdown)
// Result:
// You need to **follow *this* link** here: [sample site](https://example.org/en)
Text(.init(markdown))
你所看到的:
Text(LocalizedStringKey(textWithMarkdown))
。 - RanLearnsText(.init(yourTextVariable))
解决了这个问题。不需要markdownToAttributed
函数。请参考答案:https://dev59.com/L1MH5IYBdhLWcg3w4E7k#69898689 - Jacob Ahlberg