Highlight a Specific Part of the Text in Swiftui

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

Sample Image

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
}

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 like return 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 pass Range<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())
}
}

Image of text with red highlighting and bold

Gist

You could also just wrap a UILabel in a UIViewRepresentable, and use attributedText. But that would be cheating. :D

How to display highlighted words using swift ui

You can parse your string using regular expression and build Text based on found ranges:

struct HighlightedText: View{
let text: Text

private static let regularExpression = try! NSRegularExpression(
pattern: "###(?<content>((?!\\$\\$\\$).)*)\\$\\$\\$"
)

private struct SubstringRange {
let content: NSRange
let full: NSRange
}

init(_ string: String) {
let ranges = Self.regularExpression
.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.count)
)
.map { match in
SubstringRange(
content: match.range(withName: "content"),
full: match.range(at: 0)
)
}
var nextNotProcessedSymbol = 0
var text = Text("")
let nsString = string as NSString
func appendSubstringStartingNextIfNeeded(until endLocation: Int) {
if nextNotProcessedSymbol < endLocation {
text = text + Text(nsString.substring(
with: NSRange(
location: nextNotProcessedSymbol,
length: endLocation - nextNotProcessedSymbol
)
))
}
}
for range in ranges {
appendSubstringStartingNextIfNeeded(until: range.full.location)
text = text + Text(nsString.substring(with: range.content))
.foregroundColor(Color.red)
nextNotProcessedSymbol = range.full.upperBound
}
appendSubstringStartingNextIfNeeded(until: string.count)
self.text = text
}

var body: some View {
text
}
}

Usage:

HighlightedText("When I was ###young$$$, I thought ###time$$$ was money")

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

Sample Image

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
}

How to Highlight and Clickable if Text is URL SwiftUI

You need to create custom UIViewRepresentable for TextView.

check below code this might help you.

struct TextView: UIViewRepresentable {

@Binding var text: String
@Binding var textStyle: UIFont.TextStyle

func makeUIView(context: Context) -> UITextView {
let textView = UITextView()

textView.delegate = context.coordinator
textView.font = UIFont.preferredFont(forTextStyle: textStyle)
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.dataDetectorTypes = .link

return textView
}

func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}

func makeCoordinator() -> Coordinator {
Coordinator($text)
}

class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>

init(_ text: Binding<String>) {
self.text = text
}

func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
}
}
}

In your "Receiver User Code:-" same for "Sender User Code"

struct SenderReciverUI1: View {
@State private var message = "Hello, www.google.com. this is just testing for hyperlinks, check this out our website https://www.apple.in thank you."
@State private var textStyle = UIFont.TextStyle.body

var body: some View {
Group {
HStack(alignment: .bottom){
VStack(alignment: .leading,spacing:5) {
HStack(alignment: .bottom) {
TextView(text: $message, textStyle: $textStyle)
.foregroundColor(.white)
.padding(10)
.cornerRadius(10.0)
}
}
Spacer()
}.padding(.vertical,5)
.padding()
}
}
}

let me know if you need anything.

How to highlight text when textfield is tapped

textFieldDidBeginEditing

This is not going to achieve what you exactly want because according to UITextField life-cycle, this method will be triggered exactly when editing started and about to be end. What you need is, highlight exact moment with tapping textfield.

First separate your highlighted and normal state appearences.

extension MainView {
func textFieldActiveDisplay() {

self.isTextFieldOpen = true
let color = COLOR
let animation: CABasicAnimation = CABasicAnimation(keyPath: "borderColor")
animation.fromValue = textField.layer.borderColor
animation.toValue = color
animation.duration = 0.3
textField.layer.borderColor = color.cgColor

let borderWidth: CABasicAnimation = CABasicAnimation(keyPath: "borderWidth")
borderWidth.fromValue = 0
borderWidth.toValue = 4
borderWidth.duration = 0.2
searchBar.layer.borderWidth = 4

let group = CAAnimationGroup()
group.animations = [animation, borderWidth]
searchBar.layer.add(group, forKey: nil)

textField.font = HIGHLIGHTED_FONT
textField.textColor = HIGHLIGHTED_COLOR
}

func textFieldInActiveDisplay() {
guard isTextFieldOpen else { return }
let borderWidth: CABasicAnimation = CABasicAnimation(keyPath: "borderWidth")
borderWidth.fromValue = 4
borderWidth.toValue = 0
borderWidth.duration = 0.2
textField.layer.borderWidth = 0
textField.layer.add(borderWidth, forKey: nil)

textField.font = NORMAL_FONT
textField.textColor = NORMAL_COLOR
self.isTextFieldOpen = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.textField.subviews.forEach({$0.layer.removeAllAnimations()})
self.textField.layer.removeAllAnimations()
self.textField.layoutIfNeeded()
}
}

}

In your ViewController you need to link-up your textfield's delegate to itself.

override func viewDidLoad() {
super.viewDidLoad()
view.textField.delegate = self
}


extension MainViewController: UITextFieldDelegate, UIGestureRecognizerDelegate {

func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
view.textFieldActiveDisplay()
return true
}

func textFieldDidEndEditing(_ textField: UITextField) {
view.textFieldInActiveDisplay()
}

}

Output:
Sample Image



Related Topics



Leave a reply



Submit