Skip to content

Instantly share code, notes, and snippets.

@davidsteppenbeck
Last active May 5, 2023 05:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidsteppenbeck/2c63e9cbc77c82aab8be90c547d4e503 to your computer and use it in GitHub Desktop.
Save davidsteppenbeck/2c63e9cbc77c82aab8be90c547d4e503 to your computer and use it in GitHub Desktop.
Attributed strings in Swift: Add colored SFSymbols to strings directly in your Localizable.strings files.
import SwiftUI
enum ColorAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
enum Value: String, Codable, Hashable {
case red
case orange
case yellow
case green
case mint
case teal
case cyan
case blue
case indigo
case purple
case pink
case brown
case white
case gray
case black
case primary
case secondary
case accent
case clear
}
/// This is the name that should be used to indicate the color in the string.
/// For example, "^[Hello](color: 'blue'), World!".
static let name = "color"
}
protocol ColorAttributeValueProtocol {
/// Provides a foreground color to apply to the applicable text.
static func color(for value: ColorAttribute.Value) -> Color
}
extension ColorAttributeValueProtocol {
static func color(for value: ColorAttribute.Value) -> Color {
switch value {
case .red:
return .red
case .orange:
return .orange
case .yellow:
return .yellow
case .green:
return .green
case .mint:
return .mint
case .teal:
return .teal
case .cyan:
return .cyan
case .blue:
return .blue
case .indigo:
return .indigo
case .purple:
return .purple
case .pink:
return .pink
case .brown:
return .brown
case .white:
return .white
case .gray:
return .gray
case .black:
return .black
case .primary:
return .primary
case .secondary:
return .secondary
case .accent:
return .accentColor
case .clear:
return .clear
}
}
}
struct SymbolText<ID: Hashable>: View, Identifiable, ColorAttributeValueProtocol {
// MARK:- Properties
/// The customized text to display.
private let text: Text
var body: some View { text }
// MARK:- Methods
/// Provides an attributed string with the specified color attributes.
private static func annotate(from source: AttributedString) -> Text {
source.runs.reduce(Text("")) { text, run in
let attrSubstr = source[run.range]
let attrStr = AttributedString(attrSubstr)
if let attributeValue = run.symbol {
let str = attrStr.string()
let color = color(for: attributeValue)
return text + Text(Image(systemName: str)).foregroundColor(color)
} else {
return text + Text(attrStr)
}
}
}
// MARK:- Identifiable
// Documentation inherited from protocol.
let id: ID
// MARK:- Initialization
init(withAttributedString attrStr: AttributedString, id: ID = UUID()) {
self.text = Self.annotate(from: attrStr)
self.id = id
}
init(_ localizedKey: String.LocalizationValue, id: ID = UUID()) {
self.text = Self.annotate(from: AttributedString(localized: localizedKey, including: \.symbol))
self.id = id
}
}
extension AttributeScopes {
struct SymbolAttributeScope: AttributeScope {
let symbol: ColorAttribute
}
var symbol: SymbolAttributeScope.Type {
SymbolAttributeScope.self
}
}
extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeScopes.SymbolAttributeScope, T>) -> T {
self[T.self]
}
}
extension AttributedString {
/// The character contents of the attributed string as a string.
func string() -> String {
NSAttributedString(self).string
}
}
struct SymbolText_Previews: PreviewProvider {
static var previews: some View {
SymbolText(withAttributedString: AttributedString(localized: "LOCALIZED_STRING_KEY", including: \.symbol))
.symbolVariant(.fill)
.padding()
.background(Color(.systemBackground))
.previewLayout(.sizeThatFits)
SymbolText("Hello ^[suit.club](color: 'yellow') World!")
.symbolVariant(.fill)
.font(.title)
.fontWeight(.heavy)
.previewLayout(.sizeThatFits)
.padding()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment