How can I redraw an image in an custom NSView for an NSStatusItem? - objective-c

I am having some troubles understanding how to wire a custom NSView for an NSMenuItem to support both animation and dragging and dropping. I have the following subclass of NSView handling the bulk of the job. It draws my icon when the application launches correctly, but I have been unable to correctly setup the subview to change when I invoke the setIcon function from another caller. Is there some element of the design that I am missing?
TrayIconView.m
#import "TrayIconView.h"
#implementation TrayIconView
#synthesize statusItem;
static NSImageView *_imageView;
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
statusItem = nil;
isMenuVisible = NO;
_imageView = [[NSImageView alloc] initWithFrame:[self bounds]];
[self addSubview:_imageView];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Draw status bar background, highlighted if menu is showing
[statusItem drawStatusBarBackgroundInRect:[self bounds]
withHighlight:isMenuVisible];
}
- (void)mouseDown:(NSEvent *)event {
[[self menu] setDelegate:self];
[statusItem popUpStatusItemMenu:[self menu]];
[self setNeedsDisplay:YES];
}
- (void)rightMouseDown:(NSEvent *)event {
// Treat right-click just like left-click
[self mouseDown:event];
}
- (void)menuWillOpen:(NSMenu *)menu {
isMenuVisible = YES;
[self setNeedsDisplay:YES];
}
- (void)menuDidClose:(NSMenu *)menu {
isMenuVisible = NO;
[menu setDelegate:nil];
[self setNeedsDisplay:YES];
}
- (void)setIcon:(NSImage *)icon {
[_imageView setImage:icon];
}
TrayIconView.h
#import <Cocoa/Cocoa.h>
#interface TrayIconView : NSView
{
BOOL isMenuVisible;
}
#property (retain, nonatomic) NSStatusItem *statusItem;
- (void)setIcon:(NSImage *)icon;
#end

The solution to this problem was actually outside of the view detailed here. The caller of the interface was being double instantiated on accident, thus nulling out the reference to the previously created NSView. After correcting that concern the app draws and works just fine.
With regard to dragging, I just implemented a subclass of NSView that implemented the Cocoa draggable protocol and added it as a subview to this parent class. That allows dragging onto the currently established NSRect that contains the menubar icon.

Related

Cannot set my custom NSView as first responder

