UIMenuController not responding to first selection, only second - objective-c

I have a view with a long press gesture recognizer in it:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressDetected:)];
[self addGestureRecognizer:longPress];
[longPress release];
}
return self;
}
When the long press is detected, I want to show a UIMenuViewController above the view with a single action in it, and when that menu item is tapped I want to execute a block:
- (void)longPressDetected:(UILongPressGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self becomeFirstResponder];
UIMenuController *menuController = [UIMenuController sharedMenuController];
UIMenuItem *actionItem = [[UIMenuItem alloc] initWithTitle:#"Action" action:#selector(someActionSelector)];
[menuController setMenuItems:[NSArray arrayWithObject:actionItem]];
[actionItem release];
[menuController setTargetRect:self.frame inView:self.superview];
[menuController setMenuVisible:YES animated:YES];
}
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == #selector(copy:) || action == #selector(cut:) || action == #selector(delete:) ||
action == #selector(paste:) || action == #selector(select:) || action == #selector(selectAll:)) {
return NO;
}
else if (action == #selector(someActionSelector)) {
return YES;
}
else {
return [super canPerformAction:action withSender:sender];
}
}
- (void)someActionSelector {
if (self.actionBlock) {
self.actionBlock();
}
}
Problem is, this only works after the second long-press and tap combo. The first time I long-press on the view I see the menu, but tapping the menu does nothing. The second time I see the menu again, I tap it, then the block is executed.
Debugger shows that a breakpoint in someActionSelector is only reached on the second tap. Any idea why this is?

I figured it out. The view listening for a long press is contained inside a view that repositions some subviews when its frame is changed (by overriding setFrame:, which seems like a bad idea but I couldn't think of another way). So when the long press happened it triggered a layoutSubviews in the parent of the parent of the listening view, which set the frame of the parent of the listening view, which repositioned the listening view, which appears to break either the responder chain or deactivate the menu. The solution was to add a condition inside the overridden setFrame: to only trigger the layout if the frame actually changes, which it doesn't on the long press. I'm sure there's a better alternative for listening to frame changes that would avoid this problem entirely—feel free to suggest them in comments.

Related

How do you create a gesture recognizer for a custom cells button in a tableViewController that will be able to identify the cell it is in?

I have a custom cell which has a button that i am needing to recognize whether the user taps or does a long press. I am able to recognize both but the long press gesture only works for the most recent cell made, while the cells prior do nothing when the button gets long pressed.
//here is what i have in my cellForRowAtIndexPath
self.longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:#selector(handleLongPressGestures:)];
longPress.minimumPressDuration = 1.0f;
longPress.allowableMovement = 300.0f;
[cell.button addGestureRecognizer:longPress];
//testing LP
(void)handleLongPressGestures:(UILongPressGestureRecognizer*)sender
{
if([sender isEqual:self.longPress]){
if(sender.state == UIGestureRecognizerStateBegan){
[self performSegueWithIdentifier:#"changeValues" sender:self];
}
}
}
since it only works for the most recent cell made, i have also tried moving the initialization of the longPress properties to the view did load and assigning it to the button on creation of the cell but i still had the same results. If anyone has any insight on doing something like this it would really be appreciated.
This method worked for me and although it does not use gesture recognizer, it does do what i was wanting it to so i though it would post it incase someone else came across the same issue
- (IBAction)numberAvailablePressed:(id)sender {
pressedDown = NO;
}
- (IBAction)numberAvailableTouchedDown:(id)sender {
pressedDown = YES;
[NSTimer scheduledTimerWithTimeInterval:3.0
target:self
selector:#selector(checkIfPressed)
userInfo:nil
repeats:NO];
}
-(void)checkIfPressed
{
if(pressedDown == YES)
{
[self performSegueWithIdentifier:#"changeValues" sender:self];
}
}

Transient NSPopover swallows first click on parent window control

I have a transient NSPopover and when it's open and I click a button in the parent window the popover is dismissed instead and the click "swallowed". Only the second click on the button triggers the action properly.
Is there a way to pass the first click through to the control directly and dismiss the popover in one step?
NSPopover does seem to swallow the event for the targeted position view. Other views are fine. My solution is to get the delegate to forward the last mouse down event to the target view if hit testing reveals it was the clicked view. Unfortunatety NSApp -currentEvent is nil when the delegate gets messaged - not sure why. So I added an event monitor to the app delegate like so:
- (void)addEventMonitor
{
if (self.eventMonitor) {
return;
}
self.eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSLeftMouseDownMask) handler:^(NSEvent *incomingEvent) {
NSEvent *result = incomingEvent;
self.monitoredEvent = result;
return result;
}];
}
When the delegate closes it checks to see it the last monitored event was in the target:
- (void)popoverDidClose:(NSNotification *)notification
{
// [NSApp currentEvent] is nil here
NSEvent *event = [(BPApplicationDelegate *)[NSApp delegate] monitoredEvent];
if (event && (event.type & NSLeftMouseDown)) {
NSPoint pt = [self.targetView.superview convertPoint:event.locationInWindow fromView:nil];
if ([self.TargetView hitTest:pt]) {
[NSApp postEvent:event atStart:NO];
}
}
}

Warning: Attempt to present *** whose view is not in the window hierarchy

I'm receiving this error when I am using an attached long press gesture to get a modal view to come up using the following code:
// Long press to go to settings for one
- (void)longPressOne:(UILongPressGestureRecognizer*)gesture {
[self performSegueWithIdentifier:#"buttonOne" sender:self];
}
// Long press to go to settings for two
- (void)longPressTwo:(UILongPressGestureRecognizer*)gesture {
[self performSegueWithIdentifier:#"buttonTwo" sender:self];
}
- (void)viewDidLoad {
// Add gesture to buttonOne
UILongPressGestureRecognizer *longPressOne = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressOne:)];
[self.buttonOne addGestureRecognizer:longPressOne];
// Add gesture to buttonTwo
UILongPressGestureRecognizer *longPressTwo = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressTwo:)];
[self.buttonTwo addGestureRecognizer:longPressTwo];
}
The modal segue is hitched up on the storyboard from the viewcontroller to the destination view. I know there are reports of this problem when there are multiple segues on the storyboard, but I just have the one as I can't create a segue from the button for a long press on Storyboard.
Any idea why this is happening?
I have fixed this by altering the code for handling the gestures, as below:
// Long press to go to settings for one
- (void)longPressOne:(UILongPressGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateBegan)
{
[self performSegueWithIdentifier:#"buttonOne" sender:self];
}
}
// Long press to go to settings for two
- (void)longPressTwo:(UILongPressGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateBegan)
{
[self performSegueWithIdentifier:#"buttonTwo" sender:self];
}
}
This seems to fix the problem.

