Damiano Miazzi
Damiano Miazzi

Reputation: 2335

Highlight a specific part of the text

I'm new to Swift and I am using SwiftUI for my project where I download some weather data and I display it in the ContentView().

I would like to highlight some part of the Text if it contains some specific word, but I don't have any idea how to start.

In ContentView(), I have tried to set a function receiving the string downloaded from web and return a string. I believe this is wrong, because SwiftUI does not apply the modifiers at the all for the Text.

For example, in my ContentView() I would like the word thunderstorm to have the .bold() modifier:

struct ContentView: View {
  let testo : String = "There is a thunderstorm in the area"
  
  var body: some View {
    Text(highlight(str: testo))
  }
  
  func highlight(str: String) -> String {
    let textToSearch = "thunderstorm"
    var result = ""
    
    if str.contains(textToSearch) {
      let index = str.startIndex
      result = String( str[index])
    }
    
    return result
  }
  
}

Upvotes: 11

Views: 12137

Answers (8)

Daggerpov
Daggerpov

Reputation: 404

To highlight every occurrence of a string within a string:

This code builds directly off that of @Fengson, who's solution only highlights the first occurrence.

Say you have two variables,

let text: String // where text is the whole text
let searchText: String // where searchText is what you want highlighted

private var attributedString: AttributedString {
    var attributedString = AttributedString(text)
    let searchTextLowercased = (searchText ?? "").lowercased()
    let textLowercased = text.lowercased()
    var searchStartIndex = textLowercased.startIndex

    // Loop to find and highlight all occurrences
    while let range = textLowercased.range(of: searchTextLowercased, range: searchStartIndex..<textLowercased.endIndex) {
        // Convert String.Index to AttributedString.Index
        if let attributedRange = Range(NSRange(range, in: textLowercased), in: attributedString) {
            attributedString[attributedRange].backgroundColor = .yellow
        }
        // Move searchStartIndex to the end of the found range to continue searching
        searchStartIndex = range.upperBound
    }

    return attributedString
}

Note that I made this search case insensitive.

This is an example output within my app (note the only change I made was to the color from .yellow in the example above):

enter image description here

Upvotes: 1

HeonJin Ha
HeonJin Ha

Reputation: 1

Thanks to @Asperi and @Maria N. for their helpful code. It's works well.

I have adapted it to work without the need for space-delimited inputs. Here's the modified version:

func highlightedText(str: String, searched: [String]) -> Text {
    guard !str.isEmpty && !searched.isEmpty else { return Text(str) }
    
    var str = str
    
    let separatorText = "&%@$"
    
    for search in searched {
        str = str.replacingOccurrences(of: search, with: separatorText + search + separatorText)
    }
    
    var result: Text!
    
    let parts = str.components(separatedBy: separatorText)
    
    for part_index in parts.indices {
        result = (result == nil ? Text("") : result + Text(""))
        
        if searched.contains(parts[part_index].trimmingCharacters(in: .punctuationCharacters)) {
            result = result + Text(parts[part_index])
                .bold()
                .foregroundColor(.red)
        } else {
            result = result + Text(parts[part_index])
        }
    }
    
    return result ?? Text(str)
}

Usage Example:

let str: String = "1hr 20min"
let searched: [String] = ["hr", "min"]

highlightedText(str: str, searched: searched)

Example Image UI

Upvotes: 0

Fengson
Fengson

Reputation: 4912

If you are targeting iOS15 / macOS12 and above, you can use AttributedString. For example:

private struct HighlightedText: View {
    let text: String
    let highlighted: String

    var body: some View {
        Text(attributedString)
    }

    private var attributedString: AttributedString {
        var attributedString = AttributedString(text)

        if let range = attributedString.range(of: highlighted)) {
            attributedString[range].backgroundColor = .yellow
        }

        return attributedString
    }
}

If you want your match to be case insensitive, you could replace the line

if let range = attributedString.range(of: highlighted)

with

if let range = AttributedString(text.lowercased()).range(of: highlighted.lowercased())

// or
if let range = attributedString.range(of: highlighted, options: .caseInsensitive)

Upvotes: 10

Michał Ziobro
Michał Ziobro

Reputation: 11812

You can also make AttributedString with markdown this way

 do {
   return try AttributedString(markdown: foreignSentence.replacing(word.foreign, with: "**\(word.foreign)**"))
} catch {
   return AttributedString(foreignSentence)
}

and just use Text

Text(foreignSentenceMarkdown)

Upvotes: 0

Maria N.
Maria N.

Reputation: 660

The answer of @Asperi works well. Here is a modified variant with a search by array of single words:

enter image description here

func highlightedText(str: String, searched: [String]) -> Text {
    
    guard !str.isEmpty && !searched.isEmpty else { return Text(str) }
    
    var result: Text!
    let parts = str.components(separatedBy: " ")
    
    for part_index in parts.indices {
        result = (result == nil ? Text("") : result + Text(" "))
        if searched.contains(parts[part_index].trimmingCharacters(in: .punctuationCharacters)) {
            result = result + Text(parts[part_index])
                .bold()
                .foregroundColor(.red)
        }
        else {
            result = result + Text(parts[part_index])
        }
    }
    
    return result ?? Text(str)
}

Usage example:

        let str: String = "There is a thunderstorm in the area. Added some testing long text to demo that wrapping works correctly!"
        let searched: [String] = ["thunderstorm", "wrapping"]
        
        highlightedText(str: str, searched: searched)
            .padding()
            .background(.yellow)
        

Upvotes: 0

Asperi
Asperi

Reputation: 258365

If that requires just simple word styling then here is possible solution.

Tested with Xcode 11.4 / iOS 13.4

demo

struct ContentView: View {
    let testo : String = "There is a thunderstorm in the area. Added some testing long text to demo that wrapping works correctly!"


    var body: some View {
        hilightedText(str: testo, searched: "thunderstorm")
            .multilineTextAlignment(.leading)
    }

    func hilightedText(str: String, searched: String) -> Text {
        guard !str.isEmpty && !searched.isEmpty else { return Text(str) }

        var result: Text!
        let parts = str.components(separatedBy: searched)
        for i in parts.indices {
            result = (result == nil ? Text(parts[i]) : result + Text(parts[i]))
            if i != parts.count - 1 {
                result = result + Text(searched).bold()
            }
        }
        return result ?? Text(str)
    }
}

Note: below is previously used function, but as commented by @Lkabo it has limitations on very long strings

func hilightedText(str: String) -> Text {
    let textToSearch = "thunderstorm"
    var result: Text!

    for word in str.split(separator: " ") {
        var text = Text(word)
        if word == textToSearch {
            text = text.bold()
        }
        result = (result == nil ? text : result + Text(" ") + text)
    }
    return result ?? Text(str)
}

Upvotes: 17

user3069232
user3069232

Reputation: 8995

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

enter image description here

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
}

Upvotes: 2

BokuWaTaka
BokuWaTaka

Reputation: 168

You can concatenate with multiple Text Views.

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
  var body: some View{
    let testo : String = "There is a thunderstorm in the area"
    let stringArray = testo.components(separatedBy: " ")
    let stringToTextView = stringArray.reduce(Text(""), {
      if $1 == "thunderstorm" {
        return $0 + Text($1).bold() + Text(" ")
      } else {
        return $0 + Text($1) + Text(" ")
      }

    })
    return stringToTextView
  }
}

PlaygroundPage.current.setLiveView(ContentView())

Upvotes: 1

Related Questions