Disable TableView Interaction when Keyboard Present - objective-c

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
}

Related

SWRevealViewController closes when opened beyond 'snappoint'

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)
}
}

Long Press & Tap Recognizer for multiple UIButtons

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).

Handle long press on a single UITabBarItem

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.

IOS PopViewController unexpected animation

While back button pressed in my viewcontroller animation problem occurs.The problem is in the video at the below link:
app.bowerchat.com/images/117_1438022757542.mp4
- (IBAction)backChatBtnPressed:(id)sender
{
[self.navigationController popViewControllerAnimated:NO];
}
You can fix it by setting "clipsToBounds" property to YES (true) on base view of your view controller with messages:
Probably in viewWillAppear:
\\ View controller with messages (Swift)
override viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.view.clipsToBounds = true
}

Hiding the master view controller with UISplitViewController in iOS8

I have an iOS7 application, which was based on the Xcode master-detail template, that I am porting to iOS8. One area that has changed a lot is the UISplitViewController.
When in portrait mode, if the user taps on the detail view controller, the master view controller is dismissed:
I would also like to be able to programmatically hide the master view controller if the user taps on a row.
In iOS 7, the master view controller was displayed as a pop-over, and could be hidden as follows:
[self.masterPopoverController dismissPopoverAnimated:YES];
With iOS 8, the master is no longer a popover, so the above technique will not work.
I've tried to dismiss the master view controller:
self.dismissViewControllerAnimated(true, completion: nil)
Or tell the split view controller to display the details view controller:
self.splitViewController?.showDetailViewController(bookViewController!, sender: self)
But nothing has worked so far. Any ideas?
Extend the UISplitViewController as follows:
extension UISplitViewController {
func toggleMasterView() {
let barButtonItem = self.displayModeButtonItem()
UIApplication.sharedApplication().sendAction(barButtonItem.action, to: barButtonItem.target, from: nil, forEvent: nil)
}
}
In didSelectRowAtIndexPath or prepareForSegue, do the following:
self.splitViewController?.toggleMasterView()
This will smoothly slide the master view out of the way.
I got the idea of using the displayModeButtonItem() from this post and I am simulating a tap on it per this post.
I am not really happy with this solution, since it seems like a hack. But it works well and there seems to be no alternative yet.
Use preferredDisplayMode. In didSelectRowAtIndexPath or prepareForSegue:
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
self.splitViewController?.preferredDisplayMode = .Automatic
Unfortunately the master view abruptly disappears instead of sliding away, despite the documentation stating:
If changing the value of this property leads to an actual change in
the current display mode, the split view controller animates the
resulting change.
Hopefully there is a better way to do this that actually animates the change.
The code below hides the master view with animation
UIView.animateWithDuration(0.5) { () -> Void in
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
I was able to have the desired behavior in a Xcode 6.3 Master-Detail Application (universal) project by adding the following code in the MasterViewController's - prepareForSegue:sender: method:
if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
The complete - prepareForSegue:sender: implementation should look like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow() {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
}
}
}
Using traitCollection may also be an alternative/supplement to displayMode in some projects. For example, the following code also works for a Xcode 6.3 Master-Detail Application (universal) project:
let traits = view.traitCollection
if traits.userInterfaceIdiom == .Pad && traits.horizontalSizeClass == .Regular {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
Swift 4 update:
Insert it into prepare(for segue: ...
if splitViewController?.displayMode == .primaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .primaryHidden
}
let completion: (Bool) -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .automatic
}
UIView.animate(withDuration: 0.3, animations: animations, completion: completion)
}
Modifying the answers above this is all I needed in a method of my detail view controller that configured the view:
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
Of course it lacks the grace of animation.
try
let svc = self.splitViewController
svc.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
My solution in the Swift 1.2
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
var screen = UIScreen.mainScreen().currentMode?.size.height
if (UIDevice.currentDevice().userInterfaceIdiom == UIUserInterfaceIdiom.Pad) || screen >= 2000 && UIDevice.currentDevice().orientation.isLandscape == true && (UIDevice.currentDevice().userInterfaceIdiom == .Phone){
performSegueWithIdentifier("showDetailParse", sender: nil)
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
} else if (UIDevice.currentDevice().userInterfaceIdiom == .Phone) {
performSegueWithIdentifier("showParse", sender: nil)
}
}
for iPad add Menu button like this
UIBarButtonItem *menuButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:#"burger_menu"]
style:UIBarButtonItemStylePlain
target:self.splitViewController.displayModeButtonItem.target
action:self.splitViewController.displayModeButtonItem.action];
[self.navigationItem setLeftBarButtonItem:menuButtonItem];
This work great with both landscape and portrait mode.
To programmatically close the popover vc you just need to force the button action like this
[self.splitViewController.displayModeButtonItem.target performSelector:appDelegate.splitViewController.displayModeButtonItem.action];
Very similar to the method by phatmann, but a bit simpler in Swift 5. And it's not technically a 'hack', as it is what the iOS doc suggested.
In your prepareForSegue or other methods that handle touches, in
let barButton = self.splitViewController?.displayModeButtonItem
_ = barButton?.target?.perform(barButton?.action)
According to Apple, the splitViewController's displayModeButtonItem is set up for you to display the master view controller in a way that suits your device orientation. That is, .preferHidden in portrait mode.
All there's to do is to press the button, programatically. Or you can put it in an extension to UISplitViewController, like phatmann did.