26

How can I set a SwiftUI Text to display rendered HTML or Markdown?

Something like this:

Text(HtmlRenderedString(fromString: "<b>Hi!</b>"))

or for MD:

Text(MarkdownRenderedString(fromString: "**Bold**"))

Perhaps I need a different View?

Div
  • 1,023
  • 2
  • 8
  • 25

8 Answers8

21

If you don't need to specifically use a Text view. You can create a UIViewRepresentable that shows a WKWebView and simple call 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)
    }
}

In your body simple call this object like this:

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()
    }
}
Tomas
  • 658
  • 9
  • 20
  • My requirement is to display a html data into the along with other text data of a list of items using swiftui. However, whenever I am trying to do the above code, i don't see any view. Could you please let me know what could the reason. – DJ- Jan 13 '20 at 01:12
  • Hi @DJ, It's working on my project, I have updated my answer with a complete SwiftUI File. I mean, you will not see nothing on the "preview screen" but if you press play will work. Let me know if I've answered your question. – Tomas Jan 13 '20 at 13:15
  • 2
    Thanks for your response, it worked for it as well but not within the list . I believe this may be an issue with the sizing within the list.I will try to investigate it further. – DJ- Jan 13 '20 at 22:56
  • @DJ- I tried with UIViewRepresentable attributed multiline text. I am able to get attributed and multiline text Label for setting preferredMaxLayoutWidth from GeometryReader width. but issue with list item sizing text getting overlap on other item. Please add answer if you find the solution, Thanks in Advance. – Rohit Wankhede Jan 15 '20 at 12:34
  • Please try with my other answer. https://stackoverflow.com/a/62281735/1756736 – Tomas Jun 09 '20 at 11:39
  • Perhaps the same as DJ, I have a problem with this in a `ScrollView`. Plus, there's a latency of loading (I'm using a local file). – Chris Prince Dec 13 '20 at 20:43
  • 1
    See changes here. That's fixing for me. https://developer.apple.com/forums/thread/653935 – Chris Prince Dec 13 '20 at 20:56
  • After the implementation of changes by @ChrisPrince , you may use this link of stackoverflow to set the correct font size. https://stackoverflow.com/a/46000849/2641380 – SHS Dec 18 '20 at 06:08
21

iOS 15 (beta)

