I'd like to create a NSStatusItem that displays a progress spinner. My idea was to subclass NSProgressIndicator and use this as a NSView to pass to setView:.
// SpinnerView.h
//#import <Cocoa/Cocoa.h>
#interface SpinnerView : NSProgressIndicator {
NSStatusItem *_statusItem;
BOOL _isHighlighted;
}
- (id)initWithStatusItem:(NSStatusItem *)statusItem;
#end
// SpinnerView.m
#import "SpinnerView.h"
#implementation SpinnerView
- (id)initWithStatusItem:(NSStatusItem *)statusItem {
CGFloat thickness = [[NSStatusBar systemStatusBar] thickness];
NSRect frameRect = NSMakeRect(0.0, 0.0, thickness, thickness);
self = [super initWithFrame:frameRect];
[self setStyle:NSProgressIndicatorSpinningStyle];
[self setControlSize:NSSmallControlSize];
_statusItem = statusItem;
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
NSLog(#"drawRect");
[_statusItem drawStatusBarBackgroundInRect:dirtyRect withHighlight:_isHighlighted];
[super drawRect:dirtyRect];
}
#end
However, when I do this, the spinner is displayed but is surrounded by a white box:
Any idea why this happens, or how to fix it? If I comment out the entire drawRect: method the result is the same.
You need to just continually set the image for the status item.
Use a timer.
Related
I'm guessing I don't know enough about Quartz or CAShapeLayer manipulations, but I wanted to know how to change the fill color to custom UIView that I have.
Here's the implementation for DotView:
#define kCustomBlue [UIColor colorWithRed:181.0/255 green:228.0/255 blue:226.0/255 alpha:1]
#implementation DotView
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.backgroundColor = [UIColor clearColor];
self.opaque = YES;
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder
{
if ((self = [super initWithCoder:coder])) {
self.backgroundColor = [UIColor clearColor];
self.opaque = YES;
}
return self;
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
[circleLayer setPath:[[UIBezierPath bezierPathWithOvalInRect:rect] CGPath]];
circleLayer.fillColor = kCustomBlue.CGColor;
[self.layer addSublayer:circleLayer];
}
#end
After placing a DotView object ((nonatomic, weak) IBOutlet in my ViewController interface) in my Storyboard, I run the code and everything is fine and dandy. That object in my ViewController interface is called dot1
I want to have a property #property (nonatomic, strong) UIColor* fillingColor; which sets the fill color for the view. How do I implement that correctly?
The idea is this: there is a tap gesture recognizer object attached to the dot1 view, and everytime I tap the dot, the color changes from blue to black (then black to blue).
I'm using XCode 9.3 and have an iPhone 7 running iOS 11.2
Thanks, Anthony
Add the property to the .h:
#interface DotView: UIView
// Add this to everything else you have
#property (nonatomic, strong) UIColor* fillingColor;
#end
Then in the .m, override the setter:
- (void)setFillingColor:(UIColor *)color {
_fillingColor = color;
[self setNeedsDisplay];
}
Then in drawRect:, use your fillingColor property for the fill color:
circleLayer.fillColor = self.fillingColor.CGColor;
Please note that you do not want to be adding layers over and over every time drawRect: is called. You don't even need to use layers for this. Just fill the UIBezierPath.
I am trying to understand delegation. I have written a small project to try to tackle this. I have also had help from S.O. I am stuck on the very last part of it. My project is simple. We have a main view controller that has a button "start". This button triggers a container view that's hooked to a ContainerViewController. I have done a small animation to get the container to slide from the side. I have another button "back" that makes the container view disappear with the opposite animation. Note, I am copying a lot of code and making up the rest as I am learning, so there may be unnecessary lines, please feel free to comment.
ViewController.h
#import <UIKit/UIKit.h>
#import "ContainerViewController.h"
#interface ViewController : UIViewController <ContainerViewControllerDelegate>
- (IBAction)Start:(id)sender;
- (IBAction)back:(id)sender;
#end
Here is the m file:
#import "ViewController.h"
#interface ViewController ()
#property UIViewController *childView;
#property NSString *myReceivedValue;
#property ContainerViewController *controller;
#property IBOutlet UILabel *myLabel;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.childView = [self.storyboard instantiateViewControllerWithIdentifier:#"childVC"];
self.controller = [self.storyboard instantiateViewControllerWithIdentifier:#"childVC"];
self.controller.delegate = self;
self.childView = [self.childViewControllers lastObject];
[self.childView.view removeFromSuperview];
[self.childView removeFromParentViewController];
self.childView.view.userInteractionEnabled = NO;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)Start:(id)sender {
self.childView.view.frame = CGRectMake(0, 84, 320, 210);
[self.childView didMoveToParentViewController:self];
CATransition *transition = [CATransition animation];
transition.duration = 1;
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
[transition setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[self.childView.view.layer addAnimation:transition forKey:nil];
[self.view addSubview:self.childView.view];
self.childView.view.userInteractionEnabled = YES;
}
- (IBAction)back:(id)sender {
[self.childView willMoveToParentViewController:nil];
[UIView animateWithDuration:1
delay:0.0
usingSpringWithDamping:1
initialSpringVelocity:1
options:UIViewAnimationOptionCurveEaseIn
animations:^{
self.childView.view.frame = CGRectMake(-320, 84, 320, 210);
} completion:^(BOOL complete){
[self.childView removeFromParentViewController];
}];
}
- (void) passValue:(NSString *) theValue
{
// here is where you receive the data
}
#end
Ok, so the Container View has a pickerView of which it is the delegate and this pickerView has just an array of ten colors to chose from:
h file for the container view:
#import <UIKit/UIKit.h>
#protocol ContainerViewControllerDelegate;
#interface ContainerViewController : UIViewController <UIPickerViewDelegate>
#property NSArray *colors;
#property (weak)id <ContainerViewControllerDelegate> delegate;
#property (weak, nonatomic) IBOutlet UIPickerView *myPickerView;
- (IBAction)chosenCol:(id)sender;
#end
#protocol ContainerViewControllerDelegate <NSObject>
- (void) passValue:(NSString *) theValue;
#end
m file for the container view:
#import "ContainerViewController.h"
#interface ContainerViewController ()
#property NSString *selValue;
#end
#implementation ContainerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.colors = [[NSArray alloc] initWithObjects:#"blue", #"red", #"green", #"purple", #"black", #"white", #"orange", #"yellow", #"pink", #"violet", nil];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return 10;
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
return self.colors[row];
}
- (IBAction)chosenCol:(id)sender {
[self.delegate passValue:[self.colors objectAtIndex:[self.myPickerView selectedRowInComponent:0]]];
}
#end
Here is a picture of what it looks like. Note that the "chosen" button is just a provisional one that I put there to make sure everything is hooked alright and that I can log out the chosen color using a button in the container. What I want to do is be able to pass that color to the parent view controller. So that after I dismiss the container with its picker view I have the data stored of what color was chosen. I have had help from someone in S.O. and he's done a very good job of helping me start this up. The only thing I didn't understand is what happens when I receive the data, at the end of the m file of the parent:
- (void) passValue:(NSString *) theValue
{
// here is where you receive the data
}
This is obvioulsy noob question but I really do need it spelt out. How do I actually access the data in the parent. I asked in the comments section, and the reply was (I am changing the class, it was originally uicolor):
"No, you'll receive the data inside the method - (void) passValue:(NSString *) theValue; Put a breakpoint in that method to be sure that it's working, you can access it like this: NSString *myReceivedColor = theValue;
I tried to write word for word "NSString *myReceivedColor = theValue;" but "theValue" is unrecognised.
Ultimately, what I want, is to pass the data back to the parent so that when I hit the button "back", in the parent, the label "you chose" is updated with the chosen color".
I have never touched delegation before so I am lost. Can a charitable soul take the time to explain this last bit in very obvious terms? many thanks
UPDATE-----------------------------------------------------------------------
So, what I am looking at, is to add, at the end of my method for the "back" button,
- (IBAction)back:(id)sender {
[self.childView willMoveToParentViewController:nil];
[UIView animateWithDuration:1
delay:0.0
usingSpringWithDamping:1
initialSpringVelocity:1
options:UIViewAnimationOptionCurveEaseIn
animations:^{
self.childView.view.frame = CGRectMake(-320, 84, 320, 210);
} completion:^(BOOL complete){
[self.childView removeFromParentViewController];
}];
the couple of lines:
self.myReceivedValue = theValue;
self.myLabel.text = self.myReceivedValue;
}
To be able to update the text of myLabel to the the color I've chosen in the view container. It comes back with the error: "use of undeclared identifier "theValue". This is all new to me so I am just copying what people have said on S.O. with the hope of understanding eventually. What am I doing wrong here? tx
It looks like your delegate is nil.
self.childView = [self.storyboard instantiateViewControllerWithIdentifier:#"childVC"];
self.controller = [self.storyboard instantiateViewControllerWithIdentifier:#"childVC"];
self.controller.delegate = self;
You create two instances of "childVC" (a copy/paste typo maybe?) then set the delegate on 'controller' but you use 'childView'. Just change the childView property to be a ContainerViewController and set self.childView.delegate=self.
(BTW its an easy mistake, so many times when you're thinking "why isn't this working??" check that the delegate property is set)
EDIT
The return value property you're logging is nil b/c you never set it. You have to implement the delegate method, i.e.
-(void) passValue:(nsstring*)theValue
{
self.receivedValue = theValue
}
Also what i was saying about the chosenCol action is that is where you are calling your delegate - your 'back' action does not call this method.
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.
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
}
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