Display hidden characters in NSTextView - cocoa

I am writing a text editor for Mac OS X. I need to display hidden characters in an NSTextView (such as spaces, tabs, and special characters). I have spent a lot of time searching for how to do this but so far I have not found an answer. If anyone could point me in the right direction I would be grateful.

Here's a fully working and clean implementation
#interface GILayoutManager : NSLayoutManager
#end
#implementation GILayoutManager
- (void)drawGlyphsForGlyphRange:(NSRange)range atPoint:(NSPoint)point {
NSTextStorage* storage = self.textStorage;
NSString* string = storage.string;
for (NSUInteger glyphIndex = range.location; glyphIndex < range.location + range.length; glyphIndex++) {
NSUInteger characterIndex = [self characterIndexForGlyphAtIndex: glyphIndex];
switch ([string characterAtIndex:characterIndex]) {
case ' ': {
NSFont* font = [storage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
[self replaceGlyphAtIndex:glyphIndex withGlyph:[font glyphWithName:#"periodcentered"]];
break;
}
case '\n': {
NSFont* font = [storage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
[self replaceGlyphAtIndex:glyphIndex withGlyph:[font glyphWithName:#"carriagereturn"]];
break;
}
}
}
[super drawGlyphsForGlyphRange:range atPoint:point];
}
#end
To install, use:
[myTextView.textContainer replaceLayoutManager:[[GILayoutManager alloc] init]];
To find font glyph names, you have to go to CoreGraphics:
CGFontRef font = CGFontCreateWithFontName(CFSTR("Menlo-Regular"));
for (size_t i = 0; i < CGFontGetNumberOfGlyphs(font); ++i) {
printf("%s\n", [CFBridgingRelease(CGFontCopyGlyphNameForGlyph(font, i)) UTF8String]);
}

Have a look at the NSLayoutManager class. Your NSTextView will have a layout manager associated with it, and the layout manager is responsible for associating a character (space, tab, etc.) with a glyph (the image of that character drawn on the screen).
In your case, you would probably be most interested in the replaceGlyphAtIndex:withGlyph: method, which would allow you to replace individual glyphs.

I wrote a text editor a few years back - here's some meaningless code that should get you looking in (hopefully) the right direction (this is an NSLayoutManager subclass btw - and yes I know it's leaking like the proverbial kitchen sink):
- (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)containerOrigin
{
if ([[[[MJDocumentController sharedDocumentController] currentDocument] editor] showInvisibles])
{
//init glyphs
unichar crlf = 0x00B6;
NSString *CRLF = [[NSString alloc] initWithCharacters:&crlf length:1];
unichar space = 0x00B7;
NSString *SPACE = [[NSString alloc] initWithCharacters:&space length:1];
unichar tab = 0x2192;
NSString *TAB = [[NSString alloc] initWithCharacters:&tab length:1];
NSString *docContents = [[self textStorage] string];
NSString *glyph;
NSPoint glyphPoint;
NSRect glyphRect;
NSDictionary *attr = [[NSDictionary alloc] initWithObjectsAndKeys:[NSUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:#"invisiblesColor"]], NSForegroundColorAttributeName, nil];
//loop thru current range, drawing glyphs
int i;
for (i = glyphRange.location; i < NSMaxRange(glyphRange); i++)
{
glyph = #"";
//look for special chars
switch ([docContents characterAtIndex:i])
{
//space
case ' ':
glyph = SPACE;
break;
//tab
case '\t':
glyph = TAB;
break;
//eol
case 0x2028:
case 0x2029:
case '\n':
case '\r':
glyph = CRLF;
break;
//do nothing
default:
glyph = #"";
break;
}
//should we draw?
if ([glyph length])
{
glyphPoint = [self locationForGlyphAtIndex:i];
glyphRect = [self lineFragmentRectForGlyphAtIndex:i effectiveRange:NULL];
glyphPoint.x += glyphRect.origin.x;
glyphPoint.y = glyphRect.origin.y;
[glyph drawAtPoint:glyphPoint withAttributes:attr];
}
}
}
[super drawGlyphsForGlyphRange:glyphRange atPoint:containerOrigin];
}

I solved the problem of converting between NSGlyphs and the corresponding unichar in the NSTextView. The code below works beautifully and replaces spaces with bullets for visible text:
- (void)drawGlyphsForGlyphRange:(NSRange)range atPoint:(NSPoint)origin
{
NSFont *font = [[CURRENT_TEXT_VIEW typingAttributes]
objectForKey:NSFontAttributeName];
NSGlyph bullet = [font glyphWithName:#"bullet"];
for (int i = range.location; i != range.location + range.length; i++)
{
unsigned charIndex = [self characterIndexForGlyphAtIndex:i];
unichar c =[[[self textStorage] string] characterAtIndex:charIndex];
if (c == ' ')
[self replaceGlyphAtIndex:charIndex withGlyph:bullet];
}
[super drawGlyphsForGlyphRange:range atPoint:origin];
}

Perhaps -[NSLayoutManager setShowsControlCharacters:] and/or -[NSLayoutManager setShowsInvisibleCharacters:] will do what you want.

Here is Pol's solution in Swift:
class MyLayoutManager: NSLayoutManager {
override func drawGlyphsForGlyphRange(glyphsToShow: NSRange, atPoint origin: NSPoint) {
if let storage = self.textStorage {
let s = storage.string
let startIndex = s.startIndex
for var glyphIndex = glyphsToShow.location; glyphIndex < glyphsToShow.location + glyphsToShow.length; glyphIndex++ {
let characterIndex = self.characterIndexForGlyphAtIndex(glyphIndex)
let ch = s[startIndex.advancedBy(characterIndex)]
switch ch {
case " ":
let attrs = storage.attributesAtIndex(characterIndex, effectiveRange: nil)
if let font = attrs[NSFontAttributeName] {
let g = font.glyphWithName("periodcentered")
self.replaceGlyphAtIndex(glyphIndex, withGlyph: g)
}
case "\n":
let attrs = storage.attributesAtIndex(characterIndex, effectiveRange: nil)
if let font = attrs[NSFontAttributeName] {
// let g = font.glyphWithName("carriagereturn")
let g = font.glyphWithName("paragraph")
self.replaceGlyphAtIndex(glyphIndex, withGlyph: g)
}
case "\t":
let attrs = storage.attributesAtIndex(characterIndex, effectiveRange: nil)
if let font = attrs[NSFontAttributeName] {
let g = font.glyphWithName("arrowdblright")
self.replaceGlyphAtIndex(glyphIndex, withGlyph: g)
}
default:
break
}
}
}
super.drawGlyphsForGlyphRange(glyphsToShow, atPoint: origin)
}
}
And to list the glyph names:
func listFonts() {
let font = CGFontCreateWithFontName("Menlo-Regular")
for var i:UInt16 = 0; i < UInt16(CGFontGetNumberOfGlyphs(font)); i++ {
if let name = CGFontCopyGlyphNameForGlyph(font, i) {
print("name: \(name) at index \(i)")
}
}
}

Related

NSTextField with condensed font line spacingā€¦

I'm trying to understand how to properly display text with condensed line spacing in a text field. When I set paragraph style properties lineHeightMultiple, maximumLineHeight, and minimumLineHeight I can achieve the effect of condensing the lines, but one side effect is that the top line of text just gets clipped off. So I thought that I'd just be able to move the text down with NSBaselineOffsetAttributeName (using a negative value), but that doesn't seem to have any effect. I'm using a line height here of 70% of the point size, but the clipping gets far worse the more condensed it gets.
1) Is there a better way to produce a condensed font line spacing?
2) Or how would you move the text rendering downward so it doesn't get clipped.
<update>
Ok my answer below does address a solution when using NSTextField's. But this obviously doesn't work for NSTextView's too. I tired to override the baselineOffset in the NSLayoutManagerDelegate's shouldSetLineFragmentRect... method, but it also ignores baseline adjustments. Anyone have any suggestions when working with the NSTextView?
</update>
Thanks!
Here's the test project I'm working with https://www.dropbox.com/s/jyshqeuirujf71g/WhatThe.zip?dl=0
Codez:
self.label.wantsLayer = YES;
self.label.backgroundColor = [NSColor whiteColor];
self.label.hidden = NO;
self.label.maximumNumberOfLines = 0;
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
NSFont *font = [NSFont systemFontOfSize:80.0f];
CGFloat lineHeight = font.pointSize * .7f;
CGFloat natualLineHeight = font.ascender + ABS(font.descender) + font.leading;
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentLeft;
paragraphStyle.lineHeightMultiple = lineHeight / natualLineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.paragraphSpacing = 0.0f;
paragraphStyle.allowsDefaultTighteningForTruncation = paragraphStyle.lineBreakMode != NSLineBreakByWordWrapping && paragraphStyle.lineBreakMode != NSLineBreakByCharWrapping && paragraphStyle.lineBreakMode != NSLineBreakByClipping;
result[NSParagraphStyleAttributeName] = paragraphStyle;
result[NSKernAttributeName] = #(0.0f);
result[NSBaselineOffsetAttributeName] = #(-50.0f);
result[NSFontAttributeName] = font;
result[NSForegroundColorAttributeName] = [NSColor blackColor];
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:#"Hello\nThere" attributes:result];
self.label.attributedStringValue = attributedString;
Ok. By subclassing NSTextFieldCell I was able to offset the text correctly. It's a shame that this method works nicely in iOS-land. Maybe this will work when the unified Mac/iOS UI APIs are released this summer. šŸ˜
This will remove any negative baseline values from the string before it draws and draw inside a shifted rect.
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSRect titleRect = [self titleRectForBounds:cellFrame];
NSMutableAttributedString *string = [self.attributedStringValue mutableCopy];
__block CGFloat baselineOffset = 0.0f;
[string enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
NSNumber *offsetValue = attrs[NSBaselineOffsetAttributeName];
if (offsetValue != nil && offsetValue.floatValue < 0.0f) {
baselineOffset = MIN(baselineOffset, offsetValue.floatValue);
[string removeAttribute:NSBaselineOffsetAttributeName range:range];
}
}];
titleRect.origin.y -= baselineOffset;
[string drawInRect:titleRect];
}

Several Hyperlinks in NSTableView Cell

At the moment I have an NSTableView with a custom NSTextFieldCell that holds an NSAttributedString with some ranges with the NSLinkAttribute. I tried to integrate code from Apple's TableViewLinks example and Toomas Vather's HyperlinkTextField.
I implemented the -trackMouse Function like this:
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)flag {
BOOL result = YES;
NSUInteger hitTestResult = [self hitTestForEvent:theEvent inRect:cellFrame ofView:controlView];
if ((hitTestResult & NSCellHitContentArea) != 0) {
result = [super trackMouse:theEvent inRect:cellFrame ofView:controlView untilMouseUp:flag];
theEvent = [NSApp currentEvent];
hitTestResult = [self hitTestForEvent:theEvent inRect:cellFrame ofView:controlView];
if ((hitTestResult & NSCellHitContentArea) != 0) {
NSAttributedString* attrValue = [self.objectValues objectForKey:#"theAttributedString"];
NSMutableAttributedString* attributedStringWithLinks = [[NSMutableAttributedString alloc] initWithAttributedString:attrValue];
//HOW TO GET A RIGHT INDEX?
NSTableView* myTableView = (NSTableView *)[self controlView];
NSPoint eventPoint = [myTableView convertPoint:[theEvent locationInWindow] fromView:nil];
NSInteger myRow = [myTableView rowAtPoint:eventPoint];
NSRect myBetterViewRect = [myTableView rectOfRow:myRow];
__block NSTextView* myTextView = [[NSTextView alloc] initWithFrame:myBetterViewRect];
[myTextView.textStorage setAttributedString:attributedStringWithLinks];
NSPoint localPoint = [myTextView convertPoint:eventPoint fromView:myTableView];
NSUInteger index = [myTextView.layoutManager characterIndexForPoint:localPoint inTextContainer:myTextView.textContainer fractionOfDistanceBetweenInsertionPoints:NULL];
if (index != NSNotFound)
{
NSMutableArray* myHyperlinkInfos = [[NSMutableArray alloc] init];
NSRange stringRange = NSMakeRange(0, [attrValue length]);
[attrValue enumerateAttribute:NSLinkAttributeName inRange:stringRange options:0 usingBlock:^(id value, NSRange range, BOOL* stop)
{
if (value)
{
NSUInteger rectCount = 0;
NSRectArray rectArray = [myTextView.layoutManager rectArrayForCharacterRange:range withinSelectedCharacterRange:range inTextContainer:myTextView.textContainer rectCount:&rectCount];
for (NSUInteger i = 0; i < rectCount; i++)
{
[myHyperlinkInfos addObject:#{kHyperlinkInfoCharacterRangeKey : [NSValue valueWithRange:range], kHyperlinkInfoURLKey : value, kHyperlinkInfoRectKey : [NSValue valueWithRect:rectArray[i]]}];
}
}
}];
for (NSDictionary* info in myHyperlinkInfos)
{
NSRange range = [[info objectForKey:kHyperlinkInfoCharacterRangeKey] rangeValue];
if (NSLocationInRange(index, range))
{
NSURL* url = [NSURL URLWithString:[info objectForKey:kHyperlinkInfoURLKey]];
[[NSWorkspace sharedWorkspace] openURL:url];
}
}
}
}
}
return result;}
The character-Index when clicking into the cell's (nstextview's) text appears not to fit. So even if there are more than one link in the text, usually the last link is opened. My guess is that I donĀ“t get the nsrect of the clicked cell. If so, how could I get the right NSRect?
I am glad for any suggestions, comments, code pieces - or simpler solutions (even if this would include switching to a view-based tableview).
Thanks.

NSAttributedString highlight/background color shows between lines (ugly)

I'm trying to nicely display paragraphs of highlighted in a NSTextView. Right now, I'm doing this by creating a NSAttributedString with a background color. Here's some simplified code:
NSDictionary *attributes = #{NSBackgroundColorAttributeName:NSColor.greenColor};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:#"Here is a single line of text with single spacing" attributes:attributes];
[textView.textStorage setAttributedString:attrString];
This approach basically works, in that it produces highlighted text.
Unfortunately, when multiple lines exist, the highlight covers the vertical space between the lines in addition to the lines themselves, resulting in ugliness.
Does anyone know of a way to do this kind of highlighting in Cocoa? The picture below is basically what I'm looking for (ignore the shadow on the white boxes):
I'd be willing to use CoreText, html, or whatever is necessary to make things look nicer.
You will need to subclass NSLayoutManager and override:
- (void)fillBackgroundRectArray:(const CGRect *)rectArray
count:(NSUInteger)rectCount
forCharacterRange:(NSRange)charRange
color:(UIColor *)color;
This is the primitive method for drawing background color rectangles.
Try this:-
-(IBAction)chooseOnlylines:(id)sender
{
NSString *allTheText =[tv string];
NSArray *lines = [allTheText componentsSeparatedByString:#"\n"];
NSString *str=[[NSString alloc]init];
NSMutableAttributedString *attr;
BOOL isNext=YES;
[tv setString:#""];
for (str in lines)
{
attr=[[NSMutableAttributedString alloc]initWithString:str];
if ([str length] > 0)
{
NSRange range=NSMakeRange(0, [str length]);
[attr addAttribute:NSBackgroundColorAttributeName value:[NSColor greenColor] range:range];
[tv .textStorage appendAttributedString:attr];
isNext=YES;
}
else
{
NSString *str=#"\n";
NSAttributedString *attr=[[NSAttributedString alloc]initWithString:str];
[tv .textStorage appendAttributedString:attr];
isNext=NO;
}
if (isNext==YES)
{
NSString *str=#"\n";
NSAttributedString *attr=[[NSAttributedString alloc]initWithString:str];
[tv .textStorage appendAttributedString:attr];
}
}
}
The paragraph needs to be highlighted when user taps on it. this is how I implemented it and don't confuse with the highlight color, it is a custom NSAttributedString key I created for this purpose.
extension NSAttributedString.Key {
public static let highlightColor = NSAttributedString.Key.init("highlightColor")
}
class ReaderLayoutManager: NSLayoutManager {
// MARK: - Draw Background
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
self.enumerateLineFragments(forGlyphRange: glyphsToShow) { (_, usedRect, _, range, _) in
guard let highlightColor = self.currentHighlightColor(range: range) else { return }
guard let context = UIGraphicsGetCurrentContext() else { return }
var lineRect = usedRect
lineRect.origin.y += 10
lineRect.size.height -= 2
context.saveGState()
let path = UIBezierPath(roundedRect: lineRect, cornerRadius: 2)
highlightColor.setFill()
path.fill()
context.restoreGState()
}
}
private func currentHighlightColor(range: NSRange) -> UIColor? {
guard let textStorage = textStorage else { return nil }
guard let highlightColor = textStorage.attributes(at: range.location, effectiveRange: nil)[.highlightColor] as? UIColor else { return nil }
return highlightColor
}
}
when user clicks on it, I set the highlight color for the range and reset the TextView.
attributedString.addAttributes([.highlightColor: theme.textUnderlineColor], range: range)

how to toggle rich text formatting in NSTextView programmatically in cocoa

I want to toggle rich text formatting in NSTextView. I have tried following:
[contentView setRichText:NO];
[contentView setImportsGraphics:NO];
but, that didn't changed the NSTextView content and still allowing to do the text formatting.
Please let me know the simple way to toggle/switch rich text formatting in NSTextView just like TextEdit.
I already check the "TextEdit" sample project, but it seems to be very hard to find the usable code from it.
Thanks
Found some help from following link.
click here to see solution
Based on solution given in above link, i have created category for my view controller as follows:
#define TabWidth #"TabWidth"
#interface MyViewController (Helper)
- (NSDictionary *)defaultTextAttributes:(BOOL)forRichText;
- (void)removeAttachments;
- (void)setRichText:(BOOL)flag;
#end
#implementation MyViewController (Helper)
- (NSDictionary *)defaultTextAttributes:(BOOL)forRichText {
static NSParagraphStyle *defaultRichParaStyle = nil;
NSMutableDictionary *textAttributes = [[[NSMutableDictionary alloc] initWithCapacity:2] autorelease];
if (forRichText) {
[textAttributes setObject:[NSFont userFontOfSize:0.0] forKey:NSFontAttributeName];
if (defaultRichParaStyle == nil) { // We do this once...
NSInteger cnt;
NSString *measurementUnits = [[NSUserDefaults standardUserDefaults] objectForKey:#"AppleMeasurementUnits"];
CGFloat tabInterval = ([#"Centimeters" isEqual:measurementUnits]) ? (72.0 / 2.54) : (72.0 / 2.0); // Every cm or half inch
NSMutableParagraphStyle *paraStyle = [[[NSMutableParagraphStyle alloc] init] autorelease];
[paraStyle setTabStops:[NSArray array]]; // This first clears all tab stops
for (cnt = 0; cnt < 12; cnt++) { // Add 12 tab stops, at desired intervals...
NSTextTab *tabStop = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:tabInterval * (cnt + 1)];
[paraStyle addTabStop:tabStop];
[tabStop release];
}
defaultRichParaStyle = [paraStyle copy];
}
[textAttributes setObject:defaultRichParaStyle forKey:NSParagraphStyleAttributeName];
} else {
NSFont *plainFont = [NSFont userFixedPitchFontOfSize:0.0];
NSInteger tabWidth = [[NSUserDefaults standardUserDefaults] integerForKey:TabWidth];
CGFloat charWidth = [#" " sizeWithAttributes:[NSDictionary dictionaryWithObject:plainFont forKey:NSFontAttributeName]].width;
if (charWidth == 0) charWidth = [[plainFont screenFontWithRenderingMode:NSFontDefaultRenderingMode] maximumAdvancement].width;
// Now use a default paragraph style, but with the tab width adjusted
NSMutableParagraphStyle *mStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
[mStyle setTabStops:[NSArray array]];
[mStyle setDefaultTabInterval:(charWidth * tabWidth)];
[textAttributes setObject:[[mStyle copy] autorelease] forKey:NSParagraphStyleAttributeName];
// Also set the font
[textAttributes setObject:plainFont forKey:NSFontAttributeName];
}
return textAttributes;
}
/* Used when converting to plain text
*/
- (void)removeAttachments {
NSTextStorage *attrString = [contentView textStorage];
NSUInteger loc = 0;
NSUInteger end = [attrString length];
[attrString beginEditing];
while (loc < end) { /* Run through the string in terms of attachment runs */
NSRange attachmentRange; /* Attachment attribute run */
NSTextAttachment *attachment = [attrString attribute:NSAttachmentAttributeName atIndex:loc longestEffectiveRange:&attachmentRange inRange:NSMakeRange(loc, end-loc)];
if (attachment) { /* If there is an attachment and it is on an attachment character, remove the character */
unichar ch = [[attrString string] characterAtIndex:loc];
if (ch == NSAttachmentCharacter) {
if ([contentView shouldChangeTextInRange:NSMakeRange(loc, 1) replacementString:#""]) {
[attrString replaceCharactersInRange:NSMakeRange(loc, 1) withString:#""];
[contentView didChangeText];
}
end = [attrString length]; /* New length */
}
else loc++; /* Just skip over the current character... */
}
else loc = NSMaxRange(attachmentRange);
}
[attrString endEditing];
}
- (void)setRichText:(BOOL)flag {
NSDictionary *textAttributes;
BOOL isRichText = flag;
if (!isRichText) [self removeAttachments];
[contentView setRichText:isRichText];
[contentView setUsesRuler:isRichText]; /* If NO, this correctly gets rid
of the ruler if it was up */
if (isRichText && NO)
[contentView setRulerVisible:YES]; /* Show ruler if rich, and desired */
[contentView setImportsGraphics:isRichText];
textAttributes = [self defaultTextAttributes:isRichText];
if ([[contentView textStorage] length]) {
[[contentView textStorage] setAttributes:textAttributes range: NSMakeRange(0,[[contentView textStorage] length])];
}
[contentView setTypingAttributes:textAttributes];
}
#end
Where contentView is IBOutlet of NSTextView. Hope this will help someone or let me know if someone has shorter method.
Thanks

Strip Non-Alphanumeric Characters from an NSString

I'm looking for a quick and easy way to strip non-alphanumeric characters from an NSString. Probably something using an NSCharacterSet, but I'm tired and nothing seems to return a string containing only the alphanumeric characters in a string.
We can do this by splitting and then joining. Requires OS X 10.5+ for the componentsSeparatedByCharactersInSet:
NSCharacterSet *charactersToRemove = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
NSString *strippedReplacement = [[someString componentsSeparatedByCharactersInSet:charactersToRemove] componentsJoinedByString:#""];
In Swift, the componentsJoinedByString is replaced by join(...), so here it just replaces non-alphanumeric characters with a space.
let charactersToRemove = NSCharacterSet.alphanumericCharacterSet().invertedSet
let strippedReplacement = " ".join(someString.componentsSeparatedByCharactersInSet(charactersToRemove))
For Swift2 ...
var enteredByUser = field.text .. or whatever
let unsafeChars = NSCharacterSet.alphanumericCharacterSet().invertedSet
enteredByUser = enteredByUser
.componentsSeparatedByCharactersInSet(unsafeChars)
.joinWithSeparator("")
If you want to delete just the one character, for example delete all returns...
enteredByUser = enteredByUser
.componentsSeparatedByString("\n")
.joinWithSeparator("")
What I wound up doing was creating an NSCharacterSet and the -invertedSet method that I found (it's a wonder what an extra hour of sleep does for documentation-reading abilities). Here's the code snippet, assuming that someString is the string from which you want to remove non-alphanumeric characters:
NSCharacterSet *charactersToRemove =
[[ NSCharacterSet alphanumericCharacterSet ] invertedSet ];
NSString *trimmedReplacement =
[ someString stringByTrimmingCharactersInSet:charactersToRemove ];
trimmedReplacement will then contain someString's alphanumeric characters.
Swift 3 version of accepted answer:
let unsafeChars = CharacterSet.alphanumerics.inverted
let myStrippedString = myString.components(separatedBy: unsafeChars).joined(separator: "")
Swift 5, Extension:
extension String {
/// Will strip all non alpha characters from a string
public var alpha: String {
return components(separatedBy: CharacterSet.alphanumerics.inverted).joined()
}
}
A Cleanup Category
I have a method call stringByStrippingCharactersInSet: and stringByCollapsingWhitespace that might be convenient to just drop-in.
#implementation NSString (Cleanup)
- (NSString *)clp_stringByStrippingCharactersInSet:(NSCharacterSet *)set
{
return [[self componentsSeparatedByCharactersInSet:set] componentsJoinedByString:#""];
}
- (NSString *)clp_stringByCollapsingWhitespace
{
NSArray *components = [self componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
components = [components filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"self <> ''"]];
return [components componentsJoinedByString:#" "];
}
#end
Hereā€™s a Swift version of Cameronā€™s category as an extension:
extension String {
func stringByStrippingCharactersInSet(set:NSCharacterSet) -> String
{
return (self.componentsSeparatedByCharactersInSet(set) as NSArray).componentsJoinedByString("")
}
func stringByCollapsingWhitespace() -> String
{
var components:NSArray = self.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
let predicate = NSPredicate(format: "self <> ''", argumentArray: nil)
components = components.filteredArrayUsingPredicate(predicate)
return components.componentsJoinedByString(" ")
}
}
The plain cycle would be the faster execution time I think:
#implementation NSString(MyUtil)
- (NSString*) stripNonNumbers {
NSMutableString* res = [NSMutableString new];
//NSCharacterSet *numericSet = [NSCharacterSet decimalDigitCharacterSet];
for ( int i=0; i < self.length; ++i ) {
unichar c = [self characterAtIndex:i];
if ( c >= '0' && c <= '9' ) // this looks cleaner, but a bit slower: [numericSet characterIsMember:c])
[res appendFormat:#"%c", c];
}
return res;
}
#end
This is a more effective way than the provided answer
+ (NSString *)alphanumericString:(NSString *)s {
NSCharacterSet * charactersToRemove = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
NSMutableString * ms = [NSMutableString stringWithCapacity:[s length]];
for (NSInteger i = 0; i < s.length; ++i) {
unichar c = [s characterAtIndex:i];
if (![charactersToRemove characterIsMember:c]) {
[ms appendFormat:#"%c", c];
}
}
return ms;
}
or as a Category
#implementation NSString (Alphanumeric)
- (NSString *)alphanumericString {
NSCharacterSet * charactersToRemove = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
NSMutableString * ms = [NSMutableString stringWithCapacity:[self length]];
for (NSInteger i = 0; i < self.length; ++i) {
unichar c = [self characterAtIndex:i];
if (![charactersToRemove characterIsMember:c]) {
[ms appendFormat:#"%c", c];
}
}
return ms;
}
#end

Resources