robdashnash robdashnash - 2 months ago 21
Swift Question

Avoiding an LSP violation when subclassing

In objective-C I could subclass a view controller like the following.

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


And override the table view variable and return a more specialised table view (i.e. a subclass of UITableView). I could then just cast the table view variable as and when I needed to. In Swift, this is a little trickier, as described in this post.

So how would you subclass this view controller, to create a class that has more speciality, whilst avoiding an LSP violation. Or is subclassing a view controller (and subclassing its variables), just too tricky?

EDIT: Regarding the suggestion that my post might be similar to this post - I'm focused more on handling code duplication rather than class vs struct.

To Clarify: I am specifically looking for an approach (or best practice) in Swift that allows me to write this code once, and use it in various view controller subclasses that utilise CustomTableView instances of their own.

Answer

What about the following:

1 Some generic protocol for getting the UITableView subclass.

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

2 Then a protocol for the Observer:

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

3 Then a extension for when the observer is also a table view container. So we can reuse the code:

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 And finally we just subclass the UIViewController in which we want that functionality. Note that tableView can be of any subclass of 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)
  }
}