iOS - forward all touches through a view - events

I have a view overlayed on top of many other views. I am only using the overaly to detect some number of touches on the screen, but other than that I don't want the view to stop the behavior of other views underneath, which are scrollviews, etc. How can I forward all the touches through this overlay view? It is a subclass of UIView.

Disabling user interaction was all I needed!
Objective-C:
myWebView.userInteractionEnabled = NO;
Swift:
myWebView.isUserInteractionEnabled = false

For passing touches from an overlay view to the views underneath, implement the following method in the UIView:
Objective-C:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(#"Passing all touches to the next view (if any), in the view stack.");
return NO;
}
Swift 5:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("Passing all touches to the next view (if any), in the view stack.")
return false
}

This is an old thread, but it came up on a search, so I thought I'd add my 2c. I have a covering UIView with subviews, and only want to intercept the touches that hit one of the subviews, so I modified PixelCloudSt's answer to:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView* subview in self.subviews ) {
if ( [subview hitTest:[self convertPoint:point toView:subview] withEvent:event] != nil ) {
return YES;
}
}
return NO;
}

Improved version of #fresidue answer. You can use this UIView subclass as transparent view passing touches outside its subview. Implementation in Objective-C:
#interface PassthroughView : UIView
#end
#implementation PassthroughView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *view in self.subviews) {
if (!view.hidden && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}
#end
.
and in Swift:
class PassthroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
}
TIP:
Say then you have a large "holder" panel, perhaps with a table view behind. You make the "holder" panel PassthroughView. It will now work, you can scroll the table "through" the "holder".
But!
On top of the "holder" panel you have some labels or icons. Don't forget, of course those must simply be marked user interaction enabled OFF!
On top of the "holder" panel you have some buttons. Don't forget, of course those must simply be marked user interaction enabled ON!
Note that somewhat confusingly, the "holder" itself - the view you use PassthroughView on - must be marked user interaction enabled ON! That's ON!! (Otherwise, the code in PassthroughView simply will never be called.)

I needed to pass touches through a UIStackView. A UIView inside was transparent, but the UIStackView consumed all touches. This worked for me:
class PassThrouStackView: UIStackView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self {
return nil
}
return view
}
}
All arrangedSubviews still receive touches, but touches on the UIStackView itself went through to the view below (for me a mapView).

I had a similar issue with a UIStackView (but could be any other view).
My configuration was the following:
It's a classical case where I have a container that needed to be placed in the background, with buttons on the side. For layout purposes, I included the buttons in a UIStackView, but now the middle (empty) part of the stackView intercepts touches :-(
What I did is create a subclass of UIStackView with a property defining the subView that should be touchable.
Now, any touch on the side buttons (included in the * viewsWithActiveTouch* array) will be given to the buttons, while any touch on the stackview anywhere else than these views won't be intercepted, and therefore passed to whatever is below the stack view.
/** Subclass of UIStackView that does not accept touches, except for specific subviews given in the viewsWithActiveTouch array */
class NoTouchStackView: UIStackView {
var viewsWithActiveTouch: [UIView]?
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if let activeViews = viewsWithActiveTouch {
for view in activeViews {
if CGRectContainsPoint(view.frame, point) {
return view
}
}
}
return nil
}
}

If the view you want to forward the touches to doesn't happen to be a subview / superview, you can set up a custom property in your UIView subclass like so:
#interface SomeViewSubclass : UIView {
id forwardableTouchee;
}
#property (retain) id forwardableTouchee;
Make sure to synthesize it in your .m:
#synthesize forwardableTouchee;
And then include the following in any of your UIResponder methods such as:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.forwardableTouchee touchesBegan:touches withEvent:event];
}
Wherever you instantiate your UIView, set the forwardableTouchee property to whatever view you'd like the events to be forwarded to:
SomeViewSubclass *view = [[[SomeViewSubclass alloc] initWithFrame:someRect] autorelease];
view.forwardableTouchee = someOtherView;

