Custom NSScrollView doesn't scroll NSTableHeaderView - objective-c

I have an NSTableView embedded within a custom NSScrollView subclass, wherein I sometimes do scrolling programmatically, like so:
[[self contentView] scrollToPoint:newOffset];
[self reflectScrolledClipView:[self contentView]];
When I do this, the NSTableView scrolls fine, but its associated NSTableHeaderView doesn't move with it. If I use my mouse and scroll the NSScrollView normally, however, they move together like they should.
I figure I'm probably just missing a single line somewhere that lets the NSTableHeaderView know that it's supposed to scroll too, but I don't know what that is. Can anyone help?

Well, I don't know precisely what kind of black magic goes on under the hood when you scroll an NSScrollView containing an NSTableHeaderView with the mouse, but it looks like it handles it internally somewhere. To circumvent this, I now only scroll the NSTableView programatically (by overriding the functions that would handle user input), and then I scroll the NSTableHeaderView myself, like so:
NSTableHeader *header = [[self documentView] headerView];
[header setBoundsOrigin:NSMakePoint(newOffset.x,[header bounds].origin.y)];

I ran into the same issue on a cell-based NSTableView with Swift 5 / MacOS 14.
The NSScrollView enclosing an NSTableView owns both the contentView and the headerView of the NSTableView (and the cornerView, which I do not use), and is normally responsible of coordinating their scrolling.
When scrolling with mouse, the NSScrollView internal magic handle correctly the scrolling of the header view.
When scrolling programmatically the NSClipView using scroll(to:) + reflectScrolledClipView, NSScrollView fails to scroll of the headerView.
I use this protocol in order to scroll programmatically the headerView too, which allows me to scroll programmatically using this protocol:
extension NSTableView : ScrollingProtocol {
func getScrollView() -> NSScrollView? {
return enclosingScrollView
}
func getVisibleOrigin() -> NSPoint? {
return enclosingScrollView?.documentVisibleRect.origin
}
func scrollToOrigin(_ targetOrigin: NSPoint) {
guard let currentOrigin = getVisibleOrigin(),
let scrollView = enclosingScrollView
else { return }
if (!NSEqualPoints(targetOrigin, currentOrigin)) {
let clipView = scrollView.contentView
clipView.scroll(to: targetOrigin)
// Workaround because NSClipView.scroll(to:) does not scroll
// the headerView of NSTableView
if let headerView = headerView {
let x = targetOrigin.x
let y = headerView.bounds.origin.y
if let headerClipView = headerView.superview as? NSClipView {
headerClipView.scroll(to: NSMakePoint(x, y))
}
}
scrollView.reflectScrolledClipView(clipView)
}
}
}

Related

How to create background for NSView when using storyboards with OS X

I know how I may be able to do this programmatically. I know that when creating storyboards for iOS it's very easy and is right there in the attributes inspector. However when creating storyboards for OS X, I don't see it for any of my view controllers or the view underneath it in Xcode 6.1.1
How can I change the background of the view without having to create a view controller associated with it. My application has a lot of views that are simple, but the background changes colour from one view to the next.
Here is another way to achieve the same effect.
Add NSBox under your NSView and adjust NSBox's frame as the same with the NSView.
Change Title position to None, Box type to Custom, Border Type to None
This is the screenshot:
And than add your NSView, NSButton, NSTextField or whatever as a subview of NSBox.
This is the result:
You shouldn't need a new viewController to change the background of an NSView. If you aren't subclassing NSView, then you can simply call:
myView.wantsLayer = true
myView.layer!.backgroundColor = CGColorGetConstantColor(kCGColorBlack)
I'm not sure if you can do it strictly in the storyboard, though you can likely set the wantsLayer = true in storyboard using the User Defined Runtime Attributes.
You can change a NSView's background color in the storyboard if you subclass NSView and assign the subclass to your view:
Using this subclass implementation:
import Cocoa
class ViewWithBackgroundColor : NSView {
#IBInspectable var backgroundColor: NSColor? {
get {
guard let layer = layer, backgroundColor = layer.backgroundColor else { return nil }
return NSColor(CGColor: backgroundColor)
}
set {
wantsLayer = true
layer?.backgroundColor = newValue?.CGColor
}
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
The background color isn't shown at design time but at run-time.
Maybe somebody got an idea how to also update the view at design time (preferably without overriding the draw method).

UIScrollViews all scroll in the same time

I have a screen with several scrollViews. How to achieve this:
When i tap on one and swipe they all start to scroll. I of course know UIScrollViewDelegate methods and what i trying to do so far is combine -setContentOffset:animated: with scrollViewDidScroll and it works but only for one case - when i start scrolling with delegate scrollview.
How to dynamically change delegate? depends which scroll view user select?
Keep an array of all of your UIScrollView objects. Make sure all of their delegates point to the same object (or if that's not possible, there is some sort of handler that gets called on scrollViewDidScroll). Then use setContentOffset to adjust the offsets. You had the right idea, but you just want to make sure all scroll views except the current view (which is determined by the delegate method argument) is scrolling.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (UIScrollView *view in self.scrollViews) {
if (scrollView != view) {
[view setContentOffset:scrollView.contentOffset];
}
}
}

