SwiftUI: is there exist modifier to highlight substring of Text() view?
Once you create a Text, you cannot open it back up. Your example creates localization problems. someText1
is not actually the string to print. It's the localization key of the string. The default localized string just happens to be the key, so it works. Your attempt to search for eTex
would quietly break when you localize. So this wouldn't be a good general-purpose interface.
Even so, building a solution is very enlightening, and may be useful for specific cases.
The fundamental goal is to think of styles as attributes that are applied to ranges. That's exactly what NSAttributedString gives us, including the ability to merge and split ranges in order to manage multiple, overlapping attributes. NSAttributedString is not particularly Swift-friendly, so there could be some value in reimplementing it from scratch, but instead, I'm just going to hide it as an implementation detail.
So a TextStyle is going to be an NSAttributedString.Key and a function that translates a Text into another Text.
public struct TextStyle {
// This type is opaque because it exposes NSAttributedString details and
// requires unique keys. It can be extended by 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
}
}
TextStyle is opaque. To construct it, we expose some extensions, for example:
// 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() })
}
}
It's noteworthy here that NSAttributedString is just "an string annotated by attributes over ranges." It's not "a styled string." We can make up any attribute keys and values we want. So these are intentionally not the same attributes that Cocoa uses for formatting.
Next, we create the StyledText itself. I'm focusing first on the "model" part of this type (later we'll make it a View).
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).compactMap({ $0 }) {
let nsRange = NSRange(range, in: attributedString.string)
newAttributedString.addAttribute(style.key, value: style, range: nsRange)
}
return StyledText(attributedString: newAttributedString)
}
}
It's just a wrapper around an NSAttributedString and a way to create new StyledTexts by applying TextStyles to ranges. Some important points:
Calling
style
does not mutate the existing object. If it did, you couldn't do things likereturn StyledText("text").apply(.bold())
. You'd get an error that the value is immutable.Ranges are tricky things. NSAttributedString uses NSRange, and has a different concept of index than String. NSAttributedStrings can be a different length than the underlying String because they compose characters differently.
You can't safely take a
String.Index
from one String and apply it to another String, even if the two Strings seem identical. That's why this system takes a closure for creating ranges rather than taking a range itself.attributedString.string
is not exactly the same string as the one that was passed in. If the caller wanted to passRange<String.Index>
, it would be critical that they construct it with precisely the same string that TextStyle uses. This is easiest to ensure by using a closure and avoids a lot of corner cases.
The default style
interface handles a sequence of ranges for flexibility. But in most cases you'll probably pass just one range, so it's nice to have a convenience method for that, and for the case where you want the whole string:
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)] })
}
}
Now, the public interface for creating a StyledText:
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)
}
}
Note the verbatim
here. This StyledText doesn't support localization. It's conceivable that with work it could, but a lot more thought would have to go into that.
And finally, after all that, we can make it a View, by creating a Text for each substring that has the same attributes, applying all the styles to that Text, and then combining all the Texts into one using +
. For convenience, the Text is directly exposed so you can combine it with standard views.
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
}
}
And that's it. Using it looks like this:
// 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) }
}
struct ContentView: View {
var body: some View {
StyledText(verbatim: "someText1")
.style(.highlight(), ranges: { [$0.range(of: "eTex"), $0.range(of: "1")] })
.style(.bold())
}
}
Gist
You could also just wrap a UILabel in a UIViewRepresentable, and use attributedText
. But that would be cheating. :D
Highlight a specific part of the text in SwiftUI
iOS 13, Swift 5. There is a generic solution described in this medium article. Using it you can highlight any text anywhere with the only catch being it cannot be more then 64 characters in length, since it using bitwise masks.
https://medium.com/@marklucking/an-interesting-challenge-with-swiftui-9ebb26e77376
This is the basic code in the article.
ForEach((0 ..< letter.count), id: \.self) { column in
Text(letter[column])
.foregroundColor(colorCode(gate: Int(self.gate), no: column) ? Color.black: Color.red)
.font(Fonts.futuraCondensedMedium(size: fontSize))
}
And this one to mask the text...
func colorCode(gate:Int, no:Int) -> Bool {
let bgr = String(gate, radix:2).pad(with: "0", toLength: 16)
let bcr = String(no, radix:2).pad(with: "0", toLength: 16)
let binaryColumn = 1 << no - 1
let value = UInt64(gate) & UInt64(binaryColumn)
let vr = String(value, radix:2).pad(with: "0", toLength: 16)
print("bg ",bgr," bc ",bcr,vr)
return value > 0 ? true:false
}
Highlight a specific part of the text in SwiftUI
iOS 13, Swift 5. There is a generic solution described in this medium article. Using it you can highlight any text anywhere with the only catch being it cannot be more then 64 characters in length, since it using bitwise masks.
https://medium.com/@marklucking/an-interesting-challenge-with-swiftui-9ebb26e77376
This is the basic code in the article.
ForEach((0 ..< letter.count), id: \.self) { column in
Text(letter[column])
.foregroundColor(colorCode(gate: Int(self.gate), no: column) ? Color.black: Color.red)
.font(Fonts.futuraCondensedMedium(size: fontSize))
}
And this one to mask the text...
func colorCode(gate:Int, no:Int) -> Bool {
let bgr = String(gate, radix:2).pad(with: "0", toLength: 16)
let bcr = String(no, radix:2).pad(with: "0", toLength: 16)
let binaryColumn = 1 << no - 1
let value = UInt64(gate) & UInt64(binaryColumn)
let vr = String(value, radix:2).pad(with: "0", toLength: 16)
print("bg ",bgr," bc ",bcr,vr)
return value > 0 ? true:false
}
Making parts of text bold in SwiftUI
iOS 15+ (Swift 5.5 +)
SwiftUI has built-in support for rendering Markdown.
It is GitHub flavored markdown. AttributedString converts both inline and block styles. SwiftUI renders inline styles (but not images at this time). We use the fantastic cmark-gfm library to parse the markdown string. - SwiftUI Frameworks Engineer - developer.apple.com
See more:
What is Markdown?
Use double asterisks (**) arroud the characters that you want to make bold.
Text("**CO**rona**V**irus **D**isease of 20**19**")
Use underscore (_) arround the charachters you want to make italic.
Text("Is this text _emphasized_?")
String variable
Use init(_ value: String)
Creates a localized string key from the given string value.
let bold = "This text is **bold**"
Text(.init(bold))
String interpolation
Use init(_ value: String)
Creates a localized string key from the given string value.
let bold = "Bold"
Text(.init("This text is **\(bold)**"))
Attributed text
Use init(_ attributedContent: AttributedString)
Creates a text view that displays styled attributed content.
let markdownText = try! AttributedString(markdown: "This text is **bold**")
Text(markdownText)
See also:
init(_ attributedContent: AttributedString)
- https://developer.apple.com
SwiftUI: underlined text does not work with background material
var body: some View {
ZStack {
LinearGradient(colors: [.orange, .yellow, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.ignoresSafeArea()
Text("Some underlined text")
.underline()
.padding()
.background(
Rectangle() //Add this one line of code
.fill(.ultraThinMaterial)
)
}
}
Add this one to your code will fix your problem.
How to use Attributed String in SwiftUI
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")
}
}
SwiftUI - Value of type 'Text' has no member 'color' Error
There is No such a modifier as color()
. Use foregroundColor()
:
Text("Hello world")
.foregroundColor(.red)
Creating a big paragraph with clickable Text in SwiftUI
You are pushing this version of SwiftUI beyond its current capabilities!
Something like this would more easily be done using the advanced text handling in UIKit, or by thinking outside the box and converting the text to something Like HTML.
If you MUST use SwiftUI, your best bet would probably be to layout the formatted text first onto a tappable paragraph/block, and then use gesture recognition at the block level to detect where in the block the tap took place - indirectly determining if the tap position coincided with the “tappable” text.
Update #1:
Example: To use a UITextView (which supports attributed text), you could use the UIViewRepresentable protocol to wrap the UIKit view and make it accessible from within SwiftUI. e.g. Using Paul Hudson's UIViewRepresentable example for the code...
struct TextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
return UITextView()
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
The TextView can then be used directly within SwiftUI.
Now, while Textview gives you formatting, it does not give you the clickability you need without a lot of extra work, but a WKWebView used to render an HTML version of your text would allow you to convert the clickable text into HTML links that could be handled internal to your new SwiftUI view.
Note: The reason I say that you are pushing SwiftUI to its limits is that the current version of SwiftUI hides a lot of the configurability that is exposed in UIKit and forces you to do cartwheels to get to a solution that is often already present in UIKit.
Update #2:
Here's a clickable version that uses UITextField and a NSAttributedString:
class MyTextView: UITextView, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print(URL)
return false
}
}
struct SwiftUIView: UIViewRepresentable {
@Binding var text: NSAttributedString
func makeUIView(context: Context) -> MyTextView {
let view = MyTextView()
view.dataDetectorTypes = .link
view.isEditable = false
view.isSelectable = true
view.delegate = view
return view
}
func updateUIView(_ uiView: MyTextView, context: Context) {
uiView.attributedText = text
}
}
All you need to do now is convert the downloaded text into a suitable attributed string format and you have attributed formatting and clickability
Related Topics
How to Set Exit Code Value for a Command Line Utility in Swift
Swift Sort Dictionary by Value
What Would Be a Proper Storyboard Example of Combining Nav Bars and Tab Bars in One App
Getting a Let Value Outside a Function
Define a Swift Protocol Which Requires a Specific Type of Sequence
How to Draw a Line Between Two Points in Scenekit
Get Lat and Long from Tapped Overlay in Google Maps
How to Make a Swift Enum with Associated Values Equatable
Scenedidload Being Called Twice
How to Get Iobluetoothdevice's Battery Level, Using Swift and Appkit (Xcode for MACos)
Extend Generic Array<T> to Adopt Protocol
How to Embed Third Party Framework on Ionic Capacitor Custom Plugin
Animating a Navigation Bar Color
Compare Three Values for Equality
How to Loop Through an Array from the Second Element in Elegant Way Using Swift
Having Tab Bar and Navigationbar in the Same View in Swiftui