-Goal: Get the Origin of a particular section of Text in a UITextView
Below is a link to a Screenshot of an app I am working on. Please view it as it is easier to explain with it.
Image Link: ScreenShot
This is a start to a Fill in the Blanks style game. I am currently trying to get the x,y coordinates of each of the underscores. When a word on the bottom of the screen is tapped it will move to the next available underscore space.
Currently I have written this code to do what I need, but it is very ugly, barely works AND is not very flexible. See Below:
// self.mainTextView is where the text with the underscores is coming from
NSString *text = self.mainTextView.text;
NSString *substring = [text substringToIndex:[text rangeOfString:#"__________"].location];
CGSize size = [substring sizeWithFont:self.mainTextView.font];
CGPoint p = CGPointMake((int)size.width % (int)self.mainTextView.frame.size.width, ((int)size.width / (int)self.mainTextView.frame.size.width) * size.height);
// What is going on here is for some reason everytime there is a new
// line my x coordinate is offset by what seems to be 10 pixels...
// So was my ugly fix for it..
// The UITextView width is 280
CGRect mainTextFrame = [self.mainTextView frame];
p.x = p.x + mainTextFrame.origin.x + 9;
if ((int)size.width > 280) {
NSLog(#"width: 280");
p.x = p.x + mainTextFrame.origin.x + 10;
}
if ((int)size.width > 560) {
NSLog(#"width: 560");
p.x = p.x + mainTextFrame.origin.x + 12;
}
if ((int)size.width > 840) {
p.x = p.x + mainTextFrame.origin.x + 14;
}
p.y = p.y + mainTextFrame.origin.y + 5;
// Sender is the button that was pressed
newFrame = [sender frame];
newFrame.origin = p;
[UIView animateWithDuration:0.2
delay:0
options:UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationCurveEaseInOut
animations:^{
[sender setFrame:newFrame];
}
completion:^(BOOL finished){
}
];
So the the best question for me to ask is
What is a better way of going about this? And or do you have any suggestions? How would you go about this?
Thank you for your time in advance.
Rather than manually searching each occurrence of the underscore. make use of NSRegularExpression Which makes your task so simple and easy. After the matched Strings are found then make use of the NSTextCheckingResult to get the location of each matched strings.
More Explaination:
You can make use of the regular expression inorder get all the occurence of the underscore.This is obtanied using the following code.
NSError *error = NULL;
NSString *pattern = #"_*_"; // pattern to search underscore.
NSString *string = self.textView.text;
NSRange range = NSMakeRange(0, string.length);
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
NSArray *matches = [regex matchesInString:string options:NSMatchingProgress range:range];
Once you get all the Matching Patterns you can get the location of the matched strings using the following code.
[matches enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj isKindOfClass:[NSTextCheckingResult class]])
{
NSTextCheckingResult *match = (NSTextCheckingResult *)obj;
CGRect rect = [self frameOfTextRange:match.range inTextView:self.textView];
//get location of all the matched strings.
}
}];
Hope this answers all your concerns!
Related
I am using a standard AppKit NSPersistentDocument document base application and would like a document's window to remember its location and open in the same position it was last closed in.
Note that setting the autosavename in IB on the Window will result in all documents opening in the same position. I want a document to remember its position based on the documents filename.
I have subclassed NSPersistentDocument and currently set the autosave name in the windowControllerDidLoadNib: function. This almost works fine, except that if I open and close the same document repeatedly without closing the App then each time its window's height is increased a small amount (26 pixels), almost like its doing the cascade thing. However if I close the App completely and reopen it then the document remembers its previous position exactly. Am I doing something wrong or is this a bug. Is there some cleanup I should be doing perhaps to ensure that the window is not being resized each time its re-opened.
// NSPersistentDocument subclass
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
LOG(#"windowControllerDidLoadNib called...");
[super windowControllerDidLoadNib:aController];
if ([self autoSaveName] != nil) {
[aController setWindowFrameAutosaveName:[self autoSaveName]];
}
[aController setShouldCascadeWindows:NO];
}
- (NSString*)autoSaveName
{
return [[self fileURL] lastPathComponent];
}
If I add the following code to add 22 pixels to its height
- (NSRect)windowPositionPreference {
LOG(#"printUserDefaults called");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *autosaveNameKey = [NSString stringWithFormat:#"NSWindow Frame %#", [self autoSaveName]];
NSString *frameString = [defaults objectForKey:autosaveNameKey];
NSArray *array = [frameString componentsSeparatedByString:#" "];
CGFloat x = [[array objectAtIndex:0] floatValue];
CGFloat y = [[array objectAtIndex:1] floatValue];
CGFloat width = [[array objectAtIndex:2] floatValue];
CGFloat height = [[array objectAtIndex:3] floatValue];
NSRect rect = CGRectMake(x, y, width, height+22);
FLOG(#" window frame = %fx, %fy, %fw, %fh", x, y, width, height);
return rect;
}
and then set the frame like so in - (void)windowControllerDidLoadNib:(NSWindowController *)aController
NSRect rect = [self windowPositionPreference];
[aController.window setFrame:rect display:YES];
The position seems to be retained exactly. Surely autosavename is just meant to work.
The above was still not working but after some messing about I gave up on using autosavename because of the automatic cascading that seems to be impossible to disable.
Now I just keep my own preferences and set the window frame using the code below. So far it seems to be working perfectly and I have not had to create my own subclasses of NSWindowController or NSWindow.
// Methods in subclassed NSPersistentDocument
- (void)windowControllerDidLoadNib:(NSWindowController *)aController {
[aController setShouldCascadeWindows:NO];
_mainWindow = aController.window;
[self restoreSavedWindowPosition];
}
- (void)close {
[self saveWindowPosition];
[super close];
}
- (void)saveWindowPosition {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *autosaveNameKey = [NSString stringWithFormat:#"OSWindow Frame %#", [self autoSaveName]];
[defaults setObject:[self stringFromFrame] forKey:autosaveNameKey];
}
- (void)restoreSavedWindowPosition {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *autosaveNameKey = [NSString stringWithFormat:#"OSWindow Frame %#", [self autoSaveName]];
NSString *frameString = [defaults objectForKey:autosaveNameKey];
// Do nothing if it has not been set
if (frameString) {
[_mainWindow setFrame:[self rectFromString:frameString] display:YES animate:YES];
}
else {
// Set the default for new docs
NSRect rect = CGRectMake(70, 350, 1000, 760);
[_mainWindow setFrame:rect display:YES animate:YES];
}
return;
}
// Use the filename as the preferences key
- (NSString*)autoSaveName {
return [[self fileURL] lastPathComponent];
}
- (NSString *)stringFromFrame {
NSRect rect = _mainWindow.frame;
rect.origin.y = rect.origin.y + 26;
rect.size.height = rect.size.height - 26;
return [self stringFromRect:rect];
}
- (NSString *)stringFromRect:(NSRect)rect {
return [NSString stringWithFormat:#"%f %f %f %f", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height];
}
- (NSRect)rectFromString:(NSString*)string {
NSRect rect;
NSArray *array = [string componentsSeparatedByString:#" "];
rect.origin.x = [[array objectAtIndex:0] floatValue];
rect.origin.y = [[array objectAtIndex:1] floatValue];
rect.size.width = [[array objectAtIndex:2] floatValue];
rect.size.height = [[array objectAtIndex:3] floatValue];
return rect;
}
Is there a way to get the visible part of text in word wrapped UILabel? I mean exactly the last visible character?
I'd like to make two labels rounding the image and would like to continue the text which was out of rect for first label on the second one.
I know [NSString sizeWithFont...] but are there something reversing like [NSString stringVisibleInRect: withFont:...] ? :-)
Thank you in advance.
You could use a category to extend NSString and create the method you mention
#interface NSString (visibleText)
- (NSString*)stringVisibleInRect:(CGRect)rect withFont:(UIFont*)font;
#end
#implementation NSString (visibleText)
- (NSString*)stringVisibleInRect:(CGRect)rect withFont:(UIFont*)font
{
NSString *visibleString = #"";
for (int i = 1; i <= self.length; i++)
{
NSString *testString = [self substringToIndex:i];
CGSize stringSize = [testString sizeWithFont:font];
if (stringSize.height > rect.size.height || stringSize.width > rect.size.width)
break;
visibleString = testString;
}
return visibleString;
}
#end
Here's a O(log n) method with iOS 7 APIs. Only superficially tested, please comment if you find any bugs.
- (NSRange)hp_visibleRange
{
NSString *text = self.text;
NSRange visibleRange = NSMakeRange(NSNotFound, 0);
const NSInteger max = text.length - 1;
if (max >= 0)
{
NSInteger next = max;
const CGSize labelSize = self.bounds.size;
const CGSize maxSize = CGSizeMake(labelSize.width, CGFLOAT_MAX);
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = self.lineBreakMode;
NSDictionary * attributes = #{NSFontAttributeName:self.font, NSParagraphStyleAttributeName:paragraphStyle};
NSInteger right;
NSInteger best = 0;
do
{
right = next;
NSRange range = NSMakeRange(0, right + 1);
NSString *substring = [text substringWithRange:range];
CGSize textSize = [substring boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil].size;
if (textSize.width <= labelSize.width && textSize.height <= labelSize.height)
{
visibleRange = range;
best = right;
next = right + (max - right) / 2;
} else if (right > 0)
{
next = right - (right - best) / 2;
}
} while (next != right);
}
return visibleRange;
}
Case: The attributed string is already created. How can the size of the string be altered?
I'm guessing we could either
A) Update pointSize for all fonts in the attributed string
B) Draw the attributed string with some transform
I've got it working with the following code. One miss though is if some text in the attributedstring has not been set a font-attribute it will not be updated. So i had to encapsulate everything with font-attributes.
- (void)recalculateSizeChangeInAttributedString {
if(self.attributedStringOriginal == nil) {
self.attributedStringOriginal = [self.attributedString copy];
}
CFMutableAttributedStringRef tempString = CFAttributedStringCreateMutableCopy(CFAllocatorGetDefault(), self.attributedStringOriginal.length, (CFMutableAttributedStringRef)self.attributedStringOriginal);
int lastIndex = 0;
int limit = CFAttributedStringGetLength(tempString);
for (int index = 0; index < limit;) {
CFRange inRange = CFRangeMake(0, limit - index);
CFRange longestEffective;
CTFontRef font = (CTFontRef)CFAttributedStringGetAttribute(tempString, index, kCTFontAttributeName, &longestEffective);
if(font != nil) {
// log for testing
NSLog(#"index: %i, range: %i - %i, longest: %i - %i, attribute: %#",
index, inRange.location,
inRange.location + inRange.length,
longestEffective.location, longestEffective.location + longestEffective.length,
#"..."
);
// alter the font and set the altered font/attribute
int rangeEnd = longestEffective.length != 0 ? longestEffective.length : 1;
CTFontRef modifiedFont = CTFontCreateCopyWithAttributes(font, CTFontGetSize((CTFontRef)font) * sizeFactor, NULL, NULL);
CFAttributedStringSetAttribute(tempString, CFRangeMake(index, rangeEnd), kCTFontAttributeName, modifiedFont);
CFRelease(modifiedFont);
}
// make next loop continue where current attribute ended
index += longestEffective.length;
if(index == lastIndex)
index ++;
lastIndex = index;
}
self.attributedString = (NSMutableAttributedString *)tempString;
CFRelease(tempString);
}
This works fine on both Mac OS and iOS
#implementation NSAttributedString (Scale)
- (NSAttributedString *)attributedStringWithScale:(double)scale
{
if(scale == 1.0)
{
return self;
}
NSMutableAttributedString *copy = [self mutableCopy];
[copy beginEditing];
NSRange fullRange = NSMakeRange(0, copy.length);
[self enumerateAttribute:NSFontAttributeName inRange:fullRange options:0 usingBlock:^(UIFont *oldFont, NSRange range, BOOL *stop) {
double currentFontSize = oldFont.pointSize;
double newFontSize = currentFontSize * scale;
// don't trust -[UIFont fontWithSize:]
UIFont *scaledFont = [UIFont fontWithName:oldFont.fontName size:newFontSize];
[copy removeAttribute:NSFontAttributeName range:range];
[copy addAttribute:NSFontAttributeName value:scaledFont range:range];
}];
[self enumerateAttribute:NSParagraphStyleAttributeName inRange:fullRange options:0 usingBlock:^(NSParagraphStyle *oldParagraphStyle, NSRange range, BOOL *stop) {
NSMutableParagraphStyle *newParagraphStyle = [oldParagraphStyle mutableCopy];
newParagraphStyle.lineSpacing *= scale;
newParagraphStyle.paragraphSpacing *= scale;
newParagraphStyle.firstLineHeadIndent *= scale;
newParagraphStyle.headIndent *= scale;
newParagraphStyle.tailIndent *= scale;
newParagraphStyle.minimumLineHeight *= scale;
newParagraphStyle.maximumLineHeight *= scale;
newParagraphStyle.paragraphSpacing *= scale;
newParagraphStyle.paragraphSpacingBefore *= scale;
[copy removeAttribute:NSParagraphStyleAttributeName range:range];
[copy addAttribute:NSParagraphStyleAttributeName value:newParagraphStyle range:range];
}];
[copy endEditing];
return copy;
}
#end
Happy coding
I believe parsing is the only way. Attributed string can have quite complex format, it's your job to increase the font size.
However, if you need this trick for rendering, you can avoid string parsing - use a scale transform to increase the text size.
Is there a way to find ypos in a UITextView, or/and find which row that currently marker is active?
you need to do some calculation
find cursor position by
NSRange cursorPosition = [tf selectedRange];
substring from cursor position use this sub string to calculate width of the string by
sizeWithFont:constrainedToSize:
and then divide it by width of the your TextView width.. it will give you at which line your cursor is... It's logically seems correct... haven't tried it... try it let me know if it is working or not..
Something like that:
- (int) getCurrentLine: (UITextView *) textView{
int pos = textView.selectedRange.location;
NSString *str =textView.text;
NSCharacterSet *charset = [NSCharacterSet whitespaceAndNewlineCharacterSet];
int num = pos;
for (; num< str.length; num++){
unichar c = [str characterAtIndex:num];
if ([charset characterIsMember:c]) break;
}
str = [str substringToIndex:num];
CGSize tallerSize = CGSizeMake(textView.frame.size.width - 16, 999999);
CGSize lc1 = [#"Yyg" sizeWithFont: textView.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
CGSize lc = [str sizeWithFont: textView.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
lc.height = lc.height / lc1.height;
return lc.height;
}
I've recently downloaded the GLPaint sample code and looked at a very interesting part in it. There is a recordedPaths NSMutableArray that has points in it that are then read and drawn by GLPaint.
It's declared here:
NSMutableArray *recordedPaths;
recordedPaths = [NSMutableArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Recording" ofType:#"data"]];
if([recordedPaths count])
[self performSelector:#selector(playback:) withObject:recordedPaths afterDelay:0.2];
This is the code for playback:
- (void) playback:(NSMutableArray*)recordedPaths {
NSData* data = [recordedPaths objectAtIndex:0];
CGPoint* point = (CGPoint*)[data bytes];
NSUInteger count = [data length] / sizeof(CGPoint),
i;
//Render the current path
for(i = 0; i < count - 1; ++i, ++point)
[self renderLineFromPoint:*point toPoint:*(point + 1)];
//Render the next path after a short delay
[recordedPaths removeObjectAtIndex:0];
if([recordedPaths count])
[self performSelector:#selector(playback:) withObject:recordedPaths afterDelay:0.01];
}
From this I understand that recordedPaths is a mutable array that his in it struct c arrays of CGPoint that are then read and rendered.
I'd like to put in my own array and i've been having trouble with that.
I tried changing the recordedPaths declaration to this:
NSMutableArray *myArray = [[NSMutableArray alloc] init];
CGPoint* points;
CGPoint a = CGPointMake(50,50);
int i;
for (i=0; i<100; i++,points++) {
a = CGPointMake(i,i);
points = &a;
}
NSData *data = [NSData dataWithBytes:&points length:sizeof(*points)];
[myArray addObject:data];
This didn't work though...
Any advice?
If you look at the Recording.data you will notice that each line is its own array. To capture the ink and play it back you need to have an array of arrays. For purposes of this demo - declare a mutable array - writRay
#synthesize writRay;
//init in code
writRay = [[NSMutableArray alloc]init];
Capture the ink
// Handles the continuation of a touch.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGRect bounds = [self bounds];
UITouch* touch = [[event touchesForView:self] anyObject];
// Convert touch point from UIView referential to OpenGL one (upside-down flip)
if (firstTouch) {
firstTouch = NO;
previousLocation = [touch previousLocationInView:self];
previousLocation.y = bounds.size.height - previousLocation.y;
/******************* create a new array for this stroke's points **************/
[writRay addObject:[[NSMutableArray alloc]init]];
/***** add 1st point *********/
[[writRay objectAtIndex:[writRay count] -1]addObject:[NSValue valueWithCGPoint:previousLocation]];
} else {
location = [touch locationInView:self];
location.y = bounds.size.height - location.y;
previousLocation = [touch previousLocationInView:self];
previousLocation.y = bounds.size.height - previousLocation.y;
/********* add additional points *********/
[[writRay objectAtIndex:[writRay count] -1]addObject:[NSValue valueWithCGPoint:previousLocation]];
}
// Render the stroke
[self renderLineFromPoint:previousLocation toPoint:location];
}
Playback the ink.
- (void)playRay{
if(writRay != NULL){
for(int l = 0; l < [writRay count]; l++){
//replays my writRay -1 because of location point
for(int p = 0; p < [[writRay objectAtIndex:l]count] -1; p ++){
[self renderLineFromPoint:[[[writRay objectAtIndex:l]objectAtIndex:p]CGPointValue] toPoint:[[[writRay objectAtIndex:l]objectAtIndex:p + 1]CGPointValue]];
}
}
}
}
For best effect shake the screen to clear and call playRay from changeBrushColor in the AppController.
CGPoint* points;
CGPoint a = CGPointMake(50,50);
int i;
for (i=0; i<100; i++,points++) {
a = CGPointMake(i,i);
points = &a;
}
NSData *data = [NSData dataWithBytes:&points length:sizeof(*points)];
Wrong code.
(1) You need an array of points. Simply declaring CGPoint* points; won't create an array of points, just an uninitialized pointer of CGPoint. You need to allocate space for the array either with
CGPoint points[100];
or
CGPoint* points = malloc(sizeof(CGPoint)*100);
Remember to free the points if you choose the malloc way.
(2) To copy value to the content of the pointer you need to use
*points = a;
But I suggest you keep the pointer points invariant in the loop, since you're going to reuse it later. Use the array syntax points[i].
(3)
sizeof(*points)
Since *points is just one CGPoint, so the sizeof is always 8 bytes. You need to multiply the result by 100 to get the correct length.
(4)
[NSData dataWithBytes:&points ...
points already is a pointer to the actual data. You don't need to take the address of it again. Just pass points directly.
So the final code should look like
CGPoint* points = malloc(sizeof(CGPoint)*100); // make a cast if the compiler warns.
CGPoint a;
int i;
for (i=0; i<100; i++) {
a = CGPointMake(i,i);
points[i] = a;
}
NSData *data = [NSData dataWithBytes:points length:sizeof(*points)*100];
free(points);
I was just reading over this post as I am trying to achive a similar thing. With modifications to the original project by Apple, I was able to create a new 'shape' by modifying the code accordingly.
Note that it only draws a diagonal line... several times. But the theory is there to create your own drawing.
I've taken the code from KennyTM's post and incorporated it into the 'payback' function, this could be adapted to create the array in the initWithCoder function and then send it through like the original code but for now - this will get you a result.
CGPoint* points = malloc(sizeof(CGPoint)*100);
CGPoint a;
int iter;
for (iter=0; iter<200; iter++) {
a = CGPointMake(iter,iter);
points[iter] = a;
}
NSData *data = [NSData dataWithBytes:points length:sizeof(*points)*100];
free(points);
CGPoint* point = (CGPoint*)[data bytes];
NSUInteger count = [data length] / sizeof(CGPoint),
i;
for(i = 0; i < count - 1; ++i, ++point)
[self renderLineFromPoint:*point toPoint:*(point + 1)];
[recordedPaths removeObjectAtIndex:0];
if([recordedPaths count])
[self performSelector:#selector(playback:) withObject:recordedPaths afterDelay:0.01];
I am still in my first weeks on openGL coding, so forgive any glaring mistakes / bad methods and thanks for the help!
Hope this helps