I have Multiple Buttons on my UI, and I want to execute different functions according to different types of click,
Single Click
Double Click
Long Press
Doing it for single tap was easy for me, an IBAction with all the four buttons connected to it, but for the other types of clicks i was stuck,
I understand that i need to use the tap gesture recognizer, but I'm unable to set it to multiple UIButtons,
Here is and example of what I want to do
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var Label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func ButtonSingleTap(sender:UIButton!) {
let ButtonNumber: String = String(sender.tag)
Label.text = "Button " + ButtonNumber + " is Single Tapped"
}
func ButtonDoubleTap(sender:UIButton!) {
let ButtonNumber: String = String(sender.tag)
Label.text = "Button " + ButtonNumber + " is Double Tapped"
}
func ButtonLongTap(sender:UIButton!) {
let ButtonNumber: String = String(sender.tag)
Label.text = "Button " + ButtonNumber + " is Long Pressed"
}
}
You cannot use one instance of UIGestureRecognizer for several buttons. Each button needs its own set of gesture recognizers.
Here is an example to show you how to do it:
for button in [button1, button2, button3, button4] {
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: Selector("didLongPress:"))
let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: Selector("didDoubleTap:"))
doubleTapRecognizer.numberOfTapsRequired = 2
button.addGestureRecognizer(longPressRecognizer)
button.addGestureRecognizer(doubleTapRecognizer)
}
func didLongPress(recognizer: UILongPressGestureRecognizer) {
guard let button = recognizer.view as? UIButton else { return }
switch recognizer.state {
case .Began:
print("long press began button \(button.tag)")
case .Ended:
print("long press ended button \(button.tag)")
default:
break
}
}
func didDoubleTap(recognizer: UITapGestureRecognizer) {
guard let button = recognizer.view as? UIButton else { return }
print("double tap button \(button.tag)")
}
For this to work you have to add Outlets for your buttons (button1, button2, button3, button4).
Related
I'm trying to make the SWRevealViewController work in my application.
What I did is I didn't want to be able to open the menu from everywhere on the screen so I changed all the UIPanGestureRecognizer with UIScreenEdgePanGestureRecognizer so it would only be triggered from the side of the screen.
to achieve this I also altered thepanGestureRecognizer method. This looks as follows right now
- (UIScreenEdgePanGestureRecognizer*)panGestureRecognizer
{
if ( _panGestureRecognizer == nil )
{
_panGestureRecognizer = [[SWRevealViewControllerPanGestureRecognizer alloc] initWithTarget:self action:#selector(_handleRevealGesture:)];
_panGestureRecognizer.delegate = self;
_panGestureRecognizer.edges = UIRectEdgeLeft;
[_contentView.frontView addGestureRecognizer:_panGestureRecognizer];
}
return _panGestureRecognizer;
}
However (not sure if this change is causing my problem) when I start to open the menu from the left side and expand it over the point where it will normally snap to when opened up it will collapse back in.
so let's say it opens to 500px of the screen. When I drag it beyond those 500px it will automatically close the menu.
Also it's currently not possible to close the menu again by swiping it. What I did for now is add a gesturerecognizer which will trigger the revealViewController.revealToggle(animated: true) method.
Does anyone happen to know how to fix this?
import #import "SWRevealViewController.h"
These two gestures helps you to open and close sw menu . Drag and click for open and close SW
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
[self.view addGestureRecognizer:self.revealViewController.tapGestureRecognizer];
Open sw menu normally
[self.revealViewController revealToggleAnimated:YES]
I ended up fixing it by using the delegate methods that come with SWRevealViewController my entire code looks like this
import UIKit
import SWRevealViewController
import UIKit.UIGestureRecognizerSubclass
class ViewController: UIViewController, SWRevealViewControllerDelegate {
#IBOutlet var openMenu: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
openMenu.target = self.revealViewController()
openMenu.action = #selector(SWRevealViewController.revealToggle(_:))
revealViewController().delegate = self
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
self.hideMenuWhenTappedAround()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func revealControllerPanGestureShouldBegin(_ revealController: SWRevealViewController!) -> Bool {
let point = revealController.panGestureRecognizer().location(in: self.view)
if revealController.frontViewPosition == FrontViewPosition.left && point.x < 50.0 {
return true
}
else if revealController.frontViewPosition == FrontViewPosition.right {
return true
}
return false
}
func revealController(_ revealController: SWRevealViewController!, panGestureMovedToLocation location: CGFloat, progress: CGFloat) {
if location >= revealController.rearViewRevealWidth {
revealController.panGestureRecognizer().state = UIGestureRecognizerState.ended
}
}
}
The self.hideMenuWhentappedAround() makes sure that the menu will close when you tap anywhere on the uiview. This method is created in an extension of the UIViewController and it looks as follows.
import UIKit
import SWRevealViewController
extension UIViewController {
func hideMenuWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(SWRevealViewController.dismissMenu))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
func dismissMenu() {
self.revealViewController().revealToggle(animated: true)
}
}
I d'like to combine a UILongPressGestureRecognizer with a UIPanGestureRecognizer.
The UIPanGestureRecognizer should start with a long press. Is there a simple way to do this? or do I really have to write my own gesture recognizer?
I wan't something like on the home screen. You press on an icon and after some time the icons start wobbling. Afterwards without releasing my finger from the screen I can start dragging the icon under my finger around.
actually, you don't have to combine gesture recognizers - you can do this solely with UILongPressGestureRecognizer... You enter StateBegan once your touch(es) have stayed within 'allowableMovement' for 'minimumPressDuration'. You stay in your continuous longPressGesture as long as you don't lift any of your fingers - so you can start moving your fingers and track the movement through StateChanged.
Long-press gestures are continuous. The gesture begins (UIGestureRecognizerStateBegan) when the number of allowable fingers (numberOfTouchesRequired) have been pressed for the specified period (minimumPressDuration) and the touches do not move beyond the allowable range of movement (allowableMovement). The gesture recognizer transitions to the Change state whenever a finger moves, and it ends (UIGestureRecognizerStateEnded) when any of the fingers are lifted.
I had a bit of a hard time for this problem. The accepted answer wasn't enough. No matter what I put in that method the pan or longpress handlers would get invoked. A solution I found was as follows:
Ensure the gesture recognizers' delegates are assigned to the same class (in my case self) and ensure the delegate class is a UIGestureRecognizerDelegate.
Add the following delegate method to your class (as per the answer above):
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Add the following delegate method to your class:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && ! shouldAllowPan) {
return NO;
}
return YES;
}
Then add either a property or ivar which will track if the pan should be allowed to begin (see method above). In my case BOOL shouldAllowPan.
Set the BOOL to NO in your init or viewDidLoad. Inside your longPress handler set the BOOL to YES. I do it like this:
- (void) longPressHandler: (UILongPressGestureRecognizer *) gesture {
if(UIGestureRecognizerStateBegan == gesture.state) {
shouldAllowPan = NO;
}
if(UIGestureRecognizerStateChanged == gesture.state) {
shouldAllowPan = YES;
}
}
Inside the panHandler I do a check on the BOOL:
- (void)panHandler:(UIPanGestureRecognizer *)sender{
if(shouldAllowPan) {
// do your stuff
}
And finally reset the BOOL within the panHandler:
else if(sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateFailed || sender.state == UIGestureRecognizerStateCancelled) {
shouldAllowPan = NO;
}
And then go grab a beer to congratulate yourself. ;)
I found a solution:
This UIGestureRecognizerDelegate method does exactly what I looked for:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
Andy B's approach in Swift,
Add the UIGestureRecognizerDelegate delegate to the class
class ViewController: UIViewController, UIGestureRecognizerDelegate
Add a member variable
var shouldAllowPan: Bool = false
Add the gestures and need to add the pan gesture delegate to the VC. This is needed to fire off the shouldRecognizeSimultaneouslyWithGestureRecognizer and gestureRecognizerShouldBegin functions
// long press
let longPressRec = UILongPressGestureRecognizer(target: self, action: "longPress:")
yourView.addGestureRecognizer(longPressRec)
// drag
let panRec = UIPanGestureRecognizer(target: self, action: "draggedView:")
panRec.delegate = self
yourView.addGestureRecognizer(panRec)
Allow simultaneous gestures
func gestureRecognizer(UIGestureRecognizer,
shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
// println("shouldRecognizeSimultaneouslyWithGestureRecognizer");
return true
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
// We only allow the (drag) gesture to continue if it is within a long press
if((gestureRecognizer is UIPanGestureRecognizer) && (shouldAllowPan == false)) {
return false;
}
return true;
}
Inside the long press handler:
func longPress(sender: UILongPressGestureRecognizer) {
if(sender.state == .Began) {
// handle the long press
}
else if(sender.state == .Changed){
shouldAllowPan = true
}
else if (sender.state == .Ended) {
shouldAllowPan = false
}
}
For combinate more gesture :
Create a local variable var shouldAllowSecondGesture : Bool = false
Create the two recognizer
let longPressRec = UILongPressGestureRecognizer(target: self, action: #selector(self.startDrag(sender:)))
cell.addGestureRecognizer(longPressRec)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(sender:)))
cell.isUserInteractionEnabled = true
cell.addGestureRecognizer(panGestureRecognizer)
Extension your VC and implement GestureRecognizerDelegate for implemented this method.
extension YourViewController : UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// We only allow the (drag) gesture to continue if it is within a long press
if((gestureRecognizer is UIPanGestureRecognizer) && (shouldAllowPan == false)) {
return false
}
return true
}
#objc func startDrag(sender:UIPanGestureRecognizer) {
if(sender.state == .began) {
// handle the long press
}
else if(sender.state == .changed){
shouldAllowPan = true
}
else if (sender.state == .ended) {
shouldAllowPan = false
}
}
Read the "Subclassing Notes" section of Apple's UIGestureRecognizer Class Reference at:
https://developer.apple.com/library/prerelease/tvos/documentation/UIKit/Reference/UIGestureRecognizer_Class/
I solved this issue by implementing the desired functionality of the "action: Selector?" func of the UIPanGestureRecognizer within the "action: Selector?" func for the UILongPressGestureRecognizer.
As 'UILongPressGestureRecognizer' has no member 'translation', I calculated the translation by saving the position of the original touch and them extracting it from the actual touch position.
// in target class
var initialTouchX : CGFloat
var initialTouchX : CGFloat
// in the #objc func for the UILongPressGestureRecognizer
if sender.state == .began {
initialTouchX = sender.location(in: sender.view).x
initialTouchY = sender.location(in: sender.view).y
}
let translation = CGVector(dx: sender.location(in: sender.view).x - initialTouchX, dy: sender.location(in: sender.view).y - initialTouchY)
I use the long press gesture on a tab bar. But I only need the long press gesture for one particular tab bar item.
How can I solve this problem? Could I customize the long press gesture in tab bar?
Here's how I did it using Swift 3:
protocol MyTabControllerProtocol: class {
func tabLongPressed()
}
class MyTabController: UITabBarController {
func viewDidLoad() {
super.viewDidLoad()
viewControllers = [
// add your view controllers for each tab bar item
// NOTE: if you want view controller to respond to long press, then it should extend MyTabControllerProtocol
]
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(astroButtonItemLongPressed(_:)))
tabBar.addGestureRecognizer(longPressRecognizer)
}
func astroButtonItemLongPressed(_ recognizer: UILongPressGestureRecognizer) {
guard recognizer.state == .began else { return }
guard let tabBar = recognizer.view as? UITabBar else { return }
guard let tabBarItems = tabBar.items else { return }
guard let viewControllers = viewControllers else { return }
guard tabBarItems.count == viewControllers.count else { return }
let loc = recognizer.location(in: tabBar)
for (index, item) in tabBarItems.enumerated() {
guard let view = item.value(forKey: "view") as? UIView else { continue }
guard view.frame.contains(loc) else { continue }
if let nc = viewControllers[index] as? UINavigationController {
if let vc = nc.viewControllers.first as? MyTabControllerProtocol {
vc.tabLongPressed()
}
} else if let vc = viewControllers[index] as? MyTabControllerProtocol {
vc.tabLongPressed()
}
break
}
}
}
You can subclass UITabBarController and add a UILongPressGestureRecognizer to it's tabBar. Acting as the delegate of the gesture recognizer will allow you to be selective over when it will detect a long press. Since the tab bar item will be selected as soon as the user touches it you can use the selectedItem property to perform this check.
#interface TabBarController () <UIGestureRecognizerDelegate>
#property (nonatomic, strong) UILongPressGestureRecognizer *longPressRecognizer;
#end
#implementation TabBarController
- (void)viewDidLoad {
[super viewDidLoad];
self.longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(recognizerFired:)];
self.longPressRecognizer.delegate = self;
[self.tabBar addGestureRecognizer:self.longPressRecognizer];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
// This will ensure the long press only occurs for the
// tab bar item which has it's tag set to 1.
// You can set this in Interface Builder or in code
// wherever you are creating your tabs.
if (self.tabBar.selectedItem.tag == 1) {
return YES;
}
else {
return NO;
}
}
- (void)recognizerFired:(UILongPressGestureRecognizer *)recognizer {
// Handle the long press...
}
#end
Here is a solution in swift 5 :
Add longpress Gesture recognizer to the "Entire" tabbar using storyboard or code..
and Don't forget to let your ViewController be its delegate .. and implement the delegate method below
to check if the incoming touch is inside "one" of your tabbar subViews .. if yes return true ,, else return false ..
here are the code that will let the recognizer fire only when we longPress on the first tab:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view?.isDescendant(of: tabBar.subviews[1]) == true {return true}
return false
}
Note: tabbar.subviews array count is number of the items + 1 which is the background of the tabbar .. so if you want the view of the first item you can fint it and index 1 not 0
I did this by getting the specific tabBarItem's view that user can interact and simply added the long press gesture to that. With that way you do not have to write any protocols or subclass the TabBarViewController.
let longPressGestureRecognizer = UILongPressGestureRecognizer.init(target: self, action: #selector(longTap(_:)))
longPressGestureRecognizer.minimumPressDuration = 1.0
self.tabBarController?.orderedTabBarItemViews()[0].addGestureRecognizer(longPressGestureRecognizer)
And as for getting the tabBarItemViews :
extension UITabBarController {
func orderedTabBarItemViews() -> [UIView] {
let interactionViews = tabBar.subviews.filter({$0.isUserInteractionEnabled})
return interactionViews.sorted(by: {$0.frame.minX < $1.frame.minX})
}
P.S. : The viewController, namely "self" is the first item for the tabBarController.
If you just need to recognize a long press on one of the tabBar items, you can do this in the corresponding viewController's viewDidLoad method:
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget: self action: #selector(handleLongPress:)];
[self.tabBarController.tabBar addGestureRecognizer: longPressGesture];
And then:
- (void)handleLongPress:(UILongPressGestureRecognizer *) recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
UITabBar *tabBar = ((UITabBar* )recognizer.view);
if (tabBar.selectedItem == self.tabBarItem) {
doSomethingVeryExciting();
}
}
}
This won't fire if you just switch tabs.
I have searchBar that presents the keyboard when a user taps the searchBar and disables the keyboard when the user taps outside.
However, the tap gesture interacts with the tableView content. How can I disable interaction with the tableView while the keyboard is present?
override func viewDidLoad() {
super.viewDidLoad()
// dismiss keyboard if tapped outside of search
let tapGesture = UITapGestureRecognizer(target: self, action: Selector("hideKeyboard"))
tapGesture.cancelsTouchesInView = true //false doesn't work
tableView.addGestureRecognizer(tapGesture)
}
func hideKeyboard() {
searchBar.resignFirstResponder()
}
Add a transparent UIView when keyboard is present and remove it when keyboard is dismissed.
var searchBackgroundView = UIView()
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
searchBackgroundView = UIView(frame: CGRectMake(tableView.frame.origin.x, tableView.frame.origin.y, tableView.frame.width, tableView.frame.height))
tableView.addSubview(searchBackgroundView)
}
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
searchBackgroundView.removeFromSuperview()
}
Two quick ideas:
present a transparent button over the entire scrollview instead of using the gesture recognizer, this would catch the events and block interaction with the scroll (table) view
store-off the gesture recognizers on the scrollview class and restore them when you are done.
You can try it by checking whether the searchController is active or not. Just like..
if searchController.active {
tableView.userInteractionEnabled = false
}
else {
tableView.userInteractionEnabled = true
}
Or another way that you choose :
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
self.tableView.userInteractionEnabled = false
}
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
tableView.userInteractionEnabled = true
}
I am trying to write a single window timer application, where when the user presses the start button I want it to show another view controller with countdown etc. I'm also using story board in Xcode, where I have got a segue which connects the start button and the second view controller. However, there are only three different styles i.e. modal, sheet, and pop-over. I want to replace the first view controller the second one in the window. I cannot find a way to do that. I tried using a custom style for the segue, and in that use presentViewController: animator: method but I cannot figure out what to send as the argument for the animator:.
What is the simplest/proper way to transition from one view controller to the other in one window and vice versa?
Also in the storyboard when I select a view controller it shows an attribute called "Presentation" which can be multiple and single, what do those represent?
I think the simplest way is that swapping contentViewController of NSWindow.
// in NSViewController's subclass
#IBAction func someAction(sender: AnyObject) {
let nextViewController = ... // instantiate from storyboard or elsewhere
if let window = view.window where window.styleMask & NSFullScreenWindowMask > 0 {
// adjust view size to current window
nextViewController.view.frame = CGRectMake(0, 0, window.frame.width, window.frame.height)
}
view.window?.contentViewController = nextViewController
}
This is option #1.
If you want to use segue, create custom one and set it to segue class with identifier in IB.
class ReplaceSegue: NSStoryboardSegue {
override func perform() {
if let fromViewController = sourceController as? NSViewController {
if let toViewController = destinationController as? NSViewController {
// no animation.
fromViewController.view.window?.contentViewController = toViewController
}
}
}
}
This is option #2.
Last option is using presentViewController:animator: of NSViewController. The code below is custom NSViewControllerPresentationAnimator for dissolve animation.
class ReplacePresentationAnimator: NSObject, NSViewControllerPresentationAnimator {
func animatePresentationOfViewController(viewController: NSViewController, fromViewController: NSViewController) {
if let window = fromViewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
fromViewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
viewController.view.alphaValue = 0
window.contentViewController = viewController
viewController.view.animator().alphaValue = 1.0
})
}
}
func animateDismissalOfViewController(viewController: NSViewController, fromViewController: NSViewController) {
if let window = viewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
viewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
fromViewController.view.alphaValue = 0
window.contentViewController = fromViewController
fromViewController.view.animator().alphaValue = 1.0
})
}
}
}
Then present VC like this.
#IBAction func replaceAction(sender: AnyObject) {
let nextViewController = ... // instantiate from storyboard or elsewhere
presentViewController(nextViewController, animator: ReplacePresentationAnimator())
}
For dismissal, call presentingViewController's dismissViewController: in the presented VC.
#IBAction func dismissAction(sender: AnyObject) {
presentingViewController?.dismissViewController(self)
}
Swift4 Version
class ReplacePresentationAnimator: NSObject, NSViewControllerPresentationAnimator {
func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController) {
if let window = fromViewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
fromViewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
viewController.view.alphaValue = 0
window.contentViewController = viewController
viewController.view.animator().alphaValue = 1.0
})
}
}
func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController) {
if let window = viewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
viewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
fromViewController.view.alphaValue = 0
window.contentViewController = fromViewController
fromViewController.view.animator().alphaValue = 1.0
})
}
}
}
Hope this help.
If you have one parent view controller, you can assign child view controllers to it, and use the transition method. Example code, to be placed in viewDidLoad of the parent view controller:
if let firstController = self.storyboard?.instantiateController(withIdentifier: "firstController") as? NSViewController {
firstController.view.autoresizingMask = [.width, .height]
firstController.view.frame = self.view.bounds
self.addChild(firstController)
self.view.addSubview(firstController.view)
}
if let secondController = self.storyboard?.instantiateController(withIdentifier: "secondController") as? NSViewController {
self.addChild(secondController)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if let firstController = self.children.first, let secondController = self.children.last {
self.transition(from: firstController, to: secondController, options: .crossfade, completionHandler: nil)
}
}
It's essential that the view of the first child controller is being added as sub view to the view of the parent controller, otherwise the transition method doesn't work.
In the example above, a storyboard is used with one main view controller (= self), one child view controller with storyboard ID "firstController', and another child view controller with storyboard ID "secondController'