【问题标题】:Using protocol extension to dismiss keyboard on outside tap使用协议扩展在外部点击时关闭键盘
【发布时间】:2016-04-18 08:03:22
【问题描述】:

在我的项目中,我有几个视图控制器,它们是 UITableViewController、UIViewController 的子类,每个我想实现这种行为:

当用户在文本字段之外点击时,它应该关闭当用户在其中点击时可见的键盘。

我可以通过定义一个轻击手势识别器并关联一个选择器来关闭键盘来轻松实现它:

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

由于我必须在多个视图控制器中实现相同的行为,我试图找出一种方法来避免在多个类中使用重复代码。

我的一个选择是定义一个BaseViewController,它是UIViewController 的子类,其中定义了所有上述方法,然后将我的每个视图控制器子类化为BaseViewController。这种方法的问题是我需要定义两个BaseViewControllers,一个用于UIViewController,一个用于UITableViewController,因为我使用了两者的子类。

我尝试使用的另一个选项是 - Protocol-Oriented Programming。所以我定义了一个协议:

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

然后定义它的扩展:

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

}

在我的视图控制器中,我确认了协议:

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

问题是 - 上面的代码因异常而崩溃:

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

为了避免这种崩溃,我需要在 MyViewControllerclass 中重新定义 hideKeyboard 函数,这违背了我避免重复代码的目的:(

如果我在这里做错了什么,或者有没有更好的方法来实现我的要求,请提出建议。

【问题讨论】:

  • 看来,从 MyViewController 中,您想在 BaseViewControllers 中重用函数 hideKeyboard。但是您没有将 MyViewController 声明为 BaseViewControllers 的子类。
  • 嘿,感谢您的回复,但请注意,由于我发布的问题中指定的原因,使用BaseViewController 是我试图避免的第一个选项。目前我正在尝试使用protocol-oriented programming 代替它:)
  • 如果你使用面向协议的编程,你必须在你的 MyViewController 中实现 hideKeyboard,所以有重复的代码。该协议可帮助您定义要符合的“协议”。
  • 啊,是真的。很有趣!
  • stackoverflow.com/questions/36184912/… 看看这个线程。也许它可以帮助你

标签: swift protocols uitapgesturerecognizer


【解决方案1】:

我认为有两个可能的问题:将Self 转换为AnyObject,以及不使用新的#selector 语法。

不要将Self 转换为AnyObject,而是将协议定义为仅类协议:

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

然后使用类型约束将您的扩展仅应用于 UIViewController 的子类,并在您的代码中直接使用Self,而不是强制转换为AnyObject

extension DismissKeyboardOnOutsideTap where Self: UIViewController {

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

}

编辑:我记得这样做时遇到的另一个问题。 UITapGestureRecognizeraction 参数是一个 Objective-C 选择器,但类的 Swift 扩展不是 Objective-C。所以我将协议更改为@objc 协议,但这是一个问题,因为我的协议包含一些 Swift 选项,并且当我尝试在我的 VC 中实现该协议时它还引入了新的崩溃。

最终,我发现了一种不需要 Objective-C 选择器作为参数的替代方法;在我的例子中,我设置了一个 NSNotification 中心观察者。

在您的情况下,您最好简单地扩展UIViewController,因为UITableViewController 是一个子类,而子类继承扩展名(我认为)。

【讨论】:

    【解决方案2】:

    正如用户 ConfusedByCode 所指出的,即使面向协议的方法一开始是一个不错的方法,但由于编译器强制您使用关键字 @objc,它变得不够 Swifty。

    因此扩展UIViewController 是一种更好的方法;至少在我看来。

    为了保持一个干净的项目结构,创建一个名为UIViewController+DismissKeyboard.swift的文件,并在里面粘贴以下内容:

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

    之后,您的任何一个视图控制器或来自 Apple 的其他基类从 UIViewController 继承,例如 UITableViewController 等,都将有权访问方法 configureKeyboardDismissOnSwipeDown()

    因此,只需在每个视图控制器中调用 configureKeyboardDismissOnSwipeDown() 内的 viewDidLoad 就会自动注入向下滑动手势以关闭键盘。

    还有一点需要注意的是,每个视图控制器都需要单独调用configureKeyboardDismissOnSwipeDown()。不幸的是,这很糟糕,因为您不能简单地在扩展程序中覆盖 viewDidLoad()。此外,为什么 Apple 没有直接在键盘中实现这一点,这样我们的开发人员就不需要围绕它编写代码,这对我来说仍然是个谜。

    无论如何,这个问题可以通过一种称为Method Swizzling 的技术来解决。基本上,它是 Apple 提供的重写方法,以便它们的行为在运行时发生变化。我不会更详细地讨论方法调配,只是说玩弄它可能非常危险,因为您将修改 Apple 提供并由系统使用的经过实战测试的可靠代码。

    之后,当您在希望能够关闭键盘的视图控制器中实现上述提供的dismissKeyboard() 方法时,您就可以这样做了。

    【讨论】:

      【解决方案3】:

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

      在你的 ViewController 中

      extension myViewController: TapGestureDismissable {
          func hideKeyboard() {
              view.endEditing(true)
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-09-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多