I have a text field in my app that needs its character count limited. It works fine with regular text. But I need to take into account special characters and emojis and when the textfield reaches the limit, and I delete one character, and input for example ¢ (2 bytes) or € (3 bytes) repeatedly, it behaves as if there is no character limit at all and you can input anything.
TextField("Username", text: $ame)
.onChange(of: name, perform: { value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
if totalBytes > 14 {
let firstNBytes = Data(name.utf8.prefix(14))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the message back to the last place where it was the right size
name = maxBytesString
} else {
print("not a valid UTF-8 sequence")
}
}
})
The console does print the "not valid UTF-8" warning, but the textfield is never corrected in such an edge case. Any help? BTW, "name" is a @State variable and gets initialized successfully with the onAppear
method, if that matters.
The issue is with let firstNBytes = Data(name.utf8.prefix(14))
. As you've seen, some characters are more than 1 byte. When you simply take the 1st 14 bytes you can easily be cutting a character in half. This is why the attempt to create a String
from the truncated bytes can fail.
You need a way to trim the data byte by byte, starting at 14, until you have valid data. You may end up with a string that is only 13, 12, 11, or even fewer bytes, but you at least have a string that is less than 14 bytes.
Here is an extension to String
that will return a truncated string based on a supplied byte limit:
extension String {
func truncated(to bytes: Int, encoding: String.Encoding = .utf8) -> String? {
if let data = self.data(using: encoding) {
var tmpData = data.prefix(bytes)
repeat {
if let res = String(data: tmpData, encoding: encoding) {
return res
} else {
tmpData = tmpData.dropLast(1)
}
} while !tmpData.isEmpty
}
return nil // Should never be reached with UTF-8 encoding
}
}
With that your code becomes something like the following:
TextField("Username", text: $name)
.onChange(of: name, perform: { value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
if totalBytes > 14 {
if let newName = value.truncated(to: 14) {
name = newName
}
}
})