2

In my project I have few view controllers which are subclasses of UITableViewController, UIViewController, on each I want to implement this behavior:

When user taps outside of a text field it should dismiss the keyboard which was visible when user tapped inside it.

I can easily implement it by defining a tap gesture recognizer and associating a selector to dismiss the keyboard:

class MyViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        configureToDismissKeyboard()
    }

    private func configureToDismissKeyboard() {
        let tapGesture = UITapGestureRecognizer(target: self, action: "hideKeyboard")
        tapGesture.cancelsTouchesInView = true
        form.addGestureRecognizer(tapGesture)
    }

    func hideKeyboard() {
        form.endEditing(true)
    }
}

Since I have to implement same behavior in multiple view controllers, I am trying to identify a way to avoid using repetitive code in multiple classes.

One option for me is to define a BaseViewController, which is subclass of UIViewController, with all above methods defined within it and then subclass each of my view controller to BaseViewController. The problem with this approach is that I need to define two BaseViewControllers one for UIViewController and one for UITableViewController since I am using subclasses of both.

The other option which I am trying to use is - Protocol-Oriented Programming. So I defined a protocol:

protocol DismissKeyboardOnOutsideTap {
    var backgroundView: UIView! { get }
    func configureToDismissKeyboard()
    func hideKeyboard()
}

Then defined its extension:

extension DismissKeyboardOnOutsideTap {
    func configureToDismissKeyboard() {
        if let this = self as? AnyObject {
            let tapGesture = UITapGestureRecognizer(target: this, action: "hideKeyboard")
            tapGesture.cancelsTouchesInView = true
            backgroundView.addGestureRecognizer(tapGesture)
        }

    }

    func hideKeyboard() {
        backgroundView.endEditing(true)
    }

}

In my view controller I confirmed to the protocol:

class MyViewController: UITableViewController, DismissKeyboardOnOutsideTap {

    var backgroundView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // configuring background view to dismiss keyboard on outside tap
        backgroundView = self.tableView
        configureToDismissKeyboard()
    }
}

Problem is - above code is crashing with exception:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyProject.MyViewController hideKeyboard]: unrecognized selector sent to instance 0x7f88c1e5d700'

To avoid this crash I need to redefine hideKeyboard function within MyViewControllerclass, which is defeating my purpose of avoiding repetitive code :(

Please suggest if I am doing any thing wrong over here or is there any better way to implement my requirement.

Devarshi
  • 16,440
  • 13
  • 72
  • 125
  • It seems that, from MyViewController, you want to re-use the function hideKeyboard in your BaseViewControllers. But you did not declare that your MyViewController as subclass of BaseViewControllers. – Duyen-Hoa Apr 18 '16 at 08:16
  • Hey thanks for your reply but please note that using `BaseViewController` was the first option which I am trying to avoid because of reasons specified in my posted question. Currently I am trying to use `protocol-oriented programming` in place of it :) – Devarshi Apr 18 '16 at 08:19
  • If you use protocol-oriented programming, you have to implement the hideKeyboard in your MyViewController, and so have repetitive code. The protocol help you just define the "protocol" to be conformed. – Duyen-Hoa Apr 18 '16 at 08:47
  • Nope, we can extend protocol to provide default implementation of methods which we don't want to implement in our class, please see this link: https://www.raywenderlich.com/109156/introducing-protocol-oriented-programming-in-swift-2 – Devarshi Apr 18 '16 at 08:49
  • 1
    Ah, it's true. Very interesting! – Duyen-Hoa Apr 18 '16 at 09:14
  • I thought you will upvote me for that :P – Devarshi Apr 18 '16 at 11:23
  • 1
    http://stackoverflow.com/questions/36184912/swift-2-2-selector-in-protocol-extension-compiler-error Take a look at this thread. Maybe it can help you – Duyen-Hoa Apr 18 '16 at 12:03
  • Thanks will have a look. – Devarshi Apr 18 '16 at 12:21

