How to avoid text disappearing when using CGAffineTransform? - iphone

I want to make a button rotate in my app, and I have the following piece of code:
- (void)setFrameForButton:(UIButton *)ok {
if ([[UIDevice currentDevice] isWildcat]) {
if([UIDevice currentDevice].orientation == UIDeviceOrientationPortrait) {
ok.frame = CGRectMake(244, 840, 280, 20);
if (wasChanging) ok.transform = CGAffineTransformMakeRotation(M_PI * -0.50);
}
else {
ok.frame = CGRectMake(372, 584, 280, 20);
if (wasChanging) ok.transform = CGAffineTransformMakeRotation(M_PI * -0.50);
}
}
else {
ok.frame = CGRectMake(15.0, 375.0 ,280.0, 20.0);
}
if (wasChanging) wasChanging = NO;
}
- (void)window:(id)window willRotateToInterfaceOrientation:(int)orientation duration:(double)duration {
wasChanging = YES;
[self setFrameForButton:btn];
}
Although when I rotate the position it should be is changed and after some following rotations the text at my button disappears. What to do?

Not sure whether this is the issue, but try setting the autoresizesSubviews property of yor UIButton to NO. This might stop the UIButton from trying to transform its text in any weird way and mess it up.

Related

iOS: Scaling UITextView with pinching?

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.

Scroll effect with swipe gesture in iOS

