App crashing when trying to implement UILongPressGesture - iphone

I'm trying to implement a UILongPressGesture on one of my tables in order to be able to reorder its cells. However, every time I long-press my app crashes.
Here's the file that implements the UILongPressGesture:
#import "LstrTableViewDragToMove.h"
#import "LstrTableViewCell.h"
#implementation LstrTableViewDragToMove
{
LstrTableView *_tableView;
BOOL _dragInProgress;
CGPoint _initialTouchPoint;
int _cellIndex;
LstrTableViewCell *_currentCell;
}
-(id)initWithTableView:(LstrTableView *)tableView
{
self = [super init];
if(self)
{
_tableView = tableView;
UIGestureRecognizer *recognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPress:)];
[_tableView addGestureRecognizer:recognizer];
}
return self;
}
-(BOOL) viewContainsPoint:(UIView*)view withPoint:(CGPoint)point {
CGRect frame = view.frame;
return (frame.origin.y < point.y) && (frame.origin.y + frame.size.height) > point.y;
}
-(void)handleLongPress:(UILongPressGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self longPressStarted:recognizer];
}
if (recognizer.state == UIGestureRecognizerStateEnded) {
[self longPressEnded:recognizer];
}
}
-(void)longPressStarted:(UILongPressGestureRecognizer *)recognizer
{
NSLog(#"Started");
/*
_initialTouchPoint = [recognizer locationOfTouch:0 inView:_tableView.scrollView];
NSArray* visibleCells = _tableView.visibleCells;
for (int i=0; i < visibleCells.count; i++) {
UIView* cell = (UIView*)visibleCells[i];
if ([self viewContainsPoint:cell withPoint:_initialTouchPoint]) {
_cellIndex = i;
}
}
_currentCell = _tableView.visibleCells[_cellIndex];
_currentCell.label.allowsEditingTextAttributes = NO;
_currentCell.frame = CGRectMake(_currentCell.frame.origin.x, _currentCell.frame.origin.y, _currentCell.frame.size.width + 10.0f, _currentCell.frame.size.height + 10.0f);
[_currentCell addDropShadow];
*/
}
-(void)longPressEnded:(UILongPressGestureRecognizer *)recognizer
{
NSLog(#"Ended");
}
#end
And here's the error log:
2013-05-03 13:17:53.750 Lstr[19207:907] -[UILongPressGestureRecognizer translationInView:]: unrecognized selector sent to instance 0x20856240
2013-05-03 13:17:53.754 Lstr[19207:907] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UILongPressGestureRecognizer translationInView:]: unrecognized selector sent to instance 0x20856240'
*** First throw call stack:
(0x3302a2a3 0x3ad5297f 0x3302de07 0x3302c531 0x3302d938 0x6db43 0x34f4f1ab 0x34f1863f 0x34f482a5 0x33941f53 0x32fff5df 0x32fff291 0x32ffdf01 0x32f70ebd 0x32f70d49 0x36b492eb 0x34e86301 0x66775 0x3b189b20)
libc++abi.dylib: terminate called throwing an exception
(lldb)
Thanks in advance.
UPDATE:
Here's the method that has translationInView:
#pragma mark - horizontal pan gesture methods
-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer {
CGPoint translation = [gestureRecognizer translationInView:[self superview]];
// Check for horizontal gesture
if (fabsf(translation.x) > fabsf(translation.y)) {
return YES;
}
return NO;
}

Are you using translationInView somewhere in your code?
You should change it to locationInView:

try and declare your tableview as a property in the header file
#property(nonatomic,retain)LstrTableView *_tableView;

Related

How to complete interactive UIViewController transition?

I've been dabbling with the new iOS 7 custom transition API and looked through all the tutorials/documentation I could find but I can't seem to figure this stuff out for my specific scenario.
So essentially what I'm trying to implement is a UIPanGestureRecognizer on a view where I would swipe up and transition to a VC whose view would slide up from the bottom while the current view would slide up as I drag my finger higher.
I have no problem accomplishing this without the interaction transition, but once I implement the interaction (the pan gesture) I can't seem to complete the transition.
Here's the relevant code from the VC that conforms to the UIViewControllerTransitionDelegate which is needed to vend the animator controllers:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"Swipe"]) {
NSLog(#"PREPARE FOR SEGUE METHOD CALLED");
UIViewController *toVC = segue.destinationViewController;
[interactionController wireToViewController:toVC];
toVC.transitioningDelegate = self;
toVC.modalPresentationStyle = UIModalPresentationCustom;
}
}
#pragma mark UIViewControllerTransition Delegate Methods
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController: (UIViewController *)presented presentingController: (UIViewController *)presenting sourceController:(UIViewController *)source {
NSLog(#"PRESENTING ANIMATION CONTROLLER CALLED");
SwipeDownPresentationAnimationController *transitionController = [SwipeDownPresentationAnimationController new];
return transitionController;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
NSLog(#"DISMISS ANIMATION CONTROLLER CALLED");
DismissAnimatorViewController *transitionController = [DismissAnimatorViewController new];
return transitionController;
}
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
NSLog(#"Interaction controller for dimiss method caled");
return interactionController.interactionInProgress ? interactionController:nil;
}
NOTE: The interaction swipe is only for the dismissal of the VC which is why it's in the interactionControllerForDismissal method
Here's the code for the animator of the dismissal which works fine when I tap on a button to dismiss it:
#import "DismissAnimatorViewController.h"
#implementation DismissAnimatorViewController
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return 1.0;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
NSTimeInterval duration = [self transitionDuration:transitionContext];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
CGRect initialFrameFromVC = [transitionContext initialFrameForViewController:fromVC];
UIView *containerView = [transitionContext containerView];
CGRect screenBounds = [[UIScreen mainScreen] bounds];
NSLog(#"The screen bounds is :%#", NSStringFromCGRect(screenBounds));
toVC.view.frame = CGRectOffset(initialFrameFromVC, 0, screenBounds.size.height);
toVC.view.alpha = 0.2;
CGRect pushedPresentingFrame = CGRectOffset(initialFrameFromVC, 0, -screenBounds.size.height);
[containerView addSubview:toVC.view];
[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.6
initialSpringVelocity:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
fromVC.view.frame = pushedPresentingFrame;
fromVC.view.alpha = 0.2;
toVC.view.frame = initialFrameFromVC;
toVC.view.alpha = 1.0;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
#end
Here's the code for the UIPercentDrivenInteractiveTransition subclass which serves as the interaction controller:
#import "SwipeInteractionController.h"
#implementation SwipeInteractionController {
BOOL _shouldCompleteTransition;
UIViewController *_viewController;
}
- (void)wireToViewController:(UIViewController *)viewController {
_viewController = viewController;
[self prepareGestureRecognizerInView:_viewController.view];
}
- (void)prepareGestureRecognizerInView:(UIView*)view {
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
gesture.minimumNumberOfTouches = 1.0;
[view addGestureRecognizer:gesture];
}
- (CGFloat)completionSpeed {
return 1 - self.percentComplete;
NSLog(#"PERCENT COMPLETE:%f",self.percentComplete);
}
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer {
// CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:
// 1. Start an interactive transition!
self.interactionInProgress = YES;
[_viewController dismissViewControllerAnimated:YES completion:nil];
break;
case UIGestureRecognizerStateChanged: {
// 2. compute the current position
CGFloat fraction = fabsf(translation.y / 568);
NSLog(#"Fraction is %f",fraction);
fraction = fminf(fraction, 1.0);
fraction = fmaxf(fraction, 0.0);
// 3. should we complete?
_shouldCompleteTransition = (fraction > 0.23);
// 4. update the animation controller
[self updateInteractiveTransition:fraction];
NSLog(#"Percent complete:%f",self.percentComplete);
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
// 5. finish or cancel
NSLog(#"UI GESTURE RECOGNIZER STATE CANCELED");
self.interactionInProgress = NO;
if (!_shouldCompleteTransition || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
[self cancelInteractiveTransition];
NSLog(#"Interactive Transition is cancled.");
}
else {
NSLog(#"Interactive Transition is FINISHED");
[self finishInteractiveTransition];
}
break;
}
default:
NSLog(#"Default is being called");
break;
}
}
#end
Once again, when I run the code now and I don't swipe all the way to purposefully cancel the transition, I just get a flash and am presented with the view controller I want to swipe to. This happens regardless if the transition completes or is canceled.
However, when I dismiss via the button I get the transition specified in my animator view controller.
I can see a couple of issues here - although I cannot be certain that these will fix your problem!
Firstly, your animation controller's UIView animation completion block has the following:
[transitionContext completeTransition:YES];
Whereas it should return completion based on the result of the interaction controller as follows:
[transitionContext completeTransition:![transitionContext transitionWasCancelled]]
Also, I have found that if you tell the UIPercentDrivenInteractiveTransition that a transition is 100% complete, it does not call the animation controller completion block. As a workaround, I limit it to ~99.9%
https://github.com/ColinEberhardt/VCTransitionsLibrary/issues/4
I've created a number of example interaction and animation controllers here, that you might find useful:
https://github.com/ColinEberhardt/VCTransitionsLibrary
I had this same problem. I tried the fixes above and others, but nothing worked. Then I stumbled upon https://github.com/MrAlek/AWPercentDrivenInteractiveTransition, which fixed everything.
Once you add it to your project, just replace UIPercentDrivenInteractiveTransition with AWPercentDrivenInteractiveTransition.
Also, you have to set the animator before starting an interactive transition. In my case, I use the same class for UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning, so I just did it in init():
init() {
super.init()
self.animator = self
}

How to reset an UIGestureRecognizer?

I have a subview inside a view. That view has a PanGestureRecognizer.
I want user could zoom in inside the webview, pan around, zoom out, etc.
But I want that, when the user reaches left edge of the webview, the gesturesrecognizers inside the webView to be ignored, and only the PanGestureRecognizer from the view get called.
Well, I have accomplished this, by that code:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
float coordY = scrollView.contentOffset.y;
float correctionOffsetRightSide = scrollView.contentSize.width-webView.bounds.size.width;
if(scrollView.contentOffset.x <= 0)
{
[delegate viewOnlyRecognizeItsPan];
[scrollView setContentOffset:CGPointMake(0, coordY)];
}
else if (scrollView.contentOffset.x >= correctionOffsetRightSide)
{
[delegate viewOnlyRecognizeItsPan];
[scrollView setContentOffset:CGPointMake(correctionOffsetRightSide, coordY)];
}
}
-(void)viewOnlyRecognizeItsPan
{
for (UIGestureRecognizer *gesture in [[[[arrayDeAbas objectAtIndex:currentViewTag] webView] scrollView] gestureRecognizers])
{
[gesture requireGestureRecognizerToFail:panRecognizer];
}
}
"arrayDeAbas" is an mutableArray that stores webviews.
My problem is: after user touchs a button, I need to reset "gestures", so that it doesn't requires panRecognizer to fail anymore.
How could I could this?
EDITED:
Ok, I've founded a solution. Instead of using requireGestureRecognizerToFail:, I can simply setEnablle to YES or NO.
-(void)turnPanON
{
for (UIGestureRecognizer *gesture in [[[[arrayDeAbas objectAtIndex:currentViewTag] webView] scrollView] gestureRecognizers])
{
gesture.enabled = NO;
}
}
-(void)turnPanOFF;
{
for (UIGestureRecognizer *gesture in [[[[arrayDeAbas objectAtIndex:currentViewTag] webView] scrollView] gestureRecognizers])
{
gesture.enabled = YES;
}
}

How to recognize swipe in all 4 directions?

I need to recognize swipes in all directions (Up/Down/Left/Right). Not simultaneously, but I need to recognize them.
I tried:
UISwipeGestureRecognizer *Swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(SwipeRecognizer:)];
Swipe.direction = (UISwipeGestureRecognizerDirectionLeft |
UISwipeGestureRecognizerDirectionRight |
UISwipeGestureRecognizerDirectionDown |
UISwipeGestureRecognizerDirectionUp);
[self.view addGestureRecognizer:Swipe];
[Swipe release];
but nothing appeared on SwipeRecognizer
Here's the code for SwipeRecognizer:
- (void) SwipeRecognizer:(UISwipeGestureRecognizer *)sender {
if ( sender.direction == UISwipeGestureRecognizerDirectionLeft )
NSLog(#" *** SWIPE LEFT ***");
if ( sender.direction == UISwipeGestureRecognizerDirectionRight )
NSLog(#" *** SWIPE RIGHT ***");
if ( sender.direction == UISwipeGestureRecognizerDirectionDown )
NSLog(#" *** SWIPE DOWN ***");
if ( sender.direction == UISwipeGestureRecognizerDirectionUp )
NSLog(#" *** SWIPE UP ***");
}
How can I do this? How can assign to my Swipe object all different directions?
You set the direction like this
UISwipeGestureRecognizer *Swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(SwipeRecognizer:)];
Swipe.direction = (UISwipeGestureRecognizerDirectionLeft |
UISwipeGestureRecognizerDirectionRight |
UISwipeGestureRecognizerDirectionDown |
UISwipeGestureRecognizerDirectionUp);
That's what the direction will be when you get the callback, so it is normal that all your tests fails. If you had
- (void) SwipeRecognizer:(UISwipeGestureRecognizer *)sender {
if ( sender.direction | UISwipeGestureRecognizerDirectionLeft )
NSLog(#" *** SWIPE LEFT ***");
if ( sender.direction | UISwipeGestureRecognizerDirectionRight )
NSLog(#" *** SWIPE RIGHT ***");
if ( sender.direction | UISwipeGestureRecognizerDirectionDown )
NSLog(#" *** SWIPE DOWN ***");
if ( sender.direction | UISwipeGestureRecognizerDirectionUp )
NSLog(#" *** SWIPE UP ***");
}
The tests would succeed (but the would all succeed so you wouldn't get any information out of them). If you want to distinguish between swipes in different directions you will need separate gesture recognizers.
EDIT
As pointed out in the comments, see this answer. Apparently even this doesn't work. You should create swipe with only one direction to make your life easier.
Unfortunately you cannot use direction property for listening the recognizer; it only gives you the detected directions by the recognizer. I have used two different UISwipeGestureRecognizers for that purpose, see my answer here: https://stackoverflow.com/a/16810160/936957
I actually ran into this exact problem before. What I ended up doing was creating a UIView subclass, overriding touchesMoved: and doing some math to calculate the direction.
Here's the general idea:
#import "OmnidirectionalControl.h"
typedef NS_ENUM(NSInteger, direction) {
Down = 0, DownRight = 1,
Right = 2, UpRight = 3,
Up = 4, UpLeft = 5,
Left = 6, DownLeft = 7
};
#interface OmnidirectionalControl ()
#property (nonatomic) CGPoint startTouch;
#property (nonatomic) CGPoint endTouch;
#end
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.startTouch = [[touches allObjects][0] locationInView:self];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
self.lastTouch = [[[touches allObjects] lastObject] locationInView:self];
NSLog(#"Direction: %d", [self calculateDirectionFromTouches]);
}
-(direction)calculateDirectionFromTouches {
NSInteger xDisplacement = self.lastTouch.x-self.startTouch.x;
NSInteger yDisplacement = self.lastTouch.y-self.startTouch.y;
float angle = atan2(xDisplacement, yDisplacement);
int octant = (int)(round(8 * angle / (2 * M_PI) + 8)) % 8;
return (direction) octant;
}
For simplicity's sake in touchesMoved:, I only log the direction, but you can do what you want with that information (e.g. pass it to a delegate, post a notification with it, etc.). In touchesMoved you'll also need some method to recognize if the swipe is finished or not, but that's not quite relevant to the question so I'll leave that to you. The math in calculateDirectionFromTouches is explained here.
I finally found the simplest answer, please mark this as the answer if you agree.
If you only have one direction swipe + pan, you just say:
[myPanRecogznier requireGestureRecognizerToFail:mySwipeRecognizer];
But if you have two or more swipes, you can't pass an array into that method. For that, there's UIGestureRecognizerDelegate that you need to implement.
For example, if you want to recognize 2 swipes (left and right) and you also want to allow the user to pan up, you define the gesture recognizers as properties or instance variables, and then you set your VC as the delegate on the pan gesture recognizer:
_swipeLeft = [[UISwipeGestureRecognizer alloc] ...]; // use proper init here
_swipeRight = [[UISwipeGestureRecognizer alloc] ...]; // user proper init here
_swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;
_swipeRight.direction = UISwipeGestureRecognizerDirectionRight;
_pan = [[UIPanGestureRecognizer alloc] ...]; // use proper init here
_pan.delegate = self;
// then add recognizers to your view
You then implement - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer delegate method, like so:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if (gestureRecognizer == _pan && (otherGestureRecognizer == _swipeLeft || otherGestureRecognizer == _swipeRight)) {
return YES;
}
return NO;
}
This tells the pan gesture recognizer to only work if both left and right swipes fail to be recognize - perfect!
Hopefully in the future Apple will just let us pass an array to the requireGestureRecognizerToFail: method.
Use a UIPanGestureRecogizer and detect the swipe directions you care about. see the UIPanGestureRecognizer documentation for details. -rrh
// add pan recognizer to the view when initialized
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panRecognized:)];
[panRecognizer setDelegate:self];
[self addGestureRecognizer:panRecognizer]; // add to the view you want to detect swipe on
-(void)panRecognized:(UIPanGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateBegan) {
// you might want to do something at the start of the pan
}
CGPoint distance = [sender translationInView:self]; // get distance of pan/swipe in the view in which the gesture recognizer was added
CGPoint velocity = [sender velocityInView:self]; // get velocity of pan/swipe in the view in which the gesture recognizer was added
float usersSwipeSpeed = abs(velocity.x); // use this if you need to move an object at a speed that matches the users swipe speed
NSLog(#"swipe speed:%f", usersSwipeSpeed);
if (sender.state == UIGestureRecognizerStateEnded) {
[sender cancelsTouchesInView]; // you may or may not need this - check documentation if unsure
if (distance.x > 0) { // right
NSLog(#"user swiped right");
} else if (distance.x < 0) { //left
NSLog(#"user swiped left");
}
if (distance.y > 0) { // down
NSLog(#"user swiped down");
} else if (distance.y < 0) { //up
NSLog(#"user swiped up");
}
// Note: if you don't want both axis directions to be triggered (i.e. up and right) you can add a tolerence instead of checking the distance against 0 you could check for greater and less than 50 or 100, etc.
}
}
CHANHE IN STATEEND CODE WIH THIS
//if YOU WANT ONLY SINGLE SWIPE FROM UP,DOWN,LEFT AND RIGHT
if (sender.state == UIGestureRecognizerStateEnded) {
[sender cancelsTouchesInView]; // you may or may not need this - check documentation if unsure
if (distance.x > 0 && abs(distance.x)>abs(distance.y)) { // right
NSLog(#"user swiped right");
} else if (distance.x < 0 && abs(distance.x)>abs(distance.y)) { //left
NSLog(#"user swiped left");
}
if (distance.y > 0 && abs(distance.y)>abs(distance.x)) { // down
NSLog(#"user swiped down");
} else if (distance.y < 0 && abs(distance.y)>abs(distance.x)) { //up
NSLog(#"user swiped up");
}
}
You can add only one diagonal by SwipeGesture, then...
UISwipeGestureRecognizer *swipeV = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(action)];
UISwipeGestureRecognizer *swipeH = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(action)];
swipeH.direction = ( UISwipeGestureRecognizerDirectionLeft | UISwipeGestureRecognizerDirectionRight );
swipeV.direction = ( UISwipeGestureRecognizerDirectionUp | UISwipeGestureRecognizerDirectionDown );
[self addGestureRecognizer:swipeH];
[self addGestureRecognizer:swipeV];
self.userInteractionEnabled = YES;
UISwipeGestureRecognizer *Updown=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(handleGestureNext:)];
Updown.delegate=self;
[Updown setDirection:UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp];
[overLayView addGestureRecognizer:Updown];
UISwipeGestureRecognizer *LeftRight=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(handleGestureNext:)];
LeftRight.delegate=self;
[LeftRight setDirection:UISwipeGestureRecognizerDirectionLeft | UISwipeGestureRecognizerDirectionRight];
[overLayView addGestureRecognizer:LeftRight];
overLayView.userInteractionEnabled=NO;
-(void)handleGestureNext:(UISwipeGestureRecognizer *)recognizer
{
NSLog(#"Swipe Recevied");
}
It might not be the best solution but you can always specify different UISwipeGestureRecognizer for each swipe direction you want to detect.
In the viewDidLoad method just define the required UISwipeGestureRecognizer:
- (void)viewDidLoad{
UISwipeGestureRecognizer *recognizerUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipeUp:)];
recognizerUp.direction = UISwipeGestureRecognizerDirectionUp;
[[self view] addGestureRecognizer:recognizerUp];
UISwipeGestureRecognizer *recognizerDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipeDown:)];
recognizerDown.direction = UISwipeGestureRecognizerDirectionDown;
[[self view] addGestureRecognizer:recognizerDown];
}
Then just implement the respective methods to handle the swipes:
- (void)handleSwipeUp:(UISwipeGestureRecognizer *)sender{
if (sender.state == UIGestureRecognizerStateEnded){
NSLog(#"SWIPE UP");
}
}
- (void)handleSwipeDown:(UISwipeGestureRecognizer *)sender{
if (sender.state == UIGestureRecognizerStateEnded){
NSLog(#"SWIPE DOWN");
}
}

how to handle tiling of images on the fly

I am writing an app which would tile the images 256 * 256 and write those tile files back in the directory. I am updating my URL each time if there are any updates and tile those images back and store in the iphone folder. I am worried about two main things :
1) Memory consumption -- will the memory consumption for 5 images of size 200 KB a lot ?
2) How fast I can process my app if I have to tile 5 different URL with images at the same time ?
I have written a code to tile and save in the directory for one URL and would like to do the same for 5 URLs. Is it recommended to go with this approach or if anyone has a different approach?
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *URLString = #"http://www.abc.com/abc.html?event=123";
NSURL *url = [[NSURL alloc] initWithString:URLString];
NSData * dataImage = [NSData dataWithContentsOfURL:url];
NSString *directoryPath = [[NSBundle mainBundle] bundlePath];
UIImage *big = [UIImage imageWithData:dataImage];
[self saveTilesOfSize:(CGSize){256,256} forImage:big toDirectory:directoryPath usingPrefix:#"image_124_"];
TileView *tv = [[TileView alloc] initWithFrame:(CGRect){{0,0}, (CGSize){5000,5000}}];
[tv setTileTag:#"image_110_"];
[tv setTileDirectory:directoryPath];
[scrollView addSubview:tv];
[scrollView setContentSize:(CGSize){5000,5000}];
}
- (void)saveTilesOfSize:(CGSize)size
forImage:(UIImage*)image
toDirectory:(NSString*)directoryPath
usingPrefix:(NSString*)prefix
{
CGFloat cols = [image size].width / size.width;
CGFloat rows = [image size].height / size.height;
int fullColumns = floorf(cols);
int fullRows = floorf(rows);
CGFloat remainderWidth = [image size].width -
(fullColumns * size.width);
CGFloat remainderHeight = [image size].height -
(fullRows * size.height);
if (cols > fullColumns) fullColumns++;
if (rows > fullRows) fullRows++;
CGImageRef fullImage = [image CGImage];
for (int y = 0; y < fullRows; ++y) {
for (int x = 0; x < fullColumns; ++x) {
CGSize tileSize = size;
if (x + 1 == fullColumns && remainderWidth > 0) {
// Last column
tileSize.width = remainderWidth;
}
if (y + 1 == fullRows && remainderHeight > 0) {
// Last row
tileSize.height = remainderHeight;
}
CGImageRef tileImage = CGImageCreateWithImageInRect(fullImage,
(CGRect){{x*size.width, y*size.height},
tileSize});
NSData *imageData = UIImagePNGRepresentation([UIImage imageWithCGImage:tileImage]);
NSString *path = [NSString stringWithFormat:#"%#/%d.png",
directoryPath, prefix];
[imageData writeToFile:path atomically:NO];
}
}
}
I have implemented solution for the similar problem(the difference is, I was not saving them in directory, those were for display purpose only.), with different approach.
In my problem, I have 84 images of 250x250 dimension with size 8KB each( I added them on scrollView and on scrolling I load them, a bit similar to google maps, but more smooth). At first I was using the same approach as yours, but performance was problem. So, I used asynchornous loading concept. I wrote an UIImageView subclass with connectiond delegates, so the UIImageView subclass was responsible for loading it's image. And as loading is asynchronous so performance is far better.
As you asked
1) Memory consumption -- will the memory consumption for 5 images of size 200 KB a lot?
Ans : 5x200KB = 1MB ~ 1.2MB or so(so you will need that much memory for displaying, if you have that much amount of memory then you should not worry.).. in my case 84x8KB = 672 ~ 900KB(as I was using some additional things like activity indicator for each imageview).
2) How fast I can process my app if I have to tile 5 different URL with images at the same time ?
Ans : As you are loading it in viewDidLoad ... or in main thread then performance will be an issue(blocking may happen, as I am not completely sure whether you are using threads or not).
Quick suggestion:
1. write an UIImageView subclass which has connection delegate methods.
2. have some method that you can call from outside to message this imageView to start loading.(give the url)
3. do proper deallocation of resources like responseData and connection object, once the downloading is complete.
4. when you move from this view to other view do proper deallocation and removal of all these imageviews.
5. use intruments to look for the allocations by this.
CODE :
TileImageView.h
#interface TileImageView : UIImageView
{
NSURLConnection *serverConnection;
BOOL isImageRequested;
NSMutableData *responseData;
}
-(void) startImageDownloading:(NSString *)pRequestURL
-(void) deallocateResources;
-(BOOL) isImageRequested;
-(void)cancelConnectionRequest;
-(void) addActivityIndicator;
-(void) removeActivityIndicator;
#end
TileImageView.m
#implementation TileImageView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self)
{
// Initialization code.
isImageRequested = NO;
}
return self;
}
-(BOOL) isImageRequested
{
return isImageRequested;
}
-(void) startImageDownloading:(NSString *)pRequestURL
{
if (!isImageRequested)
{
NSURL *pServerURL = [[NSURL alloc] initWithString:pRequestURL];
if (pServerURL != nil)
{
isImageRequested = YES;
[self addActivityIndicator];
[self setBackgroundColor:[UIColor lightGrayColor]];
NSURLRequest *pServerRequest = [[NSURLRequest alloc]initWithURL:pServerURL];
serverConnection = [[NSURLConnection alloc] initWithRequest:pServerRequest delegate:self];
if(serverConnection)
{
responseData = [[NSMutableData alloc] init];
}
[pServerURL release];
[pServerRequest release];
}
}
}
-(void) addActivityIndicator
{
UIActivityIndicatorView *tempActivityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
CGFloat size = self.frame.size.width*0.12;
[tempActivityIndicator setFrame:CGRectMake(0, 0, size, size)];
[tempActivityIndicator setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/2)];
[tempActivityIndicator setTag:1000];
[tempActivityIndicator setHidesWhenStopped:YES];
[tempActivityIndicator startAnimating];
[self addSubview:tempActivityIndicator];
[tempActivityIndicator release];
}
-(void) removeActivityIndicator
{
UIActivityIndicatorView *tempActivityIndicator = (UIActivityIndicatorView *)[self viewWithTag:1000];
if (tempActivityIndicator != nil)
{
[tempActivityIndicator stopAnimating];
[tempActivityIndicator removeFromSuperview];
}
}
-(void)cancelConnectionRequest
{
if (isImageRequested && serverConnection != nil)
{
[serverConnection cancel];
[self removeActivityIndicator];
[self deallocateResources];
isImageRequested = NO;
}
}
// Name : connection: didReceiveAuthenticationChallenge:
// Description : NSURLConnectionDelegate method. Method that gets called when server sends an authentication challenge.
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
}
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
// Name : connection: didReceiveResponse:
// Description : NSURLConnectionDelegate method. Method that gets called when response for the launched URL is received..
-(void) connection:(NSURLConnection *) connection didReceiveResponse:(NSURLResponse *) response
{
[responseData setLength:0];
}
// Name : connection: didReceiveData:
// Description : NSURLConnectionDelegate method. Method that gets called when data for the launched URL is received..
-(void) connection:(NSURLConnection *) connection didReceiveData:(NSData *) data
{
[responseData appendData:data];
}
// Name : connection: didFailWithError:
// Description : NSURLConnectionDelegate method. Method that gets called when an error for the launched URL is received..
-(void) connection:(NSURLConnection *) connection didFailWithError:(NSError *) error
{
NSLog(#"Error occured while loading image : %#",error);
[self removeActivityIndicator];
[self deallocateResources];
UILabel *tempLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 150, 30)];
[tempLabel setBackgroundColor:[UIColor clearColor]];
[tempLabel setFont:[UIFont systemFontOfSize:11.0f]];
[tempLabel setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/2)];
[tempLabel setText:#"Image not available."];
[self addSubview:tempLabel];
[tempLabel release];
}
// Name : connectionDidFinishLoading
// Description : NSURLConnectionDelegate method. Method that gets called when connection loading gets finished.
-(void) connectionDidFinishLoading:(NSURLConnection *) connection
{
[self removeActivityIndicator];
UIImage *tempImage = [[UIImage alloc] initWithData:responseData];
self.image = tempImage;
[tempImage release];
[self deallocateResources];
}
-(void) deallocateResources
{
if (serverConnection != nil)
{
[serverConnection release];
serverConnection = nil;
}
if (responseData != nil)
{
[responseData release];
responseData = nil;
}
}
- (void)dealloc {
[super dealloc];
}
#end
So, If you use above code then only thing you have to do is to add the object of TileImageView and just call method -(void) startImageDownloading:(NSString *)pRequestURL.
Please use instruments to track allocations.
Update :
**How do I add TileImageView on scrollView ? :**
//like this I add 84 images in a 2D shape( 12 x 7) grid ... and once Images are added I set scrollView's contentSize as per complete grid size.
TileImageView *tileImageView = [[TileImageView alloc]initWithFrame:<myFrameAsPerMyNeeds>];
[tileImageView setTag:<this is the identifier I use for recognizing the image>];
[myImageScrollView addSubView:tileImageView];
[tileImageView release];
..later in code when user scroll's and other imageviews come in visibility.I use following code...
TileImageView *loadableImageView = (TileImageView *)[myImageScrollView viewWithTag:];
[loadableImageView startImageDownloading:];
I do not need to do anything in drawRect: , as I have no need to do custome drawing.
For Image names you can use tag property from imageView, but if you need some different name that are more like string then you can put another property in imageView for image name and set it while adding the image view. for saving data you can call your method once the image is downloaded in didFinishLoading method of TileImageView, where you can use that name.
SECODN UPDATE
How I add TileImageView on ScrollView
gridCount = 0;
rows = 7;
columns = 12;
totalGrids = rows*columns;
//*above : all are NSInteger type variable declared at class level
chunkWidth = 250;
chunkHeight = 250;
contentWidth = 0.0;
contentHeight = 0.0;
//*above : all are CGFloat type variable declared at class level
for (int i=0; i<rows; i++)
{
contentWidth = 0.0;
for (int j=0 ; j<columns; j++)
{
gridCount++;
CGRect frame = CGRectMake(contentWidth, contentHeight, chunkWidth, chunkHeight);
[self addNewImageViewWithTag:gridCount frame:frame];
contentWidth += chunkWidth;
}
contentHeight += chunkHeight;
}
[imageScrollView setContentSize:CGSizeMake(contentWidth, contentHeight)];
[imageScrollView setContentOffset:CGPointMake(0, 0)];
[imageScrollView setUserInteractionEnabled:YES];
And in ScrollViewDelegate method.
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (isZoomed)
{
xOffset = scrollView.contentOffset.x;
yOffset = scrollView.contentOffset.y;
//*above : both are CGFloat type variable declared at class level
visibleColumn = xOffset/chunkWidth+1;
visibleRow = yOffset/chunkHeight+1;
gridNumber = (visibleRow-1)*columns+visibleColumn;
adjGrid1 = gridNumber+1;
adjGrid2 = gridNumber+columns;
adjGrid3 = adjGrid2+1;
//*above : all are NSInteger type variable declared at class level
if (gridNumber ==1)
{
[self createAndSendScrollRequest:gridNumber];
}
if (adjGrid1 > 0 && adjGrid1 <= totalGrids)
{
[self createAndSendScrollRequest:adjGrid1];
}
if (adjGrid2 > 0 && adjGrid2 <= totalGrids)
{
[self createAndSendScrollRequest:adjGrid2];
}
if (adjGrid3 > 0 && adjGrid3 <= totalGrids)
{
[self createAndSendScrollRequest:adjGrid3];
}
}
}
And this is how createAndSendScrollRequest is implemented.
- (void) createAndSendScrollRequest:(NSInteger)chunkId
{
TileImageView *loadingImageView = (TileImageView *)[imageScrollView viewWithTag:chunkId];
if ([loadingImageView image]==nil)
{
[loadingImageView startImageDownloading:<and here I pass url my url is based on tag so In reality I dont pass anything I just use it from the imageview's tag property>];
}
}
Thanks,

Captured which subview is touched in UiScrollView but cannot access it in another function.

I have UIScrollView which has subviews (pictures) added do it. each time a user touches the a picture in the scrollview, it toggles a checkmark ontop of it.
NSMutableIndexSet *picturesArray; <- declared in .h
- (void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event {
if (!self.dragging) {
[self.nextResponder touchesEnded: touches withEvent:event];
NSLog(#"Touch down");
for (UITouch *touch in touches) {
for (int i = 1; i <= [self subviews].count; i++)
{
if(CGRectContainsPoint([[self viewWithTag:i]frame], [touch locationInView:self])){
NSLog(#"touched %d th view",i);
NSArray *subviews = [[self viewWithTag:i] subviews];
UIImageView *view = nil;
view = [subviews objectAtIndex:0];
if(view.hidden){
// add the index
[picturesArray addIndex:i];
view.hidden = NO; //check mark is shown
}else{
[picturesArray removeIndex:i];
view.hidden = YES; //check mark is not shown
}
// UIImageWriteToSavedPhotosAlbum([(UIImageView *)[self viewWithTag:i]image], nil, nil, nil); <- WORKS IF CALLED
}
}
}
}
Question 1: is this the best way of doing this? It seems like using a for (int i = 1; i <= [self subviews].count; i++) is pretty slow. I basically need to capture which subview was touched. I havent figured this out other than going through EACH subview
savePhotos is called and basically searches through which of the pictures was touched and saves them to the Photo Album. However the call to UIImageWriteToSavedPhotosAlbum fails. This is in the same file as TouchesEnded. But when called in TouchesEnded, it works.
(IBAction) savePhotos: (id) sender{
NSLog(#"The index set is %#",picturesArray );
const NSUInteger arrayCount = picturesArray.count;
NSUInteger *theIndexBuffer = (NSUInteger *)calloc(picturesArray.count, sizeof(NSUInteger));
UIImageWriteToSavedPhotosAlbum([(UIImageView *)[self viewWithTag:0]image], nil, nil, nil);
[picturesArray getIndexes:theIndexBuffer maxCount:arrayCount inIndexRange:nil];
for(int i = 0; i < arrayCount; i ++){
NSLog(#"Element is %d",theIndexBuffer[i]);
UIImageWriteToSavedPhotosAlbum([(UIImageView *)[self viewWithTag:i]image], nil, nil, nil); <- THIS CRASHES
}
}
Question 2: Why is it that UIImageWriteToSavedPhotosAlbum is failing ?
1) instead of using UIImageView, implement a child of UIImageView. Then try listening for touches on the individual subviews, that should solve your O(n) problem
2) something is probably getting auto-released, double check your reference counting is correct
try UIView* targetView = [self hitTest:location withEvent:nil];