3 Answers3

3

I think there are two possible problems: casting Self to AnyObject, and not using the new #selector syntax.

Instead of casting Self to AnyObject, define the protocol as a class-only protocol:

protocol DismissKeyboardOnOutsideTap: class {
    // protocol definitions...
}

Then use type constraints to apply your extension to only subclasses of UIViewController, and use Self directly in your code, rather than casting to AnyObject:

extension DismissKeyboardOnOutsideTap where Self: UIViewController {

    func configureToDismissKeyboard() {
        let gesture = UITapGestureRecognizer(target: self,
            action: #selector(Self.hideKeyboard()))
        gesture.cancelsTouchesInView = true
        backgroundView.addGestureRecognizer(gesture)
    }

}

Edit: I remembered the other problem I ran into when doing this. The action argument for UITapGestureRecognizer is an Objective-C selector, but Swift extensions to classes aren't Objective-C. So I changed the protocol to an @objc protocol, but that was a problem because my protocol included some Swift optionals, and it also introduced new crashes when I tried to implement the protocol in my VC.

Ultimately, I discovered an alternative method that didn't require an Objective-C selector as an argument; in my case, I was setting an NSNotification center observer.

In your case you might be better off simply extending UIViewController, as UITableViewController is a subclass, and subclasses inherit extensions (I think).

ConfusedByCode
  • 1,137
  • 8
  • 27
1

As pointed out by the user ConfusedByCode, even though a protocol oriented approach starts out as a nice one, it becomes un-Swifty as the compiler forces you to use the keyword @objc.

Therefore extending UIViewController is a better approach; at least in my opinion.

In order to maintain a clean project structure, create a file named UIViewController+DismissKeyboard.swift and paste the following content inside:

import UIKit

extension UIViewController {
  func configureKeyboardDismissOnTap() {
    let keyboardDismissGesture = UITapGestureRecognizer(target: self,
                                                                                                              action: #selector(self.dismissKeyboard))

    view.addGestureRecognizer(keyboardDismissGesture)
  }

  func dismissKeyboard() {
    // to be implemented inside your view controller(s) wanting to be able to dismiss the keyboard via tap gesture
  }
}

Afterwards, any one of your view controllers or other base classes from Apple inheriting from UIViewController such as UITableViewController, etc. for that matter, will have access to the method configureKeyboardDismissOnSwipeDown().

Therefore, merely calling configureKeyboardDismissOnSwipeDown() inside viewDidLoad in each of your view controllers will be automatically injecting a swipe down gesture to dismiss the keyboard.

One caveat still remaining is that, every view controller will be in need to call configureKeyboardDismissOnSwipeDown() separately. Unfortunately, this is a bummer as you can't simply override viewDidLoad() in your extension. Moreover, it's still a mystery to me as to why Apple haven't implemented this directly into the keyboard so that us developers would not need to code around it.

Anyways, this issue can be solved by a technique called Method Swizzling. Basically, it's overriding methods given by Apple so that their behaviour change at runtime. I won't go into any more detail about method swizzling any more than saying that it can be highly dangerous to play around as you would be modifying battle-tested, solid code provided by Apple and used by the system.

Afterwards, when you implement the above provided dismissKeyboard() method in a view controller where you want to be able to dismiss the keyboard, you'll be able to do so.

Can
  • 4,516
  • 6
  • 28
  • 50
0

TapGestureDismissable.swift

 @objc protocol TapGestureDismissable where Self: UIViewController {
        func hideKeyboard()
    }
    
    extension TapGestureDismissable {
        func configureTapGestureToDismissKeyboard() {
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard))
            tapGesture.cancelsTouchesInView = true
            view.addGestureRecognizer(tapGesture)
        }
    }

Inside your ViewController

extension myViewController: TapGestureDismissable {
    func hideKeyboard() {
        view.endEditing(true)
    }
}
Bassant Ashraf
  • 1,531
  • 2
  • 16
  • 23