JTRevealSidebar initialized from tableview

I would like to implement the JTRevealSidebar project (a sidebar feature very similar to the sidebar in the facebook app) into my project. My only problem is that my main view from where i want to initialize the sidebar is a UITableView, and not a UIView, which the JTRevealSidebar is setup for. Has anybody tried setting this up before with a tableview or know if it's possible?
You can return any subclass of UIViews in your sidebarDelegate callback
- (UIView *)viewForLeftSidebar {
// Use applicationViewFrame to get the correctly calculated view's frame
// for use as a reference to our sidebar's view
CGRect viewFrame = self.navigationController.applicationViewFrame;
UIView *anyView = self.leftView;
if ( ! anyView) {
anyView = [[AnySubclassOfUIView alloc] initWithFrame:viewFrame];
[self.view addSubview:anyView];
}
return anyView;
}

NSView's context NSMenu is never shown even though all the right methods are being called

I have an NSCollectionView with a bunch of NSViews in it, stacked vertically, to make it look a bit like UIKit's UITableView. Everything works as expected, except for one thing:
When right-clicking any one of the NSViews, I expect the NSMenu I set to be view's menu to be shown, but alas - nothing happens.
The crazy part is all the right methods are being called, exactly as could be expected: -rightMouseDown:, -menuForEvent: and finally -menu.
When I set up any object as the NSMenu's delegate, menuWillOpen: is not called, so it seems to me something fails over on Apple's side of things, just in between asking for the menu, and actually showing it.
Would anyone be able to shed a light on this?
Thanks in advance.
PS. For what it's worth, NSMenus I present manually (without relying on Apple's right-click handling) using popUpMenuPositioningItem:atLocation:inView: are shown.
Edit / Update / Clarification
The NSCollectionView in question is inside an NSWindow that's being shown when an NSStatusItem is clicked, like CoverSutra/TicToc/what have you. Some code from the MyWindow NSWindow subclass:
- (void)awakeFromNib {
[self setStyleMask:NSBorderlessWindowMask];
[self setExcludedFromWindowsMenu:YES];
}
- (BOOL)canBecomeMainWindow {
return YES;
}
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL)isMovable {
return NO;
}
- (void)presentFromPoint:(NSPoint)point {
point.y -= self.frame.size.height;
point.x -= self.frame.size.width / 2;
[self setFrameOrigin:point];
[self makeMainWindow];
[self makeKeyAndOrderFront:self];
}
presentFromPoint: is the method I use to present it from any point I like, in my case from just below the NSStatusItem. (Not really relevant to this problem)
My application has LSUIElement in its Info.plist set to YES by the way, so it doesn't show a menu bar or a Dock icon. It lives in the status bar, and has a window that's shown when the NSStatusItem is clicked.
The view hierarchy is as follows:
MyWindow => contentView => NSScrollView => NSCollectionView
The NSCollectionView has an NSCollectionViewItem subclass connected to its itemPrototype property, and the NSCollectionViewItem subclass has an NSView subclass connected to its view property.
The NSView subclass, in turn, has an NSMenu connected to its menu property.
And last but not least: This NSMenu has one NSMenuItem sitting inside it.
Both the NSCollectionViewItem subclass and the NSView subclass do nothing interesting as of now, they're just empty subclasses.
The NSMenu connected to the NSView's menu property is what should be shown when the NSView is right-clicked, but as I hope I have made clear: It isn't actually shown.
Update
I still have no idea what caused this problem, but I've decided to 'move on' from NSCollectionView, as it wasn't really fit for what I was trying to do anyway, and I am now using TDListView which works like a charm.

