63

How to use AttributedString in SwiftUI. There is no API available to use AttributedString in Text

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
Dattatray Deokar
  • 1,923
  • 2
  • 21
  • 31

11 Answers11

100

iOS 15 and Swift 5.5

Text now supports markdown and also you can create custom attributes:

enter image description here

You can even get defined attributes remotely like:

enter image description here


iOS 13 and 14

You can combine multiple Text objects together with a simple + operator and that will handle some of the attributions:

enter image description here

Each one can have multiple and specific modifiers


A fully supported fallback!

Since it doesn't support directly on Text (till iOS 15), you can bring the UILabel there and modify it in anyway you like:

Implementation:

struct UIKLabel: UIViewRepresentable {

    typealias TheUIView = UILabel
    fileprivate var configuration = { (view: TheUIView) in }

    func makeUIView(context: UIViewRepresentableContext<Self>) -> TheUIView { TheUIView() }
    func updateUIView(_ uiView: TheUIView, context: UIViewRepresentableContext<Self>) {
        configuration(uiView)
    }
}

Usage:

var body: some View {
    UIKLabel {
        $0.attributedText = NSAttributedString(string: "HelloWorld")
    }
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 9
    Is there a way to make one of those Texts clickable? tapGesture doesn't seem suitable as it's returning `some View` and not `Text`? – Last cookie Apr 27 '20 at 13:24
  • +1 for More native but less access. It covers some length in formatting text without adding extra and complicated codes. – Philip Borbon Jun 19 '20 at 07:12
  • 2
    Usage: "Argument passed to call that takes no arguments" – User Sep 22 '20 at 20:25
  • SwiftUI 2.0 has a struct named `Label`. You probably forgot to implement your own `Label` and it conflicted with the original version @Ixx – Mojtaba Hosseini Sep 22 '20 at 21:23
  • The "more native" version is fine for simple things but falls apart for things like pluralization and localized strings. SwiftUI doesn't appear to have any offering for this yet. – aeskreis Oct 30 '20 at 20:30
  • 2
    @MojtabaHosseini - How to enable alignment on `UIKLabel` ? Have tried wrapping on stacks and own View nothing appears to work. – eklektek Mar 25 '21 at 10:11
41

The idea of attributed string is string with attributes. In SwiftUI this idea is realised with Text attributed modifiers and + operator. Like in the below example:

SwiftUI Text with attributes

Group {
    Text("Bold")
        .fontWeight(.bold) +
    Text("Underlined")
        .underline() +
    Text("Color")
        .foregroundColor(Color.red)
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
30

iOS 15

We finally get AttributedString! It's really easy to use.

Multiple Attributed Strings with different attributes

struct ContentView: View {
    var body: some View {
        VStack(spacing: 40) {
            
            /// Note: You can replace `$0` with `string in string`
            
            VStack {
                Text("Regular")
                Text("Italics") { $0.font = Font.system(size: 17).italic() }
                Text("Bold") { $0.font = Font.system(size: 17).bold() }
                Text("Strikethrough") { $0.strikethroughStyle = Text.LineStyle(pattern: .solid, color: .red) }
                Text("Code") { $0.font = Font.system(size: 17, design: .monospaced) }
                Text("Foreground Color") { $0.foregroundColor = Color.purple }
                Text("Background Color") { $0.backgroundColor = Color.yellow }
                Text("Underline") { $0.underlineColor = Color.green }
            }
            
            VStack {
                Text("Kern") { $0.kern = CGFloat(10) }
                Text("Tracking") { $0.tracking = CGFloat(10) }
            }
            
            VStack {
                Text("Baseline Offset") { $0.baselineOffset = CGFloat(10) }
                Text("Link") { $0.link = URL(string: "https://apple.com") }
            }
        }
    }
}

/// extension to make applying AttributedString even easier
extension Text {
    init(_ string: String, configure: ((inout AttributedString) -> Void)) {
        var attributedString = AttributedString(string) /// create an `AttributedString`
        configure(&attributedString) /// configure using the closure
        self.init(attributedString) /// initialize a `Text`
    }
}

To apply attributes to specific ranges, use the range(of:options:locale:) method.

Attributed String with different colors

struct ContentView: View {
    var body: some View {
        Text("Some Attributed String") { string in
            string.foregroundColor = .blue
            if let range = string.range(of: "Attributed") { /// here!
                string[range].foregroundColor = .red
            }
        }
    }
}

See my article for more details. Also, you can use Markdown!

aheze
  • 24,434
  • 8
  • 68
  • 125
  • 1
    Thanks for putting in the work; this was immensely useful. I now can use user-defined custom fonts in my SwiftUI project with `string.font = Font.custom("Hackles", size: 16)` though I wish Apple was providing an easier way of adding custom fonts. – green_knight Apr 21 '22 at 23:29
  • @green_knight You could extend Font to add your your static function to use your custom font, so that you don't have to hardcode just inside that function and use it everywhere else in the code – user1046037 Sep 30 '22 at 15:52
16

There are many answers to this that all use UILabel or UITextView. I was curious if it would be possible to create a native SwiftUI implementation that did not rely on any UIKit functionality. This represents an implementation that fits my current needs. It's FAR from a complete implementation of the NSAttributedString spec, but it's definitely good enough for the most basic needs. The constructor for NSAttributedString that takes an HTML string is a custom category I made, very easy to implement. If someone wants to run with this and create a more robust and complete component, you'd be my hero. Sadly I don't have the time for such a project.

//
//  AttributedText.swift
//
import SwiftUI

struct AttributedTextBlock {
    let content: String
    let font: Font?
    let color: Color?
}

struct AttributedText: View {
    var attributedText: NSAttributedString?
    
    private var descriptions: [AttributedTextBlock] = []
    
    init(_ attributedText: NSAttributedString?) {
        self.attributedText = attributedText
        
        self.extractDescriptions()
    }
    
    init(stringKey: String) {
        self.init(NSAttributedString(htmlString: NSLocalizedString(stringKey, comment: "")))
    }
    
    init(htmlString: String) {
        self.init(NSAttributedString(htmlString: htmlString))
    }
    
    private mutating func extractDescriptions()  {
        if let text = attributedText {
            text.enumerateAttributes(in: NSMakeRange(0, text.length), options: [], using: { (attribute, range, stop) in
                let substring = (text.string as NSString).substring(with: range)
                let font =  (attribute[.font] as? UIFont).map { Font.custom($0.fontName, size: $0.pointSize) }
                let color = (attribute[.foregroundColor] as? UIColor).map { Color($0) }
                descriptions.append(AttributedTextBlock(content: substring,
                                                        font: font,
                                                        color: color))
            })
        }
    }
    
    var body: some View {
        descriptions.map { description in
            Text(description.content)
                .font(description.font)
                .foregroundColor(description.color)
        }.reduce(Text("")) { (result, text) in
            result + text
        }
    }
}

struct AttributedText_Previews: PreviewProvider {
    static var previews: some View {
        AttributedText(htmlString: "Hello! <b>World</b>")
    }
}
aeskreis
  • 1,923
  • 2
  • 17
  • 24
  • 1
    This is excellent work! I added a scrollview and was off to the races! I have persisted data objects that are 40K+ byte NSAttributedStrings and the view opens them up with no delays (they were taking 5 or more seconds to appear before). Would upvote more than once if I could. – Mozahler Nov 12 '20 at 18:28
  • 1
    Nice work! Unfortunately getting logs: AttributeGraph: cycle detected through attribute X when using it in a View, eventually leading to a crash :/ – buddhabath Nov 25 '20 at 17:21
  • Interesting, yeah I'm not sure why, it could be that your attributed string has a use case that the code does not account for. If you're able to find the bug, post a gist of it and I'll update the answer with the bug fix. – aeskreis Nov 29 '20 at 23:44
  • 1
    Does not work in iOS 15, Xcode 13.3.1. Preview simply shows: "Hello! World" – NeoLeon May 05 '22 at 08:32
  • @NeoLeon This class will not automatically turn an HTML string into an attributed string. you will need to write your own extension to do that. BTW this might be obsolete now since I believe iOS 15 introduced AttributedString support to the regular Text object. In addition I believe markdown is now supported which is superior to HTML for simple things like bold/italics. – aeskreis May 10 '22 at 20:51
  • Compiler complained on `self.init(NSAttributedString(htmlString: htmlString))` Changed to `self.init(NSAttributedString(string: htmlString)` – Dan Selig May 13 '22 at 17:38
13

if you want to achieve dynamic height text with NSAttributedString you can use this :

Implementation:

 struct TextWithAttributedString: View {

    var attributedText: NSAttributedString
    @State private var height: CGFloat = .zero

    var body: some View {
        InternalTextView(attributedText: attributedText, dynamicHeight: $height)
            .frame(minHeight: height)
    }

    struct InternalTextView: UIViewRepresentable {

        var attributedText: NSAttributedString
        @Binding var dynamicHeight: CGFloat

        func makeUIView(context: Context) -> UITextView {
            let textView = UITextView()
            textView.textAlignment = .justified
            textView.isScrollEnabled = false
            textView.isUserInteractionEnabled = false
            textView.showsVerticalScrollIndicator = false
            textView.showsHorizontalScrollIndicator = false
            textView.allowsEditingTextAttributes = false
            textView.backgroundColor = .clear
            textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
            textView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
            return textView
        }

        func updateUIView(_ uiView: UITextView, context: Context) {
            uiView.attributedText = attributedText
            DispatchQueue.main.async {
                dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
            }
        }
    }
}

usage:

    VStack {
       TextWithAttributedString(attributedText: viewModel.description)
         .padding([.leading, .trailing], self.horizontalPadding)
         .layoutPriority(1)
         .background(Color.clear)
    }
    .transition(.opacity)
    .animation(.linear)
sid_patel_1211
  • 151
  • 1
  • 7
HAMED POORAMIRI
  • 131
  • 1
  • 2
4

To add only one different style for iOS 14 this worked for me:

struct ItalicTextView: View {
  let text: String
  let italicText: String

  var body: some View {
    let array = text.components(separatedBy: italicText)
    array.reduce(Text(""), {
      if $1 == array.last {
        return $0 + Text($1)
      }
      return $0 + Text($1) + Text(italicText).italic()
    })
  }
}

Usage:

 var body: some View {
 HStack(alignment: .center, spacing: 0) {
    ItalicTextView(text: notification.description, italicText: "example")
      .multilineTextAlignment(.leading)
      .fixedSize(horizontal: false, vertical: true)
      .padding(.vertical, 16)
      .padding(.horizontal, 8)
  }
}

}

Diego Renau
  • 109
  • 3
1
  1. Works for MacOS
  2. Works MUCH FASTER than SwiftUI's Text(someInstanceOf_AttributedString)
  3. Ability to select text WITOUT resetting of font attributes on click or text selection
import SwiftUI
import Cocoa

@available(OSX 11.0, *)
public struct AttributedText: NSViewRepresentable {
    private let text: NSAttributedString
    
    public init(attributedString: NSAttributedString) {
        text = attributedString
    }
    
    public func makeNSView(context: Context) -> NSTextField {
        let textField = NSTextField(labelWithAttributedString: text)
        textField.isSelectable = true
        textField.allowsEditingTextAttributes = true // Fix of clear of styles on click
        
        textField.preferredMaxLayoutWidth = textField.frame.width
        
        return textField
    }
    
    public func updateNSView(_ nsView: NSTextField, context: Context) {
        nsView.attributedStringValue = text
    }
}
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
1

Since iOS 15, Text can have an AttributedString parameter.

No UIViewRepresentable necessary

Since NSAttributedString can be created from HTML, the process is straight forward:

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 {
            Text(html)
        }
    }
}

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

