iOS: Scaling UITextView with pinching? - iphone

I'm interested in creating UITextView that is expanding dynamically while typing the text, and scaling as the user pinches the screen(Similar behaviour can be found in TinyPost).
When you just type (without pinching) the textView expands fine. When you just pinch (without typing) is works fine, but when you pinch and then type, the text inside gets cut.
Here is my code:
UIPinchGestureRecognizer *pinchGestRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(scaleTextView:)];
pinchGestRecognizer.delegate = self;
[bgFrameImageView addGestureRecognizer:pinchGestRecognizer];
- (void)scaleTextView:(UIPinchGestureRecognizer *)pinchGestRecognizer{
createTextView.transform = CGAffineTransformScale(createTextView.transform, pinchGestRecognizer.scale, pinchGestRecognizer.scale);
pinchGestRecognizer.scale = 1;
}
- (void)textViewDidChange:(UITextView *)textView{
CGSize textSize = textView.contentSize;
textView.frame = CGRectMake(CGRectGetMinX(textView.frame), CGRectGetMinY(textView.frame), textSize.width, textSize.height); //update the size of the textView
}
What do you think?

Try:
- (void)scaleTextView:(UIPinchGestureRecognizer *)pinchGestRecognizer{
CGFloat scale = pinchGestRecognizer.scale;
createTextView.font = [UIFont fontWithName:createTextView.font.fontName size:createTextView.font.pointSize*scale];
[self textViewDidChange:createTextView];
}
It basically scales the font size and then recalculates the content size using your code in textViewDidChange.

To elaborate on #Cocoanetics answer above. I implemented the gesture handling idea for attributed strings on iOS 7 but it is prohibitively slow when you have too many font changes in your string. There is also a ridiculous buffering bug in iOS 7 where Change notifications keep firing long after you've stopped pinching - reminds me of the stupid keyboard buffer in early versions of PC-DOS. Anyway, I have put the code below that got this working for me - although it has only resulted in informing me that this is a waste of time and that I need to give my users some other way of scaling their fonts.
- (void)scaleTextView:(UIPinchGestureRecognizer *)pinchGestureRecognizer
{
CGFloat scale = 0;
NSMutableAttributedString *string;
switch (pinchGestureRecognizer.state) {
case UIGestureRecognizerStateBegan:
self.old_scale = 1.0;
self.last_time = [NSDate date];
break;
case UIGestureRecognizerStateChanged:
scale = pinchGestureRecognizer.scale - self.old_scale;
if( [self.last_time timeIntervalSinceNow] < -0.2 ) { // updating 5 times a second is best I can do - faster than this and we get buffered changes going on for ages!
self.last_time = [NSDate date];
string = [self getScaledStringFrom:[self.textview.attributedText mutableCopy] withScale:1.0 + scale];
if( string ) {
self.textview.attributedText = string;
self.old_scale = pinchGestureRecognizer.scale;
}
}
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed:
break;
default:
break;
}
}
- (NSMutableAttributedString*) getScaledStringFrom:(NSMutableAttributedString*)string withScale:(CGFloat)scale
{
[string beginEditing];
[string enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value) {
UIFont *oldFont = (UIFont *)value;
UIFont *newFont = [oldFont fontWithSize:oldFont.pointSize * scale];
[string removeAttribute:NSFontAttributeName range:range];
[string addAttribute:NSFontAttributeName value:newFont range:range];
}
}];
[string endEditing];
return string;
}

First of all add UIPinchGestureRecognizer in viewDidLoad method:
UIPinchGestureRecognizer *pinchOnTextfield = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handleTextFieldFontOnAddMusicVc:)];
[self.myTextField addGestureRecognizer:pinchOnTextfield];
then put this method in your viewController to scale textField font:
- (void)handleTextFieldFontOnAddMusicVc:(UIPinchGestureRecognizer *)pinchGestRecognizer {
if (pinchGestRecognizer.state == UIGestureRecognizerStateEnded || pinchGestRecognizer.state == UIGestureRecognizerStateChanged) {
CGFloat currentFontSize = self.myTextField.font.pointSize;
CGFloat newScale = currentFontSize * pinchGestRecognizer.scale;
if (newScale < 20.0) {
newScale = 20.0;
}
if (newScale > 60.0) {
newScale = 60.0;
}
self.myTextField.font = [UIFont fontWithName:self.myTextField.font.fontName size:newScale];
pinchGestRecognizer.scale = 1;
}
}

UITextView is a subclass of UIScrollView and as such you need to do what you do with any scroll view to enable zooming:
set the minimum and maximum zoom scale properties
set the viewForZooming via the scrollview delegate method
... that zooms the entire text view.
If you only want to zoom the text then you have to perform these steps via a pinch gesture recognizer:
retrieve the current attributed string
walk through the font ranges and replace the font attribute with a scaled one
replace the text in the text view with your modified version
probably you also need to set the text selection after setting the text

