Reputation: 2335
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
Reputation: 404
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):
Upvotes: 1
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)
Upvotes: 0
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
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
Reputation: 660
The answer of @Asperi works well. Here is a modified variant with a search by array of single words:
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
Reputation: 258365
If that requires just simple word styling then here is possible solution.
Tested with Xcode 11.4 / iOS 13.4
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
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
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
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