The code renders this:

Rendered HTML example

Gerd Castan
  • 6,275
  • 3
  • 44
  • 89
1

Try this, it works for me.

var body: some View {
    let nsAttributedString = NSAttributedString(string: "How to use Attributed String in SwiftUI \n How to use Attributed String in SwiftUIHow to use Attributed String in SwiftUI", attributes: [.font: UIFont.systemFont(ofSize: 17), .backgroundColor: UIColor.red])
    let attributedString = try! AttributedString(nsAttributedString, including: \.uiKit)
    return Text(attributedString)
        .multilineTextAlignment(.center)
}
Haojen
  • 686
  • 7
  • 9
0

Use UIViewRepresentable to get the UIKit Label

import Foundation
import UIKit
import SwiftUI


struct AttributedLabel: UIViewRepresentable {
    
    var attributedText: NSAttributedString
    
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.numberOfLines = 0
        label.attributedText = attributedText
        label.textAlignment = .left
        
        return label
    }
    
    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.attributedText = attributedText
    }
    
}

To use you just need to do this:

ZStack {
            AttributedLabel(attributedText: text)
    }
Tiago Mendes
  • 4,572
  • 1
  • 29
  • 35
0

Before iOS15, this support one style of markdown (font) text:

struct SingleMarkText: View {
    let text: String
    let mark: String
    let regularFont: Font
    let markFont: Font

    var body: some View {
        let array = text.components(separatedBy: mark)

        Group {
            array.enumerated()
                .reduce(Text("")) {
                    $0 + ($1.0 % 2 == 1 ? Text($1.1).font(markFont) : Text($1.1).font(regularFont))
                }
        }
    }
}

Usage:

SingleMarkText(
   text: "Hello __there__, how __are__ you?",
   mark: "__",
   regularFont: .body,
   markFont: .headline
)
.multilineTextAlignment(.center)
.foregroundColor(.black)
Sam Xu
  • 252
  • 3
  • 5