Not the best but easy solution would be:
I do not know exactly your use case, but I think it does not make sense in many cases to support in a app pinch zoom while typing text. I would just set a flag, which prevents pinch zoom while user is typing text.
If you mean by text typing, editing the text view you can end the editing at the beginning of the pinch gesture and start editing after pinch gesture did end.

Related

Right align PlaceHolder text in UITextField

I have a UITextField with the text right-aligned.
I wanted to change the color of the placeholder text, so I use - (void)drawPlaceholderInRect:(CGRect)rect method. It works great BUT the placeholder text is left-aligned now (the text remains right-aligned). I guess I can add some code to override it but I didn't find which one. Thanks in advance !
- (void)drawPlaceholderInRect:(CGRect)rect
{
[[UIColor redColor] setFill];
UIFont *font = [UIFont fontWithName:#"HelveticaNeue-Medium" size:18];
[[self placeholder] drawInRect:rect withFont:font];
}
Here is the code snippet based on Michael solution. You should create subclass of text field and add the below method. Below method basically changes x-position and width of place holder bounds.
- (CGRect)placeholderRectForBounds:(CGRect)bounds{
CGRect newbounds = bounds;
CGSize size = [[self placeholder] sizeWithAttributes:
#{NSFontAttributeName: self.font}];
int width = bounds.size.width - size.width;
newbounds.origin.x = width ;
newbounds.size.width = size.width;
return newbounds;
}
You've discovered that "drawInRect" is automagically drawing from the left edge going right.
What you need to do is adjust the "rect" passed to "drawInRect" to have left edge that allows the right edge of the drawn text to touch the right edge of your UITextField rect.
To do this, I'd recommend using this method: NSString's [self placeholder] sizeWithFont: constrainedToSize:] (assuming [self placeholder] is a NSString) which will give you the true width of the string. Then subtract the width from the right edge of the text field box and you have the left edge where you need to start your drawing from.
I enhanced #Saikiran's snippet a little, this works for me:
- (CGRect)placeholderRectForBounds:(CGRect)bounds
{
return self.editing ? ({CGRect bounds_ = [super placeholderRectForBounds:bounds];
bounds_.origin.x = bounds_.size.width
- ceilf(self.attributedPlaceholder.size.width)
+ self.inset.x;
bounds_.origin.y = .5f * (.5f * bounds_.size.height
- ceilf(self.attributedPlaceholder.size.height));
bounds_.size.width = ceilf(self.attributedPlaceholder.size.width);
bounds_.size.height = ceilf(self.attributedPlaceholder.size.height);
bounds_;
}) : [super placeholderRectForBounds:bounds];
}

How to check if UILabel text was touched?

I want to check if my UILabel was touched. But i need even more than that. Was the text touched? Right now I only get true/false if the UILabel frame was touched using this:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
if (CGRectContainsPoint([self.currentLetter frame], [touch locationInView:self.view]))
{
NSLog(#"HIT!");
}
}
Is there any way to check this? As soon as I touch somewhere outside the letter in the UILabel I want false to get returned.
I want to know when the actual black rendered "text pixles" has been touched.
Thanks!
tl;dr: You can hit test the path of the text. Gist is available here.
The approach I would go with is to check if the tap point is inside the path of the text or not. Let me give you a overview of the steps before going into detail.
Subclass UILabel
Use Core Text to get the CGPath of the text
Override pointInside:withEvent: to be able to determine if a point should be considered inside or not.
Use any "normal" touch handling like for example a tap gesture recognizer to know when a hit was made.
The big advantage of this approach is that it follows the font precisely and that you can modify the path to grow the "hittable" area like seen below. Both the black and the orange parts are tappable but only the black parts will be drawn in the label.
Subclass UILabel
I created a subclass of UILabel called TextHitTestingLabel and added a private property for the text path.
#interface TextHitTestingLabel (/*Private stuff*/)
#property (assign) CGPathRef textPath;
#end
Since iOS labels can have either a text or an attributedText so I subclassed both these methods and made them call a method to update the text path.
- (void)setText:(NSString *)text {
[super setText:text];
[self textChanged];
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
[super setAttributedText:attributedText];
[self textChanged];
}
Also, a label can be created from a NIB/Storyboard in which case the text will be set right away. In that case I check for the initial text in awake from nib.
- (void)awakeFromNib {
[self textChanged];
}
Use Core Text to get the path of the text
Core Text is a low level framework that gives you full control over the text rendering. You have to add CoreText.framework to your project and import it to your file
#import <CoreText/CoreText.h>
The first thing I do inside textChanged is to get the text. Depending on if it's iOS 6 or earlier I also have to check the attributed text. A label will only have one of these.
// Get the text
NSAttributedString *attributedString = nil;
if ([self respondsToSelector:#selector(attributedText)]) { // Available in iOS 6
attributedString = self.attributedText;
}
if (!attributedString) { // Either earlier than iOS6 or the `text` property was set instead of `attributedText`
attributedString = [[NSAttributedString alloc] initWithString:self.text
attributes:#{NSFontAttributeName: self.font}];
}
Next I create a new mutable path for all the letter glyphs.
// Create a mutable path for the paths of all the letters.
CGMutablePathRef letters = CGPathCreateMutable();
Core Text "magic"
Core Text works with lines of text and glyphs and glyph runs. For example, if I have the text: "Hello" with attributes like this " Hel lo " (spaces added for clarity). Then that is going to be one line of text with two glyph runs: one bold and one regular. The first glyph run contains 3 glyphs and the second run contains 2 glyphs.
I enumerate all the glyph runs and their glyphs and get the path with CTFontCreatePathForGlyph(). Each individual glyph path is then added to the mutable path.
// Create a line from the attributed string and get glyph runs from that line
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFArrayRef runArray = CTLineGetGlyphRuns(line);
// A line with more then one font, style, size etc will have multiple fonts.
// "Hello" formatted as " *Hel* lo " (spaces added for clarity) is two glyph
// runs: one italics and one regular. The first run contains 3 glyphs and the
// second run contains 2 glyphs.
// Note that " He *ll* o " is 3 runs even though "He" and "o" have the same font.
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
// Get the font for this glyph run.
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
// This glyph run contains one or more glyphs (letters etc.)
for (CFIndex runGlyphIndex = 0; runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
{
// Read the glyph itself and it position from the glyph run.
CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
CGGlyph glyph;
CGPoint position;
CTRunGetGlyphs(run, glyphRange, &glyph);
CTRunGetPositions(run, glyphRange, &position);
// Create a CGPath for the outline of the glyph
CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyph, NULL);
// Translate it to its position.
CGAffineTransform t = CGAffineTransformMakeTranslation(position.x, position.y);
// Add the glyph to the
CGPathAddPath(letters, &t, letter);
CGPathRelease(letter);
}
}
CFRelease(line);
The core text coordinate system is upside down compared to the regular UIView coordinate system so I then flip the path to match what we see on screen.
// Transform the path to not be upside down
CGAffineTransform t = CGAffineTransformMakeScale(1, -1); // flip 1
CGSize pathSize = CGPathGetBoundingBox(letters).size;
t = CGAffineTransformTranslate(t, 0, -pathSize.height); // move down
// Create the final path by applying the transform
CGPathRef finalPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);
// Clean up all the unused path
CGPathRelease(letters);
self.textPath = finalPath;
And now I have a complete CGPath for the text of the label.
Override pointInside:withEvent:
To customize what points the label consider as inside itself I override point inside and have it check if the point is inside the text path. Other parts of UIKit is going to call this method for hit testing.
// Override -pointInside:withEvent to determine that ourselves.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// Check if the points is inside the text path.
return CGPathContainsPoint(self.textPath, NULL, point, NO);
}
Normal touch handling
Now everything is setup to work with normal touch handling. I added a tap recognizer to my label in a NIB and connected it to a method in my view controller.
- (IBAction)labelWasTouched:(UITapGestureRecognizer *)sender {
NSLog(#"LABEL!");
}
That is all it takes. If you scrolled all the way down here and don't want to take the different pieces of code and paste them together I have the entire .m file in a Gist that you can download and use.
A note, most fonts are very, very thin compared to the precision of a touch (44px) and your users will most likely be very frustrated when the touches are considered "misses". That being said: happy coding!
Update:
To be slightly nicer to the user you can stroke the text path that you use for hit testing. This gives a larger area that hit tappable but still gives the feeling that you are tapping the text.
CGPathRef endPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);
CGMutablePathRef finalPath = CGPathCreateMutableCopy(endPath);
CGPathRef strokedPath = CGPathCreateCopyByStrokingPath(endPath, NULL, 7, kCGLineCapRound, kCGLineJoinRound, 0);
CGPathAddPath(finalPath, NULL, strokedPath);
// Clean up all the unused paths
CGPathRelease(strokedPath);
CGPathRelease(letters);
CGPathRelease(endPath);
self.textPath = finalPath;
Now the orange area in the image below is going to be tappable as well. This still feels like you are touching the text but is less annoying to the users of your app.
If you want you can take this even further to make it even easier to hit the text but at some point it is going to feel like the entire label is tappable.
The problem, as I understand it, is to detect when a tap (touch) happens on one of the glyphs that comprise the text in a UILabel. If a touch lands outside the path of any of the glyphs then it isn't counted.
Here's my solution. It assumes a UILabel* ivar named _label, and a UITapGestureRecognizer associated with the view containing the label.
- (IBAction) onTouch: (UITapGestureRecognizer*) tgr
{
CGPoint p = [tgr locationInView: _label];
// in case the background of the label isn't transparent...
UIColor* labelBackgroundColor = _label.backgroundColor;
_label.backgroundColor = [UIColor clearColor];
// get a UIImage of the label
UIGraphicsBeginImageContext( _label.bounds.size );
CGContextRef c = UIGraphicsGetCurrentContext();
[_label.layer renderInContext: c];
UIImage* i = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// restore the label's background...
_label.backgroundColor = labelBackgroundColor;
// draw the pixel we're interested in into a 1x1 bitmap
unsigned char pixel = 0x00;
c = CGBitmapContextCreate(&pixel,
1, 1, 8, 1, NULL,
kCGImageAlphaOnly);
UIGraphicsPushContext(c);
[i drawAtPoint: CGPointMake(-p.x, -p.y)];
UIGraphicsPopContext();
CGContextRelease(c);
if ( pixel != 0 )
{
NSLog( #"touched text" );
}
}
You can use a UIGestureRecognizer:
http://developer.apple.com/library/ios/#documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html
Specifically, I guess you'd like to use the UITapGestureRecognizer. If you want to recognize when the text frame is touched, then the easiest would be to make the size of your frame to fit the text with [yourLabel sizeToFit].
Anyway, to do so I will go to use a UIButton, it's the easiest option.
In case you need to detect only when the actual text and not the entire UITextField frame is tapped then it becomes much more difficult. One approach is detecting the darkness of the pixel the user tapped, but this involves some ugly code. Anyway, depending on the expected interaction within your application in can work out. Check this SO question:
iOS -- detect the color of a pixel?
I would take in consideration that not all the rendered pixel will be 100% black, so I would play with a threshold to achieve better results.
I think he wants to know whether the letter within the label is touched, not other parts of the label. Since you are willing to use a transparent image to achieve this, I would suggest that, for example you have the letter "A" with transparent background, if the color of the letter if monotonous, let's say red in this case, you could grab a CGImage of the UIImage, get the provider and render it as bitmap and sample whether the color of the point being touched is red. For other colors, you could simply sample that color using an online image editor and grab its RGB value and check against that.
You could use an UIButton instead of a label :
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIButton *tmpButton = [[UIButton alloc] initWithFrame:CGRectMake(50, 50, 100, 20)];
[tmpButton setTitle:#"KABOYA" forState:UIControlStateNormal];
[tmpButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[tmpButton addTarget:self
action:#selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:tmpButton];
}
When the Button is pressed do something here :
-(void)buttonPressed:(UIButton *)sender {
NSLog(#"Pressed !");
}
I hope it helped ;)
Assuming UILabel instance which you want to track is userInteractionEnabled.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
UIView *touchView = touch.view;
if([touchView isKindOfClass:[UILabel class]]){
NSLog(#"Touch event occured in Label %#",touchView);
}
}
First of all create and attach tap gesture recognizer and allow user interactions:
UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapGesture:)];
[self.label addGestureRecognizer:tapRecognizer];
self.label.userInteractionEnabled = YES;
Now implement -tapGesture:
- (void)tapGesture:(UITapGestureRecognizer *)recognizer
{
// Determine point touched
CGPoint point = [recognizer locationInView:self.label];
// Render UILabel in new context
UIGraphicsBeginImageContext(self.label.bounds.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.label.layer renderInContext:context];
// Getting RGBA of concrete pixel
int bpr = CGBitmapContextGetBytesPerRow(context);
unsigned char * data = CGBitmapContextGetData(context);
if (data != NULL)
{
int offset = bpr*round(point.y) + 4*round(point.x);
int red = data[offset+0];
int green = data[offset+1];
int blue = data[offset+2];
int alpha = data[offset+3];
NSLog(#"%d %d %d %d", alpha, red, green, blue);
if (alpha == 0)
{
// Here is tap out of text
}
else
{
// Here is tap right into text
}
}
UIGraphicsEndImageContext();
}
This will works on UILabel with transparent background, if this is not what you want you can compare alpha, red, green, blue with self.label.backgroundColor...
Create the Label in viewDidLoad or through IB and add tapGesture using below code with selector then when you tap on label log will be printed(which is in singletap:)
- (void)viewDidLoad
{
[super viewDidLoad];
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(30, 0, 150, 35)];
label.userInteractionEnabled = YES;
label.backgroundColor = [UIColor greenColor];
label.text = #"label";
label.textAlignment = NSTextAlignmentCenter;
UITapGestureRecognizer * single = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singletap:)];
[label addGestureRecognizer:single];
single.numberOfTapsRequired = 1;
[self.view addSubview:label];
}
-(void) singletap:(id)sender
{
NSLog(#"single tap");
//do your stuff here
}
If your found it please mark it positive
happy coding

How to make an UICollectionView with infinite paging?

I have a UICollectionView with 6 pages, and paging enabled, and a UIPageControl. What I want is, when I came to the last page, if I drag to right, UICollectionView reloads from first page seamlessly.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
// The key is repositioning without animation
if (collectionView.contentOffset.x == 0) {
// user is scrolling to the left from image 1 to image 10.
// reposition offset to show image 10 that is on the right in the scroll view
[collectionView scrollRectToVisible:CGRectMake(collectionView.frame.size.width*(pageControl.currentPage-1),0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
else if (collectionView.contentOffset.x == 1600) {
// user is scrolling to the right from image 10 to image 1.
// reposition offset to show image 1 that is on the left in the scroll view
[collectionView scrollRectToVisible:CGRectMake(0,0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
pageControlUsed = NO;
}
It doesn't work like I want. What can I do?
Here's what I ended up with for my UICollectionView (horizontal scrolling like the UIPickerView):
#implementation UIInfiniteCollectionView
- (void) recenterIfNecessary {
CGPoint currentOffset = [self contentOffset];
CGFloat contentWidth = [self contentSize].width;
// don't just snap to center, since this might be done in the middle of a drag and not aligned. Make sure we account for that offset
CGFloat offset = kCenterOffset - currentOffset.x;
int delta = -round(offset / kCellSize);
CGFloat shift = (offset + delta * kCellSize);
offset += shift;
CGFloat distanceFromCenter = fabs(offset);
// don't always recenter, just if we get too far from the center. Eliza recommends a quarter of the content width
if (distanceFromCenter > (contentWidth / 4.0)) {
self.contentOffset = CGPointMake(kCenterOffset, currentOffset.y);
// move subviews back to make it appear to stay still
for (UIView *subview in self.subviews) {
CGPoint center = subview.center;
center.x += offset;
subview.center = center;
}
// add the offset to the index (unless offset is 0, in which case we'll assume this is the first launch and not a mid-scroll)
if (currentOffset.x > 0) {
int delta = -round(offset / kCellSize);
// MODEL UPDATE GOES HERE
}
}
}
- (void) layoutSubviews { // called at every frame of scrolling
[super layoutSubviews];
[self recenterIfNecessary];
}
#end
Hope this helps someone.
I've been using the Street Scroller sample to create an infinite scroller for images. That works fine until I wanted to set pagingEnabled = YES; Tried tweaking around the recenterIfNecessary code and finally realized that it's the contentOffset.x that has to match the frame of the subview that i want visible when paging stops. This really isn't going to work in recenterIfNecessary since you have no way of knowing it will get called from layoutSubviews. If you do get it adjusted right, the subview may pop out from under your finger. I do the adjustment in scrollViewDidEndDecelerating. So far I haven't had problems with scrolling fast. It will work and simulate paging even when pagingEnabled is NO, but it looks more natural with YES.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[super scrollViewDidEndDecelerating:scrollView];
CGPoint currentOffset = [self contentOffset];
// find the subview that is the closest to the currentOffset.
CGFloat closestOriginX = 999999;
UIView *closestView = nil;
for (UIView *v in self.visibleImageViews) {
CGPoint origin = [self.imageContainerView convertPoint:v.frame.origin toView:self];
CGFloat distanceToCurrentOffset = fabs(currentOffset.x - origin.x);
if (distanceToCurrentOffset <= closestOriginX) {
closestView = v;
closestOriginX = distanceToCurrentOffset;
}
}
// found the closest view, now find the correct offset
CGPoint origin = [self.imageContainerView convertPoint:closestView.frame.origin toView:self];
CGPoint center = [self.imageContainerView convertPoint:closestView.center toView:self];
CGFloat offsetX = currentOffset.x - origin.x;
// adjust the centers of the subviews
[UIView animateWithDuration:0.1 animations:^{
for (UIView *v in self.visibleImageViews) {
v.center = [self convertPoint:CGPointMake(v.center.x+offsetX, center.y) toView:self.imageContainerView];
}
}];
}
I have not used UICollectionView for infinite scrolling, but when doing it with a UIScrollView you first adjust your content offset (instead of using scrollRectToVisible) to the location you want. Then, you loop through each subview in your scroller and adjust their coordinates either to the right or left based on the direction the user was scrolling. Finally, if either end is beyond the bounds you want them to be, move them to the far other end. Their is a very good WWDC video from apple about how to do infinite scrolling you can find here: http://developer.apple.com/videos/wwdc/2012/

How to implement an non-rectangle scroll content on iPhone's scrollview?

Typically, the scrollView's content view is a rectangle. But I would like to implement that is not a rectangle.... For example....
The yellow, Grid 6 is the current position...Here is the example flow:
User swipe to left. (cannot scroll to left) Current: 6.
User swipe to right. (scroll to right) Current: 7.
User swipe to down. (scroll to down) Current: 8.
User swipe to down. (cannot scroll to down) Current: 8.
As you can see, the Content view of the scrollView is not rectangle. Any ideas on how to implement it? Thanks.
This is an interesting idea to implement. I can think of a few approaches that might work. I tried out one, and you can find my implementation in my github repository here. Download it and try it out for yourself.
My approach is to use a normal UIScrollView, and constrain its contentOffset in the delegate's scrollViewDidScroll: method (and a few other delegate methods).
Preliminaries
First, we're going to need a constant for the page size:
static const CGSize kPageSize = { 200, 300 };
And we're going to need a data structure to hold the current x/y position in the grid of pages:
typedef struct {
int x;
int y;
} MapPosition;
We need to declare that our view controller conforms to the UIScrollViewDelegate protocol:
#interface ViewController () <UIScrollViewDelegate>
#end
And we're going to need instance variables to hold the grid (map) of pages, the current position in that grid, and the scroll view:
#implementation ViewController {
NSArray *map_;
MapPosition mapPosition_;
UIScrollView *scrollView_;
}
Initializing the map
My map is just an array of arrays, with a string name for each accessible page and [NSNull null] at inaccessible grid positions. I'll initialize the map from my view controller's init method:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
[self initMap];
}
return self;
}
- (void)initMap {
NSNull *null = [NSNull null];
map_ = #[
#[ #"1", null, #"2"],
#[ #"3", #"4", #"5" ],
#[ null, #"6", #"7" ],
#[ null, null, #"8" ],
];
mapPosition_ = (MapPosition){ 0, 0 };
}
Setting up the view hierarchy
My view hierarchy will look like this:
top-level view (gray background)
scroll view (transparent background)
content view (tan background)
page 1 view (white with a shadow)
page 2 view (white with a shadow)
page 3 view (white with a shadow)
etc.
Normally I'd set up some of my views in a xib, but since it's hard to show xibs in a stackoverflow answer, I'll do it all in code. So in my loadView method, I first set up a “content view” that will live inside the scroll view. The content view will contain a subviews for each page:
- (void)loadView {
UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [map_[0] count] * kPageSize.width, map_.count * kPageSize.height)];
contentView.backgroundColor = [UIColor colorWithHue:0.1 saturation:0.1 brightness:0.9 alpha:1];
[self addPageViewsToContentView:contentView];
Then I'll create my scroll view:
scrollView_ = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, kPageSize.width, kPageSize.height)];
scrollView_.delegate = self;
scrollView_.bounces = NO;
scrollView_.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin
| UIViewAutoresizingFlexibleRightMargin
| UIViewAutoresizingFlexibleTopMargin
| UIViewAutoresizingFlexibleBottomMargin);
I add the content view as a subview of the scroll view and set up the scroll view's content size and offset:
scrollView_.contentSize = contentView.frame.size;
[scrollView_ addSubview:contentView];
scrollView_.contentOffset = [self contentOffsetForCurrentMapPosition];
Finally, I create my top-level view and give it the scroll view as a subview:
UIView *myView = [[UIView alloc] initWithFrame:scrollView_.frame];
[myView addSubview:scrollView_];
myView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
self.view = myView;
}
Here's how I compute the scroll view's content offset for the current map position, and for any map position:
- (CGPoint)contentOffsetForCurrentMapPosition {
return [self contentOffsetForMapPosition:mapPosition_];
}
- (CGPoint)contentOffsetForMapPosition:(MapPosition)position {
return CGPointMake(position.x * kPageSize.width, position.y * kPageSize.height);
}
To create subviews of the content view for each accessible page, I loop over the map:
- (void)addPageViewsToContentView:(UIView *)contentView {
for (int y = 0, yMax = map_.count; y < yMax; ++y) {
NSArray *mapRow = map_[y];
for (int x = 0, xMax = mapRow.count; x < xMax; ++x) {
id page = mapRow[x];
if (![page isKindOfClass:[NSNull class]]) {
[self addPageViewForPage:page x:x y:y toContentView:contentView];
}
}
}
}
And here's how I create each page view:
- (void)addPageViewForPage:(NSString *)page x:(int)x y:(int)y toContentView:(UIView *)contentView {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectInset(CGRectMake(x * kPageSize.width, y * kPageSize.height, kPageSize.width, kPageSize.height), 10, 10)];
label.text = page;
label.textAlignment = NSTextAlignmentCenter;
label.layer.shadowOffset = CGSizeMake(0, 2);
label.layer.shadowRadius = 2;
label.layer.shadowOpacity = 0.3;
label.layer.shadowPath = [UIBezierPath bezierPathWithRect:label.bounds].CGPath;
label.clipsToBounds = NO;
[contentView addSubview:label];
}
Constraining the scroll view's contentOffset
As the user moves his finger around, I want to prevent the scroll view from showing an area of its content that doesn't contain a page. Whenever the scroll view scrolls (by updating its contentOffset), it sends scrollViewDidScroll: to its delegate, so I can implement scrollViewDidScroll: to reset the contentOffset if it goes out of bounds:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint contentOffset = scrollView_.contentOffset;
First, I want to constrain contentOffset so the user can only scroll horizontally or vertically, not diagonally:
CGPoint constrainedContentOffset = [self contentOffsetByConstrainingMovementToOneDimension:contentOffset];
Next, I want to constrain contentOffset so that it only shows parts of the scroll view that contain pages:
constrainedContentOffset = [self contentOffsetByConstrainingToAccessiblePoint:constrainedContentOffset];
If my constraints modified contentOffset, I need to tell the scroll view about it:
if (!CGPointEqualToPoint(contentOffset, constrainedContentOffset)) {
scrollView_.contentOffset = constrainedContentOffset;
}
Finally, I update my idea of the current map position based on the (constrained) contentOffset:
mapPosition_ = [self mapPositionForContentOffset:constrainedContentOffset];
}
Here's how I compute the map position for a given contentOffset:
- (MapPosition)mapPositionForContentOffset:(CGPoint)contentOffset {
return (MapPosition){ roundf(contentOffset.x / kPageSize.width),
roundf(contentOffset.y / kPageSize.height) };
}
Here's how I constrain the movement to just horizontal or vertical and prevent diagonal movement:
- (CGPoint)contentOffsetByConstrainingMovementToOneDimension:(CGPoint)contentOffset {
CGPoint baseContentOffset = [self contentOffsetForCurrentMapPosition];
CGFloat dx = contentOffset.x - baseContentOffset.x;
CGFloat dy = contentOffset.y - baseContentOffset.y;
if (fabsf(dx) < fabsf(dy)) {
contentOffset.x = baseContentOffset.x;
} else {
contentOffset.y = baseContentOffset.y;
}
return contentOffset;
}
Here's how I constrain contentOffset to only go where there are pages:
- (CGPoint)contentOffsetByConstrainingToAccessiblePoint:(CGPoint)contentOffset {
return [self isAccessiblePoint:contentOffset]
? contentOffset
: [self contentOffsetForCurrentMapPosition];
}
Deciding whether a point is accessible turns out to be the tricky bit. It's not enough to just round the point's coordinates to the nearest potential page center and see if that rounded point represents an actual page. That would, for example, let the user drag left/scroll right from page 1, revealing the empty space between pages 1 and 2, until page 1 is half off the screen. We need to round the point down and up to potential page centers, and see if both rounded points represent valid pages. Here's how:
- (BOOL)isAccessiblePoint:(CGPoint)point {
CGFloat x = point.x / kPageSize.width;
CGFloat y = point.y / kPageSize.height;
return [self isAccessibleMapPosition:(MapPosition){ floorf(x), floorf(y) }]
&& [self isAccessibleMapPosition:(MapPosition){ ceilf(x), ceilf(y) }];
}
Checking whether a map position is accessible means checking that it's in the bounds of the grid and that there's actually a page at that position:
- (BOOL)isAccessibleMapPosition:(MapPosition)p {
if (p.y < 0 || p.y >= map_.count)
return NO;
NSArray *mapRow = map_[p.y];
if (p.x < 0 || p.x >= mapRow.count)
return NO;
return ![mapRow[p.x] isKindOfClass:[NSNull class]];
}
Forcing the scroll view to rest at page boundaries
If you don't need to force the scroll view to rest at page boundaries, you can skip the rest of this. Everything I described above will work without the rest of this.
I tried setting pagingEnabled on the scroll view to force it to come to rest at page boundaries, but it didn't work reliably, so I have to enforce it by implementing more delegate methods.
We'll need a couple of utility functions. The first function just takes a CGFloat and returns 1 if it's positive and -1 otherwise:
static int sign(CGFloat value) {
return value > 0 ? 1 : -1;
}
The second function takes a velocity. It returns 0 if the absolute value of the velocity is below a threshold. Otherwise, it returns the sign of the velocity:
static int directionForVelocity(CGFloat velocity) {
static const CGFloat kVelocityThreshold = 0.1;
return fabsf(velocity) < kVelocityThreshold ? 0 : sign(velocity);
}
Now I can implement one of the delegate methods that the scroll view calls when the user stops dragging. In this method, I set the targetContentOffset of the scroll view to the nearest page boundary in the direction that the user was scrolling:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if (fabsf(velocity.x) > fabsf(velocity.y)) {
*targetContentOffset = [self contentOffsetForPageInHorizontalDirection:directionForVelocity(velocity.x)];
} else {
*targetContentOffset = [self contentOffsetForPageInVerticalDirection:directionForVelocity(velocity.y)];
}
}
Here's how I find the nearest page boundary in a horizontal direction. It relies on the isAccessibleMapPosition: method, which I already defined earlier for use by scrollViewDidScroll::
- (CGPoint)contentOffsetForPageInHorizontalDirection:(int)direction {
MapPosition newPosition = (MapPosition){ mapPosition_.x + direction, mapPosition_.y };
return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition];
}
And here's how I find the nearest page boundary in a vertical direction:
- (CGPoint)contentOffsetForPageInVerticalDirection:(int)direction {
MapPosition newPosition = (MapPosition){ mapPosition_.x, mapPosition_.y + direction };
return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition];
}
I discovered in testing that setting targetContentOffset did not reliably force the scroll view to come to rest on a page boundary. For example, in the iOS 5 simulator, I could drag right/scroll left from page 5, stopping halfway to page 4, and even though I was setting targetContentOffset to page 4's boundary, the scroll view would just stop scrolling with the 4/5 boundary in the middle of the screen.
To work around this bug, we have to implement two more UIScrollViewDelegate methods. This one is called when the touch ends:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[scrollView_ setContentOffset:[self contentOffsetForCurrentMapPosition] animated:YES];
}
}
And this one is called when the scroll view stops decelerating:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
CGPoint goodContentOffset = [self contentOffsetForCurrentMapPosition];
if (!CGPointEqualToPoint(scrollView_.contentOffset, goodContentOffset)) {
[scrollView_ setContentOffset:goodContentOffset animated:YES];
}
}
The End
As I said at the beginning, you can download my test implementation from my github repository and try it out for yourself.
That's all, folks!
I'm assuming you're using the UIScrollView in paged mode (swipe to show an entire new screen).
With a bit jiggery-pokery you can achieve the effect you want.
The trick is to ensure that whatever square you're currently viewing, you have the UIScrollView configured so that only the visible central view, and the surrounding view that you could scroll too, are added to the scroll view (and at the correct offset). You also must ensure that the size of the scrollable content (and the current offset) is set correctly, to prevent scrolling in a direction that would take you to no content.
Example: suppose you're viewing square 6 currently. At that point, your scroll view would just have 4 views added to it: 4, 5, 6 and 7, in the correct relative offsets. And you set the content size of the scroll view to be equivelant to 2 x 2 squares size. This will prevent scrolling down or to the left (where there are no tiles) but will allow scrolling in the correct direction.
You'll need your delegate to detect scrollViewDidEndDecelerating:. In that instance, you then have to set up your views, content offset, and content size as described above, for the new location.

