import UIKit import PlaygroundSupport extension NSAttributedString { enum MarkdownElement { case paragraph, bold } struct MarkdownStylingOptions { var font: UIFont var paragraphStyle: NSParagraphStyle = .default var boldFont: UIFont { let fontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font.fontDescriptor return UIFont(descriptor: fontDescriptor, size: font.pointSize) } } convenience init( markdownString: String, options: MarkdownStylingOptions, applyEffect: ((MarkdownElement, String) -> [NSAttributedString.Key: Any])? = nil ) { let attributedString = NSMutableAttributedString() for paragraph in markdownString.split(separator: "\n\n") { let attributedParagraph = NSMutableAttributedString() // Replace \n with \u2028 to prevent attributed string from picking up single line breaks as paragraphs. let components = paragraph.replacingOccurrences(of: "\n", with: "\u{2028}") .components(separatedBy: "**") for (index, string) in components.enumerated() { var attributes: [NSAttributedString.Key: Any] = [:] if index % 2 == 0 { attributes[.font] = options.font } else { attributes[.font] = options.boldFont attributes.merge(applyEffect?(.bold, string) ?? [:], uniquingKeysWith: { $1 }) } attributedParagraph.append(NSAttributedString(string: string, attributes: attributes)) } attributedParagraph.addAttributes( applyEffect?(.paragraph, attributedParagraph.string) ?? [:], range: NSRange(location: 0, length: attributedParagraph.length) ) // Add single line break to form a paragraph. attributedParagraph.append(NSAttributedString(string: "\n")) attributedString.append(attributedParagraph) } attributedString.addAttribute( .paragraphStyle, value: options.paragraphStyle, range: NSRange(location: 0, length: attributedString.length) ) self.init(attributedString: attributedString) } } var paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle paragraphStyle.paragraphSpacing = 20 let attributedString = NSAttributedString( markdownString: """ Paragraph **A** goes here. Paragraph **B** goes here. Paragraph **C** goes here and **that's it**. """, options: .init( font: .systemFont(ofSize: 17), paragraphStyle: paragraphStyle ), applyEffect: { element, string in print("Apply style to \(element) with source string: \(string)") return [:] } ) let textLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) textLabel.numberOfLines = 0 textLabel.attributedText = attributedString PlaygroundPage.current.liveView = textLabel