Text now supports basic 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`***")
        }
    }
}

Result:

Markdown result


However, if you store a String that contains Markdown in a property, it doesn't render. I'm pretty sure this is a bug.

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

Result:

Text does not render Markdown and renders the raw String instead

You can work around this by converting textWithMarkdown to an AttributedString, using 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)")
        }
    }
}

Result:

Markdown rendered

aheze
  • 16,189
  • 4
  • 33
  • 81
  • Great! But it does not work, if you put a string containing markdowns in a variable! Is there a solution or is it just a bug to file? – gundrabur Jun 13 '21 at 12:48
  • 1
    @gundrabur most likely a bug (I remember someone asking about this in the WWDC21 digital lounges). See my edit for a workaround – aheze Jun 13 '21 at 16:23
  • 3
    @aheze Markdown only working for string literals is intended, see [this tweet](https://twitter.com/natpanferova/status/1426082374052286470). – George Aug 30 '21 at 03:29
  • 5
    To work around a stored string not being converted to Markdown, instead of converting to an `AttributedString`, you can simply create a `LocalizedStringKey` from the string value and initialize the `Text` view with that `LocalizedStringKey`. i.e. `Text(LocalizedStringKey(textWithMarkdown))` – RanLearns Sep 07 '21 at 16:36
  • 4
    I solved this by just using `Text(.init(yourTextVariable))`. No need for a `markdownToAttributed` function. See answer: https://stackoverflow.com/a/69898689/7653367 – Jacob Ahlberg Nov 16 '21 at 13:23
10

Since I have found another solution I would like to share it with you.

Create a new View Representable

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) {}
}

And use it later like this:

HTMLText(html: "<h1>Your html string</h1>")
Tomas
  • 658
  • 9
  • 20
  • how to increase font size? – Di_Nerd Jun 16 '20 at 12:45
  • Hi @DiNerd, in the parameter "options:" of the NSAttributedString you should add a new option for the font, like this: NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .font: UIFont.boldSystemFont(ofSize: 36)], documentAttributes: nil) – Tomas Jun 16 '20 at 13:58
  • Who do you use when text is not fitting in one line? I added this lines, but it did not work: label.lineBreakMode = .byWordWrapping, label.numberOfLines = 0 – Ramis Jan 15 '21 at 07:05
  • Hi @Ramis check out this answer i think could help https://stackoverflow.com/a/58474880/129889 – Tomas Jan 16 '21 at 08:08
  • This is great thank you! I found an issue with the width of the label, it was expanding horizontally and not vertically. It turned out it's because the label was inside a ScrollView. The answer here helped fixing this if anyone has the same issue: https://stackoverflow.com/a/62788230/408286 – mota Mar 02 '21 at 17:12
5

Text can just display Strings. You can use a UIViewRepresentable with an UILabel and attributedText.

Probably attributedText text support will come later for SwiftUI.Text.

Div
  • 1,023
  • 2
  • 8
  • 25
Ugo Arangino
  • 2,534
  • 1
  • 15
  • 18
5

You can try to use the package https://github.com/iwasrobbed/Down, generate HTML or MD from you markdown string, then create a custom UILabel subclass and make it available to SwiftUI like in the following example:

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))
    }
}

I have kind of success with that but cannot get the frame of the label subclass right. Maybe I need to use GeometryReader for that.

blackjacx
  • 7,448
  • 5
  • 42
  • 50
  • Could you please give an example about how to use your code? I tried this with no success: TextWithAttributedString(attributedString: DownView(frame: .zero, markdownString: "").accessibilityAttributedValue!) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) – Mauricio Zárate Jan 04 '20 at 08:48
  • Can you please let us know how do we call this ? Can we just say TextWithAttributedString(attributedString:"
    Hello check
    ")
    – DJ- Jan 13 '20 at 22:54
  • 1
    Yes it was intended to call it using `TextWithAttributedString(attributedString:"# Hello SwiftUI")` but in the meantime I switched to another approach wich actually displays something but is also not optimal yet. If I make real progress I'll post a new answer here. – blackjacx Jan 14 '20 at 14:37
  • @blackjacx - I tried with UIViewRepresentable attributed multiline text. I am able to get attributed and multiline text Label. Setting label's preferredMaxLayoutWidth from GeometryReader width. But issue with list item sizing text getting overlap on other item. Please add answer if you find the solution, Thanks in Advance. – Rohit Wankhede Jan 15 '20 at 12:40
  • @blackjacx this doesnt convert the MD nor HTML - just outputs the raw string in the label - what am I missing? – daihovey Sep 27 '20 at 00:52
  • nvm, i just found down.toAttributedString() :) – daihovey Sep 27 '20 at 00:58
3

Some people advise to use WKWebView or UILabel, but these solutions are terribly slow or inconvenient. I couldn't find a native SwiftUI solution, so I implemented my own (AttributedText). It's quite simple and limited in its functionality, but it works quickly and satisfies my needs. You can see all features in the README.md file. Feel free to contribute if the existing functionality is not enough for you.

Code example

AttributedText("This is <b>bold</b> and <i>italic</i> text.")

Result

Example

Iaenhaall
  • 186
  • 7
2

I created a markdown library specifically for SwiftUI:

https://github.com/Lambdo-Labs/MDText

Feel free to contribute!

Andre Carrera
  • 2,406
  • 2
  • 9
  • 15
  • Are you still maintaining this? I may contribute to it, but at the moment it doesn't compile on iOS and there is a pull request that fixes it waiting to be merged. – Brett Apr 16 '20 at 01:40
2

As far as rendering HTML in swiftUI there are a number of solutions, but for rendering it as a generic UILabel via AttributedText, this is what I went with after combining a few other solutions I found.

Here is the UIViewRepresentable which you'll use from your parent swiftUI views:

//Pass in your htmlstring, and the maximum width that you are allowing for the label
//this will, in turn, pass back the size of the newly created label via the binding 'size' variable
//you must use the new size variable frame on an encompassing view of wherever this htmlAttributedLabel now resides (like in an hstack, etc.)
struct htmlAttributedLabel: UIViewRepresentable {
    @Binding var htmlText: String
    var width: CGFloat
    @Binding var size:CGSize
    var lineLimit = 0
    //var textColor = Color(.label)
 
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.lineBreakMode = .byWordWrapping
        label.numberOfLines = lineLimit
        label.preferredMaxLayoutWidth = width
        //label.textColor = textColor.uiColor()
        return label
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
    let htmlData = NSString(string: htmlText).data(using: String.Encoding.unicode.rawValue)
    let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html]
    DispatchQueue.main.async {
        do {
            let attributedString = try NSMutableAttributedString(data: htmlData!, options: options, documentAttributes: nil)
            //add attributedstring attributes here if you want
            uiView.attributedText = attributedString
            size = uiView.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
            print("htmlAttributedLabel size: \(size)")
        } catch {
            print("htmlAttributedLabel unexpected error: \(error).")
        }
    }
}

Now, to use this label effectively, you'll need to provide it a maximum width, which you can get from geometry reader. You'll also need to pass in a CGSize binding so the label can tell the parent view how much space it needs to render. You'll in turn use this size to set an encompassing view height, so that the rest of swiftUI can layout around your html label appropriately:

@State var htmlText = "Hello,<br />I am <b>HTML</b>!"
@State var size:CGSize = .zero
var body: some View {
    HStack {
        GeometryReader { geometry in
                                htmlAttributedLabel(htmlText: $htmlText, width: geometry.size.width, size: $size).frame(width:size.width, height: size.height). //the frame is important to set here, otherwise sometimes it won't render right on repeat loads, depending on how this view is presented
                       }
           }.frame(height: size.height) //most important, otherwise swiftui won't really know how to layout things around your attributed label
}

You can also set line limits, or text color, etc., and obviously you can extend this object to take in whatever UIlabel parameters you'd like to use.

smakus
  • 793
  • 6
  • 10
  • this works great but I was trying to add Font to this and without luck, any suggestions? Thanks. – clopex Nov 02 '21 at 10:26