Reputation: 113777
I searched, but surprisingly couldn't find an answer.
I have a long NSString
that I want to shorten. I want the maximum length to be around 20 characters. I read somewhere that the best solution is to use substringWithRange
. Is this the best way to truncate a string?
NSRange stringRange = {0,20};
NSString *myString = @"This is a string, it's a very long string, it's a very long string indeed";
NSString *shortString = [myString substringWithRange:stringRange];
It seems a little delicate (crashes if the string is shorter than the maximum length). I'm also not sure if it's Unicode-safe. Is there a better way to do it? Does anyone have a nice category for this?
Upvotes: 74
Views: 42316
Reputation: 120052
Using the NSLineBreakMode
to define the truncation mode, you can truncate the string like:
"a really long text to truncate with the given modes"
,,,
.truncated(.byClipping, to: 20)) // a really long text t
.truncated(.byWordWrapping, to: 20)) // a really long...
.truncated(.byTruncatingTail, to: 20)) // a really long tex...
.truncated(.byTruncatingMiddle, to: 20)) // a really...ven modes
.truncated(.byTruncatingHead, to: 20)) // ...h the given modes
Here is the extension behind this:
extension String {
func truncated(_ mode: NSLineBreakMode = .byTruncatingTail, to maxLength: Int, truncatingSign: Self = "...") -> Self {
guard self.count > maxLength else { return self }
let visibleCount = maxLength - truncatingSign.count
switch mode {
case .byWordWrapping:
let mutatedSelf = self.prefix(visibleCount)
let nLastChar = self.index(after: mutatedSelf.endIndex)
guard self[nLastChar].isWhitespace else { return mutatedSelf + truncatingSign }
var lastWord: String = ""
mutatedSelf.enumerateSubstrings(in: startIndex..., options: [.byWords, .reverse]) { (substring , _, _, stop) in
guard let substring = substring else { return }
lastWord = substring
stop = true
}
if let range = mutatedSelf.range(of: lastWord, options: .backwards, range: nil, locale: nil) {
return mutatedSelf
.replacingCharacters(in: range, with: "")
.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
+ truncatingSign
}
return mutatedSelf + truncatingSign
case .byCharWrapping: return self.prefix(visibleCount) + truncatingSign
case .byClipping: return self.prefix(maxLength) + ""
case .byTruncatingHead: return truncatingSign + self.suffix(visibleCount)
case .byTruncatingTail: return self.prefix(visibleCount) + truncatingSign
case .byTruncatingMiddle:
let middle = Double(visibleCount)/2
return self.prefix(Int(middle.rounded(.down))) + ""
+ truncatingSign
+ self.suffix(Int(middle.rounded(.up))) + ""
@unknown default: return self /* Future unknown case */
}
}
}
Upvotes: 0
Reputation: 2535
The simplest and nice solution (with 3 dots at the end of the text) :
NSString *newText = [text length] > intTextLimit ?
[[text substringToIndex:intTextLimit] stringByAppendingString:@"…"] :
text;
Upvotes: 3
Reputation: 1565
Extension to truncate at different positions (head, tail or middle).
Swift 4.2 and newer
extension String {
enum TruncationPosition {
case head
case middle
case tail
}
func truncated(limit: Int, position: TruncationPosition = .tail, leader: String = "...") -> String {
guard self.count >= limit else { return self }
switch position {
case .head:
return leader + self.suffix(limit)
case .middle:
let halfCount = (limit - leader.count).quotientAndRemainder(dividingBy: 2)
let headCharactersCount = halfCount.quotient + halfCount.remainder
let tailCharactersCount = halfCount.quotient
return String(self.prefix(headCharactersCount)) + leader + String(self.suffix(tailCharactersCount))
case .tail:
return self.prefix(limit) + leader
}
}
}
Upvotes: 3
Reputation: 5477
let trimToCharacter = 20
let shortString = String(myString.prefix(trimToCharacter))
Happy Coding.
Upvotes: 28
Reputation: 73034
Since this answer isn't actually in this list, the simplest and most sensible one-liner:
NSString *myString = @"This is a string, it's a very long string, it's a very long string indeed";
myString = (myString.length > 20) ? [myString substringToIndex:20] : myString;
Upvotes: 15
Reputation: 561
If you want to truncate from end use:
[fileName substringToIndex:anyNumber];
If you want to truncate from start:
[fileName substringFromIndex:anyNumber];
Upvotes: 0
Reputation: 7862
//Short the string if string more than 45 chars
if([self.tableCellNames[indexPath.section] length] > 40) {
// define the range you're interested in
NSRange stringRange = {0, MIN([self.tableCellNames[indexPath.section] length], 40)};
// adjust the range to include dependent chars
stringRange = [self.tableCellNames[indexPath.section]
rangeOfComposedCharacterSequencesForRange:stringRange];
// Now you can create the short string
NSString *shortStringTitle = [self.tableCellNames[indexPath.section] substringWithRange:stringRange];
shortStringTitle = [shortStringTitle stringByAppendingString:@"..."];
titleLabel.text = shortStringTitle;
} else {
titleLabel.text = self.tableCellNames[indexPath.section];
}
// VKJ
Upvotes: 2
Reputation: 111
A shorter solution is:
NSString *shortString = ([myString length]>MINLENGTH ? [myString substringToIndex:MINLENGTH] : myString);
Upvotes: 11
Reputation: 7255
Actually the part about being "Unicode safe" was dead on, as many characters combine in unicode which the suggested answers don't consider.
For example, if you want to type é. One way of doing it is by typing "e"(0x65)+combining accent" ́"(0x301). Now, if you type "café" like this and truncate 4 chars, you'll get "cafe". This might cause problems in some places.
If you don't care about this, other answers work fine. Otherwise, do this:
// define the range you're interested in
NSRange stringRange = {0, MIN([myString length], 20)};
// adjust the range to include dependent chars
stringRange = [myString rangeOfComposedCharacterSequencesForRange:stringRange];
// Now you can create the short string
NSString *shortString = [myString substringWithRange:stringRange];
Note that in this way your range might be longer than your initial range length. In the café example above, your range will expand to a length of 5, even though you still have 4 "glyphs". If you absolutely need to have a length less than what you indicated, you need to check for this.
Upvotes: 123
Reputation: 96984
Could use a ternary operation:
NSString *shortString = (stringRange.length <= [myString length]) ? myString : [myString substringWithRange:stringRange];
Or for more control over the end result:
if (stringRange.length > [myString length])
// throw exception, ignore error, or set shortString to myString
else
shortString = [myString substringWithRange:stringRange];
Upvotes: 3
Reputation: 12800
All NSString operations are Unicode-safe, as NSString is essentially a unichar array internally. Even if the string is in a different encoding it's converted to your specified encoding when it's displayed.
Upvotes: 0
Reputation: 27601
It seems a little delicate (crashes if the string is shorter than the maximum length)
Then why not fix that part of it?
NSRange stringRange = {0, MIN([myString length], 20)};
Upvotes: 7