I'm curious, is there a way to embed an SFSymbol into an AttributedString?
I would like to be able to include an SFSymbol in a string that has styling applied to it. To illustrate, in the following example, I'd like the "Hello, world!" to be blue, and the globe icon to be red. Using LocalizedStringKey as I have done below lets me embed the SFSymbol in the Text, but I don't have styling control over the parts as I would with an AttributedString.
struct ContentView: View {
// I'd like to use AttributedString instead.
var string: LocalizedStringKey {
"Hello, world! \(Image(systemName: "globe"))"
}
var body: some View {
Text(string)
}
}
If there's a different way (not using AttributedString) of achieving the same result, that would probably be appreciated too!
If you don't need to consider localisation problems like changing the position of the image within the text depending on locale, the simple solution of concatenating Text
s (koen's answer) can be used.
The SwiftUI attribute scope does not include the attachment
attribute or anything similar, so SwiftUI cannot display an AttributedString
with attachments. LocalizedStringKey
probably uses a different system from AttributedStringKey
.
What you can do instead is use UIKit/AppKit to display the string (their attribute scopes support attachment
). For example, here is a UIViewRepresentable
modified from Asperi's answer here.
struct LabelView: View {
let text: AttributedString
@State private var height: CGFloat = .zero
var body: some View {
InternalLabelView(text: text, dynamicHeight: $height)
.frame(minHeight: height)
}
struct InternalLabelView: UIViewRepresentable {
let text: AttributedString
@Binding var dynamicHeight: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.attributedText = NSAttributedString(text)
DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}
This implementation resizes itself to fit the content and also works in a ScrollView
. You may need a slightly different implementation depending on your needs.
Instead of using a dynamicHeight
binding, also consider overriding sizeThatFits
if you are on iOS 16+ (in which case you can put everything in LabelView
and don't need InternalLabelView
).
struct LabelView: UIViewRepresentable {
let text: AttributedString
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return label
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UILabel, context: Context) -> CGSize? {
uiView.sizeThatFits(.init(width: proposal.width ?? .infinity, height: proposal.height ?? .infinity))
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.attributedText = NSAttributedString(text)
}
}
To support localisation, you need some kind of placeholder substring. Here is an example:
struct ContentView: View {
@Environment(\.locale) var locale
var body: some View {
LabelView(text: localizedAttributedStringWithImage("Hello @@@ World", images: ["@@@": "globe"]))
}
// images is a dictionary with placeholders as keys and system image names as values
func localizedAttributedStringWithImage(_ string: String.LocalizationValue, images: [String: String]) -> AttributedString {
var attributedString = AttributedString(localized: string, locale: locale)
// adding some random coloring just as an example
// remember to use UIColors because we are displaying this using UIKit
attributedString.foregroundColor = UIColor.blue
for (placeholder, image) in images {
guard let range = attributedString.range(of: placeholder) else { continue }
var attachment = AttributedString(NSAttributedString(attachment: .init(image: UIImage(systemName: image)!)))
attachment.foregroundColor = UIColor.red
attributedString.replaceSubrange(range, with: attachment)
}
return attributedString
}
}