【问题标题】:Avoiding an LSP violation when subclassing子类化时避免违反 LSP
【发布时间】:2017-02-11 03:30:01
【问题描述】:

在 Objective-C 中,我可以继承一个视图控制器,如下所示。

class KeyboardObserverViewController: UIViewController {

    var tableView: UITableView?

    init() {
        super.init(nibName: nil, bundle: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(KeyboardObserverViewController.keyboardDidShow(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(KeyboardObserverViewController.keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func keyboardDidShow(_ notification: Notification) {
        let rect = ((notification as NSNotification).userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
        if let tableView = tableView {
            let insets = UIEdgeInsetsMake(tableView.contentInset.top, 0, rect.height, 0)
            tableView.contentInset = insets
            tableView.scrollIndicatorInsets = insets
        }
    }

    func keyboardWillHide(_ notification: Notification) {
        if let tableView = tableView {
            let insets = UIEdgeInsetsMake(tableView.contentInset.top, 0, 0, 0)
            UIView.animate(withDuration: 0.3, animations: {
                tableView.contentInset = insets
                tableView.scrollIndicatorInsets = insets
            })
        }
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

并覆盖表视图变量并返回更专业的表视图(即 UITableView 的子类)。然后,我可以在需要时转换表视图变量。在 Swift 中,这有点棘手,如 this post 中所述。

那么你将如何子类化这个视图控制器,以创建一个具有更多特殊性的类,同时避免LSP violation。还是对视图控制器进行子类化(并对其变量进行子类化),太棘手了?

编辑:关于我的帖子可能类似于 this post 的建议 - 我更关注处理代码重复而不是类与结构。

澄清:我专门在 Swift 中寻找一种方法(或最佳实践),它允许我编写此代码一次,并在使用其 CustomTableView 实例的各种视图控制器子类中使用它拥有。

【问题讨论】:

  • 类可以在 Swift 中被子类化,就像在 ObjC 中一样。使用结构体,您可以使用协议,然后使用protocol extensions 为所有采用它的类型提供可用的属性和方法。
  • sketchyTech 您愿意提供一个更详细描述此问题的答案吗?
  • sketchyTech 我将编辑我的帖子以突出手头的问题,更清晰一点。你不是唯一一个讨论过这些观点的人。
  • 所以您正在寻找一种面向协议的方式来添加观察者和处理回调?或者它与 UITableView 有什么关系?请澄清
  • 你说“[在 Objective-C 中] 我可以在我需要的时候转换表格视图变量”。如果您愿意强制转换访问,您可以在 Swift 中执行相同的操作:if let specialTable = self.table as? TableSubclass {,并且您根本不需要覆盖该属性。这是你想要避免的吗?

标签: ios swift inheritance


【解决方案1】:

下面的呢:

1 一些用于获取UITableView 子类的通用协议。

protocol TableViewContainer {
  associatedtype T : UITableView
  var tableView : T? { get }
}

2 然后是 Observer 的协议:

protocol KeyboardEventsObserver {
  func registerKeyboardEvents()
  func keyboardDidShow(_ notification: Notification)
  func keyboardWillHide(_ notification: Notification)
}

3 当观察者也是一个表视图容器时的扩展。所以我们可以重用代码:

extension KeyboardEventsObserver where Self : TableViewContainer {

  func registerKeyboardEvents() {
    NotificationCenter.default.addObserver(forName: .UIKeyboardDidShow, object: nil, queue: nil) {
      notification in
      self.keyboardDidShow(notification)
    }
    NotificationCenter.default.addObserver(forName: .UIKeyboardWillHide, object: nil, queue: nil) {
      notification in
      self.keyboardWillHide(notification)
    }
  }

  func keyboardDidShow(_ notification: Notification) {
    let rect = ((notification as NSNotification).userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    if let tableView = tableView {
      let insets = UIEdgeInsetsMake(tableView.contentInset.top, 0, rect.height, 0)
      tableView.contentInset = insets
      tableView.scrollIndicatorInsets = insets
      tableView.backgroundColor = UIColor.red
    }
  }

  func keyboardWillHide(_ notification: Notification) {
    if let tableView = tableView {
      let insets = UIEdgeInsetsMake(tableView.contentInset.top, 0, 0, 0)
      UIView.animate(withDuration: 0.3, animations: {
        tableView.contentInset = insets
        tableView.scrollIndicatorInsets = insets
      })
      tableView.backgroundColor = UIColor.green
    }
  }
}

4 最后,我们只是将我们想要该功能的UIViewController 子类化。请注意,tableView 可以是 UITableView 的任何子类。

class MyCustomTableView : UITableView {

}

class SomeController : UIViewController, KeyboardEventsObserver, TableViewContainer {

  @IBOutlet var tableView: MyCustomTableView?

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    registerKeyboardEvents()
  }

  override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    NotificationCenter.default.removeObserver(self)
  }
}

【讨论】:

  • 我不喜欢在协议中使用计算属性,然后需要在实现的类中覆盖它,专门用于出口。你认为如果不同的开发人员会在你的项目中开发一个特性,他会理解这个需求吗?而不是仅仅从情节提要中添加您的相关出口?
  • @OhadM 问题清楚地表明他想使用 UITableView 的不同子类,这就是该协议具有关联类型的原因。您也可以将 IBoutlet 添加到该属性。
  • 很棒的帖子@FranMowinckel,看起来你在正确的轨道上。很少有顾虑:'registerKeyboardEvents' 函数有一个随机控制器的选择器,所以您大概是在建议我们在某处为这些选择器设置一个通用视图控制器?其次,我只是在操场上使用了这段代码,无法让它与自定义 UITableView 子类一起使用。此外,我想您实际上必须在视图控制器示例的 init 中调用“registerKeyboardEvents”,对吗?你测试过这段代码吗?
  • @robdashnash,你是对的。根据我使用 Swift 2.2 的经验,不可能使用抽象类中的方法添加观察者,这就是为什么你没有得到你想要的回调的原因。这就是为什么我的答案有不同的方法。也许在 Swift 3.0 中它确实有效?
  • @robdashnash 对不起,这只是一个快速的答案,现在它已经过测试并且可以工作了。
【解决方案2】:

如果您乐于在 Objective-C 的控制器子类中转换表格视图,您可以在 Swift 中同样这样做:

import Foundation

class Table : NSObject {

    var inset: CGFloat = 0
}

class NotifiedController : NSObject {

    var table: Table?

    override init() {}

    func didGetNotification() {

        self.table?.inset = 10
    }
}

class WishingTable : Table {

    var twinklingStarCount: Int = 0
}

class WishingController : NotifiedController {

    override init() {

        super.init()

        self.table = WishingTable()
    }

    func makeAWish() {

        if let wishingTable = self.table as? WishingTable {
            wishingTable.twinklingStarCount += 1
        }
    }
}

这根本不需要您覆盖该属性。

【讨论】:

  • 这太棒了!我很确定我这样做了,并且之前遇到过编译器错误。反正我当时一定是做错了什么。我很高兴我还是问了这个问题。感谢您,我发现这种方法在 Swift 中是合法的,并且我发现了面向协议的方法(由@FranMowinckel 回答提供),这也是一个很好的解决方案。再次感谢。
  • 很高兴我能帮上忙,@robdashnash。
【解决方案3】:

要支持子类化,试试这个:

class KeyboardObserverViewController: UIViewController {
    func keyboardAvoidingTableView() -> UITableView? {
        return nil
    }
    ...
}

class SomeViewController: KeyboardObserverViewController {
    var tableView: MyTableView? // <- your table view as usual
    override func keyboardAvoidingTableView() -> UITableView? {
        return self.tableView
    }
}

在一般情况下,您可以反过来做,我更喜欢这个作为您提到的子类化问题的一般解决方法:

class KeyboardObserverViewController: UIViewController {
    var tableView: UITableView?
    ...
}

class SomeViewController: KeyboardObserverViewController {
    init() {
        ....
        self.tableView = MyTableView()
    }
    var myTableView: MyTableView? {
        return self.tableView as? MyTableView
    }
}

虽然在这种特殊情况下,我看到keyboardAvoidingTableView(或代码中的 tableView)不是作为存储视图引用的变量,而是作为“使用键盘移动哪个表视图”。更重要的是,您可以将其设置为 UIScrollView,而不是在您的应用中假设更广泛的情况:

func keyboardAvoidingScrollView() -> UIScrollView?

【讨论】:

  • 感谢 hybridcattt(有趣的名字:D)。这有点hacky,但你不觉得吗?您现在有两个变量,它们声称的类型不同,但指向同一个引用。
  • 我认为你完全错过了 OP 的实际要求。
  • 对不起,hybridcatt,但这有很多问题。在不超过字符限制的情况下,我无法在此处输入评论。
  • @OhadM 我已经阅读了这个问题,我完全理解它。即使作者问这里是否有面向协议的解决方案,他也提到他一直在努力寻找使用子类的解决方案——这就是我的答案。
  • 不用担心hybridcatt。这一切都是由社区驱动的,所以发布您认为的解决方案,看看情况如何。
【解决方案4】:

作为使用泛型协议的替代方案,您可以直接在类定义中使用泛型。您仍然不能覆盖 view 属性:与协议一样,您必须在基类中提供额外的通用类型属性,但这实际上是额外代码的唯一开销。

这些是示例基类(从我的其他答案中重用)。控制器类是参数化的,第二个(计算的)属性提供表作为正确的子类类型。

import Foundation

class Table : NSObject {

    var inset: CGFloat = 0
}

class NotifiedController<SpecializedTable : Table> : NSObject {

    var table: Table?

    // Giving this a good name is a bit tricky...
    var specializedTable: SpecializedTable? {
        return self.table as? SpecializedTable
    }

    func didGetNotification() {

        self.table?.inset = 10
    }
}

现在可以定义子类:

class WishingTable : Table {

    var twinklingStarCount: Int = 0
}

class WishingController : NotifiedController<WishingTable> {

    override init() {

        super.init()

        self.table = WishingTable()
    }

    func makeAWish() {

        self.specializedTable?.twinklingStarCount += 1
    }
}

请注意,控制器子类在 inheritance 子句中声明其特化。

需要注意的是,这些类,因为它们使用 Swift 泛型,现在在 Objective-C 代码中不可用。但通用协议解决方案也是如此。

【讨论】:

  • 好东西。再次感谢!
猜你喜欢
  • 2016-03-28
  • 1970-01-01
  • 2013-06-15
  • 1970-01-01
  • 2023-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多