10

Hello I'm new to Swift and 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
  }

}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
Damiano Miazzi
  • 1,514
  • 1
  • 16
  • 38
  • Does this answer your question? [SwiftUI: is there exist modifier to highlight substring of Text() view?](https://stackoverflow.com/questions/59426359/swiftui-is-there-exist-modifier-to-highlight-substring-of-text-view) – Andrew_STOP_RU_WAR_IN_UA Sep 29 '20 at 00:30
  • Maybe [this answer](https://stackoverflow.com/a/73882420/7905172) helps you to accomplish the desired result. – Valeriu Gavriluta Sep 28 '22 at 13:44

6 Answers6

14

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)
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
7

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)
yo1995
  • 471
  • 3
  • 7
Fengson
  • 4,751
  • 8
  • 37
  • 62
  • Hi @Fengson, you solution is working well and i found it optimised, just one thing make it more scalable. In my case i have to highlight the text on the bases of text location (like `start` and `length` is given instead of searched Text. your solution will be helpfull. – Vikash Kumar Chaubey Jul 26 '23 at 07:48
  • @VikashKumarChaubey If you have a start and end that you want to highlight, why not just create a range out of those and proceed similarly to what I've proposed above? Unless I've mistaken your ask. – Fengson Jul 29 '23 at 06:43
2

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
}
user3069232
  • 8,587
  • 7
  • 46
  • 87
1

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())

BokuWaTaka
  • 168
  • 6
0

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)
        
Maria N.
  • 601
  • 4
  • 5
0

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)
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143