I've a NSButton with both an Image and Alternate Image. I would like the alternate image to be shown on hover. To solve this, I've extended the NSButton to show the alternate image when hovering the view. Is there a better solution to this?
My solution:
#interface HoverButton()
#property (nonatomic, strong) NSTrackingArea *trackingArea;
#property (nonatomic, strong) NSImage *imageTmp;
#end
#implementation HoverButton
-(void)mouseEntered:(NSEvent *)theEvent {
[super mouseEntered:theEvent];
[self updateImages];
self.image = self.alternateImage;
}
-(void)mouseExited:(NSEvent *)theEvent {
[super mouseExited:theEvent];
self.image = self.imageTmp;
}
- (void)updateImages {
self.imageTmp = self.image;
}
-(void)updateTrackingAreas
{
if(self.trackingArea != nil) {
[self removeTrackingArea:self.trackingArea];
}
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
self.trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:self.trackingArea];
}
#end
I would say this is better suited to an NSButtonCell subclass. you can do it in one method and it won't apply to all NSButtons (I doubt thats what you actually want). Just set your button cell type in IB to your custom subclass.
here is some code I just wrote and tested that works:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
if (_alternateImageOrKeyEquivalentFont && _bcFlags2.mouseInside) {
// the draw bezel call is optional. maybe you don't want it
[self drawBezelWithFrame:cellFrame inView:controlView];
[self drawImage:_alternateImageOrKeyEquivalentFont
withFrame:cellFrame
inView:controlView];
} else {
[super drawInteriorWithFrame:cellFrame
inView:controlView];
}
}
you will need to set the showsBorderOnlyWhileMouseInside to YES probably in an init method for the cell.
CustomButton.h
#interface CustomButton : NSButton
#property (getter=isMouseInside) BOOL mouseInside;
#end
CustomButton.m
#implementation CustomButton
+ (Class)cellClass
{
return [CustomButtonCell class];
}
- (instancetype)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self commonCustomButtonInit];
}
return self;
}
- (void)commonCustomButtonInit
{
NSTrackingArea *trackingArea = nil;
trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
- (void)mouseEntered:(NSEvent *)event
{
self.mouseInside = YES;
if ([self.cell isKindOfClass:[CustomButtonCell class]])
{
CustomButtonCell *cell = self.cell;
cell.mouseInside = YES;
}
}
-(void)mouseExited:(NSEvent *)event
{
self.mouseInside = NO;
if ([self.cell isKindOfClass:[CustomButtonCell class]])
{
CustomButtonCell *cell = self.cell;
cell.mouseInside = NO;
}
}
#end
CustomButtonCell.h
#interface CustomButtonCell : NSButtonCell
#property (getter=isMouseInside) BOOL mouseInside;
#end
CustomButtonCell.m
#implementation CustomButtonCell
#end
Also see this answer.
Okay, here is the problem:
I have a NSTextView and I add my custom NSButton using:
[_textView addSubview:button];
Then, inside my NSButton subclass, I have (along with the NSTrackingArea stuff):
- (void)mouseEntered:(NSEvent *)event{
[[NSCursor arrowCursor] set];
}
- (void)mouseExited:(NSEvent *)theEvent{
[[NSCursor arrowCursor] set];
}
- (void)mouseDown:(NSEvent *)theEvent{
[[NSCursor arrowCursor] set];
}
- (void)mouseUp:(NSEvent *)theEvent{
[[NSCursor arrowCursor] set];
}
But when I hover it, the cursor remains the same IBeamCursor (because it's a NSTextView). Only when I press the button, the cursor gets updated. And then, when I move the mouse, still inside the button, the cursor goes back to the IBeamCursor.
Any ideas on how to do this? Thank you!
Adding a tracking area that only tracks enter/exit events seems to be not enough for NSTextView subviews. Somehow the textview always wins and sets it's IBeamCursor.
You can try to always enable tracking for mouse move events (NSTrackingMouseMoved) when adding the tracking area in your NSButton subclass:
#import "SSWHoverButton.h"
#interface SSWHoverButton()
{
NSTrackingArea* trackingArea;
}
#end
#implementation SSWHoverButton
- (void)mouseMoved:(NSEvent*)theEvent
{
[[NSCursor arrowCursor] set];
}
- (void)updateTrackingAreas
{
if(trackingArea != nil)
{
[self removeTrackingArea:trackingArea];
}
NSTrackingAreaOptions opts = (NSTrackingMouseMoved|NSTrackingActiveAlways);
trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
- (void)dealloc
{
[self removeTrackingArea:trackingArea];
}
#end
Swift 5 variant:
import Cocoa
class InsideTextButton: NSButton {
var trackingArea: NSTrackingArea?
override func mouseMoved(with event: NSEvent) {
NSCursor.arrow.set()
}
override func updateTrackingAreas() {
if let area = trackingArea {
removeTrackingArea(area)
}
trackingArea = NSTrackingArea(rect: self.bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil)
if let area = trackingArea {
addTrackingArea(area)
}
}
deinit {
if let area = trackingArea {
removeTrackingArea(area)
}
}
}
I would use a UILabel to allow users to select a date with UIDatePicker.
To do this, I created an UILabel subclass overwriting the inputView and the inputAccessoryView properties making them writable; I also implemented the -(BOOL) canBecomeFirstResponder and the -(BOOL) isUserInteractionEnabled methods returning YES for both.
Then I assigned an instance of UIDatePIcker to the inputView property.
At this point my expectation is that when the label is tapped an UIDatePicker should appear, but nothing happens.
Any help?
This is the code:
YPInteractiveUILabel.h
#interface YPInteractiveUILabel : UILabel
#property (readwrite) UIView *inputView;
#property (readwrite) UIView *inputAccessoryView;
- (BOOL) canBecomeFirstResponder;
- (BOOL) isUserInteractionEnabled;
#end
YPInteractiveUILabel.h
#import "YPInteractiveUILabel.h"
#implementation YPInteractiveUILabel
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
UIDatePicker *datePicker = [[UIDatePicker alloc] init];
[self setInputView:datePicker];
}
return self;
}
- (BOOL)isUserInteractionEnabled
{
return YES;
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
#end
UILabel + UIDatePicker -- Swift version with Done button.
import UIKit
class DatePickerLabel: UILabel {
private let _inputView: UIView? = {
let picker = UIDatePicker()
return picker
}()
private let _inputAccessoryToolbar: UIToolbar = {
let toolBar = UIToolbar()
toolBar.barStyle = UIBarStyle.Default
toolBar.translucent = true
toolBar.sizeToFit()
return toolBar
}()
override var inputView: UIView? {
return _inputView
}
override var inputAccessoryView: UIView? {
return _inputAccessoryToolbar
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let doneButton = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.Plain, target: self, action: #selector(doneClick))
let spaceButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FlexibleSpace, target: nil, action: nil)
_inputAccessoryToolbar.setItems([ spaceButton, doneButton], animated: false)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(launchPicker))
self.addGestureRecognizer(tapRecognizer)
}
override func canBecomeFirstResponder() -> Bool {
return true
}
#objc private func launchPicker() {
becomeFirstResponder()
}
#objc private func doneClick() {
resignFirstResponder()
}
}
You can just ovveride inputView getter method, like explained in Apple documentation:
- (UIView *)inputView {
return myInputView;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
Then add a gesture or a button to call becomeFirstResponder:
- (void)showInputView:(id)sender {
[self becomeFirstResponder];
}
How about something like this. Rather than subclass the label, just add a gesture recognizer to it, and bring up the picker in the tap recognizer's handler. In the picker's action method, populate the label and dismiss the picker. This example works, but you'd probably want to add some animation to make it look better:
- (void)viewDidLoad {
[super viewDidLoad];
self.label.userInteractionEnabled = YES;
UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(launchPicker:)];
[self.label addGestureRecognizer:tapper];
}
-(void)launchPicker:(UITapGestureRecognizer *) tapper {
UIDatePicker *picker = [[UIDatePicker alloc] initWithFrame:CGRectMake(5, 150, 300, 200)];
[picker addTarget:self action:#selector(updateLabel:) forControlEvents:UIControlEventValueChanged];
[self.view addSubview:picker];
}
-(IBAction)updateLabel:(UIDatePicker *)sender {
self.label.text = [NSString stringWithFormat:#"%#",sender.date ];
[sender removeFromSuperview];
}
Thanks to the suggestions (especially the comment from NeverBe and the answer proposed by rdelmar) I found the problem in my code.
In brief, in order to show the input label, a call to the becomeFirstResponder method when the user tap the label is needed.
Following the UILabel subclass implementation corrected (the header file remains the same):
#implementation YPInteractiveUILabel
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
UIDatePicker *datePicker = [[UIDatePicker alloc] init];
[self setInputView:datePicker];
UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(launchPicker:)];
[self addGestureRecognizer:tapper];
}
return self;
}
- (BOOL)isUserInteractionEnabled
{
return YES;
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
-(void)launchPicker:(UITapGestureRecognizer *) tapper
{
[self becomeFirstResponder];
}
#end
This is a code snipet of #dmitriy-kirakosyan updated to Swift 5
class DatePickerLabel: UILabel {
private let _inputView: UIView? = {
let picker = UIDatePicker()
return picker
}()
private let _inputAccessoryToolbar: UIToolbar = {
let toolBar = UIToolbar()
toolBar.barStyle = UIBarStyle.default
toolBar.isTranslucent = true
toolBar.sizeToFit()
return toolBar
}()
override var inputView: UIView? {
return _inputView
}
override var inputAccessoryView: UIView? {
return _inputAccessoryToolbar
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let doneButton = UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.plain, target: self, action: #selector(doneClick))
let spaceButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
_inputAccessoryToolbar.setItems([ spaceButton, doneButton], animated: false)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(launchPicker))
self.addGestureRecognizer(tapRecognizer)
}
override var canBecomeFirstResponder: Bool {
return true
}
#objc private func launchPicker() {
becomeFirstResponder()
}
#objc private func doneClick() {
resignFirstResponder()
}
}
I know this is an old question but this might still be useful to someone.
There is another way to solve this - there is no need to complicate things with gesture recognizers...
GTPDateLabel.h
#interface GTPDateLabel : UILabel
#property (readonly, retain) UIView *inputView;
#end
GTPDateLabel.m
#import "GTPDateLabel.h"
#implementation GTPDateLabel
#synthesize inputView = _inputView;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.userInteractionEnabled = YES;
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
self.userInteractionEnabled = YES;
}
return self;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
[self becomeFirstResponder];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
-(UIView *)inputView
{
if (!_inputView)
{
UIDatePicker *datePicker = [[UIDatePicker alloc] init];
_inputView = datePicker;
}
return _inputView;
}
#end
Note that you should also set the delegate and in case of custom UIPicker also dataSource...
Here is the UILabel that shows PickerView, in Swift 4
final class DatePickerLabel: UILabel {
private let pickerView: UIPickerView
private let toolbar: UIToolbar
required init(pickerView: UIPickerView, toolbar: UIToolbar) {
self.pickerView = pickerView
self.toolbar = toolbar
super.init(frame: .zero)
let recogniser = UITapGestureRecognizer(target: self, action: #selector(tapped))
addGestureRecognizer(recogniser)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override var inputView: UIView? {
return pickerView
}
override var inputAccessoryView: UIView? {
return toolbar
}
override var canBecomeFirstResponder: Bool {
return true
}
#objc private func tapped() {
becomeFirstResponder()
}
}
This is my situation. I have a viewController. With at the top three buttons and below the buttons a containerview. When you push a button, the container view should change.
This is what I have done in storyboard.
Dragged a view controller into my storyboard. added 3 buttons and a containerview into it.
This view controller is off class viewController
Dragged a second viewcontroller into it. And controlled dragged from the containerView to this view controller. and selected embed segue.
Now in code I do the following for controller viewController
-(IBAction)chooseFirstController:(id)sender {
[self.childViewControllers.lastObject switchToFirst];
}
-(IBAction)chooseSecondController:(id)sender {
[self.childViewControllers.lastObject switchToSecond];
}
-(IBAction)chooseThirdController:(id)sender {
[self.childViewControllers.lastObject switchToThird];
}
And for my containerController.h I do the following.
#interface ContainerViewController : ViewController
#property (nonatomic,strong) FirstController *cont1;
#property (nonatomic,strong) SecondController *cont2;
#property (nonatomic,strong) ThirdController *cont3;
#property (nonatomic,strong) ContainerViewController *currentController;
And in my container.m
- (void)viewDidLoad
{
[super viewDidLoad];
self.cont1 = [[FirstController alloc]initWithNibName:#"FirstController" bundle:nil];
self.cont2 = [[SecondController alloc]initWithNibName:#"SecondController" bundle:nil];
self.cont3 = [[ThirdController alloc]initWithNibName:#"ThirdViewController" bundle:nil];
[self addChildViewController:self.cont1];
self.currentController = self.cont1;
[self.view addSubview:self.cont1.view];
}
-(void)switchToFirst {
if (self.currentController != self.cont1) {
[self addChildViewController:self.cont1];
[self moveToNewController:self.cont1];
}
}
-(void)switchToSecond {
if (self.currentController != self.cont2) {
[self addChildViewController:self.cont2];
[self moveToNewController:self.cont2];
}
}
-(void)switchToThird {
if (self.currentController != self.cont3) {
[self addChildViewController:self.cont3];
[self moveToNewController:self.cont3];
}
}
-(void)moveToNewController:(id) newController {
[self.currentController willMoveToParentViewController:nil];
[self transitionFromViewController:self.currentController toViewController:newController duration:.6 options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{}
completion:^(BOOL finished) {
[self.currentController removeFromParentViewController];
[newController didMoveToParentViewController:self];
self.currentController = newController;
}];
}
But I keep getting error "No known instance method for selector switchtoFirst" in my IBActions.
Can anybody help me?
Thanks in advance.
I would guess that lastObject is returning a UIViewController and doesn't know it's a ContainerViewController - hence doesn't know about the switchtoFirst method.
Try something like:
-(IBAction)chooseFirstController:(id)sender {
UIViewController *vc = self.childViewControllers.lastObject;
if([vc isKindOfClass:[ContainerViewController class]]) {
ContainerViewController *containerVC = (ContainerViewController *)vc;
[containerVC switchtoFirst];
}
}
I want to implement iAd in my viewController so that i created the singleton class for iAd here the code that i used but it doesnt display iAd in my viewController.
adWhirlSingleton.h
#import <Foundation/Foundation.h>
#import "iAd/ADBannerView.h"
#interface adWhirlSingleton : NSObject <ADBannerViewDelegate> {
ADBannerView *adView;
UIViewController *displayVC;
}
#property (strong, nonatomic) ADBannerView *adView;
#property (strong, nonatomic) UIViewController *displayVC;
+(id)sharedAdSingleton;
-(void)adjustAdSize:(CGFloat)x:(CGFloat)y;
#end
adWhirlSingleton.m
#import "adWhirlSingleton.h"
#implementation adWhirlSingleton
static adWhirlSingleton* _sharedAdSingleton = nil;
#synthesize adView, displayVC;
+(id)sharedAdSingleton
{
#synchronized(self)
{
if(!_sharedAdSingleton)
_sharedAdSingleton = [[self alloc] init];
return _sharedAdSingleton;
}
return nil;
}
+(id)alloc
{
#synchronized([adWhirlSingleton class])
{
NSAssert(_sharedAdSingleton == nil, #"Attempted to allocate a second instance of a singleton.");
_sharedAdSingleton = [super alloc];
return _sharedAdSingleton;
}
return nil;
}
-(id)init
{
self = [super init];
if (self != nil) {
// initialize stuff here
self.adView.delegate=self;
}
return self;
}
-(void)dealloc
{
displayVC = nil;
if (adView) {
[adView removeFromSuperview]; //Remove ad view from superview
[adView setDelegate:nil];
adView = nil;
}
[super dealloc];
}
-(void)adjustAdSize:(CGFloat)x :(CGFloat)y
{
[UIView beginAnimations:#"AdResize" context:nil];
[UIView setAnimationDuration:0.7];
adView.frame = CGRectMake(x, y, 320, 50);
[UIView commitAnimations];
}
-(BOOL)adWhirlTestMode
{
return YES;
}
-(NSString *)adWhirlApplicationKey
{
return #"xxxxxxxxxxxxx";
}
-(UIViewController *)viewControllerForPresentingModalView
{
return displayVC;
}
-(void)bannerViewDidLoadAd:(ADBannerView *)banner {
[self adjustAdSize:0 :410];
}
#end
myViewController.m
#import "adWhirlSingleton.h"
-(void)viewWillAppear:(BOOL)animated {
adWhirlSingleton *adWhirlSingle = [adWhirlSingleton sharedAdSingleton];
adWhirlSingle.displayVC = self;
[adWhirlSingle adjustAdSize:0 :self.view.frame.size.height -50];
[self.view addSubview:adWhirlSingle.adView];
[self.view bringSubviewToFront:adWhirlSingle.adView];
NSLog(#"Ad Banner View");
}
This is how i implemented the singleton class for iAd when i excute this i didnt get iAd displayed over my ViewController.
If anyone know how to implement this please help me to get out of this issue. Thanks in Advance.