How can you add a UIGestureRecognizer to a UIBarButtonItem as in the common undo/redo UIPopoverController scheme on iPad apps?

Problem
In my iPad app, I cannot attach a popover to a button bar item only after press-and-hold events. But this seems to be standard for undo/redo. How do other apps do this?
Background
I have an undo button (UIBarButtonSystemItemUndo) in the toolbar of my UIKit (iPad) app. When I press the undo button, it fires it's action which is undo:, and that executes correctly.
However, the "standard UE convention" for undo/redo on iPad is that pressing undo executes an undo but pressing and holding the button reveals a popover controller where the user selected either "undo" or "redo" until the controller is dismissed.
The normal way to attach a popover controller is with presentPopoverFromBarButtonItem:, and I can configure this easily enough. To get this to show only after press-and-hold we have to set a view to respond to "long press" gesture events as in this snippet:
UILongPressGestureRecognizer *longPressOnUndoGesture = [[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:#selector(handleLongPressOnUndoGesture:)];
//Broken because there is no customView in a UIBarButtonSystemItemUndo item
[self.undoButtonItem.customView addGestureRecognizer:longPressOnUndoGesture];
[longPressOnUndoGesture release];
With this, after a press-and-hold on the view the method handleLongPressOnUndoGesture: will get called, and within this method I will configure and display the popover for undo/redo. So far, so good.
The problem with this is that there is no view to attach to. self.undoButtonItem is a UIButtonBarItem, not a view.
Possible solutions
1) [The ideal] Attach the gesture recognizer to the button bar item. It is possible to attach a gesture recognizer to a view, but UIButtonBarItem is not a view. It does have a property for .customView, but that property is nil when the buttonbaritem is a standard system type (in this case it is).
2) Use another view. I could use the UIToolbar but that would require some weird hit-testing and be an all around hack, if even possible in the first place. There is no other alternative view to use that I can think of.
3) Use the customView property. Standard types like UIBarButtonSystemItemUndo have no customView (it is nil). Setting the customView will erase the standard contents which it needs to have. This would amount to re-implementing all the look and function of UIBarButtonSystemItemUndo, again if even possible to do.
Question
How can I attach a gesture recognizer to this "button"? More specifically, how can I implement the standard press-and-hold-to-show-redo-popover in an iPad app?
Ideas? Thank you very much, especially if someone actually has this working in their app (I'm thinking of you, omni) and wants to share...
Note: this no longer works as of iOS 11
In lieu of that mess with trying to find the UIBarButtonItem's view in the toolbar's subview list, you can also try this, once the item is added to the toolbar:
[barButtonItem valueForKey:#"view"];
This uses the Key-Value Coding framework to access the UIBarButtonItem's private _view variable, where it keeps the view it created.
Granted, I don't know where this falls in terms of Apple's private API thing (this is public method used to access a private variable of a public class - not like accessing private frameworks to make fancy Apple-only effects or anything), but it does work, and rather painlessly.
This is an old question, but it still comes up in google searches, and all of the other answers are overly complicated.
I have a buttonbar, with buttonbar items, that call an action:forEvent: method when pressed.
In that method, add these lines:
bool longpress=NO;
UITouch *touch=[[[event allTouches] allObjects] objectAtIndex:0];
if(touch.tapCount==0) longpress=YES;
If it was a single tap, tapCount is one. If it was a double tap, tapCount is two. If it's a long press, tapCount is zero.
Option 1 is indeed possible. Unfortunately it's a painful thing to find the UIView that the UIBarButtonItem creates. Here's how I found it:
[[[myToolbar subviews] objectAtIndex:[[myToolbar items] indexOfObject:myBarButton]] addGestureRecognizer:myGesture];
This is more difficult than it ought to be, but this is clearly designed to stop people from fooling around with the buttons look and feel.
Note that Fixed/Flexible spaces are not counted as views!
In order to handle spaces you must have some way of detecting them, and sadly the SDK simply has no easy way to do this. There are solutions and here are a few of them:
1) Set the UIBarButtonItem's tag value to it's index from left to right on the toolbar. This requires too much manual work to keep it in sync IMO.
2) Set any spaces' enabled property to NO. Then use this code snippet to set the tag values for you:
NSUInteger index = 0;
for (UIBarButtonItem *anItem in [myToolbar items]) {
if (anItem.enabled) {
// For enabled items set a tag.
anItem.tag = index;
index ++;
}
}
// Tag is now equal to subview index.
[[[myToolbar subviews] objectAtIndex:myButton.tag] addGestureRecognizer:myGesture];
Of course this has a potential pitfall if you disable a button for some other reason.
3) Manually code the toolbar and handle the indexes yourself. As you'll be building the UIBarButtonItem's yourself, so you'll know in advance what index they'll be in the subviews. You could extend this idea to collecting up the UIView's in advance for later use, if necessary.
Instead of groping around for a subview you can create the button on your own and add a button bar item with a custom view. Then you hook up the GR to your custom button.
While this question is now over a year old, this is still a pretty annoying problem. I've submitted a bug report to Apple (rdar://9982911) and I suggest that anybody else who feels the same duplicate it.
You also can simply do this...
let longPress = UILongPressGestureRecognizer(target: self, action: "longPress:")
navigationController?.toolbar.addGestureRecognizer(longPress)
func longPress(sender: UILongPressGestureRecognizer) {
let location = sender.locationInView(navigationController?.toolbar)
println(location)
}
Until iOS 11, let barbuttonView = barButton.value(forKey: "view") as? UIView will give us the reference to the view for barButton in which we can easily add gestures, but in iOS 11 the things are quite different, the above line of code will end up with nil so adding tap gesture to the view for key "view" is meaningless.
No worries we can still add tap gestures to the UIBarItems, since it have a property customView. What we can do is create a button with height & width 24 pt(according to Apple Human Interface Guidelines) and then assign the custom view as the newly created button. The below code will help you perform one action for single tap and another for tapping bar button 5 times.
NOTE For this purpose you must already have a reference to the barbuttonitem.
func setupTapGestureForSettingsButton() {
let multiTapGesture = UITapGestureRecognizer()
multiTapGesture.numberOfTapsRequired = 5
multiTapGesture.numberOfTouchesRequired = 1
multiTapGesture.addTarget(self, action: #selector(HomeTVC.askForPassword))
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
button.addTarget(self, action: #selector(changeSettings(_:)), for: .touchUpInside)
let image = UIImage(named: "test_image")withRenderingMode(.alwaysTemplate)
button.setImage(image, for: .normal)
button.tintColor = ColorConstant.Palette.Blue
settingButton.customView = button
settingButton.customView?.addGestureRecognizer(multiTapGesture)
}
I tried something similar to what Ben suggested. I created a custom view with a UIButton and used that as the customView for the UIBarButtonItem. There were a couple of things I didn't like about this approach:
The button needed to be styled to not stick out like a sore thumb on the UIToolBar
With a UILongPressGestureRecognizer I didn't seem to get the click event for "Touch up Inside" (This could/is most likely be programing error on my part.)
Instead I settled for something hackish at best but it works for me. I'm used XCode 4.2 and I'm using ARC in the code below. I created a new UIViewController subclass called CustomBarButtonItemView. In the CustomBarButtonItemView.xib file I created a UIToolBar and added a single UIBarButtonItem to the toolbar. I then shrunk the toolbar to almost the width of the button. I then connected the File's Owner view property to the UIToolBar.
Then in my ViewController's viewDidLoad: message I created two UIGestureRecognizers. The first was a UILongPressGestureRecognizer for the click-and-hold and second was UITapGestureRecognizer. I can't seem to properly get the action for the UIBarButtonItem in the view so I fake it with the UITapGestureRecognizer. The UIBarButtonItem does show itself as being clicked and the UITapGestureRecognizer takes care of the action just as if the action and target for the UIBarButtonItem was set.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressGestured)];
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(buttonPressed:)];
CustomBarButtomItemView* customBarButtonViewController = [[CustomBarButtomItemView alloc] initWithNibName:#"CustomBarButtonItemView" bundle:nil];
self.barButtonItem.customView = customBarButtonViewController.view;
longPress.minimumPressDuration = 1.0;
[self.barButtonItem.customView addGestureRecognizer:longPress];
[self.barButtonItem.customView addGestureRecognizer:singleTap];
}
-(IBAction)buttonPressed:(id)sender{
NSLog(#"Button Pressed");
};
-(void)longPressGestured{
NSLog(#"Long Press Gestured");
}
Now when a single click occurs in the ViewController's barButtonItem (Connected via the xib file) the tap gesture calls the buttonPressed: message. If the button is held down longPressGestured is fired.
For changing the appearance of the UIBarButton I'd suggest making a property for CustomBarButtonItemView to allow access to the Custom BarButton and store it in the ViewController class. When the longPressGestured message is sent you can change the system icon of the button.
One gotcha I've found is the customview property takes the view as is. If you alter the custom UIBarButtonitem from the CustomBarButtonItemView.xib to change the label to #"really long string" for example the button will resize itself but only the left most part of the button shown is in the view being watched by the UIGestuerRecognizer instances.
I tried #voi1d's solution, which worked great until I changed the title of the button that I had added a long press gesture to. Changing the title appears to create a new UIView for the button that replaces the original, thus causing the added gesture to stop working as soon as a change is made to the button (which happens frequently in my app).
My solution was to subclass UIToolbar and override the addSubview: method. I also created a property that holds the pointer to the target of my gesture. Here's the exact code:
- (void)addSubview:(UIView *)view {
// This method is overridden in order to add a long-press gesture recognizer
// to a UIBarButtonItem. Apple makes this way too difficult, but I am clever!
[super addSubview:view];
// NOTE - this depends the button of interest being 150 pixels across (I know...)
if (view.frame.size.width == 150) {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:targetOfGestureRecognizers
action:#selector(showChapterMenu:)];
[view addGestureRecognizer:longPress];
}
}
In my particular situation, the button I'm interested in is 150 pixels across (and it's the only button that is), so that's the test I use. It's probably not the safest test, but it works for me. Obviously you'd have to come up with your own test and supply your own gesture and selector.
The benefit of doing it this way is that any time my UIBarButtonItem changes (and thus creates a new view), my custom gesture gets attached, so it always works!
I know this is old but I spent a night banging my head against the wall trying to find an acceptable solution. I didn't want to use the customView property because would get rid of all of the built in functionality like button tint, disabled tint, and the long press would be subjected to such a small hit box while UIBarButtonItems spread their hit box out quite a ways. I came up with this solution that I think works really well and is only a slight pain to implement.
In my case, the first 2 buttons on my bar would go to the same place if long pressed, so I just needed to detect that a press happened before a certain X point. I added the long press gesture recognizer to the UIToolbar (also works if you add it to a UINavigationBar) and then added an extra UIBarButtonItem that's 1 pixel wide right after the 2nd button. When the view loads, I add a UIView that's a single pixel wide to that UIBarButtonItem as it's customView. Now, I can test the point where the long press happened and then see if it's X is less than the X of the customview's frame. Here's a little Swift 3 Code
#IBOutlet var thinSpacer: UIBarButtonItem!
func viewDidLoad() {
...
let thinView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 22))
self.thinSpacer.customView = thinView
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(gestureRecognizer:)))
self.navigationController?.toolbar.addGestureRecognizer(longPress)
...
}
func longPressed(gestureRecognizer: UIGestureRecognizer) {
guard gestureRecognizer.state == .began, let spacer = self.thinSpacer.customView else { return }
let point = gestureRecognizer.location(ofTouch: 0, in: gestureRecognizer.view)
if point.x < spacer.frame.origin.x {
print("Long Press Success!")
} else {
print("Long Pressed Somewhere Else")
}
}
Definitely not ideal, but easy enough for my use case. If you need a specify a long press on specific buttons in specific locations, it gets a little more annoying but you should be able to surround the buttons you need to detect the long press on with thin spacers and then just check that your point's X is between both of those spacers.
#voi1d's 2nd option answer is the most useful for those not wanting to rewrite all the functionality of UIBarButtonItem's. I wrapped this in a category so that you can just do:
[myToolbar addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton];
with a little error handling in case you are interested. NOTE: each time you add or remove items from the toolbar using setItems, you will have to re-add any gesture recognizers -- I guess UIToolbar recreates the holding UIViews every time you adjust the items array.
UIToolbar+Gesture.h
#import <UIKit/UIKit.h>
#interface UIToolbar (Gesture)
- (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton;
#end
UIToolbar+Gesture.m
#import "UIToolbar+Gesture.h"
#implementation UIToolbar (Gesture)
- (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton {
NSUInteger index = 0;
NSInteger savedTag = barButton.tag;
barButton.tag = NSNotFound;
for (UIBarButtonItem *anItem in [self items]) {
if (anItem.enabled) {
anItem.tag = index;
index ++;
}
}
if (NSNotFound != barButton.tag) {
[[[self subviews] objectAtIndex:barButton.tag] addGestureRecognizer:recognizer];
}
barButton.tag = savedTag;
}
#end
I know it is not the best solution, but I am going to post a rather easy solution that worked for me.
I have created a simple extension for UIBarButtonItem:
fileprivate extension UIBarButtonItem {
var view: UIView? {
return value(forKey: "view") as? UIView
}
func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
view?.addGestureRecognizer(gestureRecognizer)
}
}
After this, you can simply add your gesture recognizers to the items in your ViewController's viewDidLoad method:
#IBOutlet weak var myBarButtonItem: UIBarButtonItem!
func setupLongPressObservation() {
let recognizer = UILongPressGestureRecognizer(
target: self, action: #selector(self.didLongPressMyBarButtonItem(recognizer:)))
myBarButtonItem.addGestureRecognizer(recognizer)
}
#utopians answer in Swift 4.2
#objc func myAction(_ sender: UIBarButtonItem, forEvent event:UIEvent) {
let longPressed:Bool = (event.allTouches?.first?.tapCount).map {$0 == 0} ?? false
... handle long press ...
}
Ready for use UIBarButtonItem subclass:
#objc protocol BarButtonItemDelegate {
func longPress(in barButtonItem: BarButtonItem)
}
class BarButtonItem: UIBarButtonItem {
#IBOutlet weak var delegate: BarButtonItemDelegate?
private let button = UIButton(type: .system)
override init() {
super.init()
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
let recognizer = UILongPressGestureRecognizer(
target: self,
action: #selector(longPress)
)
button.addGestureRecognizer(recognizer)
button.setImage(image, for: .normal)
button.tintColor = tintColor
customView = button
}
override var action: Selector? {
set {
if let action = newValue {
button.addTarget(target, action: action, for: .touchUpInside)
}
}
get { return nil }
}
#objc private func longPress(sender: UILongPressGestureRecognizer) {
if sender.state == .began {
delegate?.longPress(in: self)
}
}
}
This is the most Swift-friendly and least hacky way I came up with. Works in iOS 12.
Swift 5
var longPressTimer: Timer?
let button = UIButton()
button.addTarget(self, action: #selector(touchDown), for: .touchDown)
button.addTarget(self, action: #selector(touchUp), for: .touchUpInside)
button.addTarget(self, action: #selector(touchCancel), for: .touchCancel)
let undoBarButton = UIBarButtonItem(customView: button)
#objc func touchDown() {
longPressTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(longPressed), userInfo: nil, repeats: false)
}
#objc func touchUp() {
if longPressTimer?.isValid == false { return } // Long press already activated
longPressTimer?.invalidate()
longPressTimer = nil
// Do tap action
}
#objc func touchCancel() {
longPressTimer?.invalidate()
longPressTimer = nil
}
#objc func longPressed() {
// Do long press action
}