-
-
Save richardgroves/884190cbb79bedeadddf1eae4332d1e6 to your computer and use it in GitHub Desktop.
This file contains 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
// | |
// TextStyle.swift | |
// | |
// Created by Rob Napier on 12/20/19. | |
// Copyright © 2019 Rob Napier. All rights reserved. | |
// | |
import SwiftUI | |
public struct TextStyle { | |
// This type is opaque because it exposes NSAttributedString details and requires unique keys. | |
// It can be extended, however, by using public static methods. | |
// Properties are internal to be accessed by StyledText | |
internal let key: NSAttributedString.Key | |
internal let apply: (Text) -> Text | |
private init(key: NSAttributedString.Key, apply: @escaping (Text) -> Text) { | |
self.key = key | |
self.apply = apply | |
} | |
} | |
// Public methods for building styles | |
public extension TextStyle { | |
static func foregroundColor(_ color: Color) -> TextStyle { | |
TextStyle(key: .init("TextStyleForegroundColor"), apply: { $0.foregroundColor(color) }) | |
} | |
static func bold() -> TextStyle { | |
TextStyle(key: .init("TextStyleBold"), apply: { $0.bold() }) | |
} | |
} | |
public struct StyledText { | |
// This is a value type. Don't be tempted to use NSMutableAttributedString here unless | |
// you also implement copy-on-write. | |
private var attributedString: NSAttributedString | |
private init(attributedString: NSAttributedString) { | |
self.attributedString = attributedString | |
} | |
public func style<S>(_ style: TextStyle, | |
ranges: (String) -> S) -> StyledText | |
where S: Sequence, S.Element == Range<String.Index> | |
{ | |
// Remember this is a value type. If you want to avoid this copy, | |
// then you need to implement copy-on-write. | |
let newAttributedString = NSMutableAttributedString(attributedString: attributedString) | |
for range in ranges(attributedString.string) { | |
let nsRange = NSRange(range, in: attributedString.string) | |
newAttributedString.addAttribute(style.key, value: style, range: nsRange) | |
} | |
return StyledText(attributedString: newAttributedString) | |
} | |
} | |
public extension StyledText { | |
// A convenience extension to apply to a single range. | |
func style(_ style: TextStyle, | |
range: (String) -> Range<String.Index> = { $0.startIndex..<$0.endIndex }) -> StyledText { | |
self.style(style, ranges: { [range($0)] }) | |
} | |
} | |
extension StyledText { | |
public init(verbatim content: String, styles: [TextStyle] = []) { | |
let attributes = styles.reduce(into: [:]) { result, style in | |
result[style.key] = style | |
} | |
attributedString = NSMutableAttributedString(string: content, attributes: attributes) | |
} | |
} | |
extension StyledText: View { | |
public var body: some View { text() } | |
public func text() -> Text { | |
var text: Text = Text(verbatim: "") | |
attributedString | |
.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), | |
options: []) | |
{ (attributes, range, _) in | |
let string = attributedString.attributedSubstring(from: range).string | |
let modifiers = attributes.values.map { $0 as! TextStyle } | |
text = text + modifiers.reduce(Text(verbatim: string)) { segment, style in | |
style.apply(segment) | |
} | |
} | |
return text | |
} | |
} | |
struct ContentView: View { | |
var body: some View { | |
StyledText(verbatim: "👩👩👦someText1") | |
.style(.highlight(), ranges: { [$0.range(of: "eTex")!, $0.range(of: "1")!] }) | |
.style(.bold()) | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} | |
// An internal convenience extension that could be defined outside this pacakge. | |
// This wouldn't be a general-purpose way to highlight, but shows how a caller could create | |
// their own extensions | |
extension TextStyle { | |
static func highlight() -> TextStyle { .foregroundColor(.red) } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment