Handling touch events within a child UIScrollView - iphone

I'm displaying a series of images in a UIScrollView. I pretty much want to replicate the Photos application.
My current architecture is:
A parent UIScrollView with content size that is wide enough for x number of pages + some extra space for margins in between the images.
Each image is contained in a UIImageView.
Each UIImageView is contained within its own UIScrollview which are then the subviews of the parent UIScrollView.
So I basically have a row of UIScrollViews within a parent UIScrollView.
The parent UIScrollView has paging enabled so I can scroll from page to page without any problems.
The problem is how to seamlessly pan around a zoomed-in image. I have overridden the viewForZoomingInScrollView method to return the appropriate UIImageView when the user pinches in/out. I have overriden the scrollViewDidEndZooming method to set the parent view's canCancelContentTouches property to NO if the zoom scale is greater than 1.
So users are able to pan around an image. However, they must hold their finger down for a moment to get past the small delay the parent scroll view has before sending touch events down to the subviews. Also, once the user is panning in one image, the next/prev images do not enter the viewable area when the user has reached the border of the current image.
Any ideas?
Thanks.

Yay! I tried to approach the problem with just one UIScrollView, and I think I found a solution.
Before the user starts zooming (in viewForZoomingInScrollView:), I switch the scroll view into zooming mode (remove all additional pages, reset content size and offset). When the user zooms out to scale 1.00 (in scrollViewDidEndZooming:withView:atScale:), I switch back to paging view (add all pages back, adjust content size and offset).
Here's the code of a simple view controller that does just that. This sample switches between, zooms and pans three large UIImageViews.
Note that a single view controller with a handful of functions is all it takes, no need to subclass UIScrollView or something.
typedef enum {
ScrollViewModeNotInitialized, // view has just been loaded
ScrollViewModePaging, // fully zoomed out, swiping enabled
ScrollViewModeZooming, // zoomed in, panning enabled
ScrollViewModeAnimatingFullZoomOut, // fully zoomed out, animations not yet finished
ScrollViewModeInTransition, // during the call to setPagingMode to ignore scrollViewDidScroll events
} ScrollViewMode;
#interface ScrollingMadnessViewController : UIViewController <UIScrollViewDelegate> {
UIScrollView *scrollView;
NSArray *pageViews;
NSUInteger currentPage;
ScrollViewMode scrollViewMode;
}
#end
#implementation ScrollingMadnessViewController
- (void)setPagingMode {
NSLog(#"setPagingMode");
if (scrollViewMode != ScrollViewModeAnimatingFullZoomOut && scrollViewMode != ScrollViewModeNotInitialized)
return; // setPagingMode is called after a delay, so something might have changed since it was scheduled
scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
// reposition pages side by side, add them back to the view
CGSize pageSize = scrollView.frame.size;
NSUInteger page = 0;
for (UIView *view in pageViews) {
if (!view.superview)
[scrollView addSubview:view];
view.frame = CGRectMake(pageSize.width * page++, 0, pageSize.width, pageSize.height);
}
scrollView.pagingEnabled = YES;
scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = NO;
scrollView.contentSize = CGSizeMake(pageSize.width * [pageViews count], pageSize.height);
scrollView.contentOffset = CGPointMake(pageSize.width * currentPage, 0);
scrollViewMode = ScrollViewModePaging;
}
- (void)setZoomingMode {
NSLog(#"setZoomingMode");
scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
CGSize pageSize = scrollView.frame.size;
// hide all pages besides the current one
NSUInteger page = 0;
for (UIView *view in pageViews)
if (currentPage != page++)
[view removeFromSuperview];
// move the current page to (0, 0), as if no other pages ever existed
[[pageViews objectAtIndex:currentPage] setFrame:CGRectMake(0, 0, pageSize.width, pageSize.height)];
scrollView.pagingEnabled = NO;
scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = YES;
scrollView.contentSize = pageSize;
scrollView.contentOffset = CGPointZero;
scrollViewMode = ScrollViewModeZooming;
}
- (void)loadView {
CGRect frame = [UIScreen mainScreen].applicationFrame;
scrollView = [[UIScrollView alloc] initWithFrame:frame];
scrollView.delegate = self;
scrollView.maximumZoomScale = 2.0f;
scrollView.minimumZoomScale = 1.0f;
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"red.png"]];
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"green.png"]];
UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"yellow-blue.png"]];
// in a real app, you most likely want to have an array of view controllers, not views;
// also should be instantiating those views and view controllers lazily
pageViews = [[NSArray alloc] initWithObjects:imageView1, imageView2, imageView3, nil];
self.view = scrollView;
}
- (void)setCurrentPage:(NSUInteger)page {
if (page == currentPage)
return;
currentPage = page;
// in a real app, this would be a good place to instantiate more view controllers -- see SDK examples
}
- (void)viewDidLoad {
scrollViewMode = ScrollViewModeNotInitialized;
[self setPagingMode];
}
- (void)viewDidUnload {
[pageViews release]; // need to release all page views here; our array is created in loadView, so just releasing it
pageViews = nil;
}
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(setPagingMode) object:nil];
CGPoint offset = scrollView.contentOffset;
NSLog(#"scrollViewDidScroll: (%f, %f)", offset.x, offset.y);
if (scrollViewMode == ScrollViewModeAnimatingFullZoomOut && ABS(offset.x) < 1e-5 && ABS(offset.y) < 1e-5)
// bouncing is still possible (and actually happened for me), so wait a bit more to be sure
[self performSelector:#selector(setPagingMode) withObject:nil afterDelay:0.1];
else if (scrollViewMode == ScrollViewModePaging)
[self setCurrentPage:roundf(scrollView.contentOffset.x / scrollView.frame.size.width)];
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)aScrollView {
if (scrollViewMode != ScrollViewModeZooming)
[self setZoomingMode];
return [pageViews objectAtIndex:currentPage];
}
- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
NSLog(#"scrollViewDidEndZooming: scale = %f", scale);
if (fabsf(scale - 1.0) < 1e-5) {
if (scrollView.zoomBouncing)
NSLog(#"scrollViewDidEndZooming, but zoomBouncing is still true!");
// cannot call setPagingMode now because scrollView will bounce after a call to this method, resetting contentOffset to (0, 0)
scrollViewMode = ScrollViewModeAnimatingFullZoomOut;
// however sometimes bouncing will not take place
[self performSelector:#selector(setPagingMode) withObject:nil afterDelay:0.2];
}
}
#end
Runnable sample project is available at http://github.com/andreyvit/ScrollingMadness/ (if you don't use Git, just click Download button there). A README is available there, explaining why the code was written the way it is.
(The sample project also illustrates how to zoom a scroll view programmatically, and has ZoomScrollView class that encapsulates a solution to that. It is a neat class, but is not required for this trick. If you want an example that does not use ZoomScrollView, go back a few commits in commit history.)
P.S. For the sake of completeness, there's TTScrollView — UIScrollView reimplemented from scratch. It's part of the great and famous Three20 library. I don't like how it feels to the user, but it does make implementing paging/scrolling/zooming dead simple.
P.P.S. The real Photo app by Apple has pre-SDK code and uses pre-SDK classes. One can spot two classes derived from pre-SDK variant of UIScrollView inside PhotoLibrary framework, however it is not immediately clear what they do (and they do quite a lot). I can easily believe this effect used to be harder to achieve in pre-SDK times.

I had to do a similar setup, but I basically custom wrote the whole thing. I'm not sure how you're going to get around the problem of 'handing off' touch events from the child UIScrollView to the parent UISscrollView when you reach the edge. You might try overriding UITouchesBegan:withEvent: in your parent UIScrollView, and dumping directly to the child. Good luck!

This is the Andrey's code with c# translation for monotouch developers...
First you need to edit your xib file.. Insert 3 view controllers and make outlets like _page1, _page2, _mainpage.. and link these outlets with views. note you must referance to view controller's view outlet with _mainpage view. (sorry for my english)
public partial class Test_Details_Controller : UIViewController
{
private UIPageControl _pageCont;
private UIScrollView _scView;
private Object[] _pageViews;
private int _currentPageIndex;
private bool _rotationInProgress;
void InitializeAfterLoad ()
{
this.Title = "Test";
this._pageCont = CreatePageControll();
}
private UIPageControl CreatePageControll()
{
UIPageControl pageControll = new UIPageControl( new RectangleF( 146,348, 38, 20 ) );
pageControll.BackgroundColor = UIColor.Red;
pageControll.Alpha = 0.7f;
return pageControll;
}
private void UpdatePageControll(UIPageControl cont, int current, int pages, UIView showed)
{
cont.CurrentPage = current;
cont.Pages = pages;
cont.UpdateCurrentPageDisplay();
UIPageControl.AnimationsEnabled = true;
UIPageControl.BeginAnimations(string.Empty, this.Handle);
cont.Frame = new RectangleF(showed.Frame.Location.X
, cont.Frame.Location.Y , pageSize().Width, cont.Frame.Height);
UIPageControl.CommitAnimations();
}
private UIView loadViewForPage(int pageIndex){
UIView _view = null;
switch ( pageIndex ) {
case 1:
_view = this._page1;
break;
case 2:
_view = this._page2;
break;
default:
_view = this._page1;
break;
}
return _view;
}
private int numberOfPages(){
return (int)this._pageViews.Count();
}
private UIView viewForPage( int pageIndex ){
UIView pageView;
if(this._pageViews.ElementAt( pageIndex ) == null)
{
pageView = loadViewForPage( pageIndex );
_pageViews[ pageIndex ] = pageView;
}
else{
pageView = (UIView)_pageViews[ pageIndex ];
}
_scView.AddSubview( pageView );
return pageView;
}
private SizeF pageSize(){
return this._scView.Frame.Size;
}
private bool isPageLoaded( int pageIndex ){
return this._pageViews.ElementAt( pageIndex ) != null;
}
private void layoutPage( int pageIndex ){
SizeF pageSize = this.pageSize();
((UIView)this._pageViews[pageIndex]).Frame = new RectangleF( pageIndex * pageSize.Width,0, pageSize.Width, pageSize.Height );
this.viewForPage( pageIndex );
}
private void loadView(){
this._scView = new UIScrollView();
this._scView.Delegate = new ScViewDelegate( this );
this._scView.PagingEnabled = true;
this._scView.ShowsHorizontalScrollIndicator = false;
this._scView.ShowsVerticalScrollIndicator = false;
this._scView.Layer.BorderWidth = 2;
this._scView.AddSubview( _pageCont );
this.View = this._scView;
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
InitializeAfterLoad ();
this._pageViews = new Object[]{ _page1, _page2 };
this.loadView();
}
private void currentPageIndexDidChange(){
this.layoutPage( _currentPageIndex );
if(_currentPageIndex+1 < this.numberOfPages()){
this.layoutPage( _currentPageIndex + 1 );
}
if(_currentPageIndex >0){
this.layoutPage( _currentPageIndex - 1 );
}
this.UpdatePageControll( _pageCont, _currentPageIndex, this.numberOfPages(), ((UIView)this._pageViews[_currentPageIndex]) );
this._scView.BringSubviewToFront( _pageCont );
this.NavigationController.Title = string.Format( "{0} of {1}", _currentPageIndex + 1, this.numberOfPages() );
}
private void layoutPages(){
SizeF pageSize = this.pageSize();
this._scView.ContentSize = new SizeF( this.numberOfPages() * pageSize.Width, pageSize.Height );
// move all visible pages to their places, because otherwise they may overlap
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if(this.isPageLoaded( pageIndex ))
this.layoutPage( pageIndex );
}
}
public override void ViewWillAppear (bool animated)
{
this.layoutPages();
this.currentPageIndexDidChange();
this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );
}
class ScViewDelegate : UIScrollViewDelegate
{
Test_Details_Controller id;
public ScViewDelegate ( Test_Details_Controller id )
{
this.id = id;
}
public override void Scrolled (UIScrollView scrollView)
{
if(id._rotationInProgress)
return;// UIScrollView layoutSubviews code adjusts contentOffset, breaking our logic
SizeF pageSize = id.pageSize();
int newPageIndex = ((int)id._scView.ContentOffset.X + (int)pageSize.Width / 2) / (int)pageSize.Width;
if( newPageIndex == id._currentPageIndex )
return;
id._currentPageIndex = newPageIndex;
id.currentPageIndexDidChange();
}
}
public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
{
return toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown;
}
public override void WillRotate (UIInterfaceOrientation toInterfaceOrientation, double duration)
{
_rotationInProgress = true;
// hide other page views because they may overlap the current page during animation
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if(this.isPageLoaded( pageIndex ))
this.viewForPage( pageIndex ).Hidden = ( pageIndex != _currentPageIndex );
}
}
public override void WillAnimateRotation (UIInterfaceOrientation toInterfaceOrientation, double duration)
{
// resize and reposition the page view, but use the current contentOffset as page origin
// (note that the scrollview has already been resized by the time this method is called)
SizeF pageSize = this.pageSize();
UIView pageView = this.viewForPage( _currentPageIndex );
this.viewForPage( _currentPageIndex ).Frame = new RectangleF( this._scView.ContentOffset.X, 0, pageSize.Width, pageSize.Height );
}
public override void DidRotate (UIInterfaceOrientation fromInterfaceOrientation)
{
base.DidRotate (fromInterfaceOrientation);
// adjust frames according to the new page size - this does not cause any visible changes
this.layoutPages();
this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );
//unhide
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if( this.isPageLoaded( pageIndex ) )
this.viewForPage( pageIndex ).Hidden = false;
}
_rotationInProgress = false;
}
public override void DidReceiveMemoryWarning ()
{
//SuperHandle = DidReceiveMemoryWarning();
if(this._pageViews != null)
{
// unload non-visible pages in case the memory is scarse
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if( pageIndex < _currentPageIndex - 1 || pageIndex > _currentPageIndex + 1 )
if( this.isPageLoaded(pageIndex) ){
UIView pageview = (UIView)this._pageViews[ pageIndex ];
this._pageViews[ pageIndex ] = null;
pageview.RemoveFromSuperview();
}
}
}
}
public override void ViewDidUnload ()
{
this._pageViews = null;
this._scView = null;
}
}

I have a sample project and it works perfect.
https://code.google.com/p/uiscrollview-touch-events/

Related

Set Uibutton Random Position on UIView

I want to set 5 buttons on uiview at random position. Buttons need maintain some spacing to each other. I mean buttons should not overlap to each other.
All buttons set on UIView come from corners with rotation animation.
btn1.transform = CGAffineTransformMakeRotation(40);
btn2.transform = CGAffineTransformMakeRotation(60);
btn3.transform = CGAffineTransformMakeRotation(90);
btn4.transform = CGAffineTransformMakeRotation(30);
btn5.transform = CGAffineTransformMakeRotation(20);
I Can rotate buttons using above code but can you pls. help me for set buttons on random position with out overlapping by each other.
If points are fix than I can set buttons with animation by this code but I want random position of buttons.
[AnimationView moveBubble:CGPointMake(18, 142) duration:1 : btn1];
[AnimationView moveBubble:CGPointMake(118, 142) duration:1 : btn2];
[AnimationView moveBubble:CGPointMake(193, 142) duration:1 : btn3];
[AnimationView moveBubble:CGPointMake(18, 216) duration:1 : btn4];
Thanks in advance.
1st, add buttons to an NSArray, only to make things easier:
NSArray *buttonArray = #[btn1,btn2,btn3,btn4,btn5];
Now, this code tries to Arrange them at random positions.
int xTemp, yTemp;
for (int i = 0; i < 5; i++) {
while (YES) {
xTemp = arc4random_uniform(view.frame.size.width - [buttonArray[i] frame].size.width);
yTemp = arc4random_uniform(view.frame.size.height - [buttonArray[i] frame].size.height);
if (![self positionx:xTemp y:yTemp intersectsAnyButtonTillIndex:i inButtonArray:buttonArray]) {
[AnimationView moveBubble:CGPointMake(xTemp, yTemp) duration:1 : buttonArray[i]];
break;
}
}
}
Implement this function somewhere too:
- (BOOL) positionx:(int)xTemp y:(int)yTemp intersectsAnyButtonTillIndex:(int)index inButtonArray:(NSArray *)buttonArray {
//Again please change the < to <= , I'm sorry, doing too many things at once.
for (int i = 0; i <= index; i++) {
CGRect frame = [buttonArray[i] frame];
//EDIT : In the logic earlier, I had wrongly done a minus where I should have done a plus.
if ((xTemp > frame.origin.x && xTemp < (frame.size.width + frame.origin.x)) && (yTemp > frame.origin.y && yTemp < (frame.size.height + frame.origin.y))) return YES;
}
return NO;
OK this is a workign soln., I hope, just added something to WolfLink's answer. Check This.
for (UIButton *button in buttonArray) {
button.frame = CGRectMake(arc4random_uniform(view.frame.size.width - button.frame.size.width), arc4random_uniform(view.frame.size.height - button.frame.size.height), button.frame.size.width, button.frame.size.height);
while ([self button:button intersectsButtonInArray:buttonArray]) {
button.frame = CGRectMake(arc4random_uniform(view.frame.size.width - button.frame.size.width), arc4random_uniform(view.frame.size.height - button.frame.size.height), button.frame.size.width, button.frame.size.height);
}
//another function
-(BOOL)button:(UIButton *)button intersectsButtonInArray:(NSArray *)array {
for (UIButton *testButton in array) {
if (CGRectIntersectsRect(button.frame, testButton.frame) && ![button isEqual:testButton]) {
return YES;
}
}
return NO;
}
Based on spiritofmysoul.wordpress's code:
//in the function where you randomize the buttons
NSArray *buttonArray = #[btn1,btn2,btn3,btn4,btn5];
for (UIButton *button in buttonArray) {
float widthOffset = self.frame.size.width-button.frame.size.width;
float heightOffset = self.frame.size.height-button.frame.size.height;
button.frame = CGRectMake(arc4random()%widthOffset, arc4random()%heightOffset, button.frame.size.width, button.frame.size.height);
while ([self button:button intersectsButtonInArray:buttonArray]) {
button.frame = CGRectMake(arc4random(), arc4random(), button.frame.size.width, button.frame.size.height);
}
//another function
-(BOOL)button:(UIButton *)button intersectsButtonInArray:(NSArray *)array {
for (UIButton *testButton in array) {
if (CGRectIntersectsRect(button.frame, testButton.frame) && ![button isEqual:testButton]) {
return YES;
}
}
return NO;
}
Beware: This will work well for small amounts of buttons on a large space but as you add buttons and you run out of space, this method will take much longer to run. If there is not enough space for all the buttons, it will become an infinite loop.

Make scrollbar always visible on UIScrollView?

I need to make a scrollbar always visible on viewDidLoad so that the user can understand that there is content to scroll. I did the following:
[myscrollView flashScrollIndicators];
But then the scrollbars only appear for some time after viewDidLoad and disappear again only to reappear when the user touches the screen..
I need to make scrollbars always visible. How can I do it?
Apple indirectly discourage constantly displaying scroll indicators in their iOS Human Interface Guidelines but guidelines are just guidelines for a reason, they don't account for every scenario and sometimes you may need to politely ignore them.
The scroll indicators of any content views are UIImageView subviews of those content views. This means you can access the scroll indicators of a UIScrollView as you would any of its other subviews (i.e. myScrollView.subviews) and modify the scroll indicators as you would any UIImageView (e.g. scrollIndicatorImageView.backgroundColor = [UIColor redColor];).
The most popular solution appears to be the following code:
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#implementation UIImageView (ForScrollView)
- (void) setAlpha:(float)alpha {
if (self.superview.tag == noDisableVerticalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.height < sc.contentSize.height) {
return;
}
}
}
}
if (self.superview.tag == noDisableHorizontalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.width < sc.contentSize.width) {
return;
}
}
}
}
[super setAlpha:alpha];
}
#end
Which is originally credited to this source.
This defines a category for UIImageView that defines a custom setter for the alpha property. This works because at some point in the underlying code for the UIScrollView, it will set its scroll indicator's alpha property to 0 in order to hide it. At this point it will run through our category and, if the hosting UIScrollView has the right tag, it will ignore the value being set, leaving it displayed.
In order to use this solution ensure your UIScrollView has the appropriate tag e.g.
If you want to display the scroll indicator from the moment its UIScrollView is visible simply flash the scroll indicators when the view appears .e.g
- (void)viewDidAppear:(BOOL)animate
{
[super viewDidAppear:animate];
[self.scrollView flashScrollIndicators];
}
Additional SO references:
UIScrollView - showing the scroll bar
UIScrollView indicator always show?
Scroll Indicators Visibility
Make scrollbars always visible in uiscrollview
I want to offer my solution. I don't like the most popular variant with category (overriding methods in category can be the reason of some indetermination what method should be called in runtime, since there is two methods with the same selector).
I use swizzling instead. And also I don't need to use tags.
Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
The scroll indicator (vertical scroll indicator in this case) will be always at the screen.
Update November, 2019
Starting from iOS 13 UIScrollView subclasses are changed. Now scroll indicators are inherited from UIView and has their own private class called _UIScrollViewScrollIndicator. This means, that they are not subclasses of UIImageView now, so old method won't work anymore.
Also we are not able to implement subclass of _UIScrollViewScrollIndicator because it is private class and we don't have access to it. So the only solution is to use runtime. Now to have support for iOS 13 and earlier implement the next steps:
Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
} else if ([NSStringFromClass(view.class) isEqualToString:#"_UIScrollViewScrollIndicator"]) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found scroll indicator, (be sure to create AlwaysOpaqueScrollIndicator in runtime earlier!)
// Current implementation is in AlwaysOpaqueScrollTableView class
object_setClass(view, NSClassFromString(#"AlwaysOpaqueScrollIndicator"));
break;
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class (this is for iOS earlier than 13)
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
Add these methods somewhere in you code (either the same view controller as in step 1, or to the desired UIScrollView subclass).
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Create child class from _UIScrollViewScrollIndicator since it is private
Class alwaysOpaqueScrollIndicatorClass = objc_allocateClassPair(NSClassFromString(#"_UIScrollViewScrollIndicator"), "AlwaysOpaqueScrollIndicator", 0);
objc_registerClassPair(alwaysOpaqueScrollIndicatorClass);
// Swizzle setAlpha: method of this class to custom
Class replacementMethodClass = [self class];
SEL originalSelector = #selector(setAlpha:);
SEL swizzledSelector = #selector(alwaysOpaque_setAlpha:);
Method originalMethod = class_getInstanceMethod(alwaysOpaqueScrollIndicatorClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(replacementMethodClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(alwaysOpaqueScrollIndicatorClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(alwaysOpaqueScrollIndicatorClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)alwaysOpaque_setAlpha:(CGFloat)alpha {
[self alwaysOpaque_setAlpha:1.0];
}
This step creates the subclass of _UIScrollViewScrollIndicator called AlwaysOpaqueScrollIndicator in runtime and swizzle setAlpha: method implementation to alwaysOpaque_setAlpha:.
Do not forget to add
#import <objc/runtime.h>
to the files you've inserted this code. Thanks to #Smartcat for reminder about this
I dont know whether this will work or not. But just a hint for you.
Scrollbar inside the Scrollview is a Imageview. Which is a subview of UIScrollview
So get the Scrollbar Imageview of the UIscrollview. Then try to set that image property hidden to NO or Change Alpha value
static const int UIScrollViewHorizontalBarIndexOffset = 0;
static const int UIScrollViewVerticalBarIndexOffset = 1;
-(UIImageView *)scrollbarImageViewWithIndex:(int)indexOffset
{
int viewsCount = [[yourScrollview subviews] count];
UIImageView *scrollBar = [[yourScrollview subviews] objectAtIndex:viewsCount - indexOffset - 1];
return scrollBar;
}
-(void) viewDidLoad
{
//Some Code
//Get Scrollbar
UIImageView *scrollBar = [self scrollbarImageViewWithIndex: UIScrollViewVerticalBarIndexOffset];
//The try setting hidden property/ alpha value
scrollBar.hidden=NO;
}
Got reference from here
This is Swift version of #Accid Bright's answer:
class AlwaysOpaqueImageView: UIImageView {
override var alpha: CGFloat {
didSet {
alpha = 1
}
}
static func setScrollbarToAlwaysVisible(from scrollView: UIScrollView) {
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for view in scrollView.subviews {
if view.isKind(of: UIImageView.self),
view.alpha == 0 && view.autoresizingMask == UIView.AutoresizingMask.flexibleLeftMargin,
view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width,
scrollView.frame.size.height < scrollView.contentSize.height {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, AlwaysOpaqueImageView.self)
break
}
}
// Ask to flash indicator to turn it on
scrollView.flashScrollIndicators()
}
}
One difference is that setting scrollbar is extracted out as a static method.

Animating sequence of images in cocos2d

i have this code to show animation of a image
-(void) blink:(ccTime) delta {
animateblink ++; //adds 1 every frame
if (animateblink <= 6 ) { //if we included frames to show for breaking and the current frame is less than the max number of frames to play
if (animateblink < 6) {
[sprite setTexture:[[CCSprite spriteWithFile:[NSString stringWithFormat:#"%#_blinkk00%i.png", baseImageName,animateblink]] texture]];
[self unschedule:_cmd];
[self schedule:#selector(openEyes:) interval:0.1f];
[self schedule:#selector(moveSprite:) interval:0.1f];
}
}
}
i have 6 images of animating like
dragonimage_blinkk001,dragonimage_blinkk002,dragonimage_blinkk003,dragonimage_blinkk004,dragonimage_blinkk005,dragonimage_blinkk006 like that
i put two methods,
1: for animation time
2: for movement of the image
the code is
-(void) openEyes:(ccTime) delta {
[sprite setTexture:[[CCSprite spriteWithFile:[NSString stringWithFormat:#"%#.png", baseImageName]] texture]];
[self unschedule:_cmd];
int blinkInterval = (arc4random() % 6) + 1; // range 3 to 8
[self schedule:#selector(blink:) interval:blinkInterval];
}
-(void)moveSprite:(ccTime)delta
{
movementCounter ++;
if (movementCounter == 1000) {
[self unschedule:_cmd];
}
else
{
spriteimagename.position = ccp(spriteimagename.position.x+10,spriteimagename.position.y);
}
}
but on the first method the animation time is not correct,there is a lot of delay of animation,i just want to show animation of the images randomly and fast,its a dragon flying animation.
my sec on method is not at all working,i didn't get any movement of that image.
i hope you understand my issue.how to solve the above two method issues.
Thanks in advance.
Quick solution is to add frameIndex member in cocos2d CCSpriteFrame class. i don't know is that allowed to change. But it works fine.
#interface CCSpriteFrame : NSObject <NSCopying>
{
int frameIndex;
....
}
//Here in code initialize frames with unique index
CCAnimation* animation;
NSMutableArray *animFrames2 = [NSMutableArray array];
for( int i=1;i<=15;i++)
{
CCSpriteFrame *frame = [cache spriteFrameByName:[NSString stringWithFormat:#"ActerSmash_%d.png",i]];
frame.frameIndex = (int)i;
[animFrames2 addObject:frame];
}
animation = [CCAnimation animationWithFrames:animFrames2];
CCAnimate *action = [CCAnimate actionWithDuration:ACTER_SMASH_SPEED animation:animation restoreOriginalFrame:YES];
action.tag = kTagAnimationSmashAnim ;
mActer.AnimSmash = action;
//Checking frame Index
CCAnimate *anim = mActer.AnimSmash ;
for(CCSpriteFrame *frame in anim.animation.frames)
{
if([mActer isFrameDisplayed:frame] ) )
{
if(frame.frameIndex == 5 )
{
//Do something..
}
}
}

Paged UIScrollview with lazy delayed loading

I need help with the uiscrollview implementation with the following requirements:
1) pagingEnabled = true.
2) lazy pages loading.
3) pages are loading in the background. So i need at first run loading the page X, then get the notification that the page is fully loaded and only then allow the user to scroll to it.
4) ability to change the current page.
My first attempt was to override the scrollViewDidEndDeacelerating and scrollViewDidScroll, but I had troubles with stucking on half of pages (when you stop the scroll on the half of the page and then wait for new page to add to the scroll) and empty pages (when the user scrolled too fast).
My second attempt was to override the layoutSubviews method of the UIScrollView and do all calculations there. But it seems to be very sofisticated.
So, I'd love to find any examples of similar implementations.
Now I have the code like this:
I've implemented scrollViewWillBeginDragging and scrollViewDidEndDecelerating:
- (void)scrollViewWillBeginDragging:(UIScrollView *)aScrollView
{
isScrolling = YES;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)aScrollView
{
isScrolling = NO;
// Here we load the page that was prepared when the user was scrolling
if (needDisplay > -1) {
//NSLog(#"Loading queued page %d", needDisplay);
[self loadScrollViewWithPage:needDisplay forceAddToSuperview:NO animated:YES];
needDisplay = -1;
}
// minLoadedPageIndex - index of the first loaded page.
int selectedIndex = MIN(floor((aScrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1 + minLoadedPageIndex, [photoItems count] - 1);
[self loadScrollViewWithPage:selectedIndex - 1 forceAddToSuperview:NO animated:YES];
[self loadScrollViewWithPage:selectedIndex forceAddToSuperview:NO animated:YES];
[self loadScrollViewWithPage:selectedIndex + 1 forceAddToSuperview:NO animated:YES];
}
In loadScrollViewWithPage I create the page view controller which loads the data from the server in the background. I don't add the view to the scrollview until it loads the data from the server.
- (void)loadScrollViewWithPage:(int)page forceAddToSuperview:(BOOL)value animated:(BOOL)animated
{
DetailController *controller = page >= viewControllers.count ? [NSNull null] :[viewControllers objectAtIndex:page];
if ((NSNull *)controller == [NSNull null])
{
controller = [[DetailController alloc] initWithNibName:#"DetailController" bundle:nil];
controller.delegate = self;
controller.view.hidden = NO; //this will call viewDidLoad.
if (page >= viewControllers.count) {
[viewControllers addObject:controller];
}
else {
[viewControllers replaceObjectAtIndex:page withObject:controller];
}
[controller release];
}
// add the controller's view to the scroll view
if (controller.view && controller.view.superview == nil && (controller.isLoadedOrFailed || value)) {
[self setNumberOfVisiblePages:visiblePagesCount+1];
if (page < selectedIndex) {
// We are adding the page to the left of the current page,
// so we need to adjust the content offset.
CGFloat offset = (int)scrollView.contentOffset.x % (int)scrollView.frame.size.width;
offset = scrollView.frame.size.width * (selectedIndex - minLoadedPageIndex) + offset;
[scrollView setContentOffset:CGPointMake(offset, 0.0) animated:animated];
}
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * (page - minLoadedPageIndex);
frame.origin.y = 0;
controller.view.frame = frame;
[scrollView addSubview:controller.view];
[controller viewWillAppear:NO];
}
}
Also I have a detailControllerDidFinishDownload method which is called when data for the page view controller has been loaded.
- (void)detailControllerDidFinishDownload:(DetailController *)controller
{
... //here I calculate new minLoadedPageIndex value
// reset pages frames
if (minLoadedPageIndex < oldMinPage) {
for(int i=oldMinPage;i < [viewControllers count]; i++) {
DetailController *detailsController = [viewControllers objectAtIndex:i];
if ((NSNull *)detailsController != [NSNull null]) {
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * (i - minLoadedPageIndex);
frame.origin.y = 0;
[detailsController.view setFrame:frame];
}
}
}
// load the page now or delay load until the scrolling will be finished
if (!isScrolling) {
[self loadScrollViewWithPage:[photoItems indexOfObject:controller.photoItem] forceAddToSuperview:NO animated:NO];
}
else {
needDisplay = [photoItems indexOfObject:controller.photoItem];
//NSLog(#"pageControlUsed is used!!! %d", [photoItems indexOfObject:controller.photoItem]);
}
}
The problem I have now is that sometimes the scroll stucks on the middle (or somewhere near to middle) of the pages and it won't go to the nearest page bounce until I slightly more it. My tests showed that this situation happens if I scroll out of content view frame (the scroll view have bounces on) and wait for the new page to load. In 1 of the 10 times the scroll stucks.
Thanks, Mikhail!
There are a lot of things you are requiring at the same time. I suggest you have a look at this example.
http://ykyuen.wordpress.com/2010/05/22/iphone-uiscrollview-with-paging-example/
It's a very good starting point.

UIScrollView With next and previous button Action

i am new in iphone,i am using horizontal scrollview these are horizontally scrolling properly
but i want these horizontally scrolling on previous and next button action.please help me asap.
Thanks:)
Here are the steps that was worked for me
in .h file write following code.
int scrollMove;
UIScrollView *aScrView;
-(IBAction)nextBtnAction:(id)sender;
-(IBAction)previousBtnAction:(id)sender;
in .m file
- (void)viewDidLoad
{
[super viewDidLoad];
scrollMove=50;
aScrView=[[UIScrollView alloc]init];
aScrView.frame=CGRectMake(25,50, 270, 50);
aScrView.delegate=self;
aScrView.contentSize=CGSizeMake(1200, 0);
[self.view addSubview:aScrView];
// Do any additional setup after loading the view from its nib.
}
-(IBAction)nextBtnAction:(id)sender{
float coordinate = 2.0f;
//scrollMove=50;
[aScrView setContentOffset:CGPointMake(scrollMove * coordinate, 0) animated:YES];
scrollMove=scrollMove+50;
}
-(IBAction)previousBtnAction:(id)sender{
float coordinate = 1.0f;
[aScrView setContentOffset:CGPointMake(scrollMove * coordinate, 0) animated:YES];
scrollMove=scrollMove-50;
}
Hope this helps you!!!
Use this one when you want to go on next visible content of Scroll view:
(Put this one inside next button method)
float coordinate = 2.0f;
[nodeScrollView setContentOffset:CGPointMake(460 * coordinate, 0) animated:YES];
move on previous visible content view:
float coordinate = 1.0f;
[nodeScrollView setContentOffset:CGPointMake(460 * coordinate, 0) animated:YES];
(Put this one inside previous button method)
**You need some mathematical calculation with coordinate
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * pageNumberYouWantToGoTo;
frame.origin.y = 0;
[scrollView scrollRectToVisible:frame animated:YES];
- (void)viewDidLoad
{
x=0;
y=320;
}
- (IBAction)next:(id)sender
{
y=320;
[scrl_Knot setContentOffset:CGPointMake(x+320,0) animated:YES];
x+=320;
}
- (IBAction)pre:(id)sender
{
x=0;
[scrl_Knot setContentOffset:CGPointMake(y,0) animated:YES];
y-=320;
}
- (IBAction)pre:(id)sender
{
btnNext.enabled = TRUE;
imageID--;
[scrl_venuelist setContentOffset:CGPointMake(imageID*273, 0) animated:YES];
pagecontrol.currentPage=imageID;
if(imageID <= 0)
{
btnPrevious.enabled = FALSE;
}
}
- (IBAction)next:(id)sender
{
btnPrevious.enabled = TRUE;
imageID++;
[scrl_venuelist setContentOffset:CGPointMake(imageID*273, 0) animated:YES];
pagecontrol.currentPage=imageID;
if(imageID >= imageArr.count-1)
{
btnNext.enabled = FALSE;
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
imageID = scrollView.contentOffset.x / scrollView.frame.size.width;
pagecontrol.currentPage=imageID;
if(imageID <= 0)
{
btnPrevious.enabled = FALSE;
imageID = 0;
}
else
{
btnPrevious.enabled = TRUE;
}
if(imageID >= imageArr.count-1)
{
btnNext.enabled = FALSE;
imageID = imageArr.count-1;
}
else
{
btnNext.enabled = TRUE;
}
}
- (void)viewDidLoad
{
scrl_venuelist.delegate=self;
scrl_venuelist.contentSize =CGSizeMake(273 * [imageArr count], 137);
if(imageID == 0)
{
btnPrevious.enabled = FALSE;
if(imageID == imageArr.count-1)
{
btnNext.enabled = FALSE;
}
}
else if(imageID == imageArr.count-1)
{
btnNext.enabled = FALSE;
}
else if(imageID > imageArr.count-1)
{
imageID = [imageArr count]-1;
}
scrl_venuelist.pagingEnabled=YES;
pagecontrol.numberOfPages=[imageArr count];
pagecontrol.currentPage=0;
}
If Any one Looking for way of doing in C# for Xamarin
public int CurrentIndex
{
get => (int)Math.Round(this.scrollView.ContentOffset.X / this.scrollView.Frame.Width);
set => this.scrollView.ScrollRectToVisible(new CGRect(new CGPoint(this.scrollView.Frame.Size.Width * value, 0), this.scrollView.Frame.Size), true);
}
This getter and Setter should provide you current page as well let you scroll to the specified page
On Button Actions Just Update Current Index to new Values, Which could current Index +1/-1.