Exceeding max Text("") concatenation length - SwiftUI -
Asked Answered
H

2

4

With reference to the answer posted by Asperi (https://stackoverflow.com/users/12299030/asperi) on Question: Highlight a specific part of the text in SwiftUI

I have found his answer quite useful, however, when my String input exceeds 32k characters the app crashes, so I am assuming the String() is a max of 32k and am looking for a work around.

In my app, if someone searches for the word "pancake", the search word will be stored and when the user looks at the detail page (of lets say a recipe), the word pancake will highlight. All works well with this answer, but when the recipe exceeds 32k characters, the app crashes with exceeding index range messages. (specific error message: Thread 1: EXC_BAD_ACCESS (code=2, address=0x16d43ffb4))

Here is the modified code from the answer on that question:

This will print the data:

hilightedText(str: self.recipes.last!.recipeData!)
                        .multilineTextAlignment(.leading)
                        .font(.system(size: CGFloat( settings.fontSize )))

There is obviously more to this code above, but in essence, it iterates a database, and finds the last record containing 'search word' and displays the recipeData here, which is a large string contained in the database.

to implement the highlightedText functionality:

    func hilightedText(str: String) -> Text {
        let textToSearch = searched
        var result: Text!
        for word in str.split(separator: " ") {
            var text = Text(word)
            if word.uppercased().contains(textToSearch.uppercased()) {
                text = text.bold().foregroundColor(.yellow)
            }
            //THIS NEXT LINE has been identified as the problem:
            result = (result == nil ? text : result + Text(" ") + text)
        }
        return result
    }

I've modified the answer from Asperi slightly to suit my needs and all works really well, unless I come across a recipeData entry that is larger than 32k in size, as stated before.

I have tried Replacing String with a few other data types and nothing works..

Any ideas?

Thank you!

UPDATE:

After lengthy discussion in the comments, it appears that the root cause of the issue is at some point, for some records, I am exceeding the maximum Text("") concatenations.

In the above code, each word is split out, evaluated and added to the long string "result" which winds up looking like this: Text("word") + Text(" ") + Text("Word") and so on.

This is done, so I can easily apply color attributes per word, but it would seem that once I hit a certain number of words (which is less that 32k, one record was 22k and crashed), the app crashes.

Leo suggested https://mcmap.net/q/264509/-how-to-use-attributed-string-in-swiftui this thread as an alternative and I will have to attempt to implement that instead.

Thank you..

Hula answered 19/7, 2020 at 0:32 Comment(11)
Not related to your question but you can simply use localizedStandardContains (diacritics and case insensitive) word.localizedStandardContains(textToSearch)Minnick
Thx Leo, Ill clean that up if I can hopefully get it working with longer strings..Hula
btw for word in str.split(whereSeparator: \.isWhitespace)Minnick
the whereSeparator: \.isWhitespace seems to remove all line breaks (\n)...Hula
I am starting to suspect that String length exceeding 32k is NOT my issue. Doing some further debugging, will change question when I figure it out..Hula
no, i am starting to think I have a nil problem tho..Hula
no such luck so far.. the adding Text to Text could be causing some issues. recoding it a bit, will modify question to show slimmed down version that still does not work..Hula
looks like my error is caused by this line: result = (result == nil ? text : result + Text(" ") + text)Hula
right, thats the issue, the text attirbs get applied on a word for word basis.. can't seem to get around it, but the concat does seem to be causing my issue.. maybe having so many Text("word1") + Text("word2") + Text("wordN") is causing problems with larger records. It may be exceeding the size of something somewhere. sucks.. gonna step away and think about it for a while. Thx for your help. If I figure it out, ill update the questions..Hula
Thats gotta be it, I am prob hitting a Text("") concatenation limit.. I will give that suggestion a try, that may be the only way. Thank you Leo!Hula
figured it out, see answerHula
C
4

Hmm... unexpected limitation... anyway - learn something new.

Ok, here is improved algorithm, which should move that limitation far away.

Tested with Xcode 12 / iOS 14. (also updated code in referenced topic Highlight a specific part of the text in SwiftUI)

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

    var result = Text("")

    var range = str.startIndex..<str.endIndex
    repeat {
        guard let found = str.range(of: searched, options: .caseInsensitive, range: range, locale: nil) else {
            result = result + Text(str[range])
            break
        }

        let prefix = str[range.lowerBound..<found.lowerBound]
        result = result + Text(prefix) + Text(str[found]).bold().foregroundColor(.yellow)

        range = found.upperBound..<str.endIndex
    } while (true)

    return result
}

demo

Chitter answered 19/7, 2020 at 5:30 Comment(5)
Thank you also Asperi, This answer also addresses some unintended consequences that the other answer does not, such as preserving the original whitespace and highlighting multiple search terms if word pairs appear together. - Accepted answer. Thx!Hula
You can use localizedStandardRange for diacritics and caseInsensitive search. guard let found = str[range].localizedStandardRange(of: searched)Minnick
var range = str.startIndex... repeat { guard let found = str[range].localizedStandardRange(of: searched) else { result = result + Text(str[range]) break } result = result + Text(str[range.lowerBound..<found.lowerBound]) + Text(str[found]).bold().foregroundColor(.yellow) range = found.upperBound... } while trueMinnick
or var range = str.startIndex... while let found = str[range].localizedStandardRange(of: searched) { result = result + Text(str[range.lowerBound..<found.lowerBound]) + Text(str[found]).bold().foregroundColor(.yellow) range = found.upperBound... } return result + Text(str[range])Minnick
Now I've just got to figure out how to do this in Kotlin, lol, ughHula
H
0

After much discussion in the comments, it became clear that I was hitting a maximum Text() concatenations limit, so beware, apparently there is one.

I realized however that I only needed to have a split Text("Word") when that particular word required special formatting (IE highlighting, etc), otherwise, I could concat all of the raw strings together and send that as a Text("String of words").

This approach mitigated the action of having every single word sent as a Text("Word" by itself and cut down greatly on the number of Text()'s being returned.

see code below that solved the issue:

func hilightedText(str: String) -> Text {
    let textToSearch = searched
    var result = Text(" ")
    var words: String = " "
    var foundWord = false
    for line in str.split(whereSeparator: \.isNewline) {
        for word in line.split(whereSeparator: \.isWhitespace) {
            if word.localizedStandardContains(textToSearch) {
                foundWord = true
                result += Text(words) + Text(" ") + Text(word).bold().foregroundColor(.yellow)

            } else {
                if foundWord {
                    words = ""
                }
                foundWord = false
                words += " " + word
            } 
        }
        words += "\n\n"
    }
    return result + Text(" ") + Text(words)
}

extension Text {
    static func += (lhs: inout Text, rhs: Text) {
        lhs = lhs + rhs
    }
}

It could use some cleanup as also discussed in the comments for splitting by whitespace, etc, but this was just to overcome the issue of crashing. Needs some additional testing before I call it good, but no more crashing..

ADDED: the suggestion to use separator by .isWhiteSpace worked, but when I put it back together, everything was a space, no more line breaks, so I added the extra split by line breaks to preserve the line breaks.

Hula answered 19/7, 2020 at 2:58 Comment(1)
Comments are not for extended discussion; this conversation has been moved to chat.Halfhour

© 2022 - 2024 — McMap. All rights reserved.