Objective C: Having buttons work inside a mutable array - objective-c

I'm making a game that involves have buttons move across the screen until the user taps them. What I have so far is buttons moving across being generated randomly. My problem is when a new button is created, the last one stops moving. I'm trying to stick them into an array, but I'm not sure that's really gonna keep them moving.
-(void) generateStickFig:(NSTimer *)timer {
int x = random() % 100;
NSMutableArray *enemies = (NSMutableArray *)timer.userInfo;
if (x == 1) {
stickFig = [[UIButton alloc] initWithFrame:CGRectMake(0, 650, 50, 50)];
[stickFig setBackgroundColor:[UIColor blackColor]];
[stickFig addTarget:self action:#selector(tapFig:) forControlEvents:UIControlEventTouchUpInside];
// [enemies setObject:object forKey:[NSString stringWithFormat:#"object%i",i]];
[enemies addObject:stickFig];
int b = [enemies count];
[self.view addSubview:stickFig];
arrayNum++;
}
CGPoint oldPosition = stickFig.center;
stickFig.center = CGPointMake(oldPosition.x + 1 , oldPosition.y);
}

Where the buttons are stored is immaterial to moving them. You need to have a routine which animates them. You can do this by hand by moving them a small amount in response to a timer tick. The easiest method there would be fast enumeration over your array.
So you'd have the generation routine you have in your statement, a movement routine, and a touch IBAction handler which removes the button from the array. The generation routine, and the movement routine would both be called in your timer tick handler. (I tend to call that method "handleTick")
In high level psuedo-code it'd be something like this:
//tick handler
handleTick:
one out a hundred times, make a new button
give it a random starting location
store it in the buttons array
every time:
for button in buttons:
move button a few pixels
//button touch handler
buttonWasTouched:button :
[buttons removeObject: button];
I'm just getting into the system provided animation, so I don't know if your button can accept touches while being animated, but I wouldn't be surprised.

In order to know what button was touched from the array, you can use the tag option for the button:
- (IBAction) buttonTouched:(id) sender withEvent:(UIEvent *) event
{
UIButton *btn = (UIButton *)sender;
// in my case, I store the position on the array as a tag for the button
NSUInteger index = btn.tag;
UIControl *control = sender;
// Now you know what button was touched
// Apply actions on to it: remove from superview, animate explosion...
}
Cheers

Related

Elegant way to implement press and hold continuous event firing?

I often have a need to fire a sequence of events as a result of holding down a button. Think of a + button that increments a field: tapping it should increment it by 1, but tap & hold should say increment this by 1 every second until the button is released. Another example is the scrubbing function when holding down the backwards or forwards button in an audio player type app.
I usually resort to the following strategy:
On touchDownInside I set up a repeating timer with my desired interval.
On touchUpInside I invalidate and release the timer.
But for every such button I need a separate timer instance variable, and 2 target-actions, and 2 method implementations. (This is assuming I'm writing a generic class and don't want to impose restrictions on the max number of simultaneous touches).
Is there a more elegant way to solve this that I'm missing?
Register the events for every button by:
[button addTarget:self action:#selector(touchDown:withEvent:) forControlEvents:UIControlEventTouchDown];
[button addTarget:self action:#selector(touchUpInside:withEvent:) forControlEvents:UIControlEventTouchUpInside];
For each button, set the tag attribute:
button.tag = 1; // 2, 3, 4 ... etc
In the handler, do whatever you need. Identify a button by the tag:
- (IBAction) touchDown:(Button *)button withEvent:(UIEvent *) event
{
NSLog("%d", button.tag);
}
I suggest UILongPressGestureRecognizer.
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(addOpenInService:)];
longPress.delegate = self;
longPress.minimumPressDuration = 0.7;
[aView addGestureRecognizer:longPress];
[longPress release];
longPress = nil;
On firing the event, you can get the call in
- (void) addOpenInService: (UILongPressGestureRecognizer *) objRecognizer
{
// Do Something
}
Likewise you can use UITapGestureRecognizer for recognizing user taps.
Hope this helps. :)

Objective-C: (Weird) UIButton frame NSLogs different coordinates vs. where it displays (and receives touches)

Updated Solution: The principle problem was that my speed calculation was returning a whole number - 0 for all cases except for the object at index 3, where it was 1 - despite being labeled instantiated as a float. The result? The buttons were in an 'infinite' animation (an animation with 0 speed) from origin to destination.
////////////////////////////////////////
Weird: I am drawing some buttons on my view programmatically, and while troubleshooting my code I discovered that the buttons' frame/position/center (in Location A) differs from where they actually display (Location B). This is my first time coming across this phenomena..
(To make matters worse, I'm using hitTest:withEvent: and returning buttons based on their frame, so I am actually tracking hits (and firing actions) on touches to Location A, even though the buttons themselves are being displayed in Location B.)
Notes & Caveats: One of the buttons appears correctly. I don't know why. All buttons animate correctly, because the animations run off their frames.
I'm sure I'm missing something in my code, but I've been over it for hours and given that I the frame is reporting wrong I need more eyes!
Diagram:
+-----------------------+
|(mainV) |
| |
| +----+|
| |(sB)||
| +----+|
| +----+|
| |(sB)||
| +----+|
| +----+|
| |(sB)||
| +----+|
| +----+|
| |(sB)||
| +----+|
| +----+|
| |(dW)||
| +----+|
|+---------------------+|
||(mV) ||
|+---------------------+|
+-----------------------+
Legend:
(mainV) = main view/view controller, parent view of everything
(mV) = menuView, subview of (mainV)
(dW) = drawButton, subview of (mV), and the 'anchor' from which I build my other buttons
(sB) = instance of sizeButton, subview of (mV), using hitTest:withEvent: to get touches outside of the superview's frame
Code & Comments:
Central Button Creation Method, where I initially call & create the buttons, most vars are instance variables
- (void)addSizeSubButtons {
BOOL showAnimation = NO;
if ([drawSizeButtonsArray count] == 0) {
for (int i = 0; i < kNumberOfDrawSizes; i++) {
// this method is defined below
// short story is it makes 4 buttons and adds them to an array
[self makeSizeButton];
}
showAnimation = YES;
}
// drawSizeButtonsArray is the array from before - see makeSizeButtons below
for (UIButton *sizeButton in drawSizeButtonsArray) {
int buttonIndex = [drawSizeButtonsArray indexOfObject:sizeButton];
// drawButton is my unofficial anchor point - its center is {287.8, -24.25}
// sizing code in general works and I don't believe is at play here
// see (dW) in the diagram above..
[sizeButton setFrame:CGRectMake(0,
0,
drawButton.bounds.size.height * kStackButtonRatio,
drawButton.bounds.size.height * kStackButtonRatio)];
CGPoint position = CGPointMake(drawButton.center.x,
drawButton.center.y - sizeButton.bounds.size.height * (1.1 + buttonIndex));
[sizeButton.layer setPosition:position];
NSLog(#"sizeButton made at %# named %#",NSStringFromCGPoint(sizeButton.center),sizeButton);
// NSLog Output (all coords relative to (mV) from legend above:
// sizeButton made at {287.8, -66.6} (this is loop #1/sizeButton atIndex:0)
// sizeButton made at {287.8, -105.1} (this is loop #2/sizeButton atIndex:1)
// sizeButton made at {287.8, -143.6} (this is loop #3/sizeButton atIndex:2)
// sizeButton made at {287.8, -182.1} (this is loop #4/sizeButton atIndex:3)
// note: I have tested that the buttons are all different/at different memory addresses
// --------------------
// HOWEVER, visual display locations are as follows:
// button atIndex:0 # {287.8, -24.25} (remember this is the same as drawButton!)
// button atIndex:1 # {287.8, -24.25} (also wrong, and also hidden behind drawButton)
// button atIndex:2 # {287.8, -24.25} (three in a row in the wrong place)
// button atIndex:3 # {287.8, -182.1} (!! HARK! this one is actually in the right place.. no clue why one is and the other three are not)
[sizeButton.layer setCornerRadius:sizeButton.bounds.size.width / 7.5];
}
// animations are actually working as desired, because they run off the frame
// as soon as the animation kicks off, buttons all appear in the right place
// and take the correct action; still, i will include the core method just in case
if (showAnimation == YES) {
for (UIButton *sizeButton in drawSizeButtonsArray) {
// so my button is moving FROM its anchor TO its actual center
CGPoint origin = drawButton.center;
CGPoint destination = sizeButton.center;
// this is just to create a visual effect
float speed = ( [drawSizeButtonsArray indexOfObject:sizeButton] + 1 ) / [drawSizeButtonsArray count];
// my central animation method, included below
CABasicAnimation *slideButton = [self slideButton:sizeButton
fromPoint:origin
toPoint:destination
atSpeed:speed];
[sizeButton.layer addAnimation:slideButton
forKey:#"slideSizeButtonAnimation"];
}
}
}
Button Creator Helper/Worker Method called from above
- (void)makeSizeButton {
UIButton *sizeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[drawSizeButtonsArray addObject:sizeButton];
// button presses
[sizeButton setUserInteractionEnabled:YES];
[sizeButton addTarget:self
action:#selector(drawSizeButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
// TO DO: remove these debug titles and replace with dots
[sizeButton setTitle:[NSString stringWithFormat:#"%d",[drawSizeButtonsArray indexOfObject:sizeButton]]
forState:UIControlStateNormal];
[sizeButton setTitleColor:[UIColor blackColor]
forState:UIControlStateNormal];
[sizeButton setBackgroundColor:[UIColor whiteColor]];
// show it!
[self addSubview:sizeButton];
[self sendSubviewToBack:sizeButton];
}
Animation Helper Method
- (CABasicAnimation *)slideButton:(UIButton *)button
fromPoint:(CGPoint)origin
toPoint:(CGPoint)destination
atSpeed:(float)speed{
// now show animation
CABasicAnimation *slideAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
[slideAnimation setDelegate:self];
// from & to values
[slideAnimation setFromValue:[NSValue valueWithCGPoint:origin]];
[slideAnimation setToValue:[NSValue valueWithCGPoint:destination]];
// easing in and out.. oh yeah!
[slideAnimation setDuration:kAnimationTiming / speed];
CAMediaTimingFunction *easeInOut = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[slideAnimation setTimingFunction:easeInOut];
NSLog(#"slide animation from %# to %#",NSStringFromCGPoint(origin),NSStringFromCGPoint(destination));
// NSLog Output: (all these values are correct/what I intended)
// AND recall that all animations display properly!
// in fact, judging by NSLog, everything is working fine.. but visually it is not
// slide animation from {287.8, -24.25} to {287.8, -66.6} (for button atIndex:0)
// slide animation from {287.8, -24.25} to {287.8, -105.1} (for button atIndex:1)
// slide animation from {287.8, -24.25} to {287.8, -143.6} (for button atIndex:2)
// slide animation from {287.8, -24.25} to {287.8, -182.1} (for button atIndex:3)
return slideAnimation;
}
Does this problem still occur if you disable your animations?
Are you applying transforms to any of your views?
One thing I'd note: it's weird to set the frame of a UIView and then adjust its layer's position. How about using [sizeButton setCenter:position] rather than accessing the layer. I don't think that's the problem, but it might have something to do with it. Similarly, why not use UIView's animation methods rather than diving into CABasicAnimation land?

simple animation using an array of images

So this is what I did: I populated the nib with an image view and a round rect button. When the round rect button is pressed, a simple animation is shown using an array of images...code I used to do this looks like this:
imageview.animationImages = images;
and...
[imageview startAnimating];
and then...
[self.view addSubview:imageview];
But the issue is after I added the subview, the superview is though not visible but its still there, so even though the button is not visible it still can be pressed to fire the animation once again. How do I disable this button after the animation is fired. I hope you understood me.Thank you.
I'm assuming you're using an IBAction with a sender parameter. At the end of this method do
UIButton *temp = (UIButton *) sender
temp.enabled = NO;

How to differentiate the mouseDown event from mouseDragged in a Transparent NSWindow

I have a Transparent NSWindow with an simple icon in it that can be dragged around the screen.
My code is:
.h:
#interface CustomView : NSWindow{
}
#property (assign) NSPoint initialLocation;
.m
#synthesize initialLocation;
- (id) initWithContentRect: (NSRect) contentRect
styleMask: (NSUInteger) aStyle
backing: (NSBackingStoreType) bufferingType
defer: (BOOL) flag{
if (![super initWithContentRect: contentRect
styleMask: NSBorderlessWindowMask
backing: bufferingType
defer: flag]) return nil;
[self setBackgroundColor: [NSColor clearColor]];
[self setOpaque:NO];
[NSApp activateIgnoringOtherApps:YES];
return self;
}
- (void)mouseDragged:(NSEvent *)theEvent {
NSRect screenVisibleFrame = [[NSScreen mainScreen] visibleFrame];
NSRect windowFrame = [self frame];
NSPoint newOrigin = windowFrame.origin;
// Get the mouse location in window coordinates.
NSPoint currentLocation = [theEvent locationInWindow];
// Update the origin with the difference between the new mouse location and the old mouse location.
newOrigin.x += (currentLocation.x - initialLocation.x);
newOrigin.y += (currentLocation.y - initialLocation.y);
// Don't let window get dragged up under the menu bar
if ((newOrigin.y + windowFrame.size.height) > (screenVisibleFrame.origin.y + screenVisibleFrame.size.height)) {
newOrigin.y = screenVisibleFrame.origin.y + (screenVisibleFrame.size.height - windowFrame.size.height);
}
// Move the window to the new location
[self setFrameOrigin:newOrigin];
}
- (void)mouseDown:(NSEvent *)theEvent {
// Get the mouse location in window coordinates.
self.initialLocation = [theEvent locationInWindow];
}
I want to display a NSPopover when the users clicks the image of the transparent window. But, as you can see in the code, the mouseDown event is used to get the mouse location (the code above was taken from an example).
What can i do to know when the user clicks the icon just to drag it around or simply clicked it to display the NSPopover?
Thank you
This is the classic situation of receiving the defining event after you need it in order to begin the action. Specifically, you can't know if the mouseDown is the beginning of a drag until after the drag starts. However, you want to act upon that mouseDown if a drag doesn't start.
In iOS (I realize that's not directly relevant to the code here, but it is instructional), there's an entire API built around letting iOS attempt to make these kinds of decisions for you. The entire Gesture system is based on the idea that the user starts to do something that might be one of many different actions, and thus needs to be resolved over time, possibly resulting in cancelled actions during the tracking period.
On OS X, we don't have many systems to help out with this, so if you have something that needs to handle a click and a drag differentially, you will need to defer your next action until a guard time has passed, and if that passes, you can perform the original action. In this case, you will likely want to do the following:
In the mouseDown, begin an NSTimer set for an appropriate guard time (not so long that people will accidentally move the pointer, and not so short that you'll trigger before the user drags) in order to call you back later to trigger the popover.
In the mouseDragged, use a guard area to make sure that if the user just twitches a little, it doesn't count as a drag. This can be irritating, as it sometimes results in needing to drag something farther than it seems necessary in order to begin a drag, so you'll want to either find a magic constant somewhere, or do some experimentation. When the guard area is exceeded, then begin your legitimate drag operation by canceling the NSTimer with [timer invalidate] and do your drag.
In the callback for the timer, display your popover. If the user dragged, the NSTimer will have been invalidated, causing it not to fire, and so the popover won't be displayed.

Animated UIImageView which should recognize taps on it (at every location the UIImageView is at)

I'm making an iPhone App which involves balloons (UIImageViews) being spawned from the bottom of the screen and moving up to the top of the screen.
The aim is to have the UIImageViews set up so that a tap gesture on a balloon causes the balloons to be removed from the screen.
I have set up the GestureRecognizer and taps are being recognized.
The problem I am having is that the animation method I am using does not provide information about where the balloon is being drawn at a specific point in time, and treats it as already having reached the final point mentioned.
This causes the problem of the taps being recognized only at the top of the screen(which is the balloon's final destination).
I have pasted my code for animation here:
UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction;
[UIView animateWithDuration:2.0 delay:0.0 options:options animations:^{
balloonImageView.center = p;
} completion:^(BOOL finished) {
if (finished) {
balloonImageView.hidden= TRUE;
balloonImageView.image=nil;
}
I keep track of where an image is by giving a unique tag to a UIImageView and checking if gesture.view.tag = anyBalloonObject.tag and if it does I destroy the balloon.
My basic question is, is there another animation tool I can use? I understand that I can set a balloon's finish point to slightly higher than where it is on screen until it reaches the top but that seems like it is very taxing on memory.
Do you have any recommendations? I looked through the Core Animation reference, and didn't find any methods that suited my needs.
In addition will the Multi-threading aspects of UIViewAnimation affect the accuracy of the tap system?
Thank You,
Vik
You shouldnt animate game-objects (objects you interact with while they are moving) like that.
You should rather use a NSTimer to move the ballons.
-(void)viewDidLoad {
//Set up a timer to run the update-function
timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(update) userInfo:NULL repeats:YES];
}
-(void) update {
for (int i = [balloonArray count] - 1; i >= 0; i--) { //Loop through an array containing all your balloons.
UIImageView *aBalloon = [balloonArray objectAtIndex:i]; //Select balloon at the current loop index
aBallon.center = CGPointMake(aBallon.center.x, aBalloon.center.y + 1); //Change the center of that balloon, in this case: make it go upwards.
}
}
Here is what you need to declare in .h:
NSTimer *timer;
NSMutableArray *ballonArray;
//You should use properties on these also..
You will have to create a spawn method which adds balloons to the screen AND to the array.
When you have done that, it's simple to implement the touch methods, You can just loop trough the balloons and check:
//You need an excact copy of the loop from earlier around this if statement!
If (CGRectContainsPoint(aBalloon.frame, touchlocation)) { //you can do your own tap detection here inthe if-statement
//pop the balloon and remove it from the array!
}
Hope that isnt too scary-looking, good luck.