Last active
May 5, 2023 05:26
-
-
Save davidsteppenbeck/a51a55605cc93e696b3cf25066626173 to your computer and use it in GitHub Desktop.
Attributed strings in Swift: Add color to strings directly in your Localizable.strings files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ColorText<ID: Hashable>: View, Identifiable, ColorAttributeValueProtocol { | |
// MARK:- Properties | |
/// The customized attributed string to display. | |
private let attributedString: AttributedString | |
var body: some View { | |
Text(attributedString) | |
} | |
// MARK:- Methods | |
/// Provides an attributed string with the specified color attributes. | |
private static func annotate(from source: AttributedString) -> AttributedString { | |
var attrStr = source | |
for run in attrStr.runs { | |
guard let attributeValue = run.color else { continue } | |
attrStr[run.range].foregroundColor = color(for: attributeValue) | |
} | |
return attrStr | |
} | |
// MARK:- Identifiable | |
// Documentation inherited from protocol. | |
let id: ID | |
// MARK:- Initialization | |
init(withAttributedString attrStr: AttributedString, id: ID = UUID()) { | |
self.attributedString = Self.annotate(from: attrStr) | |
self.id = id | |
} | |
init(_ localizedKey: String.LocalizationValue, id: ID = UUID()) { | |
self.attributedString = Self.annotate(from: AttributedString(localized: localizedKey, including: \.color)) | |
self.id = id | |
} | |
} | |
extension AttributeScopes { | |
struct ColorAttributeScope: AttributeScope { | |
let color: ColorAttribute | |
} | |
var color: ColorAttributeScope.Type { | |
ColorAttributeScope.self | |
} | |
} | |
extension AttributeDynamicLookup { | |
subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeScopes.ColorAttributeScope, T>) -> T { | |
self[T.self] | |
} | |
} | |
struct ColorText_Previews: PreviewProvider { | |
static var previews: some View { | |
ColorText(withAttributedString: AttributedString(localized: "LOCALIZED_STRING_KEY", including: \.color)) | |
.padding() | |
.background(Color(.systemBackground)) | |
.previewLayout(.sizeThatFits) | |
ColorText("This is ^[colored](color: 'purple') text!") | |
.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