Position cursor to end of text in uitextview and scroll to position

Using the code below, I put text from a plist into a textView. The textView is not yet firstresponder; the text is initially for reading only. In iOS4 the goToEndOfNote code positions the cursor at the end of the text AND scrolls to that position. In 3.1.3 it doesn't scroll to the end until the screen is touched (which isn't required unless a change or addition is required), making the textView firstresponder. I would like it to work in 3.1.3 as it does in 4.0. Any ideas please. Thanks.
...
self.temp = [[[NSMutableArray alloc] initWithContentsOfFile:myPlistPath] autorelease];
self.textView.text = [self.temp objectAtIndex:0];
[self goToEndOfNote];
//[self performSelector:#selector(goToEndOfNote) withObject:nil afterDelay:0.1];
}
- (void) goToEndOfNote {
NSUInteger length = self.textView.text.length;
self.textView.selectedRange = NSMakeRange(length, 0);
}
I use setContentOffset:animated to scroll to the top of a UITextView in one of my apps. Should work for scrolling to the bottom, too. Try:
- (void) goToEndOfNote {
NSUInteger length = self.textView.text.length;
self.textView.selectedRange = NSMakeRange(length, 0);
[textView setContentOffset:CGPointMake(0, length) animated:YES];
}
You could also wrap that up so it only happens for 3.1.3 and below:
- (void) goToEndOfNote {
NSUInteger length = self.textView.text.length;
self.textView.selectedRange = NSMakeRange(length, 0);
NSString* systemVersion = [[UIDevice currentDevice] systemVersion];
float version = [systemVersion floatValue];
if (version < 3.2) {
[textView setContentOffset:CGPointMake(0, length) animated:YES];
}
}
Not sure if this is THE answer but it works.
In 3.1.3, with the original code, the cursor was at the end but the scroll was at the top. In 4.0, both were at the bottom.
NSUInteger length = self.textView.text.length;
self.textView.selectedRange = NSMakeRange(length, 0);
I then noticed that in 3.1.3, switching the 0 and the length, the scroll was at the bottom but the cursor was at the top.
NSUInteger length = self.textView.text.length;
self.textView.selectedRange = NSMakeRange(0, length);
Putting the two together worked. It scrolls to the bottom NSMakeRange(0, length) presumably to the end of the range, then the NSMakeRange(length, 0) puts the cursor there, all with no change to what it does in 4.0
NSUInteger length = self.textView.text.length;
self.textView.selectedRange = NSMakeRange(0, length);
self.textView.selectedRange = NSMakeRange(length, 0);
You can use [UITextView -setSelectedRange:] to set a (location = TEXT_VIEW_STRING_LENGTH,length=0) range to bring the cursor to the end of text, and then call [UITextView -scrollRangeToVisible:] with the same range to scroll cursor to visible.
It's late but i found working solution for this . it needs a little hack
- (void) textViewDidBeginEditing:(UITextView*)textview
{
[self performSelector:#selector(placeCursorAtEnd:) withObject:textview afterDelay:0.01];
}
- (void)placeCursorAtEnd:(UITextView *)textview
{
NSUInteger length = textview.text.length;
textview.selectedRange = NSMakeRange(length, 0);
[textView setContentOffset:CGPointMake(0, length) animated:YES];
}