In Swift 5
class ThroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let slideView = subviews.first else {
return false
}
return slideView.hitTest(convert(point, to: slideView), with: event) != nil
}
}

Looks like even thou its quite a lot of answers here, there is no one clean in swift that I needed.
So I took answer from #fresidue here and converted it to swift as it's what now mostly developers want to use here.
It solved my problem where I have some transparent toolbar with button but I want toolbar to be invisible to user and touch events should go through.
isUserInteractionEnabled = false as some stated is not an option based on my testing.
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subview in subviews {
if subview.hitTest(convert(point, to: subview), with: event) != nil {
return true
}
}
return false
}

I had couple of labels inside StackView and I didn't have much success with the solutions above, instead I solved my problem using below code:
let item = ResponsiveLabel()
// Configure label
stackView.addArrangedSubview(item)
Subclassing UIStackView:
class PassThrouStackView:UIStackView{
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.arrangedSubviews {
let convertedPoint = convert(point, to: subview)
let labelPoint = subview.point(inside: convertedPoint, with: event)
if (labelPoint){
return subview
}
}
return nil
}
}
Then you could do something like:
class ResponsiveLabel:UILabel{
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Respond to touch
}
}

Try something like this...
for (UIView *view in subviews)
[view touchesBegan:touches withEvent:event];
The code above, in your touchesBegan method for example would pass the touches to all of the subviews of view.

The situation I was trying to do was build a control panel using controls inside nested UIStackView’s. Some of the controls had UITextField’s others with UIButton’s. Also, there were labels to identify the controls. What I wanted to do was put a big “invisible” button behind the control panel so that if a user tapped on an area outside a button or text field, that I could then catch that and take action - primarily dismiss any keyboard if a text field was active (resignFirstResponder). However, tapping on a label or other blank area in the control panel would not pass things through. The above discussions were helpful in coming up with my answer below.
Basically, I sub-classed UIStackView and overwrote the “point(inside:with) routine to look for the type of controls that needed the touch and “ignore” things like labels that I wanted to ignore. It also checks for inside UIStackView’s so that things can recurse into the control panel structure.
The code is a perhaps a little more verbose than it should be. But it was helpful in debugging and hopefully provides more clarity in what the routine is doing. Just be sure in Interface Builder to change the class of the UIStackView's to PassThruStack.
class PassThruStack: UIStackView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for view in self.subviews {
if !view.isHidden {
let isStack = view is UIStackView
let isButton = view is UIButton
let isText = view is UITextField
if isStack || isButton || isText {
let pointInside = view.point(inside: self.convert(point, to: view), with: event)
if pointInside {
return true
}
}
}
}
return false
}
}

As suggested by #PixelCloudStv if you want to throw touched from one view to another but with some additional control over this process - subclass UIView
//header
#interface TouchView : UIView
#property (assign, nonatomic) CGRect activeRect;
#end
//implementation
#import "TouchView.h"
#implementation TouchView
#pragma mark - Ovverride
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL moveTouch = YES;
if (CGRectContainsPoint(self.activeRect, point)) {
moveTouch = NO;
}
return moveTouch;
}
#end
After in interfaceBuilder just set class of View to TouchView and set active rect with your rect. Also u can change and implement other logic.

Related

Detecting when a button is touched in SKScene?

I created a storyboard and added a scene with two buttons. I cannot figure out how to know when a button is pressed on my GameScene.swift class.
How can this be done?
You can you touchesBegan for that.
Here is example code for you:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches{
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.playButton{
//your code
}
}
}
You appear to be mixing UIKit and SpriteKit here. I would personally advise against using UIButtons in conjunction with Sprite Kit. Is there a specific reason for doing so?
There are two ways you can implement button behavior within a Sprite Kit scene:
have the SKScene object handle the touches
have the button itself handle the touches
Dharmesh's answer uses method (1), where he implements the -touchesBegan method.
In my current project, I am using an SKNode subclass as a button (2). I am unfamiliar with Swift syntax so I have posted Objective-C code from my project instead. The method calls are similar though and should help illustrate the point.
If you want an SKNode to receive touches, set userInteractionEnabled to YES. Otherwise, the closest ancestor with userInteractionEnabled = YES (which typically is the containing SKScene) will receive a -touchesBegan/-touchesMoved/-touchesEnded message.
#interface VTObject : SKNode
#end
...
#implementation VTObject
- (instancetype)init {
if (self = [super init]) {
self.userInteractionEnabled = YES;
}
return self;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"button touched!");
}
#end
You should add the UIButton programatically, instead of in IB, to the SKScene's SKView (in didMoveToView for example). You can then set the target for the button with button.addTarget:action:forControlEvents:. Just remember to call button.removeFromSuperview() in willMoveFromView otherwise you'll see the buttons in your next scene.

