I am attempting to make a basic game which requires a serious of buttons to control player movement. Keep in mind I am using cocos-2d. My goal is to have the buttons be holdable and move a sprite when held down. The code i am using now looks like this.
CCMenuItemHoldable.h
#interface CCMenuItemSpriteHoldable : CCMenuItemSprite {
bool buttonHeld;
}
#property (readonly, nonatomic) bool buttonHeld;
CCMenuItemHoldable.m
#implementation CCMenuItemSpriteHoldable
#synthesize buttonHeld;
-(void) selected
{
[super selected];
buttonHeld = true;
[self setOpacity:128];
}
-(void) unselected
{
[super unselected];
buttonHeld = false;
[self setOpacity:64];
}
#end
and for the set up of the buttons
rightBtn = [CCMenuItemSpriteHoldable itemFromNormalSprite:[CCSprite spriteWithFile:#"art/hud/right.png"] selectedSprite:[CCSprite spriteWithFile:#"art/hud/right.png"] target:self selector:#selector(rightButtonPressed)];
CCMenu *directionalMenu = [CCMenu menuWithItems:leftBtn, rightBtn, nil];
[directionalMenu alignItemsHorizontallyWithPadding:0];
[directionalMenu setPosition:ccp(110,48)];
[self addChild:directionalMenu];
This all seems to work fine but when i do
-(void)rightButtonPressed:(id) sender
{
if([sender buttonHeld])
targetX = 10;
else{
targetX = 0;
}
}
The crash has been fixed but I am trying to get my sprite to move. In my game tick function I add the value of targetX to the position of the sprite on a timer, still no movement.
Please, always include a crash log when asking questions about crashes.
In your case, I can guess the problem. You are adding this selector:
#selector(rightButtonPressed)
Your method is called
rightButtonPressed:(id)sender
Which would be, as a selector, rightButtonPressed: - note the colon indicating that an argument is passed. Either change the method so it has no argument, or add a colon to the selector when you create the button.
The crash log would be telling you this - it would say "unrecognised selector sent to..." with the name of the receiving class, and the name of the selector.
Related
I'm trying to implement a UIImageView that shows different .png (organized in a NSArray) at each tap. When the tap reaches the last .png, the next tap will show the first .png and so on.
I found an old answer here:
Add 4 image in imageview and change the picture with tap or swipe gesture
that seems to go in the right direction. However, it doesn't seem to be applicable in the new XCode and there is no more possibility to ask clarifications or updates since the topic has been closed.
Does anybody have an idea on how to subclass then entire ImageView to get the expected result?
The solution was much easier than I thought.
As a custom value to the selector cannot be passed in the way I asked, I created an helper property that holds the index of the currently displayed image, increment that on every tap and set the image accordingly.
#interface YourViewController ()
#property (nonatomic, strong) NSArray *images;
#property (nonatomic, assign) NSInteger currentImageIndex;
#end
#implementation YourViewController
- (void)viewDidLoad {
// Here you fill your images array
self.images = ...
// Set currentImageIndex to 0 and display first image
self.currentImageIndex = 0;
[self displayImageWithIndex:self.currentImageIndex];
// Additional code, like your button target/action
}
- (void)myButtonWasTapped:(id)sender
{
if(self.currentImageIndex + 1 < [self.images count]) {
self.currentImageIndex++;
} else {
self.currentImageIndex = 0;
}
[self displayImageWithIndex: self.currentImageIndex];
}
- (void)displayImageWithIndex:(NSInteger)index {
self.imageView.image = [self.images objectAtIndex:index];
}
#end
Please note: there might be a better approach to this, but this is what it worked in my case.
Hope this can be helpful.
There's a bug in MapKit that can cause duplicate callout views on an annotation. If the timing is just right, an annotation view can get re-used while it is being selected and apparently just before the callout view is actually added to it. As a result, the old callout view gets stuck there, and the new callout will appear on top of or next to it. Here's what this can look like in an OS X app:
There's only one annotation on this map. If you click elsewhere on the map to deselect the annotation, only one of the callouts disappears. In some cases you might have two callouts with completely different information, which is where things get really confusing for someone using your app.
Here's the majority of a sample OS X project I put together that illustrates this bug:
#import MapKit;
#import "AppDelegate.h"
#import "JUNMapAnnotation.h"
#interface AppDelegate () <MKMapViewDelegate>
#property (weak) IBOutlet NSWindow *window;
#property (weak) IBOutlet MKMapView *mapView;
#property BOOL firstPin;
- (void)placeAndSelectPin;
- (JUNMapAnnotation *)placePin;
- (void)clearPins;
#end
#implementation AppDelegate
- (IBAction)dropSomePins:(id)sender {
self.firstPin = YES;
[self placeAndSelectPin];
[self performSelector:#selector(placeAndSelectPin) withObject:nil afterDelay:0.0001];
}
#pragma mark - Private methods
- (void)placeAndSelectPin {
[self clearPins];
JUNMapAnnotation *annotation = [self placePin];
[self.mapView deselectAnnotation:annotation animated:NO];
[self.mapView selectAnnotation:annotation animated:YES];
}
- (JUNMapAnnotation *)placePin {
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(50.0,50.0);
JUNMapAnnotation *annotation = [[JUNMapAnnotation alloc] initWithCoordinate:coord];
annotation.title = #"Annotation";
annotation.subtitle = (self.firstPin) ? #"This is an annotation with a longer subtitle" : #"This is an annotation";
[self.mapView addAnnotation:annotation];
self.firstPin = NO;
return annotation;
}
- (void)clearPins {
[self.mapView removeAnnotations:self.mapView.annotations];
}
#pragma mark - MKMapViewDelegate
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
if ([annotation isKindOfClass:[JUNMapAnnotation class]]) {
static NSString *identifier = #"annotationView";
MKPinAnnotationView *view = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view == nil) {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = YES;
NSLog(#"new annotation view");
} else {
view.annotation = annotation;
}
return view;
}
return nil;
}
#end
The same bug seems to exist in iOS, though I've had a tougher time recreating it there.
While I'm waiting on Apple to fix this, I'd like to work around it as much as possible. So far I've come up with a few possibilities:
Don't re-use annotation views. From what I can tell this seems like the only way to completely avoid the bug, but it seems pretty inefficient.
When an annotation view is re-used in mapView:viewForAnnotation:, remove all of its subviews. Currently it seems like the callout is the only subview, though it doesn't seem like a particularly safe hack. It also only sort of works—it doesn't prevent duplicate callouts from appearing, it just keeps them from sticking around forever. (When this bug first happens, there actually aren't any subviews yet.)
Combine both of those: if dequeueReusableAnnotationViewWithIdentifier: returns a view that has any subviews, ignore it and create a new one. This seems a lot safer than 2 and isn't nearly as inefficient as 1. But as with 2 it's not a complete workaround.
I've also tried adding deselectAnnotation:animated: in every place I can think of, but I can't find anything that works. I assume that once the annotation view is re-used, the MapView loses track of the first callout, so none of its normal methods will get rid of it.
this is a bit out of left field, but..
try registering the same cell class with 2 different reuse identifiers. in viewForAnnotation:, alternate between using each identifier when dequeueing a cell. this should prevent grabbing from the same queue twice in succession.
I've tried everything I can think of, including all the suggestions I've found here on SO and on other mailing lists, but I cannot figure out how to programmatically collapse an NSSplitView pane with an animation while Auto Layout is on.
Here's what I have right now (written in Swift for fun), but it falls down in multiple ways:
#IBAction func toggleSourceList(sender: AnyObject?) {
let isOpen = !splitView.isSubviewCollapsed(sourceList.view.superview!)
let position = (isOpen ? 0 : self.lastWidth)
if isOpen {
self.lastWidth = sourceList.view.frame.size.width
}
NSAnimationContext.runAnimationGroup({ context in
context.allowsImplicitAnimation = true
context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
context.duration = self.duration
self.splitView.setPosition(position, ofDividerAtIndex: 0)
}, completionHandler: { () -> Void in
})
}
The desired behavior and appearance is that of Mail.app, which animates really nicely.
I have a full example app available at https://github.com/mdiep/NSSplitViewTest.
Objective-C:
[[splitViewItem animator] setCollapse:YES]
Swift:
splitViewItem.animator().collapsed = true
From Apple’s help:
Whether or not the child ViewController corresponding to the
SplitViewItem is collapsed in the SplitViewController. The default is
NO. This can be set with the animator proxy to animate the collapse or
uncollapse. The exact animation used can be customized by setting it
in the -animations dictionary with a key of "collapsed". If this is
set to YES before it is added to the SplitViewController, it will be
initially collapsed and the SplitViewController will not cause the
view to be loaded until it is uncollapsed. This is KVC/KVO compliant
and will be updated if the value changes from user interaction.
I was eventually able to figure this out with some help. I've transformed my test project into a reusable NSSplitView subclass: https://github.com/mdiep/MDPSplitView
For some reason none of the methods of animating frames worked for my scrollview. I didn't try animating the constraints though.
I ended up creating a custom animation to animate the divider position. If anyone is interested, here is my solution:
Animation .h:
#interface MySplitViewAnimation : NSAnimation <NSAnimationDelegate>
#property (nonatomic, strong) NSSplitView* splitView;
#property (nonatomic) NSInteger dividerIndex;
#property (nonatomic) float startPosition;
#property (nonatomic) float endPosition;
#property (nonatomic, strong) void (^completionBlock)();
- (instancetype)initWithSplitView:(NSSplitView*)splitView
dividerAtIndex:(NSInteger)dividerIndex
from:(float)startPosition
to:(float)endPosition
completionBlock:(void (^)())completionBlock;
#end
Animation .m
#implementation MySplitViewAnimation
- (instancetype)initWithSplitView:(NSSplitView*)splitView
dividerAtIndex:(NSInteger)dividerIndex
from:(float)startPosition
to:(float)endPosition
completionBlock:(void (^)())completionBlock;
{
if (self = [super init]) {
self.splitView = splitView;
self.dividerIndex = dividerIndex;
self.startPosition = startPosition;
self.endPosition = endPosition;
self.completionBlock = completionBlock;
[self setDuration:0.333333];
[self setAnimationBlockingMode:NSAnimationNonblocking];
[self setAnimationCurve:NSAnimationEaseIn];
[self setFrameRate:30.0];
[self setDelegate:self];
}
return self;
}
- (void)setCurrentProgress:(NSAnimationProgress)progress
{
[super setCurrentProgress:progress];
float newPosition = self.startPosition + ((self.endPosition - self.startPosition) * progress);
[self.splitView setPosition:newPosition
ofDividerAtIndex:self.dividerIndex];
if (progress == 1.0) {
self.completionBlock();
}
}
#end
I'm using it like this - I have a 3 pane splitter view, and am moving the right pane in/out by a fixed amount (235).
- (IBAction)togglePropertiesPane:(id)sender
{
if (self.rightPane.isHidden) {
self.rightPane.hidden = NO;
[[[MySplitViewAnimation alloc] initWithSplitView:_splitView
dividerAtIndex:1
from:_splitView.frame.size.width
to:_splitView.frame.size.width - 235
completionBlock:^{
;
}] startAnimation];
}
else {
[[[MySplitViewAnimation alloc] initWithSplitView:_splitView
dividerAtIndex:1
from:_splitView.frame.size.width - 235
to:_splitView.frame.size.width
completionBlock:^{
self.rightPane.hidden = YES;
}] startAnimation];
}
}
/// Collapse the sidebar
func collapsePanel(_ number: Int = 0){
guard number < self.splitViewItems.count else {
return
}
let panel = self.splitViewItems[number]
if panel.isCollapsed {
panel.animator().isCollapsed = false
} else {
panel.animator().isCollapsed = true
}
}
I will also add, because it took me quite a while to figure this out, that setting collapseBehavior = .useConstraints on your NSSplitViewItem (or items) may help immensely if you have lots of constraints defining the layouts of your subviews. My split view animations didn't look right until I did this. YMMV.
If you're using Auto-Layout and you want to animate some aspect of the view's dimensions/position, you might have more luck animating the constraints themselves. I've had a quick go with an NSSplitView but have so far only met with limited success. I can get a split to expand and collapse following a button push, but I've ended up having to try to hack my way around loads of other problems caused by interfering with the constraints. In case your unfamiliar with it, here's a simple constraint animation:
- (IBAction)animate:(NSButton *)sender {
/* Shrink view to invisible */
NSLayoutConstraint *constraint = self.viewWidthConstraint;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[[NSAnimationContext currentContext] setDuration:0.33];
[[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];
[[constraint animator] setConstant:0];
} completionHandler:^{
/* Do Some clean-up, if required */
}];
Bear in mind you can only animate a constraints constant, you can't animate its priority.
NSSplitViewItem (i.e. arranged subview of NSSplitView) can be fully collapsed, if it can reach Zero dimension (width or height). So, we just need to deactivate appropriate constrains before animation and allow view to reach Zero dimension. After animation we can activate needed constraints again.
See my comment for SO question How to expand and collapse NSSplitView subviews with animation?.
This is a solution that doesn't require any subclasses or categories, works without NSSplitViewController (which requires macOS 10.10+), supports auto layout, animates the views, and works on macOS 10.8+.
As others have suggested, the solution is to use an NSAnimationContext, but the trick is to set context.allowsImplicitAnimation = YES (Apple docs). Then just set the divider position as one would normally.
#import <Quartz/Quartz.h>
#import <QuartzCore/QuartzCore.h>
- (IBAction)toggleLeftPane:(id)sender
{
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
context.allowsImplicitAnimation = YES;
context.duration = 0.25; // seconds
context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
if ([self.splitView isSubviewCollapsed:self.leftPane]) {
// -> expand
[self.splitView setPosition:self.leftPane.frame.size.width ofDividerAtIndex:0];
} else {
// <- collapse
_lastLeftPaneWidth = self.leftPane.frame.size.width;
// optional: remember current width to restore to same size
[self.splitView setPosition:0 ofDividerAtIndex:0];
}
[self.splitView layoutSubtreeIfNeeded];
}];
}
Use auto layout to constrain the subviews (width, min/max sizes, etc.). Make sure to check "Core Animation Layer" in Interface Builder (i.e. set views to be layer backed) for the split view and all subviews — this is required for the transitions to be animated. (It will still work, but without animation.)
A full working project is available here: https://github.com/demitri/SplitViewAutoLayout.
I am an experienced iOS developer, but this has really stumped me. I am simultaneously submitting a problem report to Apple.
I'm adding annotations to a MKMapKit map (Xcode 4.6). Each annotation is a MyAnnotation class; MyAnnotation defines a property, location_id, that I use to keep track of that annotation.
The problem is simple: I want the MyAnnotation with a location_id of -1 to appear in front of everything else.
To do this, I am overriding mapView:didAddAnnotationViews: in my MKMapViewDelegate:
-(void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
// Loop through any newly added views, arranging them (z-index)
for (MKAnnotationView* view in views) {
// Check the location ID
if([view.annotation isKindOfClass:[MyAnnotation class]] && [((MyAnnotation*)(view.annotation)).location_id intValue]==-1 ) {
// -1: Bring to front
[[view superview] bringSubviewToFront:view];
NSLog(#"to FRONT: %#",((MyAnnotation*)view.annotation).location_id);
} else {
// Something else: send to back
[[view superview] sendSubviewToBack:view];
NSLog(#"to BACK: %#",((MyAnnotation*)view.annotation).location_id);
}
}
}
this works just fine. I have an "add" button that adds an annotation to a random location near the center of my map. Each time I push the "add" button, a new annotation appears; but nothing hides the annotation with the location_id of -1.
** UNTIL ** I scroll!
As soon as I start scrolling, all of my annotations are rearranged (z-order) and my careful stacking no longer applies.
The really confusing thing is, I've done icon order stacking before with no problem whatsoever. I've created a brand new, single view app to test this problem out; sending MKAnnotationView items to the back or front only works until you scroll. There is nothing else in this skeleton app except the code described above. I'm wondering if there is some kind of bug in the latest MapKit framework.
The original problem was in trying to add new annotations when the user scrolled (mapView:regionDidChangeAnimated:). The annotation adds; the mapView:didAddAnnotationViews: code fires; and then the order is scrambled by an unseen hand in the framework (presumably as the scroll completes).
In case you're interested, here is my viewForAnnotation:
-(MKAnnotationView*)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
// Is this an A91Location?
if([annotation isKindOfClass:[MyAnnotation class]]){
MyAnnotation* ann=(MyAnnotation*)annotation;
NSLog(#"viewForAnnotation with A91Location ID %#",ann.location_id);
if([ann.location_id intValue]==-1){
// If the ID is -1, use a green pin
MKPinAnnotationView* green_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
green_pin.pinColor=MKPinAnnotationColorGreen;
green_pin.enabled=NO;
return green_pin;
} else {
// Otherwise, use a default (red) pin
MKPinAnnotationView* red_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
red_pin.enabled=NO;
return red_pin;
}
}
// Everything else
return nil;
}
And my class:
#interface MyAnnotation : NSObject <MKAnnotation>
#property (strong, nonatomic, readonly) NSNumber* location_id;
#property (strong, nonatomic, readonly) NSString* name;
#property (strong, nonatomic, readonly) NSString* description;
#property (nonatomic) CLLocationCoordinate2D coordinate;
-(id) initWithID:(NSNumber*)location_id name: (NSString*) name description:(NSString*) description location:(CLLocationCoordinate2D) location;
// For MKAnnotation protocol... return name and description, respectively
-(NSString*)title;
-(NSString*)subtitle;
#end
Could you possibly try the following as a work around:
1) Remove your code in mapView:didAddAnnotationViews:
2) Pickup when the map is moved or pinched (I assume this is what you consider to be a scroll right?) - I do this with gestures rather than the mapView regionChanged as I have had bad experiences such as unexplained behaviour with the latter.
//recognise the paning gesture to fire the didDragMap method
UIPanGestureRecognizer* panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didDragMap:)];
[panRec setDelegate:self];
[self.mapView addGestureRecognizer:panRec];
//recognise the pinching gesture to fire the didPinchMap method
UIPinchGestureRecognizer *pinchRec = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:#selector(didPinchMap:)];
[pinchRec setDelegate:self];
//[pinchRec setDelaysTouchesBegan:YES];
[self.mapView addGestureRecognizer:pinchRec];
//recognise the doubleTap
UITapGestureRecognizer *doubleTapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didPinchMap:)];
[doubleTapRec setDelegate:self];
doubleTapRec.numberOfTapsRequired = 2;
[self.mapView addGestureRecognizer:doubleTapRec];
3) write a custom "plotAnnotations" function to show your annotations. This function will loop through all your annotations and save them in an array "annotationsToShow" ordered by your location id DESC so that your location_id -1 are added last. Use [self.mapView addAnnotations:annotationsToShow]; to display them.
4) Call plotAnnotations in your gestureRecognizer functions
- (void)didDragMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
- (void)didPinchMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
5) You may need to delete all annotations before displaying the new ones in 3) [self.mapView removeAnnotations:annotationsToRemove];
I am using a NSColorWell which is set to continuously update. I need to know when the user is done editing the control (mouse up) from the color picker in the color panel.
I installed an event monitor and am successfully receiving mouse down and mouse moved messages, however NSColorPanel appears to block mouse up.
The bottom line is that I want to add the final selected color to my undo stack without all the intermediate colors generated while the user is choosing their selection.
Is there a way of creating a custom NSColorPanel and replacing the shared panel with the thought of overriding its mouseUp and sending a message?
In my research this issue has been broached on a few occasions, however I have not read a successful resolution.
Regards,
- George Lawrence Storm, Keencoyote Invention Services
I discovered that if we observe color keypath of NSColorPanel we get called one extra time on mouse up events. This allows us to ignore action messages from NSColorWell when the left mouse button is down and to get the final color from keypath observer.
In this application delegate example code colorChanged: is a NSColorWell action method.
void* const ColorPanelColorContext = (void*)1001;
#interface AppDelegate()
#property (weak) NSColorWell *updatingColorWell;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSColorPanel *colorPanel = [NSColorPanel sharedColorPanel];
[colorPanel addObserver:self forKeyPath:#"color"
options:0 context:ColorPanelColorContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (context == ColorPanelColorContext) {
if (![self isLeftMouseButtonDown]) {
if (self.updatingColorWell) {
NSColorWell *colorWell = self.updatingColorWell;
[colorWell sendAction:[colorWell action] to:[colorWell target]];
}
self.updatingColorWell = nil;
}
}
}
- (IBAction)colorChanged:(id)sender {
if ([self isLeftMouseButtonDown]) {
self.updatingColorWell = sender;
} else {
NSColorWell *colorWell = sender;
[self updateFinalColor:[colorWell color]];
self.updatingColorWell = nil;
}
}
- (void)updateFinalColor:(NSColor*)color {
// Do something with the final color...
}
- (BOOL)isLeftMouseButtonDown {
return ([NSEvent pressedMouseButtons] & 1) == 1;
}
#end
In the Interface Builder, select your color well, and then uncheck the Continuous checkbox in the Attributes Inspector. Additionally, add the following line of code somewhere appropriate like in the applicationDidFinishLaunching: method or awakeFromNib method:
[[NSColorPanel sharedColorPanel] setContinuous:NO];
In other words, both the shared color panel and your color well need to have continuous set to NO in order for this to work properly.
The proper way to do what you want is to use NSUndoManager's -begin/endUndoGrouping. So you'd do something like this
[undoManager beginUndoGrouping];
// ... whatever code you need to show the color picker
// ...then when the color has been chosen
[undoManager endUndoGrouping];
The purpose of undo groups is exactly what you're trying to accomplish - to make all changes into a single undo.