nevan king
nevan king

Reputation: 113777

How can I truncate an NSString to a set length?

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

Answers (12)

Mojtaba Hosseini
Mojtaba Hosseini

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

Yossi
Yossi

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

Kedar Sukerkar
Kedar Sukerkar

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

Md. Ibrahim Hassan
Md. Ibrahim Hassan

Reputation: 5477

Swift 4

let trimToCharacter = 20
let shortString = String(myString.prefix(trimToCharacter))

Happy Coding.

Upvotes: 28

brandonscript
brandonscript

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

Gaurav
Gaurav

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

Vinod Joshi
Vinod Joshi

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

Phoenix
Phoenix

Reputation: 111

A shorter solution is:

NSString *shortString = ([myString length]>MINLENGTH ? [myString substringToIndex:MINLENGTH] : myString);

Upvotes: 11

mohsenr
mohsenr

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

Alex Reynolds
Alex Reynolds

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

Jon Shier
Jon Shier

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

Shaggy Frog
Shaggy Frog

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

Related Questions