How to use AttributedString in SwiftUI. There is no API available to use AttributedString in Text
iOS 15 and Swift 5.5
Text
now supports markdown and also you can create custom attributes:
You can even get defined attributes remotely like:
iOS 13 and 14
You can combine multiple Text
objects together with a simple +
operator and that will handle some of the attributions:
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")
}
}
Label
. You probably forgot to implement your own Label
and it conflicted with the original version @Ixx –
Colwin UIKLabel
? Have tried wrapping on stacks and own View nothing appears to work. –
Obelize 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:
Group {
Text("Bold")
.fontWeight(.bold) +
Text("Underlined")
.underline() +
Text("Color")
.foregroundColor(Color.red)
}
iOS 15
We finally get AttributedString
! It's really easy to use.
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.
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!
string.font = Font.custom("Hackles", size: 16)
though I wish Apple was providing an easier way of adding custom fonts. –
Baresark 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>")
}
}
self.init(NSAttributedString(htmlString: htmlString))
Changed to self.init(NSAttributedString(string: htmlString)
–
Happiness 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)
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)
}
}
}
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:
- Works for MacOS
- Works MUCH FASTER than SwiftUI's
Text(someInstanceOf_AttributedString)
- 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
}
}
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)
}
NSAttributedString
to create an AttributedString
. –
Extravascular 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)
}
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)
© 2022 - 2024 — McMap. All rights reserved.
some View
and notText
? – Watercolor