Sorry to make things more complicated, but reaching for NSRegularExpression and NSRange brings another complication with it that has been overlooked: UTF‐16.
To be honest, the code in your original post at the very top is probably more‐or‐less what I would have gone with after as many years working with Swift as Swift has existed.
If you are feeling discouraged, then just revert to your original code and ignore what follows.
However, if you do want to use regular expressions—and you are willing to keep learning—then you need to make some adjustments.
The Search Range
NSRange(location: 0, length: outputString.count)
You are lucky in that that cannot trigger a fatal error, but it could still lop off the tail end of the string. count is the number of extended grapheme clusters, which Swift calls Character. On the other hand, length is the number of UTF‐16 code units. So you need to pass utf16.count to length instead.
// This is an “x” with 20 macrons (◌̄) stacked on it.
let complex = "x̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄"
print(complex.count) // 1
print(complex.utf16.count) // 21
let string = "\(complex) keyword == theEight"
let mixedUpRange = NSRange(location: 0, length: string.count)
print((string as NSString).substring(with: mixedUpRange))
// x̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄
let correctRange = NSRange(location: 0, length: string.utf16.count)
print((string as NSString).substring(with: correctRange))
// x̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄ keyword == theEight
So if you pass the mixed‐up range as your search range, the thing you are looking for may be beyond the end of where you are searching.
It is worth noting that you do not even need to leave ASCII before the two to diverge. Even a carriage return and line feed combination will cause a difference:
let complex = "\r\n"
print(complex.count) // 1
print(complex.utf16.count) // 2
Extracting the Match
Here you need to decide whether you want 8 Characters (grapheme clusters) or 8 UTF16.CodeUnits.
let outputString = "... keyword == simple+x̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄"
The original version pulled 8 Characters:
dataFirst8 = String(dataINeed!.prefix(8))
// simple+x̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄̄
But the new version attempts to pull 8 UTF16.CodeUnits:
let regexPattern = "keyword.* (.{8}).*$"
// ...
let matchRange = match!.range(at: 1)
// simple+x
However, the new version can contain indices that are invalid for String, so the conversion back to String.Index can fail even when there is a valid match:
let matchRange = match!.range(at: 1)
// simple+x
let rangeFromNSRange = Range(matchRange, in: outputString)
// nil
// (Or trap, if you don’t remove the force unwrap (!)
// that was originally at the end of this line.)
So instead you’ll need to do the conversion yourself in a way that preserves intra‐Character indices:
let matchRange = match!.range(at: 1)
// simple+x
let utf16 = outputString.utf16
let lowerBound = utf16.index(
utf16.startIndex,
offsetBy: matchRange.location)
let upperBound = utf16.index(
utf16.startIndex,
offsetBy: matchRange.location + matchRange.length)
let first8CharactersOfThatOnePart
= String(outputString.utf16[lowerBound ..< upperBound])