I have a UIButton subclass that does some custom drawing and animations. That is all working fine and dandy.
However, most of my buttons dismiss the current view via their superview calling [self dismissViewControllerAnimated] once it is confirmed with the model that whatever the button push was supposed to accomplish was actually done, and I want there to be a delay to allow the animation to complete before dismissing the view.
I am able to easily enough animate the UIButton subclass on touchesEnded and then call [super touchesEnded], which works fine except that it doesn't let my animations finish before dismissing the view. Like this:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.foo"];
//set up myAnimation's properties
[self.layer addAnimation:shakeAnimation forKey:nil];
[super touchesEnded:touches withEvent:event]; //works! but no delay
}
My first attempt at creating a delay was by using CATransaction, as follows:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.foo"];
//set up myAnimation's properties
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[super touchesEnded:touches withEvent:event]; //doesn't seem to do anything :-/
}];
[self.layer addAnimation:shakeAnimation forKey:nil];
[CATransaction commit];
}
Which, as far as I can tell, is executing CATransaction's completionBlock, but it just isn't doing anything.
I've also tried assigning the touches and event arguments from touchesEnded to both properties and global variables, and then executing [super touchesEnded] in another method called by an NSTimer. The same thing seems to be occurring where the code is executing, but my call to [super touchesEnded] isn't doing anything.
I've dug around online for hours. Added stubs of the other touches methods from UIResponder which just contain [super touches...]. Tried setting up my global variables for the method called by NSTimer differently (I very well may be missing something in regards to global variables...). This button is being created by the Storyboard, but I've set the class to my custom class, so I don't think UIButton's +(UIButton *)buttonWithType method is affecting this.
What am I missing? Is there some small thing I'm forgetting about or is there just no way to delay the call to [super touchesEnded] from a UIButton subclass?
I was not able to solve this, only find a way around it.
My last stab at solving this was to figure out whether the [super touchesEnded...] inside the completion block was being executed in a thread that was different from the thread it was executed in when it was outside the completion block... and no, they both appear to be the main thread (Apple's documentation on CATransaction does state that its completionBlock is always run in the main thread).
So in case someone else is banging their head against this, here's my less-than-elegant solution:
1.) In my subclass of UIButton I created a weak property called containingVC.
2.) In every single (ugh) VC that uses the custom button class I have to do this:
#implemenation VCThatUsesCustomButtonsOneOfWayTooMany
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.firstCustomButton.containingVC = self;
self.secondCustomButton.containingVC = self;
self.thirdCustomButton.containingVC = self;
....
self.lastCustomButton.containingVC = self;
//you're probably better off using an IBOutletColletion and NSArray's makeObjectPerformSelector:withObject...
}
#end
3.) Then in my custom UIButton class I have something like this:
-(void)animateForPushDismissCurrView
{
CAAnimation *buttonAnimation = ...make animation
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[self.containingVC performSegueWithIdentifier:#"segueID" sender:self.containingVC];
}];
[self.layer addAnimation:buttonAnimation forKey:nil];
[CATransaction commit];
}
4.) Then in whatever VC that the user is currently interacting with, after making sure the button has done whatever it was supposed to do (in my case it checks with the model to confirm that the relevant change was made), each button has to call [someCustomButton animateForPushDismissCurrView] which then animates the button press and then fires the UIStoryboardSegue that actually dismisses the view.
Obviously this would work for going deeper, not just unwinding, but you'd need additional logic in the custom button's -(void)animateForPush method or a separate method entirely.
Again, if I'm missing something here, I'd love to know what it is. This seems like an absurd number of hoops to jump through to accomplish what seems like a simple task.
Lastly, and most importantly, if it just won't work with the [super touchesEnded...] method in CATransaction's completionBlock, I'd like to know WHY. I suspect that it has something to do with threads or the weirdness that is Objective-C's super keyword.
Related
I'm trying to do some CALayer stuff on a UIButton that resides inside a uitableviewcell.
I've imported but the CALayer manipulation is not working.
When I moved it to cellForRowAtIndexPath method it works fine.
I do want to make the change once inside the class of the cell itself and not inside the table creation table.
Do you have any ideas ?
By the way, I do see the NSLog text on the init (so I know it entered the block).
Thanks !
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
NSLog(#"STARTED coder init in testResultCell");
[[self.bubbleMore layer] setMasksToBounds:YES];
[[self.bubbleMore layer] setBorderWidth:3.0f];
[[self.bubbleMore layer] setBorderColor:[UIColor blueColor].CGColor];
[[self.bubbleMore layer] setCornerRadius:20.0f];
}
return self;
}
Move the code to awakeWithNib. Your outlets won't be connected yet in initWithCoder. From Apple's documentation:
When an object receives an awakeFromNib message, it is guaranteed to have all its outlet instance variables set.
User interface objects are not available until viewDidLoad is called, which actually loads controls from your xib or Storyboard. Please move your code to viewDidLoad and it should work just fine.
If you add a breakpoint and look at the self.bubbleMore, it is going to be nil in initWithCode function.
I have a custom CALayer with an animatable property called progress. The basic code is:
#implementation AnimatedPieChartLayer
#dynamic progress;
+ (BOOL)needsDisplayForKey:(NSString *)key {
return [key isEqualToString: #"progress"] || [super needsDisplayForKey: key];
}
- (id <CAAction>)actionForKey:(NSString *)key {
if ([self presentationLayer] != nil) {
if ([key isEqualToString: #"progress"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath: key];
[anim setFromValue: [[self presentationLayer] valueForKey: key]];
[anim setDuration: 0.75f];
return anim;
}
}
return [super actionForKey: key];
}
- (void)drawInContext:(CGContextRef)context {
// do stuff
}
#end
In the view controller I do this, to try and get my animation to start from the beginning every time:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear: animated];
[pieChart setProgress: 0.0];
}
This all works perfectly. I put the layer on a view, the view in a view controller, and it all works as expected.
The complication is that my view controller is inside a UIScrollView. This presents a situation where the user could swipe away from my view controller before the CALayer animation completes. If they swipe back quickly, the CALayer animation does something weird. It reverses. It's like the animation is trying to go back to the beginning. Once it gets there it starts over, and animates the way it is supposed to.
So the question is, how can I prevent this. I want the animation to start from the beginning every time the view is displayed – regardless of what happened last time.
Any suggestions?
UPDATE
Here's a visual example of the problem:
Working:
Failing:
In the setup you have there, you have an implicit animation for the key "progress" so that whenever the progress property of the layer changes, it animates. This works both when the value increases and when it decreases (as seen in your second image).
To restore the layer to the default 0 progress state without an animation, you can wrap the property change in a CATransaction where all actions are disabled. This will disable the implicit animation so that you can start over from 0 progress.
[CATransaction begin];
[CATransaction setDisableActions:YES]; // all animations are disabled ...
[pieChart setProgress: 0.0];
[CATransaction commit]; // ... until this line
Probably too simple a suggestion to merit an 'answer,' but I would suggest canceling the animation. Looks like you could rewrite this code pretty easily so that the progress updates were checking some complete flag and then when the view get hidden, kill it. This thread has some good ideas on it.
I have a few custom UIView objects that all handle drawing like this:
- (void)drawRect:(CGRect)rect {
// ^ I init the layers 1 and 2
[self.layer insertSublayer:layer1 atIndex:0]; // 1 or more
[self.layer insertSublayer:layer2 atIndex:1];
}
They also have a - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; with nothing else inside but a NSLog.
I add them all inside my main ViewController like so:
- (void)viewDidLoad
{
[super viewDidLoad];
CustomView *myViewWith1Layer = [[CustomView alloc] initWithFrame:CGRectMake(20, 20, 440, 260)];
[self.view addSubview:myViewWith1Layer];
CustomViewLayered *myViewWith2Layer = [[CustomViewLayered alloc] initWithFrame:CGRectMake(40, 260, 200, -120)];
[self.view addSubview:myViewWith2Layers];
}
When I run my app, if I tap on a view that has only a single layer - I get my NSLog to show, and everything's fine. If, on the other hand, I tap on views with 1+ layers, the app crashes (objc_msgSend log shows up with "EXC_BAD_ACCES (code=1, address=..."). I guess this is somehow related with ARC, which I have enabled.
How do I add multiple layers to a view, without it being messed up by ARC?
I don't think that this is an ARC problem, but creating and inserting the layers in drawRect
is wrong. This should be done (only once) in the init method of the view, e.g. in initWithFrame.
In my case, the solution was definitely related to ARC.
When initialising my delegates, I immediately assigned them to the layer.delegate property, and ARC would remove the object from existence immediately after that.
So for each layer, I add a strong #property (strong, nonatomic) delegatesClass *delegatesName and initialise straight to the property. After that, I assign the layer.delegate = self.delegatesName.
This did solve the issue, although I'm not sure if it is the right way to do things.
I am a newbie in Objective-C and trying to make a simple app in which when you touch the object it will move randomly for a while and stops. then you need to touch it again so it will move again and stop after a while.
I have searched for touch method and some tutorials, but the problem is I don't know how to start. It is like I need a function to move the object and one to touch it, but I don't know how to connect them and use them.
here is a tutorial which helped me a lot to get a view of functionality and it actually function in opposite way of my app. but still I can not start programming on my own.
http://xcodenoobies.blogspot.se/2010/11/under-construction.html
Any help would be appreciated, regarding how to program my logic and how to find the right methods and how to find the type of variables I need.
Step 1: Put this code in your ViewDidLoad Method, in which i have created some UIImageView and add it to View Randomly
[self.view setTag:1];
for(int i=0;i<4;i++)
{
int x = arc4random()%300;
int y = arc4random()%400;
#warning set Image here
UIImageView *imgview = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"someImage.png"]];
[imgview setFrame:CGRectMake(x, y, 25, 25)];
[imgview setUserInteractionEnabled:YES];
[self.view addSubview:imgview];
}
Step 2 : Define touchBegan Method to handle touch and move objects around the view, we have set Tag = 1 for ViewController ,because we dont want to move our mainview, only subviews will be moved
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
if([[touch view] tag] != 1)
{
[UIView animateWithDuration:0.25f animations:^{
int x = arc4random()%300;
int y = arc4random()%400;
[[touch view] setCenter:CGPointMake(x, y)];
}];
}
}
What you need is to add a gesture recognizer to the view you want to be able to touch:
// In a view controller or in a UIView subclass
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self addGestureRecognizer:tapGestureRecognizer];
// Or [self.view addGestureRecognizer:tapGestureRecognizer];
- (void)handleTap:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded) {
// Animate the view, move it around for a while etc.
// For animating a view use animateWithDuration:animations:
}
}
If I get you correctly I would say that the easiest way to achieve what you request is to create a UIButton in Interface Builder and connect it to an IBAction which moves it to a random spot.. You can then add a custom graphic to the button..
Create a public method in your ViewController with return type IBAction
Create a button in IB and connect its "Touch Up Inside" outlet to your IBAction
I your IBAction method, generate a random x and y coordinate within the screens bounds and animate the movement to this point.
I will/can not go into details on the specific code since it would take way to much space.
Note that your question is very open and vague which is not considered good style on StackOverflow. Also, you might wan't to save stuff like animations until you are a bit more experienced with iOS and Objective-C
-V
Actually Apple made a demo for this.
http://developer.apple.com/library/ios/#samplecode/MoveMe/Introduction/Intro.html
You can try to modify this code to your needs. And the actual functions you where looking for where:
- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
If this is the answer you where looking for please click "answered" so this question can be considered as closed :-).
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
is a method called when user touches the view. if you override this in mainview (big view) you will have to find if touched point is where the object is with some helper method as described in your link. else you can override your object's class & implement the method so you dont have to explicitly find if touched point is on the object or not.
for your requirement. i'd say override the uiimageview & inside that put the touchesbegan implementation it will just work fine.
.h file
#interface StarView : UIImageView
#end
.m file
#implementation StarView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// your code in the link with the related methods
Destination = CGPointMake(arc4random() % 320, arc4random() % 480);
// calculate steps based on speed specified and distance between the current location of the sprite and the new destination
xamt = ((Destination.x - self.center.x) / speed);
yamt = ((Destination.y - self.center.y) / speed);
// ask timer to execute moveBall repeatedly each 0.2 seconds. Execute the timer.
mainTimer = [NSTimer scheduledTimerWithTimeInterval:(0.02) target:self selector:#selector(moveBall) userInfo:nil repeats: NO];
}
dont forget to copy the moveBall method after this.
In your mainview just make an instance of StarView & add to mainview
I study iOS SDK using this book and in 4 Chapter, when it tells you how to hide number pad with background tap I have a problem. I do that author says but nothing happened. How I can do this?
What book's author says:
1)Create new method backgroundTap in ViewController.h
- (IBAction)touchBackground:(id)sender;
2)Add method in ViewController.m
-(void)touchBackground:(id)sender{
[nameField resignFirstResponder];
[numberField resignFirstResponder];}
3)Change class identity of View object in Interface Builder(as I understand in Xcode 4 its Control) from UIView to UIControl
4)Connect TouchDown method in events list with File's Owner and check method backgroundTap.
This is my code samples
ViewController.h
ViewController.m
P.S. I'm sorry for my English, I translated all information from the book to english because I have russian translate.
I think you might make this simpler by just implementing a method like this:
- (IBAction)backgroundTouched:(id)sender {
[self.view endEditing:YES];
}
make the background view UIControl instead of UIView in interface builder and hook the touches up event to this method.
Have a look at this small example project. I have done exactly this in the secondViewController where touching the background dismissed the keyboard. You can also see how I have changed the background to a UIControl in secondViewController.xib
Something you might want to try, without changing the background view, is:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[nameField resignFirstResponder];
[numberField resignFirstResponder];
}
You may have done Step 4 ("Connect TouchDown method in events list with File's Owner and check method backgroundTap") incorrectly. Include an NSLog in your touchBackground method to check whether the method is being called.
-(void)touchBackground:(id)sender{
[nameField resignFirstResponder];
[numberField resignFirstResponder];
NSLog(#"touchBackground was called");
}
If the message ("touchBackground was called") doesn't show up in your console, then you known touchBackground is not being called.