Changing the width of the space character in NSTextView - objective-c

I’m trying to make a reader application to help a girl with reading difficulties. Some research shows that just changing the colors of the text, background and shadow can really help kids out so I’m trying to allow her to do that. It’s just a big NSTextView with buttons so she can change the font size, color, background color, shadow properties, letter spacing, line spacing and word spacing. I know you can do most of this just using Word but I’m trying to make it as intuitive/fun as possible for her.
The place where I could use a hand is in changing the size of the spacing between words. Currently I’m just searching for a string of spaces equal to the number of spaces I expect to be there and then replacing with more or less spaces it as follows:
- (IBAction)increaseSpacing:(id)sender{
NSInteger spacing = [[NSUserDefaults standardUserDefaults] integerForKey:#"wordSpacing"];
NSMutableString * oldString = [ NSMutableString stringWithCapacity:0];
NSMutableString * newString =[ NSMutableString stringWithCapacity:0];
for (int i = 0; i < spacing; i+=1) {
[oldString appendString:#" "];
}
[newString setString:oldString];
[newString appendString:#" "];
[[[textView textStorage] mutableString] replaceOccurrencesOfString:oldString
withString:newString options:0
range:NSMakeRange(0, [[textView textStorage] length])];
spacing += 1;
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInteger: spacing] forKey:#"wordSpacing"];
}
- (IBAction)reduceSpacing:(id)sender{
NSInteger spacing = [[NSUserDefaults standardUserDefaults] integerForKey:#"wordSpacing"];
if (spacing > 1) {
NSMutableString * oldString = [ NSMutableString stringWithCapacity:0];
NSMutableString * newString =[ NSMutableString stringWithCapacity:0];
for (int i = 0; i < spacing-1; i+=1) {
[newString appendString:#" "];
}
[oldString setString:newString];
[oldString appendString:#" "];
[[[textView textStorage] mutableString] replaceOccurrencesOfString:oldString
withString:newString options:0
range:NSMakeRange(0, [[textView textStorage] length])];
spacing -= 1;
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInteger: spacing] forKey:#"wordSpacing"];
}
}
This approach feels sloppy to me, especially when moving the cursor around with arrow keys. I could just change the font size of a space character when it’s typed, but that would also change the line height. Is there a way that I can just change the width of the space character? Thanks in advance for your help.

My eventual solution was to swap out spaces for blank images (blanks) that have the adjusted width.
Basic components:
a) Method to replace spaces with blanks
b) Method to replace blanks with spaces
c) NSValueTransformer for the NSTextView to do (a) for transformedValue and (b) for reverseTransformedValue
d) NSTextViewDelegate to do (a) when the text changes
e) Subclass NSTextView to do (b) on copied or cut text before sending to pasteboard
f) Action assigned to the stepper to make the size changes
Code for each part is below:
a) AppDelegate method to replace spaces with blanks
- (NSAttributedString * ) replaceSpacesWithBlanks:(NSString *)replaceString {
CGFloat imageWidth = [[NSUserDefaults standardUserDefaults] integerForKey:#"wordSpacing"];
NSImage * pic = [[NSImage alloc] initWithSize:NSMakeSize(imageWidth, 1.0f)];
NSTextAttachmentCell *attachmentCell = [[NSTextAttachmentCell alloc] initImageCell:pic];
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
[attachment setAttachmentCell: attachmentCell ];
NSAttributedString *replacementString = [NSAttributedString attributedStringWithAttachment: attachment];
NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithString:replaceString];
NSRange range = [[mutableString string] rangeOfString:#" "];
while (range.location != NSNotFound) {
[mutableString replaceCharactersInRange:range withAttributedString:replacementString];
range = [[mutableString string] rangeOfString:#" "];
}
return [[NSAttributedString alloc] initWithAttributedString: mutableString];
}
b) AppDelegate method to replace blanks with spaces
- (NSString * ) replaceBlanksWithSpaces:(NSAttributedString *)replaceAttributedString {
NSMutableAttributedString * mutAttrString = [[NSMutableAttributedString alloc] initWithAttributedString:replaceAttributedString];
for (int index = 0; index < mutAttrString.length; index += 1) {
NSRange theRange;
NSDictionary * theAttributes = [mutAttrString attributesAtIndex:index effectiveRange:&theRange];
NSTextAttachment *theAttachment = [theAttributes objectForKey:NSAttachmentAttributeName];
if(theAttachment != NULL) {
[mutAttrString replaceCharactersInRange:theRange withString:#" "];
}
}
return mutAttrString.string;
}
c) NSValueTransformer for the NSTextView to replace spaces with blanks for transformedValue and replace blanks with spaces for reverseTransformedValue
#implementation DBAttributedStringTransformer
- (id)init
{
self = [super init];
if (self) {
appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
}
return self;
}
+ (Class)transformedValueClass
{
return [NSAttributedString class];
}
+ (BOOL)allowsReverseTransformation
{
return YES;
}
- (id)transformedValue:(id)value
{
return [appDelegate replaceSpacesWithBlanks:value];
}
- (id)reverseTransformedValue:(id)value
{
return [appDelegate replaceBlanksWithSpaces:value];
}
d) NSTextViewDelegate to replace spaces with blanks when the text changes
#implementation DBTextViewDelegate
-(void)awakeFromNib {
appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
}
- (void)textViewDidChangeSelection:(NSNotification *)aNotification{
// Need to keep track of where the cursor should be reinserted
textLength = myTextView.string.length;
insertionPoint = [[[myTextView selectedRanges] objectAtIndex:0] rangeValue].location;
}
//replaces spaces with blank image and puts cursor back in correct position
- (void)textDidChange:(NSNotification *)aNotification{
NSInteger newTextLength = myTextView.string.length;
NSInteger newInsertionPoint = insertionPoint + newTextLength - textLength;
NSString * stringValue = [[NSUserDefaults standardUserDefaults] stringForKey:#"textViewString"];
NSAttributedString * attrStringWithBlanks = [[ NSAttributedString alloc] initWithAttributedString:[appDelegate replaceSpacesWithBlanks:stringValue ]];
NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:attrStringWithBlanks];
[myTextView.textStorage setAttributedString: mutableString];
//Put the cursor back where it was
[myTextView setSelectedRange:NSMakeRange(newInsertionPoint, 0)];
}
e) Subclass NSTextView to replace blanks with spaces on copied or cut text before writing to pasteboard
#implementation DBTextView
-(void)awakeFromNib {
appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
}
-(void) selectedTextToClipBoard{
NSRange selectedRange = [self selectedRange];
NSAttributedString * selectedText = [[self textStorage] attributedSubstringFromRange: selectedRange];
NSString * textWithoutBlanks = [appDelegate replaceBlanksWithSpaces:selectedText];
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSArray *copiedObject = [NSArray arrayWithObject:textWithoutBlanks];
[pasteboard writeObjects:copiedObject];
}
-(void) copy:(id)sender{
[self selectedTextToClipBoard];
}
-(void) cut:(id)sender{
[self selectedTextToClipBoard];
// Delete selected text so it acts like a cut
NSRange selectedRange = [self selectedRange];
[[self textStorage] deleteCharactersInRange:selectedRange];
}
f) Action assigned to the stepper to make the size changes
- (IBAction)changeWordSpacing:(id)sender {
CGFloat imageWidth = [[NSUserDefaults standardUserDefaults] integerForKey:#"wordSpacing"];
NSImage * pic = [[NSImage alloc] initWithSize:NSMakeSize(imageWidth, 1.0f)];
NSTextAttachmentCell *attachmentCell = [[NSTextAttachmentCell alloc] initImageCell:pic];
NSMutableAttributedString * mutAttrString = [[NSMutableAttributedString alloc] initWithAttributedString:[textView textStorage]];
for (int index = 0; index < mutAttrString.length; index += 1) {
NSRange theRange;
NSDictionary * theAttributes = [mutAttrString attributesAtIndex:index effectiveRange:&theRange];
NSTextAttachment *theAttachment = [theAttributes objectForKey:NSAttachmentAttributeName];
if(theAttachment != NULL) {
[theAttachment setAttachmentCell: attachmentCell ];
}
}
[[textView textStorage] setAttributedString:mutAttrString];
}
Also, NSTextView should be set to “Continuously Updates Value”

It is possible to adjust the font kerning specifically for space characters. Here is a simple way to do that using the new AttributedString:
var searchRange = text.startIndex..<text.endIndex
while let range = text[searchRange].range(of: " ") {
text[range].mergeAttributes(AttributeContainer([.kern: 10]))
searchRange = range.upperBound..<text.endIndex
}
You may use text[range].kern = 10 if you are using SwiftUI's Text view, but as of Xcode 13.4 the SwiftUI.Kern attribute created in that way will not convert properly for NSAttributedStrings.

Related

How to format UITextField with NSNumberFormatter?

Hope this helps others, since I haven't seen any similar post on the web that shows how to format a text field using NSNumberFormatter but at the same time keep the UITextField cursor position to where it should naturally be. Those, because after formatting, the NSString from inside the UITextField and setting it back to the field you end up with the cursor placed an the end of the field.
Also it will be nice to convert it to Swift for those that needs it.
And here is my answer to the issue, I am using a UIKeyboardTypeNumberPad, but also it will work fine with a UIKeyboardTypeDecimalPad, if other keyboard types are used, feel free to add a regex before using the next code:
- (int)getCharOccurencies:(NSString *)character inString:(NSString *)string{
NSMutableArray *characters = [[NSMutableArray alloc] initWithCapacity:[string length]];
for (int i=0; i < [string length]; i++) {
NSString *ichar = [NSString stringWithFormat:#"%c", [string characterAtIndex:i]];
[characters addObject:ichar];
}
int count = 0;
for (NSString *ichar in characters) {
if ([ichar isEqualToString:character]) {
count++;
}
}
return count;
}
- (void)selectTextForInput:(UITextField *)input atRange:(NSRange)range {
UITextPosition *start = [input positionFromPosition:[input beginningOfDocument]
offset:range.location];
UITextPosition *end = [input positionFromPosition:start
offset:range.length];
[input setSelectedTextRange:[input textRangeFromPosition:start toPosition:end]];
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{
NSString *charUsedToFormat = #",";
NSMutableString *textString = [NSMutableString stringWithString:[textField text]];
[textField setTintColor:[UIColor darkGrayColor]];
if ([string isEqualToString:charUsedToFormat]) {
return NO;
}
if (range.location == 0 && [string isEqualToString:#"0"]) {
return NO;
}
if ([[textString stringByReplacingCharactersInRange:range withString:string] length] == 0) {
textField.text = #"";
[self selectTextForInput:textField atRange:NSMakeRange(0, 0)];
return NO;
}
NSString *replacebleString = [textString substringWithRange:range];
if (string.length == 0 && [replacebleString isEqualToString:charUsedToFormat]) {
NSRange newRange = NSMakeRange( range.location - 1, range.length);
range = newRange;
textString = [NSMutableString stringWithString:[textString stringByReplacingCharactersInRange:newRange withString:string]];
}else{
textString = [NSMutableString stringWithString:[textString stringByReplacingCharactersInRange:range withString:string]];
}
int commmaCountBefore = [self getCharOccurencies:charUsedToFormat inString:textString];
textString = [NSMutableString stringWithString:[textString stringByReplacingOccurrencesOfString:charUsedToFormat withString:#""]];
NSNumber *firstNumber = [NSNumber numberWithDouble:[textString doubleValue]];
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
//just in case
[formatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[formatter setGroupingSeparator:charUsedToFormat];
[formatter setDecimalSeparator:#""];
[formatter setMaximumFractionDigits:0];
textString = [NSMutableString stringWithString:[formatter stringForObjectValue:firstNumber]];
textField.text = textString;
int commmaCountAfter = [self getCharOccurencies:charUsedToFormat inString:textString];
int commaDif = commmaCountAfter - commmaCountBefore;
int cursorPossition = (int)range.location + (int)string.length + commaDif;
//set cursor position
NSLog(#"cursorPossition: %d", cursorPossition);
[self selectTextForInput:textField atRange:NSMakeRange(cursorPossition, 0)];
return NO;
}

Annotations on MapView: Coordinates

I have two NSStrings (address, and key) which contain the coordinates (longitude and latitude) in form of numbers (34,56789...):
NSString *key = [allKeys objectAtIndex:i];
NSObject *obj = [DictionaryMap objectForKey:key];
NSString *address = [NSString stringWithFormat:#"%#", obj];
CLLocationCoordinate2D anyLocation;
anyLocation.latitude = [address doubleValue];
anyLocation.longitude = [key doubleValue];
MKPointAnnotation *annotationPoint2 = [[MKPointAnnotation alloc] init]; annotationPoint2.coordinate = anyLocation;
annotationPoint2.title = #"Event";
annotationPoint2.subtitle = #"Microsoft's headquarters2";
[mapView addAnnotation:annotationPoint2];
...But I can't understand why it doesn't plot in the same point as the coordinates written. I think this doesn't work:
[address doubleValue]
So I tried replacing it with:
location.latitude = NSNumber/NSString
but it gives an error.
UPDATE:
IN VIEW DID LOAD:
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPressGesture:)];
[self.mapView addGestureRecognizer:longPressGesture];
[mapView.userLocation setTitle:#"I am here"];
..then...
-(void)handleLongPressGesture:(UIGestureRecognizer*)sender {
// This is important if you only want to receive one tap and hold event
if (sender.state == UIGestureRecognizerStateEnded)
{
[self.mapView removeGestureRecognizer:sender];
}
else
{
// Here we get the CGPoint for the touch and convert it to latitude and longitude coordinates to display on the map
CGPoint point = [sender locationInView:self.mapView];
CLLocationCoordinate2D locCoord = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
// Then all you have to do is create the annotation and add it to the map
MKPointAnnotation *annotationPoint = [[MKPointAnnotation alloc] init]; annotationPoint.coordinate = locCoord;
annotationPoint.title = #"Microsoft";
annotationPoint.subtitle = #"Microsoft's headquarters";
[mapView addAnnotation:annotationPoint];
NSString *latitude = [[NSString alloc] initWithFormat:#"%f",locCoord.latitude];
NSString *longitude = [[NSString alloc] initWithFormat:#"%f", locCoord.longitude];
NSLog(latitude);
NSLog(longitude);
[[NSUserDefaults standardUserDefaults]setObject:latitude forKey:#"FolderLatitude"];
[[NSUserDefaults standardUserDefaults]setObject:longitude forKey:#"FolderLongitude"];
}
}
...I then save the coordinates in a JSON file and then read them from the file.
Print out the values that you are setting for lat/lon. It sounds like these values are not being converted to double properly. If the strings are not in "xxx.xxx" format then they will not convert to doubles properly when you call doubleValue.
Finally I understood: This is the correct way to make it work:
NSString *myString = (string initialization here)
float stringFloat = [myString floatValue];
anyLocation.longitude = stringFloat;

Customizing UITextField for Currency format works well with iOS 4.x but in iOS 5 gives null

I am customizing UItextField for local currency symbol and comma, using following link :
http://www.thepensiveprogrammer.com/2010/03/customizing-uitextfield-formatting-for.html
NSNumber *actualNumber = [currencyFormatter numberFromString:[mstring
stringByReplacingOccurrencesOfString:localeSeparator withString:#""]];
In iOS 5 this actual number is always null and in iOS 4.x it is working fine
My code's main method for this purpose is :
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (textField.tag == 1)
{
if(true)
{
NSMutableString* mstring = [[textField text] mutableCopy];
if([mstring length] == 0)
{
//special case...nothing in the field yet, so set a currency symbol first
[mstring appendString:[[NSLocale currentLocale] objectForKey:NSLocaleCurrencySymbol]];
//now append the replacement string
[mstring appendString:string];
}
else
{
//adding a char or deleting?
if([string length] > 0)
{
[mstring insertString:string atIndex:range.location];
}
else
{
//delete case - the length of replacement string is zero for a delete
[mstring deleteCharactersInRange:range];
}
}
NSString* localeSeparator = [[NSLocale currentLocale]
objectForKey:NSLocaleGroupingSeparator];
NSNumber *actualNumber = [currencyFormatter numberFromString:[mstring
stringByReplacingOccurrencesOfString:localeSeparator
withString:#""]];
NSLog(#"%#",actualNumber);
[textField setText:[currencyFormatter stringFromNumber:actualNumber]];
[mstring release];
}
//always return no since we are manually changing the text field
return NO;
}
else
{
return YES;
}
}
and This is the initialization
NSLocale *paklocal = [[[NSLocale alloc] initWithLocaleIdentifier:#"en_PAK"] autorelease];
currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[currencyFormatter setMaximumFractionDigits:0];
[currencyFormatter setLocale:paklocal];
NSMutableCharacterSet *numberSet = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];
[numberSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
nonNumberSet = [[numberSet invertedSet] retain];
[numberSet release];
I think you're having a problem because textField:shouldChangeCharactersInRange:replacementString: adds the currency symbol for [NSLocale currentLocale], which may be different from the locale used by currencyFormatter. In the simulator on my computer, it added $ (dollar) signs, to mstring, which were logically enough rejected by currencyFormatter.
When you construct paklocal, store it along with currencyFormatter and use it instead of [NSLocale currentLocale].
If you have further trouble with currencyFormatter, use NSLog to display the string you send into it.

How to set the UILabel's UIColor from a Plist

the question is in the title, How to set the UILabel's UIColor from a Plist ?
i tried this :
UIColor *colorLabel;
i add a NSString row in my Plist, and wrote redColor as a value but doesnt work...
How can i handle it ?
Thanks guys.
I would personally store the RGBA values instead of a string and then you can just use
+ (UIColor *)colorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha
Do not do the below
Just as an interesting side note the most inflexible way would be to use the UIColor convenience methods like this
[UIColor performSelector:NSSelectorFromString(#"redColor")]
I think you need convert from string to an UIColor. You put colors into your plist by hex-colors (for red - ff0000) and then use something like following function for get UIColor.
+ (UIColor *) colorWithHexString: (NSString *) stringToConvert
{
    NSString *cString = [[stringToConvert stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
    // String should be 6 or 8 characters
    if ([cString length] < 6) return [UIColor blackColor];
    // strip 0X if it appears
    if ([cString hasPrefix:#"0X"]) cString = [cString substringFromIndex:2];
    if ([cString length] != 6) return [UIColor blackColor];
    // Separate into r, g, b substrings
    NSRange range;
    range.location = 0;
    range.length = 2;
    NSString *rString = [cString substringWithRange:range];
    range.location = 2;
    NSString *gString = [cString substringWithRange:range];
    range.location = 4;
    NSString *bString = [cString substringWithRange:range];
    // Scan values
    unsigned int r, g, b;
    [[NSScanner scannerWithString:rString] scanHexInt:&r];
    [[NSScanner scannerWithString:gString] scanHexInt:&g];
    [[NSScanner scannerWithString:bString] scanHexInt:&b];
    
    return [UIColor colorWithRed:((float) r / 255.0f)
                       green:((float) g / 255.0f)
                            blue:((float) b / 255.0f)
                       alpha:1.0f];
}
To preserve human readability, I did a category for this:
#implementation UIColor (EPPZRepresenter)
NSString *NSStringFromUIColor(UIColor *color)
{
const CGFloat *components = CGColorGetComponents(color.CGColor);
return [NSString stringWithFormat:#"[%f, %f, %f, %f]",
components[0],
components[1],
components[2],
components[3]];
}
UIColor *UIColorFromNSString(NSString *string)
{
NSString *componentsString = [[string stringByReplacingOccurrencesOfString:#"[" withString:#""] stringByReplacingOccurrencesOfString:#"]" withString:#""];
NSArray *components = [componentsString componentsSeparatedByString:#", "];
return [UIColor colorWithRed:[(NSString*)components[0] floatValue]
green:[(NSString*)components[1] floatValue]
blue:[(NSString*)components[2] floatValue]
alpha:[(NSString*)components[3] floatValue]];
}
#end
The same formatting that is used by NSStringFromCGAffineTransform. This is actually a part of a bigger scale plist object representer in eppz!kit at GitHub.

How can I convert the characters in an NSString object to UILabel objects?

I'm trying to figure out how to take the individual characters in an NSString object and create UILabels from them, with the UILabel text set to the individual character.
I'm new to Cocoa, but so far I have this...
NSString *myString = #"This is a string object";
for(int i = 0; i < [myString length]; i++)
{
//Store the character
UniChar chr = [myString characterAtIndex:i];
//Stuck here, I need to convert character back to an NSString object so I can...
//Create the UILabel
UILabel *lbl = [[UILabel alloc] initWithFrame....];
[lbl setText:strCharacter];
//Add the label to the view
[[self view] addSubView:lbl];
}
Aside from where I'm stuck, my approach already feels very hackish, but I'm a noob and still learning. Any suggestions for how to approach this would be very helpful.
Thanks so much for all your help!
You want to use -substringWithRange: with a substring of length 1.
NSString *myString = #"This is a string object";
NSView *const parentView = [self superview];
const NSUInteger len = [myString length];
for (NSRange r = NSMakeRange(0, 1); r.location < len; r.location += 1) {
NSString *charString = [myString substringWithRange:r];
/* Create a UILabel. */
UILabel *label = [[UILabel alloc] initWithFrame....];
[lbl setText:charString];
/* Transfer ownership to |parentView|. */
[parentView addSubView:label];
[label release];
}