I've created a simple subclass of NSView, but calling becomeFirstResponder seems to have no effect - the framework doesn't even call my acceptsFirstResponder method.
However, if I click with my mouse on my view it seems to becomes the first responder and I can receive keyDown events.
Here is my NSView subclass:
#interface MyView : NSView
#end
#implementation MyView
- (void) keyDown: (NSEvent*) with {
NSLog(#"keyDown");
}
- (BOOL)acceptsFirstResponder {
NSLog(#"acceptsFirstResponder");
return YES;
}
- (BOOL)canBecomeKeyView {
NSLog(#"canBecomeKeyView");
return YES;
}
#end
And here is how I'm setting it up in my AppDelegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect frameRect = NSMakeRect(0,0,100,100);
MyView *myView = [[MyView alloc] initWithFrame:frameRect];
[_window.contentView addSubview:myView];
[myView becomeFirstResponder];
}
What am I doing wrong?
Whoops - the documentation says 'Never invoke this method directly'. So I should call
[_window makeFirstResponder:myView];
instead of
[myView becomeFirstResponder];
Thanks to Willeke for the hint.

How do I present UIViewController from SKscene with social framework?

I am making a game like Flappy Bird. How do I present a UIViewController from SKScene?
First of all, I tell my environments
Mac OS X 10.9
Xcode 5.0.2
Sprite Kit(framework), social.framework(framework) are added in my project
My goal is to display a "Share" button upon Game Over. Tapping the share button image should present a SLComposeViewController (Twitter Share). The contents of the scene should not change.
I'd like to solve bellow issue and change display from GameOverScene to tweetSheet(display)
composed with social.framework.
The issue
[self presentViewController:tweetSheet animated:YES completion:nil];
//Error:No visible #interface for 'GameOverScene' declares the selector "presentViewController":animated:completion:
My coding files are below(I extracted parts of important codes).
ViewController.h
import <UIKit/UIKit.h>
import <SpriteKit/SpriteKit.h>
import <iAd/iAd.h>
#interface ViewController : UIViewController<ADBannerViewDelegate><br>
#end
GameOverScene.h
#import <SpriteKit/SpriteKit.h>
#class SpriteViewController;
#interface GameOverScene : SKScene {
}
#end
GameOverScene.m
#import "GameOverScene.h"
#import "NewGameScene.h"
#import "MainScene.h"
#import <Social/Social.h>
#implementation GameOverScene {
//The twitter button
SKSpriteNode *_twitterbutton;
}
- (id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size]) {
//Creating the twitterbutton with the twitterbutton image from Images.xcassets
_twitterbutton = [SKSpriteNode spriteNodeWithImageNamed:#"twitterbutton"];
[_twitterbutton setSize:CGSizeMake(50, 50)];
[_twitterbutton setPosition:CGPointMake(self.size.width/2, self.size.height/5 + 50)];
//Adding the twitter button
[self addChild:_twitterbutton];
//Again, this is important, otherwise we can't identify what button is pressed
_twitterbutton.name = #"twitterbutton";
[_twitterbutton setPosition:CGPointMake(self.size.width/2, self.size.height/5 + 50)]
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//Same as in NewGameScene menu
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
//Is the twitter button touched?
if([node.name isEqualToString:#"twitterbutton"]){
if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]){
SLComposeViewController *tweetSheet = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
[tweetSheet setInitialText:#"TestTweet from the Game !!"];
[self presentViewController:tweetSheet animated:YES completion:nil];
**//Error:No visible #interface for 'GameOverScene' declares the selector "presentViewController":animated:completion:**
}
}
ViewControlloer.m
#import "ViewController.h"
#import "NewGameScene.h"
#implementation ViewController
//Loads the view onto our main class
- (void)loadView
{
self.view = [[SKView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
}
//Executes when view finishes loading
- (void)viewWillLayoutSubviews
{
[super viewDidLoad];
//Set the resize mode to flexible width and height
[self.view setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
//Create our view from our original view
//Make sure to leave originalContentView in, otherwise the app will crash
SKView *skView = (SKView *)self.originalContentView;
//We create a new NewGameScene according to the current dimensions
SKScene *scene = [NewGameScene sceneWithSize:skView.bounds.size];
//Create a transition class with animation type fade and a duration of .4 seconds
SKTransition *transition = [SKTransition fadeWithDuration:.4];
//Present the menu view (NewGameScene) with our fade in transition
[skView presentScene:scene transition:transition];
}
#end
You cannot present a viewController from within a SKScene as it is actually only being rendered on a SKView. You need a way to send a message to the viewController, which in turn will present the viewController. For this, you can use delegation.
Add the following protocol definition to your SKScene's .h file:
#protocol sceneDelegate <NSObject>
-(void)showShareScreen;
#end
And declare a delegate property in the interface:
#property (weak, nonatomic) id <sceneDelegate> delegate;
Then, at the point where you want to present the share screen, instead of the line:
[self presentViewController:tweetSheet animated:YES completion:nil];
Use this line:
[self.delegate showShareScreen];
Now, in your viewController's .h file, implement the protocol:
#interface ViewController : UIViewController <sceneDelegate>
And, in your .m file, add the following line before you present the scene:
scene.delegate = self;
Then add the following method there:
-(void)presentShareScreen
{
if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter])
{
SLComposeViewController *tweetSheet = [SLComposeViewController
composeViewControllerForServiceType:SLServiceTypeTwitter];
[tweetSheet setInitialText:#"TestTweet from the Game !!"];
[self presentViewController:tweetSheet animated:YES completion:nil];
}
}
An alternate method would be to use NSNotificationCenter
Keep the -presentShareScreen method as described in the previous alternative.
Add the viewController as a listener to the notification in it's -viewDidLoad method:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(presentShareScreen) name:#"showShareScreen" object:nil];
Then, in the scene at the point where you want to show this viewController, use this line:
[[NSNotificationCenter defaultCenter] postNotificationName:#"showShareScreen" object:nil];

UIGestureRecognizer - Get the reference to the touched UIViewController Instead of its View?

How do I get a reference to the UIViewController of a touched view?
I am using a UIPanGestureRecognizer on the view of a UIViewController. Here's how I initialize it:
TaskUIViewController *thisTaskController = [[TaskUIViewController alloc]init];
[[self view]addSubview:[thisTaskController view]];
UIPanGestureRecognizer *panRec = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(handlePan:)];
[[thisTaskController view] addGestureRecognizer:panRec];
In the tiggered action triggered using the gesture recognizer I am able to get the view from the parameter using recognizer.view
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
UIView *touchedView = [[UIView alloc]init];
touchedView = (UIView*)[recognizer view];
...
}
However what I really need is the underlying UIViewController of the view touched. How can I get a reference to the UIViewController that contains this view instead of only the UIView?
I would say that it is more a design issue than just getting a reference. So I would follow several simple advises:
Owner should catch events from its view. I.e. TaskUIViewController sould be a target to UIPanGestureRecognizer which you added to its view.
If a controller has a sub-controller and waits from its sub-controller some responses - implement this as delegate.
You have memory leak in your "handlePan:" method.
Here is a skeleton to solve your issue:
#protocol CallbackFromMySubcontroller <NSObject>
- (void)calbackFromTaskUIViewControllerOnPanGesture:(UIViewController*)fromController;
#end
#interface OwnerController : UIViewController <CallbackFromMySubcontroller>
#end
#implementation OwnerController
- (id)init
{
...
TaskUIViewController *thisTaskController = [[TaskUIViewController alloc] init];
...
}
- (void)viewDidLoad
{
...
[self.view addSubview:thisTaskController.view];
...
}
- (void)calbackFromTaskUIViewControllerOnPanGesture:(UIViewController*)fromController
{
NSLog(#"Yahoo. I got an event from my subController's view");
}
#end
#interface TaskUIViewController : UIViewController {
id <CallbackFromMySubcontroller> delegate;
}
#end
#implementation TaskUIViewController
- (id)initWithOwner:(id<CallbackFromMySubcontroller>)owner
{
...
delegate = owner;
...
}
- (void)viewDidLoad
{
UIPanGestureRecognizer *panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.view addGestureRecognizer:panRec];
[panRec release];
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
...
[delegate calbackFromTaskUIViewControllerOnPanGesture:self];
...
}
#end
[touchedView nextResponder] will return the UIViewController object that manages touchedView (if it has one) or touchedView's superview (if it doesn’t have a UIViewController object that manages it).
For more information, see the UIResponder Class Reference. (UIViewController and UIView are subclasses of UIResponder.)
In your case, since you happen to know that touchedView is your viewController's view (and not, for instance, a subview of your viewController's view), you can just use:
TaskUIViewController *touchedController = (TaskUIViewController *)[touchedView nextResponder];
In the more general case, you could work up the responder chain until you find an object of kind UIViewController:
id aNextResponder = [touchedView nextResponder];
while (aNextResponder != nil)
{
if ([aNextResponder isKindOfClass:[UIViewController class]])
{
// we have found the viewController that manages touchedView,
// so we break out of the while loop:
break;
}
else
{
// we have yet to find the managing viewController,
// so we examine the next responder in the responder chain
aNextResponder = [aNextResponder nextResponder];
}
}
// outside the while loop. at this point aNextResponder points to
// touchedView's managing viewController (or nil if it doesn't have one).
UIViewController *eureka = (UIViewController *)aNextResponder;

NSButtonCell hover image, transparent background on click?

I've implemented an NSButton with an image in it.
When the user hovers it, the image changes to something else, and then back on.
Normal Status :
On Hover :
The code I'm using for the NSButtonCell is :
Interface :
#import <Foundation/Foundation.h>
#interface DKHoverButtonCell : NSButtonCell
{
NSImage *_oldImage;
NSImage *hoverImage;
}
#property (retain) NSImage *hoverImage;
#end
Implementation :
#import "DKHoverButtonCell.h"
#interface NSButtonCell()
- (void)_updateMouseTracking;
#end
#implementation DKHoverButtonCell
#synthesize hoverImage;
- (void)mouseEntered:(NSEvent *)event {
if (hoverImage != nil && [hoverImage isValid]) {
_oldImage = [[(NSButton *)[self controlView] image] retain];
[(NSButton *)[self controlView] setImage:hoverImage];
}
}
- (void)mouseExited:(NSEvent *)event {
if (_oldImage != nil && [_oldImage isValid]) {
[(NSButton *)[self controlView] setImage:_oldImage];
[_oldImage release];
_oldImage = nil;
}
}
- (void)_updateMouseTracking {
[super _updateMouseTracking];
if ([self controlView] != nil && [[self controlView] respondsToSelector:#selector(_setMouseTrackingForCell:)]) {
[[self controlView] performSelector:#selector(_setMouseTrackingForCell:) withObject:self];
}
}
- (void)setHoverImage:(NSImage *)newImage {
[newImage retain];
[hoverImage release];
hoverImage = newImage;
[[self controlView] setNeedsDisplay:YES];
}
- (void)dealloc {
[_oldImage release];
[hoverImage release];
[super dealloc];
}
#end
Now, here is the issue :
although the above controls works 100% (with a "X" rounded image, and transparent background"), when the user clicks on it, it displays a "white"-ish background, and not retain my old "transparent" background
How should I go about resolving this?
You need to change button's type to Momentary Change. You can change it in Attributes inspector:
Or change programatically:
[button setButtonType:NSMomentaryChangeButton];
But for your case it should be sufficient to use showsBorderOnlyWhileMouseInside property of NSButtonCell which has been around since the beginning (OS X 10.0). This would show the button's border only when the mouse is hovering inside the button. Combine that with a border style that is filled and light gray, it would be pretty close to the result you've achieved.
There's no subclassing required nor using undocumented API functions (in your case, _updateMouseTracking).
closeButton.bezelStyle = .inline
closeButton.setButtonType(.momentaryPushIn)
if let buttonCell = closeButton.cell as? NSButtonCell {
buttonCell.showsBorderOnlyWhileMouseInside = true
}

UINavigationController: Apply Common Padding/Margin to all Popped View Controller's Views

I have a UINavigationController and I would like the view of every view controller that is popped onto the stack to have a common padding/margin (e.g. 25 pixels on all sides). What is the best way to accomplish this?
I originally thought that I could implement UINavigationControllerDelegate and inside the navigationController:didShowViewController:animated or navigationController:willShowViewController:animated methods, simply change the frame of the view controller that was about to be displayed. This does not seem to have an effect though.
I tried to do the same thing inside the view controller's viewDidAppear and viewWillAppear methods, but this also did not work. Ideally, I don't want to put any logic in the controllers anyway, as they may not always be used inside a navigation controller.
One last idea that I haven't tried yet is to create a "wrapper" UIViewController that would actually get pushed onto this stack. This wrapper would add the real view controller's view as a subview with a frame that would provide the desired margin. The downside here is that I would need to subclass UINavigationController and override pushViewController:animated, where the wrapper would be initialized and pushed. Apple's documentation indicates that UINavigationController is not meant to be subclassed.
Thanks in advance.
I solved this by putting a "wrapper" UIView around the UIViewController's view instead of the UIViewController itself. The wrapper view then pads the subview by setting the subview's frame in the layoutSubviews method.
I've attached the code I used for convenience. To use, replace your UINavigationController with the PaddedNavigationController, and set the PaddedNavigationController's insets property.
PaddedNavigationController.h:
#import <Foundation/Foundation.h>
#interface PaddedNavigationController : UINavigationController
{
UIEdgeInsets _insets;
}
#property (nonatomic, assign) UIEdgeInsets insets;
#end
PaddedNavigationController.m:
#import "PaddedNavigationController.h"
#interface PaddedView : UIView
{
UIView *_view;
UIEdgeInsets _insets;
}
#property (nonatomic, assign) UIEdgeInsets insets;
+ (PaddedView *) wrapView:(UIView *)view withInsets:(UIEdgeInsets)insets;
- (id) initWithView:(UIView *)view insets:(UIEdgeInsets)insets;
#end
#implementation PaddedNavigationController
#synthesize insets = _insets;
- (void) pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
//check if the UIViewController's view has already been wrapped by the PaddedView; don't want to wrap it twice
if(![viewController.view isKindOfClass:[PaddedView class]])
{
viewController.view = [PaddedView wrapView:viewController.view withInsets:self.insets];
}
[super pushViewController:viewController animated:animated];
}
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets;
//loop through this navigation controller's view controllers and set the new insets on any PaddedViews
for(UIViewController *viewController in self.viewControllers)
{
if([viewController.view isKindOfClass:[PaddedView class]])
{
PaddedView *padded = (PaddedView *)viewController.view;
padded.insets = insets;
}
}
}
#end
#implementation PaddedView
#synthesize insets = _insets;
+ (PaddedView *) wrapView:(UIView *)view withInsets:(UIEdgeInsets)insets
{
return [[[PaddedView alloc] initWithView:view insets:insets] autorelease];
}
- (id) initWithView:(UIView *)view insets:(UIEdgeInsets)insets
{
if(self = [super initWithFrame:view.frame])
{
_insets = insets;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_view = [view retain];
[self addSubview:view];
}
return self;
}
- (void) dealloc
{
[_view release];
[super dealloc];
}
- (void) layoutSubviews
{
//apply the insets to the subview
_view.frame = CGRectMake(self.insets.left, self.insets.top, self.frame.size.width - self.insets.left - self.insets.right, self.frame.size.height - self.insets.top - self.insets.bottom);
}
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets;
//we need to re-layout the subviews as the insets have changed
[self layoutSubviews];
}
#end