iOS 8 presentationController determine if really is popover

I'm using the new adaptive "Present As Popover" capability of iOS 8. I wired up a simple segue in the StoryBoard to do the presentation. It works great on an iPhone 6 Plus as it presents the view as a popover and on an iPhone 4s it shows as a full screen view (sheet style).
The problem is when shown as a full screen view, I need to add a "Done" button to the view so dismissViewControllerAnimated can be called. And I don't want to show the "done" button when it's shown as a popover.
I tried looking at the properties of both presentationController and popoverPresentationController, and I can find nothing that tells me if it is actually being shown as a popover.
NSLog( #"View loaded %lx", (long)self.presentationController.adaptivePresentationStyle ); // UIModalPresentationFullScreen
NSLog( #"View loaded %lx", (long)self.presentationController.presentationStyle ); // UIModalPresentationPopover
NSLog( #"View loaded %lx", (long)self.popoverPresentationController.adaptivePresentationStyle ); // UIModalPresentationFullScreen
NSLog( #"View loaded %lx", (long)self.popoverPresentationController.presentationStyle ); // UIModalPresentationPopover
adaptivePresentationStyle always returns UIModalPresentationFullScreen and presentationStyle always returns UIModalPresentationPopover
When looking at the UITraitCollection I did find a trait called "_UITraitNameInteractionModel" which was only set to 1 when it was actually displayed as a Popover. However, Apple doesn't provide direct access to that trait through the traitCollection of popoverPresentationController.
The best way (least smelly) I've found to do this is to use the UIPopoverPresentationControllerDelegate.
• Ensure the presented view controller is set as the UIPopoverPresentationControllerDelegate on the UIPopoverPresentationController being used to manage the presentation. I'm using a Storyboard so set this in prepareForSegue:
segue.destinationViewController.popoverPresentationController.delegate = presentedVC;
• Create a property in the presented view controller to keep track of this state:
#property (nonatomic, assign) BOOL amDisplayedInAPopover;
• And add the following delegate method (or add to your existing delegate method):
- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
{
// This method is only called if we are presented in a popover
self.amDisplayedInAPopover = YES;
}
• And then finally in viewWillAppear: - viewDidLoad: is too early, the delegate prepare method is called between viewDidLoad: and viewWillAppear:
if (self.amDisplayedInAPopover) {
// Hide the offending buttons in whatever manner you do so
self.navigationItem.leftBarButtonItem = nil;
}
Edit: Simpler method!
Just set the delegate (making sure your presentedVC adopts the UIPopoverPresentationControllerDelegate):
segue.destinationViewController.popoverPresentationController.delegate = presentedVC;
And supply the method:
- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
{
// This method is only called if we are presented in a popover
// Hide the offending buttons in whatever manner you do so
self.navigationItem.leftBarButtonItem = nil;
}
I check to see if the popoverPresentationController's arrowDirection is set after the view is laid out. For my purposes, this works well enough and covers the case of popovers on smaller screened devices.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if (popoverPresentationController?.arrowDirection != UIPopoverArrowDirection.Unknown) {
// This view controller is running in a popover
NSLog("I'm running in a Popover")
}
}
How about
if (self.modalPresentationStyle == UIModalPresentationPopover)
It's working for me
The official way to implement this is first remove the Done button from your view controller and second, when adapting to compact embed your view controller in a navigation controller, adding the done button as a navigation item:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.FullScreen
}
func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
let navigationController = UINavigationController(rootViewController: controller.presentedViewController)
let btnDone = UIBarButtonItem(title: "Done", style: .Done, target: self, action: "dismiss")
navigationController.topViewController.navigationItem.rightBarButtonItem = btnDone
return navigationController
}
func dismiss() {
self.dismissViewControllerAnimated(true, completion: nil)
}
Full Tutorial
I tested all solutions presented in this post. Sorry, none works correctly in all cases. For example in iPad split view presentation style can change while dragging split view line, so we need specific notification for that.
After few hours of researches i found solution in apple sample (swift):
https://developer.apple.com/library/ios/samplecode/AdaptivePhotos/Introduction/Intro.html#//apple_ref/doc/uid/TP40014636
Here is the same solution in obj-c.
First in prepareForSegue function set the popoverPresentationController delegate. It can be also set in MyViewController "init", but not in "viewDidLoad" (because first willPresentWithAdaptiveStyle is called before viewDidLoad).
MyViewController *controller = [segue destinationViewController];
controller.popoverPresentationController.delegate = (MyViewController *)controller;
Now MyViewController object will receive this notification every time iOS changes presentation style, including first presenting. Here is example implementation which shows/hides "Close" button in navigationController:
- (void)presentationController:(UIPresentationController *)presentationController
willPresentWithAdaptiveStyle:(UIModalPresentationStyle)style
transitionCoordinator:(nullable id<UIViewControllerTransitionCoordinator>)transitionCoordinator {
if (style == UIModalPresentationNone) {
// style set in storyboard not changed (popover), hide close button
self.topViewController.navigationItem.leftBarButtonItem = nil;
} else {
// style changed by iOS (to fullscreen or page sheet), show close button
UIBarButtonItem *closeButton =
[[UIBarButtonItem alloc] initWithTitle:#"Close" style:UIBarButtonItemStylePlain target:self action:#selector(closeAction)];
self.topViewController.navigationItem.leftBarButtonItem = closeButton;
}
}
- (void)closeAction {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
The UIPresentationController which manages your view controller is presenting it by setting the modalPresentationStyle to UIModalPresentationPopover.
As per UIViewController reference:
presentingViewController
The view controller that presented this view
controller. (read-only)
modalPresentationStyle
UIModalPresentationPopover: In a horizontally regular environment, a presentation style where the content is displayed in a popover view. The background content is dimmed and taps
outside the popover cause the popover to be dismissed. If you do not
want taps to dismiss the popover, you can assign one or more views to
the passthroughViews property of the associated
UIPopoverPresentationController object, which you can get from the
popoverPresentationController property.
We can therefore determine whether your view controller is inside a popover or presented modally by checking the horizontalSizeClass as follows (I assumed your button is a UIBarButtonItem)
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.presentingViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular)
self.navigationItem.leftBarButtonItem = nil; // remove the button
}
The safest place to check this is in viewWillAppear: as otherwise the presentingViewController may be nil.
Solution that works with multitasking
Assign the presenting controller as the popover's delegate
...
controller.popoverPresentationController.delegate = controller;
[self presentViewController:controller animated:YES completion:nil];
Then, in the controller, implement the delegate methods:
- (void)presentationController:(UIPresentationController *)presentationController willPresentWithAdaptiveStyle:(UIModalPresentationStyle)style transitionCoordinator:(id<UIViewControllerTransitionCoordinator>)transitionCoordinator
{
if (style != UIModalPresentationNone)
{
// Exited popover mode
self.navigationItem.leftBarButtonItem = button;
}
}
- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
{
// Entered popover mode
self.navigationItem.leftBarButtonItem = nil;
}
My tricky solution, works perfectly.
In the PopoverViewController's viewDidLoad.
if (self.view.superview!.bounds != UIScreen.main.bounds) {
print("This is a popover!")
}
The idea is simple, A Popover's view size is never equal to the device screen size unless it's not a Popover.