I have a view which has two labels. When I swipe left I fill next content to label text. Similarly swiping right loads previous content. I want to give an effect to labels like they are scrolling from left or right.
I used a scrollview before but it had a memory problem. So I'm using one view, and swipe gesture loads next or previous content. I want to add scrollview's sliding effect to labels. How can I do that?
I'm not quite sure precisely what effect you're looking for, but you could do something like this, which creates a new, temporary label, puts it off screen, animates the moving it over the label you have on screen, and then when done, resets the old one and deletes the temporary label. This is what a non-autolayout implementation might look like:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
UISwipeGestureRecognizer *left = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(leftSwipe:)];
[left setDirection:UISwipeGestureRecognizerDirectionLeft];
[self.view addGestureRecognizer:left];
// if non-ARC, release it
// [release left];
self.label1.text = #"Mo";
}
- (void)leftSwipe:(UISwipeGestureRecognizer *)gesture
{
NSString *newText;
UILabel *existingLabel = self.label1;
// in my example, I'm just going to toggle the value between Mo and Curly
if ([existingLabel.text isEqualToString:#"Curly"])
newText = #"Mo";
else
newText = #"Curly";
// create new label
UILabel *tempLabel = [[UILabel alloc] initWithFrame:existingLabel.frame];
[existingLabel.superview addSubview:tempLabel];
tempLabel.text = newText;
// move the new label off-frame to the right
tempLabel.transform = CGAffineTransformMakeTranslation(tempLabel.superview.bounds.size.width, 0);
// animate the sliding of them into place
[UIView animateWithDuration:0.5
animations:^{
tempLabel.transform = CGAffineTransformIdentity;
existingLabel.transform = CGAffineTransformMakeTranslation(-existingLabel.superview.bounds.size.width, 0);
}
completion:^(BOOL finished) {
existingLabel.text = newText;
existingLabel.transform = CGAffineTransformIdentity;
[tempLabel removeFromSuperview];
}];
// if non-ARC, release it
// [release tempLabel];
}
This animation animates the label with respect to its superview. You may want to ensure that the superview is set to "clip subviews". This way, the animation will be constrained to the bounds of that superview, which yields a slightly more polished look.
Note, if using auto layout, the idea is the same (though the execution is more complicated). Basically configure your constraints so new view is off to the right, then, in animation block update/replace the constraints so the original label is off to the left and the new one is in the spot of the original label, and, finally, in the completion block reset the constraints of the original label and remove the temporary label.
By the way, this is all infinitely easier if you're comfortable with one of the built in transitions:
- (void)leftSwipe:(UISwipeGestureRecognizer *)gesture
{
NSString *newText;
UILabel *existingLabel = self.label1;
// in my example, I'm just going to toggle the value between Mo and Curly
if ([existingLabel.text isEqualToString:#"Curly"])
newText = #"Mo";
else
newText = #"Curly";
[UIView transitionWithView:existingLabel // or try `existingLabel.superview`
duration:0.5
options:UIViewAnimationOptionTransitionFlipFromRight
animations:^{
existingLabel.text = newText;
}
completion:nil];
}
If you prefer animation that behaves more like a scroll view (i.e., with continuous feedback on the gesture), it might look something like the following:
- (void)viewDidLoad
{
[super viewDidLoad];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panHandler:)];
[self.view addGestureRecognizer:pan];
self.label1.text = #"Mo";
}
- (void)panHandler:(UIPanGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateBegan)
{
_panLabel = [[UILabel alloc] init];
// in my example, I'm just going to toggle the value between Mo and Curly
// you'll presumably set the label contents based upon the direction of the
// pan (if positive, swiping to the right, grab the "previous" label, if negative
// pan, grab the "next" label)
if ([self.label1.text isEqualToString:#"Curly"])
_newText = #"Mo";
else
_newText = #"Curly";
// set the text
_panLabel.text = _newText;
// set the frame to just be off screen
_panLabel.frame = CGRectMake(self.label1.frame.origin.x + self.containerView.frame.size.width,
self.label1.frame.origin.y,
self.label1.frame.size.width,
self.label1.frame.size.height);
[self.containerView addSubview:_panLabel];
_originalCenter = self.label1.center; // save where the original label originally was
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
CGPoint translate = [sender translationInView:self.containerView];
if (translate.x > 0)
{
_panLabel.center = CGPointMake(_originalCenter.x - self.containerView.frame.size.width + translate.x, _originalCenter.y);
self.label1.center = CGPointMake(_originalCenter.x + translate.x, _originalCenter.y);
}
else
{
_panLabel.center = CGPointMake(_originalCenter.x + self.containerView.frame.size.width + translate.x, _originalCenter.y);
self.label1.center = CGPointMake(_originalCenter.x + translate.x, _originalCenter.y);
}
}
else if (sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateFailed || sender.state == UIGestureRecognizerStateCancelled)
{
CGPoint translate = [sender translationInView:self.containerView];
CGPoint finalNewFieldLocation;
CGPoint finalOriginalFieldLocation;
BOOL panSucceeded;
if (sender.state == UIGestureRecognizerStateFailed ||
sender.state == UIGestureRecognizerStateCancelled)
{
panSucceeded = NO;
}
else
{
// by factoring in the velocity, we can capture a flick more accurately
//
// (by the way, I don't like iOS's velocity, because if you stop moving, it records the velocity
// prior to stopping the move rather than noting that you actually stopped, so I usually calculate my own,
// but I'll leave this as is for purposes of this example)
CGPoint velocity = [sender velocityInView:self.containerView];
if (translate.x < 0)
panSucceeded = ((translate.x + velocity.x * 0.5) < -(self.containerView.frame.size.width / 2));
else
panSucceeded = ((translate.x + velocity.x * 0.5) > (self.containerView.frame.size.width / 2));
}
if (panSucceeded)
{
// if we succeeded, finish moving the stuff
finalNewFieldLocation = _originalCenter;
if (translate.x < 0)
finalOriginalFieldLocation = CGPointMake(_originalCenter.x - self.containerView.frame.size.width, _originalCenter.y);
else
finalOriginalFieldLocation = CGPointMake(_originalCenter.x + self.containerView.frame.size.width, _originalCenter.y);
}
else
{
// if we didn't, then just return everything to where it was
finalOriginalFieldLocation = _originalCenter;
if (translate.x < 0)
finalNewFieldLocation = CGPointMake(_originalCenter.x + self.containerView.frame.size.width, _originalCenter.y);
else
finalNewFieldLocation = CGPointMake(_originalCenter.x - self.containerView.frame.size.width, _originalCenter.y);
}
// animate the moving of stuff to their final locations, and on completion, clean everything up
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
_panLabel.center = finalNewFieldLocation;
self.label1.center = finalOriginalFieldLocation;
}
completion:^(BOOL finished) {
if (panSucceeded)
self.label1.text = _newText;
self.label1.center = _originalCenter;
[_panLabel removeFromSuperview];
_panLabel = nil; // in non-ARC, release instead
}
];
}
}
Note, I have put both the original label, as well as the new label being panned on, in a container UIView (called containerView, surprisingly enough), so that I can clip the animation to that container.

Programmatically zooming a UIWebView without gestures - Center Point not calculated correctly

The Problem
I have a UIWebView inside my iPad application which I need to zoom programmatically, but without the use of gestures/taps. The app. has a "+" and a "-" button for zooming in-and-out in user-defined increments (it is an app. for the visually impaired).
This +/- zoom-button functionality previously worked 100% fine when my app. used a UIImageView inside of a UIScrollView (instead of the WebView). The ImageView was the actual view which was zoomed, and the CenterPoint was calculated for the ScrollView's zoomToRect method.
I now have a WebView, which I know contains a ScrollView as a subview. I tried adapting the code which previously worked with the ImageView/ScrollView combo to instead zoom the WebView's ScrollView, but it is no longer calculating the CenterPoint correctly for zoomToRect:.
What Happens:
The WebView zooms correctly in-terms of the zoom-level, but the center point is always wrong. For some reason, the screen always zooms-in on the top-left every time.
Another odd problem, is that after the very first time you zoom-in, you cannot scroll in the WebView past the visible portion of the screen. If you try to, it shows a bit of the content past the bounds of the currently visible area, but it instantly snaps-back.
What I have tried:
I am trying to zoom the UIWebView's ScrollView.
I create a pointer to the ScrollView, and set "self" as its delegate. I then setup various variables, such as scrSize (the size of the view to zoom) and ZoomHandler (explained below):
- (void)viewDidLoad {
// ... Various UIWebView setup ...
[self LoadURL];
// Zooming & scrollview setup
zoomHandler = [[ZoomHandler alloc] initWithZoomLevel: ZOOM_STEP];
scrSize = CGPointMake(self.WebView.frame.size.width, self.WebView.frame.size.height);
scrollView = [WebView.subviews objectAtIndex:0];
[scrollView setTag:ZOOM_VIEW_TAG];
[scrollView setMinimumZoomScale:MINIMUM_SCALE];
[scrollView setZoomScale:1];
[scrollView setMaximumZoomScale:10];
scrollView.bounces = FALSE;
scrollView.bouncesZoom = FALSE;
scrollView.clipsToBounds = NO;
[scrollView setDelegate:self];
[super viewDidLoad];
}
To override the WebView's default zooming limitations, I inject this Javascript into the loaded webpage in the webViewDidFinishLoad: method:
function increaseMaxZoomFactor() {
var element = document.createElement('meta');
element.name = "viewport";
element.content = "maximum-scale=10 minimum-scale=1 initial-scale=1 user-scalable=yes width=device-width height=device-height;"
var head = document.getElementsByTagName('head')[0];
head.appendChild(element);
}
CenterPoint Code:
This code is used to calculate the CenterPoint to pass into zoomToRect:. This worked 100% fine when I was zooming an ImageView inside of a ScrollView.
-(IBAction)zoomOut {
float newScale = [scrollView zoomScale] / ZOOM_STEP;
if( [scrollView zoomScale] > MINIMUM_SCALE) {
[self handleZoomWith:newScale andZoomType: FALSE];
}
}
-(IBAction)zoomIn {
float newScale = [scrollView zoomScale] * ZOOM_STEP;
if( [scrollView zoomScale] < MAXIMUM_SCALE){
[self handleZoomWith:newScale andZoomType: TRUE];
}
}
-(void)handleZoomWith: (float) newScale andZoomType:(BOOL) isZoomIn {
CGPoint newOrigin = [zoomHandler getNewOriginFromViewLocation: [scrollView contentOffset]
viewSize: scrSize andZoomType: isZoomIn];
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:newOrigin];
[scrollView zoomToRect:zoomRect animated:YES];
}
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center {
CGRect zoomRect;
// At a zoom scale of 1.0, it would be the size of the scrollView's bounds.
// As the zoom scale decreases, so more content is visible, the size of the rect grows.
zoomRect.size.height = [WebView frame].size.height / scale;
zoomRect.size.width = [WebView frame].size.width / scale;
// Choose an origin so as to get the right center.
zoomRect.origin.x = center.x / scale;
zoomRect.origin.y = center.y / scale;
return zoomRect;
}
/**
Determine the origin [THIS IS INSIDE ZOOMHANDLER]
*/
-(CGPoint) getNewOriginFromViewLocation: (CGPoint) oldOrigin viewSize: (CGPoint) viewSize andZoomType:(BOOL) isZoomIn {
// Calculate original center (add the half of the width/height of the screen)
float oldCenterX = oldOrigin.x + (viewSize.x / 2);
float oldCenterY = oldOrigin.y + (viewSize.y / 2);
// Xalculate the new center
CGPoint newCenter;
if(isZoomIn) {
newCenter = CGPointMake(oldCenterX * zoomLevel, oldCenterY * zoomLevel);
} else {
newCenter = CGPointMake(oldCenterX / zoomLevel, oldCenterY / zoomLevel);
}
// Calculate the new origin (deduct the half of the width/height of the screen)
float newOriginX = newCenter.x - (viewSize.x / 2);
float newOriginY = newCenter.y - (viewSize.y / 2);
return CGPointMake(newOriginX, newOriginY);
}
Does anyone have any idea why the CenterPoint is not being calculated correctly? Any help would be GREATLY appreciated; I have been stuck on this for a week now.
Thanks,
Alex
Have you tried implementing the zoom using JavaScript rather than the UIWebView's scrollview ?
I believe you can zoom the webview by calling:
[webView stringByEvaluatingJavaScriptFromString:#"document.body.style.zoom = 5.0;"]; (use your ZOOM_STEP as the value)
You could also calculate the center of the browser window using JS:
posY = getScreenCenterY();
posX = getScreenCenterX();
function getScreenCenterY() {
var y = 0;
y = getScrollOffset()+(getInnerHeight()/2);
return(y);
}
function getScreenCenterX() {
return(document.body.clientWidth/2);
}
function getInnerHeight() {
var y;
if (self.innerHeight) // all except Explorer
{
y = self.innerHeight;
}
else if (document.documentElement && document.documentElement.clientHeight)
// Explorer 6 Strict Mode
{
y = document.documentElement.clientHeight;
}
else if (document.body) // other Explorers
{
y = document.body.clientHeight;
}
return(y);
}
function getScrollOffset() {
var y;
if (self.pageYOffset) // all except Explorer
{
y = self.pageYOffset;
}
else if (document.documentElement &&
document.documentElement.scrollTop) // Explorer 6 Strict
{
y = document.documentElement.scrollTop;
}
else if (document.body) // all other Explorers
{
y = document.body.scrollTop;
}
return(y);
}
(source: http://sliceofcake.wordpress.com/2007/09/13/use-javascript-to-find-the-center-of-the-browser/)
for (UIScrollView *scroll in [myPDFView subviews]) {
//Set the zoom level.
[scroll setZoomScale:2.5f animated:YES];
}

MPMoviePlayerViewController in UIModalPresentationFormSheet Problem

My problem is that i have a MPMoviePlayerViewController embeded inside a modalviewcontroller that has the formsheet atribute and when the video goes to fullscreen using the pinch or the arrows the controls dont work.
I have figured out that they dont work because only touches inside the rectangle that makes the modalviewcontroller are registered. for example, double tapping to zoom inside the rectangle works while everywhere else it doesnt.
This is a problem since the movie controls can't be used due to this problem. Can anyone help?
Here is how I solved it. I changed the size of the modal view controller when the video was going into fullscreen.
-(void)movieDidEnterFullscreen:(NSNotification *)notification{
NSLog(#"did enter");
self.navigationController.view.superview.frame = CGRectMake(0, 0, 1500,1500);
self.navigationController.view.superview.center = self.view.center;
[mpviewController moviePlayer].controlStyle = MPMovieControlStyleDefault;
}
-(void)movieDidExitFullscreen:(NSNotification *)notification{
NSLog(#"did exit");
UIDevice *device = [UIDevice currentDevice];
[device beginGeneratingDeviceOrientationNotifications];
if (([device orientation] == UIDeviceOrientationLandscapeLeft) || ([device orientation] == UIDeviceOrientationLandscapeRight)){
self.navigationController.view.superview.frame = CGRectMake(0, 0, 620,540);
self.navigationController.view.superview.center = CGPointMake(384, 512);
}
else {
self.navigationController.view.superview.frame = CGRectMake(0, 0, 540,620);
self.navigationController.view.superview.center = CGPointMake(384, 512);
}
[mpviewController moviePlayer].controlStyle = MPMovieControlStyleEmbedded;
}

How to implement UIScrollView with 1000+ subviews?

I am struggling with writing portion of an app which should behave like the native iphone photo app. Looked at iphone sdk app development book from Orielly which gave an example code for implementing this so-called page-flicking. The code there first created all subviews and then hide/unhide them. At a given time only 3 subviews are visible rest are hidden. After much effort I got it working with app which at that time had only around 15 pages.
As soon as I added 300 pages, it became clear that there are performance/memory issues with that approach of pre-allocating so many subviews. Then I thought may be for my case I should just allocate 3 subviews and instead of hide/unhide them. May be I should just remove/add subviews at runtime. But can't figure out whether UIScrollView can dynamically update contents. For example, at the start there are 3 frames at different x-offsets ( 0, 320, 640 ) from the screen as understood by UIScrollView. Once user moves to 3rd page how do I make sure I am able to add 4th page and remove 1st page and yet UIScrollView doesn't get confused ?
Hoping there is a standard solution to this kind of problem...can someone guide ?
Following what has been said, you can show thousand of elements using only a limited amount of resources (and yes, it's a bit of a Flyweight pattern indeed). Here's some code that might help you do what you want.
The UntitledViewController class just contains a UIScroll and sets itself as its delegate. We have an NSArray with NSString instances inside as data model (there could be potentially thousands of NSStrings in it), and we want to show each one in a UILabel, using horizontal scrolling. When the user scrolls, we shift the UILabels to put one on the left, another on the right, so that everything is ready for the next scroll event.
Here's the interface, rather straightforward:
#interface UntitledViewController : UIViewController <UIScrollViewDelegate>
{
#private
UIScrollView *_scrollView;
NSArray *_objects;
UILabel *_detailLabel1;
UILabel *_detailLabel2;
UILabel *_detailLabel3;
}
#end
And here's the implementation for that class:
#interface UntitledViewController ()
- (void)replaceHiddenLabels;
- (void)displayLabelsAroundIndex:(NSInteger)index;
#end
#implementation UntitledViewController
- (void)dealloc
{
[_objects release];
[_scrollView release];
[_detailLabel1 release];
[_detailLabel2 release];
[_detailLabel3 release];
[super dealloc];
}
- (void)viewDidLoad
{
[super viewDidLoad];
_objects = [[NSArray alloc] initWithObjects:#"first", #"second", #"third",
#"fourth", #"fifth", #"sixth", #"seventh", #"eight", #"ninth", #"tenth", nil];
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 460.0)];
_scrollView.contentSize = CGSizeMake(320.0 * [_objects count], 460.0);
_scrollView.showsVerticalScrollIndicator = NO;
_scrollView.showsHorizontalScrollIndicator = YES;
_scrollView.alwaysBounceHorizontal = YES;
_scrollView.alwaysBounceVertical = NO;
_scrollView.pagingEnabled = YES;
_scrollView.delegate = self;
_detailLabel1 = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 460.0)];
_detailLabel1.textAlignment = UITextAlignmentCenter;
_detailLabel1.font = [UIFont boldSystemFontOfSize:30.0];
_detailLabel2 = [[UILabel alloc] initWithFrame:CGRectMake(320.0, 0.0, 320.0, 460.0)];
_detailLabel2.textAlignment = UITextAlignmentCenter;
_detailLabel2.font = [UIFont boldSystemFontOfSize:30.0];
_detailLabel3 = [[UILabel alloc] initWithFrame:CGRectMake(640.0, 0.0, 320.0, 460.0)];
_detailLabel3.textAlignment = UITextAlignmentCenter;
_detailLabel3.font = [UIFont boldSystemFontOfSize:30.0];
// We are going to show all the contents of the _objects array
// using only these three UILabel instances, making them jump
// right and left, replacing them as required:
[_scrollView addSubview:_detailLabel1];
[_scrollView addSubview:_detailLabel2];
[_scrollView addSubview:_detailLabel3];
[self.view addSubview:_scrollView];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[_scrollView flashScrollIndicators];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self displayLabelsAroundIndex:0];
}
- (void)didReceiveMemoryWarning
{
// Here you could release the data source, but make sure
// you rebuild it in a lazy-loading way as soon as you need it again...
[super didReceiveMemoryWarning];
}
#pragma mark -
#pragma mark UIScrollViewDelegate methods
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
// Do some initialization here, before the scroll view starts moving!
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self replaceHiddenLabels];
}
- (void)displayLabelsAroundIndex:(NSInteger)index
{
NSInteger count = [_objects count];
if (index >= 0 && index < count)
{
NSString *text = [_objects objectAtIndex:index];
_detailLabel1.frame = CGRectMake(320.0 * index, 0.0, 320.0, 460.0);
_detailLabel1.text = text;
[_scrollView scrollRectToVisible:CGRectMake(320.0 * index, 0.0, 320.0, 460.0) animated:NO];
if (index < (count - 1))
{
text = [_objects objectAtIndex:(index + 1)];
_detailLabel2.frame = CGRectMake(320.0 * (index + 1), 0.0, 320.0, 460.0);
_detailLabel2.text = text;
}
if (index > 0)
{
text = [_objects objectAtIndex:(index - 1)];
_detailLabel3.frame = CGRectMake(320.0 * (index - 1), 0.0, 320.0, 460.0);
_detailLabel3.text = text;
}
}
}
- (void)replaceHiddenLabels
{
static const double pageWidth = 320.0;
NSInteger currentIndex = ((_scrollView.contentOffset.x - pageWidth) / pageWidth) + 1;
UILabel *currentLabel = nil;
UILabel *previousLabel = nil;
UILabel *nextLabel = nil;
if (CGRectContainsPoint(_detailLabel1.frame, _scrollView.contentOffset))
{
currentLabel = _detailLabel1;
previousLabel = _detailLabel2;
nextLabel = _detailLabel3;
}
else if (CGRectContainsPoint(_detailLabel2.frame, _scrollView.contentOffset))
{
currentLabel = _detailLabel2;
previousLabel = _detailLabel1;
nextLabel = _detailLabel3;
}
else
{
currentLabel = _detailLabel3;
previousLabel = _detailLabel1;
nextLabel = _detailLabel2;
}
currentLabel.frame = CGRectMake(320.0 * currentIndex, 0.0, 320.0, 460.0);
currentLabel.text = [_objects objectAtIndex:currentIndex];
// Now move the other ones around
// and set them ready for the next scroll
if (currentIndex < [_objects count] - 1)
{
nextLabel.frame = CGRectMake(320.0 * (currentIndex + 1), 0.0, 320.0, 460.0);
nextLabel.text = [_objects objectAtIndex:(currentIndex + 1)];
}
if (currentIndex >= 1)
{
previousLabel.frame = CGRectMake(320.0 * (currentIndex - 1), 0.0, 320.0, 460.0);
previousLabel.text = [_objects objectAtIndex:(currentIndex - 1)];
}
}
#end
Hope this helps!
UIScrollView is just a subclass of UIView so it's possible to add and remove subviews at runtime. Assuming you have fixed width photos (320px) and there are 300 of them, then your main view would be 300 * 320 pixels wide. When creating the scroll view, initialize the frame to be that wide.
So the scroll view's frame would have the dimensions (0, 0) to (96000, 480). Whenever you are adding a subview, you will have to change it's frame so it fits in the correct position in its parent view.
So let's say, we are adding the 4th photo to the scroll view. It's frame would be from (960, 480) to (1280, 480). That is easily to calculate, if you can somehow associate an index with each picture. Then use this to calculate the picture's frame where indexes start at 0:
Top-Left -- (320 * (index - 1), 0)
to
Bottom-Right -- (320 * index, 480)
Removing the first picture/subview should be easy. Keep an array of the 3 subviews currently on-screen. Whenever you are adding a new subview to the screen, also add it to the end of this array, and then remove the first subview in this array from the screen too.
Many thanks to Adrian for his very simple and powerfull code sample.
There was just one issue with this code : when the user made a "double scroll" (I.E. when he did not wait for the animation to stop and rescroll the scrollview again and again).
In this case, the refresh for the position of the 3 subviews is effective only when the "scrollViewDidEndDecelerating" method is invoked, and the result is a delay before the apparition of the subviews on the screen.
This can be easily avoided by adding few lines of code :
in the interface, just add this :
int refPage, currentPage;
in the implementation, initialize refPage and currentPage in the "viewDidLoad" method like this :
refpage = 0;
curentPage = 0;
in the implementation, just add the "scrollViewDidScroll" method, like this :
- (void)scrollViewDidScroll:(UIScrollView *)sender{
int currentPosition = floor(_scrollView.contentOffset.x);
currentPage = MAX(0,floor(currentPosition / 340));
//340 is the width of the scrollview...
if(currentPage != refPage) {
refPage = currentPage;
[self replaceHiddenLabels];
}
}
et voilĂ  !
Now, the subviews are correctly replaced in the correct positions, even if the user never stop the animation and if the "scrollViewDidEndDecelerating" method is never invoked !