How to draw custom window controls (close, minimize, and zoom buttons)

I've made an attempt to draw custom NSButtons, but it seems I'm reinventing the wheel here. Is there a way to just replace the default images used for the close, minimize and zoom buttons?
Several apps already do it:
OSX 10.8's Reminders app (they appear dark grey when the window is not key, vs most appear light grey)
Tweetbot (All buttons look totally custom)
More info:
I can generate the system defaults as such standardWindowButton:NSWindowCloseButton. But from there the setImage setter doesn't change the appearance of the buttons.
Edit: Since I wrote this, INAppStore has implemented a pretty nice way to do this with INWindowButton. If you're looking for a drag and drop solution check there, but the code below will still help you implement your own.
So I couldn't find a way to alter the standardWindowButtons. Here is a walkthrough of how I created my own buttons.
Note: There are 4 states the buttons can be in
Window inactive
Window active - normal
Window active - hover
Window active - press
On to the walkthrough!
Step 1: Hide the pre-existing buttons
NSButton *windowButton = [self standardWindowButton:NSWindowCloseButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowMiniaturizeButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowZoomButton];
[windowButton setHidden:YES];
Step 2: Setup the view in Interface Builder
You'll notice on hover the buttons all change to their hover state, so we need a container view to pick up the hover.
Create a container view to be 54px wide x 16px tall.
Create 3 Square style NSButtons, each 14px wide x 16px tall inside the container view.
Space out the buttons so there is are 6px gaps in-between.
Setup the buttons
In the attributes inspector, set the Image property for each button to the window-active-normal image.
Set the Alternate image property to the window-active-press image.
Turn Bordered off.
Set the Type to Momentary Change.
For each button set the identifier to close,minimize or zoom (Below you'll see how you can use this to make the NSButton subclass simpler)
Step 3: Subclass the container view & buttons
Container:
Create a new file, subclass NSView. Here we are going to use Notification Center to tell the buttons when they should switch to their hover state.
HMTrafficLightButtonsContainer.m
// Tells the view to pick up the hover event
- (void)viewDidMoveToWindow {
[self addTrackingRect:[self bounds]
owner:self
userData:nil
assumeInside:NO];
}
// When the mouse enters/exits we send out these notifications
- (void)mouseEntered:(NSEvent *)theEvent {
[[NSNotificationCenter defaultCenter] postNotificationName:#"HMTrafficButtonMouseEnter" object:self];
}
- (void)mouseExited:(NSEvent *)theEvent {
[[NSNotificationCenter defaultCenter] postNotificationName:#"HMTrafficButtonMouseExit" object:self];
}
Buttons:
Create a new file, this time subclass NSButton. This one's a bit more to explain so I'll just post all the code.
HMTrafficLightButton.m
#implementation HMTrafficLightButton {
NSImage *inactive;
NSImage *active;
NSImage *hover;
NSImage *press;
BOOL activeState;
BOOL hoverState;
BOOL pressedState;
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self setup];
}
return self;
}
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
// Setup images, we use the identifier to chose which image to load
active = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-active",self.identifier]];
hover = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-hover",self.identifier]];
press = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-press",self.identifier]];
inactive = [NSImage imageNamed:#"window-button-all-inactive"];
// Checks to see if window is active or inactive when the `init` is called
if ([self.window isMainWindow] && [[NSApplication sharedApplication] isActive]) {
[self setActiveState];
} else {
[self setInactiveState];
}
// Watch for hover notifications from the container view
// Also watches for notifications for when the window
// becomes/resigns main
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(setActiveState)
name:NSWindowDidBecomeMainNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(setInactiveState)
name:NSWindowDidResignMainNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(hoverIn)
name:#"HMTrafficButtonMouseEnter"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(hoverOut)
name:#"HMTrafficButtonMouseExit"
object:nil];
}
- (void)mouseDown:(NSEvent *)theEvent {
pressedState = YES;
hoverState = NO;
[super mouseDown:theEvent];
}
- (void)mouseUp:(NSEvent *)theEvent {
pressedState = NO;
hoverState = YES;
[super mouseUp:theEvent];
}
- (void)setActiveState {
activeState = YES;
if (hoverState) {
[self setImage:hover];
} else {
[self setImage:active];
}
}
- (void)setInactiveState {
activeState = NO;
[self setImage:inactive];
}
- (void)hoverIn {
hoverState = YES;
[self setImage:hover];
}
- (void)hoverOut {
hoverState = NO;
if (activeState) {
[self setImage:active];
} else {
[self setImage:inactive];
}
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
In IB set the Custom Class of the container view and all 3 buttons to their respective classes that we just created.
Step 4: Set the button actions
These methods, called from the view controller, are the same as the standardWindowButtons'. Link them to the buttons in IB.
- (IBAction)clickCloseButton:(id)sender {
[self.view.window close];
}
- (IBAction)clickMinimizeButton:(id)sender {
[self.view.window miniaturize:sender];
}
- (IBAction)clickZoomButton:(id)sender {
[self.view.window zoom:sender];
}
Step 5: Add the view to the window
I have a separate xib and view controller setup specifically for the window controls. The view controller is called HMWindowControlsController
(HMWindowControlsController*) windowControlsController = [[HMWindowControlsController alloc] initWithNibName:#"WindowControls" bundle:nil];
NSView *windowControlsView = windowControlsController.view;
// Set the position of the window controls, the x is 7 px, the y will
// depend on your titlebar height.
windowControlsView.frame = NSMakeRect(7.0, 10.0, 54.0, 16.0);
// Add to target view
[targetView addSubview:windowControlsView];
Hope this helps. This is a pretty lengthy post, if you think I've made a mistake or left something out please let me know.

UIGestureRecognizer blocks subview for handling touch events

I'm trying to figure out how this is done the right way. I've tried to depict the situation:
I'm adding a UITableView as a subview of a UIView. The UIView responds to a tap- and pinchGestureRecognizer, but when doing so, the tableview stops reacting to those two gestures (it still reacts to swipes).
I've made it work with the following code, but it's obviously not a nice solution and I'm sure there is a better way. This is put in the UIView (the superview):
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if([super hitTest:point withEvent:event] == self) {
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:YES];
}
return self;
}
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:NO];
}
return [self.subviews lastObject];
}
I had a very similar problem and found my solution in this SO question. In summary, set yourself as the delegate for your UIGestureRecognizer and then check the targeted view before allowing your recognizer to process the touch. The relevant delegate method is:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
The blocking of touch events to subviews is the default behaviour. You can change this behaviour:
UITapGestureRecognizer *r = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(agentPickerTapped:)];
r.cancelsTouchesInView = NO;
[agentPicker addGestureRecognizer:r];
I was displaying a dropdown subview that had its own tableview. As a result, the touch.view would sometimes return classes like UITableViewCell. I had to step through the superclass(es) to ensure it was the subclass I thought it was:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
UIView *view = touch.view;
while (view.class != UIView.class) {
// Check if superclass is of type dropdown
if (view.class == dropDown.class) { // dropDown is an ivar; replace with your own
NSLog(#"Is of type dropdown; returning NO");
return NO;
} else {
view = view.superview;
}
}
return YES;
}
Building on #Pin Shih Wang answer. We ignore all taps other than those on the view containing the tap gesture recognizer. All taps are forwarded to the view hierarchy as normal as we've set tapGestureRecognizer.cancelsTouchesInView = false. Here is the code in Swift3/4:
func ensureBackgroundTapDismissesKeyboard() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handleTap(recognizer: UIGestureRecognizer) {
let location = recognizer.location(in: self.view)
let hitTestView = self.view.hitTest(location, with: UIEvent())
if hitTestView?.gestureRecognizers?.contains(recognizer) == .some(true) {
// I dismiss the keyboard on a tap on the scroll view
// REPLACE with own logic
self.view.endEditing(true)
}
}
One possibility is to subclass your gesture recognizer (if you haven't already) and override -touchesBegan:withEvent: such that it determines whether each touch began in an excluded subview and calls -ignoreTouch:forEvent: for that touch if it did.
Obviously, you'll also need to add a property to keep track of the excluded subview, or perhaps better, an array of excluded subviews.
It is possible to do without inherit any class.
you can check gestureRecognizers in gesture's callback selector
if view.gestureRecognizers not contains your gestureRecognizer,just ignore it
for example
- (void)viewDidLoad
{
UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
singleTapGesture.numberOfTapsRequired = 1;
}
check view.gestureRecognizers here
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer
{
UIEvent *event = [[UIEvent alloc] init];
CGPoint location = [gestureRecognizer locationInView:self.view];
//check actually view you hit via hitTest
UIView *view = [self.view hitTest:location withEvent:event];
if ([view.gestureRecognizers containsObject:gestureRecognizer]) {
//your UIView
//do something
}
else {
//your UITableView or some thing else...
//ignore
}
}
I created a UIGestureRecognizer subclass designed for blocking all gesture recognizers attached to a superviews of a specific view.
It's part of my WEPopover project. You can find it here.
implement a delegate for all the recognizers of the parentView and put the gestureRecognizer method in the delegate that is responsible for simultaneous triggering of recognizers:
func gestureRecognizer(UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer:UIGestureRecognizer) -> Bool {
if (otherGestureRecognizer.view.isDescendantOfView(gestureRecognizer.view)) {
return true
} else {
return false
}
}
U can use the fail methods if u want to make the children be triggered but not the parent recognizers:
https://developer.apple.com/reference/uikit/uigesturerecognizerdelegate
I was also doing a popover and this is how I did it
func didTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.locationInView(tableView)
if let _ = tableView.indexPathForRowAtPoint(tapLocation) {
sender.cancelsTouchesInView = false
}
else {
delegate?.menuDimissed()
}
}
You can turn it off and on.... in my code i did something like this as i needed to turn it off when the keyboard was not showing, you can apply it to your situation:
call this is viewdidload etc:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(notifyShowKeyboard:) name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:#selector(notifyHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];
then create the two methods:
-(void) notifyShowKeyboard:(NSNotification *)inNotification
{
tap.enabled=true; // turn the gesture on
}
-(void) notifyHideKeyboard:(NSNotification *)inNotification
{
tap.enabled=false; //turn the gesture off so it wont consume the touch event
}
What this does is disables the tap. I had to turn tap into a instance variable and release it in dealloc though.