NSTableView: detecting a mouse click together with the row and column

I'm trying to detect when a mouse click occurs in an NSTableView, and when it does, to determine the row and column of the cell that was clicked.
So far I've tried to use NSTableViewSelectionDidChangeNotification, but there are two problems:
It only triggers when the selection changes, whereas I want every mouse click, even if it is on the currently selected row.
The clickedRow and clickedColumn properties of NSTableView are both -1 when my delegate is called.
Is there a better (and correct) way of doing this?
There is a simple way.
Tested with Swift 3.0.2 on macOS 10.12.2 and Xcode 8.2.1
Let
tableView.action = #selector(onItemClicked)
Then
#objc private func onItemClicked() {
print("row \(tableView.clickedRow), col \(tableView.clickedColumn) clicked")
}
To catch the user clicking a row (only, when the user clicks a row, not when it is selected programmatically) :
Subclass your NSTableView and declare a protocol
MyTableView.h
#protocol ExtendedTableViewDelegate <NSObject>
- (void)tableView:(NSTableView *)tableView didClickedRow:(NSInteger)row;
#end
#interface MyTableView : NSTableView
#property (nonatomic, weak) id<ExtendedTableViewDelegate> extendedDelegate;
#end
MyTableView.m
Handle the mouse down event (note, the delegate callback is not called when the user clicks outside, maybe you want to handle that too, in that case, just comment out the condition "if (clickedRow != -1)")
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint globalLocation = [theEvent locationInWindow];
NSPoint localLocation = [self convertPoint:globalLocation fromView:nil];
NSInteger clickedRow = [self rowAtPoint:localLocation];
[super mouseDown:theEvent];
if (clickedRow != -1) {
[self.extendedDelegate tableView:self didClickedRow:clickedRow];
}
}
Make your WC, VC conform to ExtendedTableViewDelegate.
#interface MyViewController : DocumentBaseViewController<ExtendedTableViewDelegate, NSTableViewDelegate, NSTableViewDataSource>
set the extendedDelegate of the MyTableView to your WC, VC (MyViewController)
somewhere in MyTableView.m
self.myTableView.extendedDelegate = self
Implement the callback in delegate (MyViewController.m)
- (void)tableView:(NSTableView *)tableView didClickedRow:(NSInteger)row {
// have fun
}
I would prefer doing as follows.
Override
-(BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row;
Provide super implementation;
RequiredRow = row;
RequiredColumn = [tableView clickedColumn];
Hope this helps.
If someone is looking for a Swift 3/4/5 version of Peter Lapisu's answer:
Add an extension for the NSTableView (NSTableView+Clickable.swift):
import Foundation
import Cocoa
extension NSTableView {
open override func mouseDown(with event: NSEvent) {
let globalLocation = event.locationInWindow
let localLocation = self.convert(globalLocation, from: nil)
let clickedRow = self.row(at: localLocation)
super.mouseDown(with: event)
if (clickedRow != -1) {
(self.delegate as? NSTableViewClickableDelegate)?.tableView(self, didClickRow: clickedRow)
}
}
}
protocol NSTableViewClickableDelegate: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, didClickRow row: Int)
}
Then to use it, make sure you implement the new delegate protocol:
extension MyViewController: NSTableViewClickableDelegate {
#nonobjc func tableView(_ tableView: NSTableView, didClickRow row: Int) {
Swift.print("Clicked row \(row)")
}
}
The #nonobjc attribute silences the warning about it being close to didClick.
Just in case someone was looking for it in SWIFT and / or for NSOutlineView.
Based on #Peter Lapisu instructions.
class MYOutlineViewDelegate: NSOutlineView, NSOutlineViewDelegate,NSOutlineViewDataSource{
//....
}
extension MYOutlineViewDelegate{
func outlineView(outlineView: NSOutlineView, didClickTableRow item: AnyObject?) {
//Click stuff
}
override func mouseDown(theEvent: NSEvent) {
let globalLocation:NSPoint = theEvent.locationInWindow
let localLocation:NSPoint = self.convertPoint(globalLocation, fromView: nil)
let clickedRow:Int = self.rowAtPoint(localLocation)
super.mouseDown(theEvent)
if (clickedRow != -1) {
self.outlineView(self, didClickTableRow: self.itemAtRow(clickedRow))
}
}}
see the tableViewSelectionIsChanging notification, here are the the comments from NSTableView.h
/*
Optional - Called when the selection is about to be changed, but note, tableViewSelectionIsChanging: is only called when mouse events are changing the selection and not keyboard events.
*/
I concede that this might not be the surest way to correlate your mouse clicks, but it is another area to investigate, seeing that you are interested in mouse clicks.

NSImageView double click action

I have some NSImageView in my Mac App where the user can drag'n drop objects like .png or .pdf, to store them into User Shared Defaults, that works fine.
I would now like to set an action for when user double click on these NSImageView, but it seems to be a little bit difficult (I had no trouble for NSTableView, but 'setDoubleAction' is not available for NSImage, and tons of answers (here or with google) concerning NSImageView's actions point to making a NSButton instead of NSImageView, so that doesn't help)
Here is part of my AppDelegate.h:
#interface AppDelegate : NSObject <NSApplicationDelegate>{
(...)
#property (assign) IBOutlet NSImageView *iconeStatus;
(...)
#end
and here is part of my AppDelegate.m:
#import "AppDelegate.h"
#implementation AppDelegate
(...)
#synthesize iconeStatus = _iconeStatus;
(...)
- (void)awakeFromNib {
(...)
[_iconeStatus setTarget:self];
[_iconeStatus setAction:#selector(doubleClick:)];
(...)
}
(...)
- (void)doubleClick:(id)object {
//make sound if that works ...
[[NSSound soundNamed:#"Basso"] play];
}
But that doesn't work.
Can anyone tell me what's the easiest way to do this ?
You need to subclass NSImageView and add the following method to your subclass's implementation:
- (void)mouseDown:(NSEvent *)theEvent
{
NSInteger clickCount = [theEvent clickCount];
if (clickCount > 1) {
// User at least double clicked in image view
}
}
Code for Swift 4. Again the NSImageView is subclassed and the mouseDown function is overridden.
class MyImageView: NSImageView {
override func mouseDown(with event: NSEvent) {
let clickCount: Int = event.clickCount
if clickCount > 1 {
// User at least double clicked in image view
}
}
}
Another solution using extension:
extension NSImageView {
override open func mouseDown(with event: NSEvent) {
// your code here
}
}
Although this will add that functionality to every NSImageView, so perhaps that's not what you're looking for.

selectText of NSTextField on focus

Could anybody suggest a method to select all the text of an NSTextField when the user clicks it?
I did find suggestions to subclass NSTextField and then use mouseDown or firstResponder,` but it's beyond my skill for now. So I was hoping either there would be an easier solution or somebody might be kind enough to detail the steps required.
There isn't an easier solution, you need to subclass NSTextField to do what you want. You will need to learn how to handle subclassing if you are to do anything useful in Cocoa.
Text fields can be relatively complex to subclass because an NSTextField uses a separate NSTextView object called the Field Editor to handle the actual editing. This text view is returned by the NSWindow object for the NSTextField and it is re-used for all text fields on the page.
Like any NSResponder subclass, NSTextField responds to the methods -acceptsFirstResponder and -becomeFirstResponder. These are called when the window wants to give focus to a particular control or view. If you return YES from both these methods then your control/view will have first responder status, which means it's the active control. However, as mentioned, an NSTextField actually gives the field editor first responder status when clicked, so you need to do something like this in your NSTextField subclass:
#implementation MCTextField
- (BOOL)becomeFirstResponder
{
BOOL result = [super becomeFirstResponder];
if(result)
[self performSelector:#selector(selectText:) withObject:self afterDelay:0];
return result;
}
#end
This first calls the superclass' implementation of -becomeFirstResponder which will do the hard work of managing the field editor. It then calls -selectText: which selects all the text in the field, but it does so after a delay of 0 seconds, which will delay until the next run through the event loop. This means that the selection will occur after the field editor has been fully configured.
The answer from #Rob presumably worked at one point, but as #Daniel noted, it does not work any more. It looks like Cocoa wants to track the mouse and drag out a selection in response to a click, and trying to select the text in response to becomeFirstResponder does not play well with that.
The mouse event needs to be intercepted, then, to prevent that tracking. More or less by trial and error, I've found a solution which seems to work on OS X 10.10:
#interface MyAutoselectTextField : NSTextField
#end
#implementation MyAutoselectTextField
- (void)mouseDown:(NSEvent *)theEvent
{
[[self currentEditor] selectAll:nil];
}
#end
As far as I can tell, by the time mouseDown: gets called the field editor has already been set up, probably as a side effect of becomeFirstResponder. Calling selectAll: then selects the contents of the field editor. Calling selectText: on self instead does not work well, presumably because the field editor is set up. Note that the override of mouseDown: here does not call super; super would run a tracking loop that would drag out a selection, and we don't want that behavior. Note that this mouseDown: override doesn't affect selection once the textfield has become first responder, because at that point it is the field editor's mouseDown: that is getting called.
I have no idea what range of OS X versions this works across; if you care, you'll need to test it. Unfortunately, working with NSTextField is always a little fragile because the way field editors work is so strange and so dependent upon the implementation details in super.
Some updates with Swift:
import Cocoa
class TextFieldSubclass: NSTextField {
override func mouseDown(theEvent: NSEvent) {
super.mouseDown(theEvent)
if let textEditor = currentEditor() {
textEditor.selectAll(self)
}
}
}
Or for precise selection:
import Cocoa
class TextFieldSubclass: NSTextField {
override func mouseDown(theEvent: NSEvent) {
super.mouseDown(theEvent)
if let textEditor = currentEditor() {
textEditor.selectedRange = NSMakeRange(location, length)
}
}
}
Swift version, works for me:
import Cocoa
class TextFieldSubclass: NSTextField {
override func becomeFirstResponder() -> Bool {
let source = CGEventSourceCreate(CGEventSourceStateID.HIDSystemState)
let tapLocation = CGEventTapLocation.CGHIDEventTap
let cmdA = CGEventCreateKeyboardEvent(source, 0x00, true)
CGEventSetFlags(cmdA, CGEventFlags.MaskCommand)
CGEventPost(tapLocation, cmdA)
return true
}
}
I like Konstantin's solution, but that one will select on every mouse down. Here's a variation I'm using to select in mouseDown method, but only if it just became the first responder:
class SelectingTextField: NSTextField {
var wantsSelectAll = false
override func becomeFirstResponder() -> Bool {
wantsSelectAll = true
return super.becomeFirstResponder()
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if wantsSelectAll {
selectText(self)
wantsSelectAll = false
}
}
}
I know this is an old question, but here's an update for those who might stumble over this.
Override NSTextField and hook into becomeFirstResponder(). You won't have to worry about managing delegates or clicks. The tricky part is finding the field editor for the focused text field first before asking it to select all the text.
Objective-C
// in AutoselectOnFocusTextField.h
#interface AutoselectOnFocusTextField : NSTextField
#end
// in AutoselectOnFocusTextField.m
#implementation AutoselectOnFocusTextField
- (BOOL)becomeFirstResponder {
if (![super becomeFirstResponder]) {
return NO;
}
NSText* fieldEditor = [self currentEditor];
if (fieldEditor != nil) {
[fieldEditor performSelector:#selector(selectAll:) withObject:self afterDelay:0.0];
}
return YES;
}
#end
Swift
class AutoselectOnFocusTextField: NSTextField {
override func becomeFirstResponder() -> Bool {
guard super.becomeFirstResponder() else {
return false
}
if let editor = self.currentEditor() {
editor.perform(#selector(selectAll(_:)), with: self, afterDelay: 0)
}
return true
}
}

Resources