3

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.

Community
  • 1
  • 1
  • 1
    Classes can be subclassed in Swift as they can in ObjC. With structs you can use protocols and then [protocol extensions](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Extensions.html) to provide properties and methods available to all types that adopt it. – sketchyTech Oct 03 '16 at 08:34
  • sketchyTech would you like to contribute an answer describing this in more detail please ? –  Oct 03 '16 at 08:35
  • sketchyTech I will edit my post to highlight the issue at hand, a little more cleary. your not the only one who has discussed these points. –  Oct 03 '16 at 08:40
  • What is it about Swift that is "hacky" with regards to subclassing? You could implement this pretty much the same way in Swift. Yes, you can also use protocols to accomplish this task but they don't necessarily replace subclassing. –  Oct 03 '16 at 08:51
  • Without seeing some code to support your claim, I'm afraid I can't agree with you ColGraff. –  Oct 03 '16 at 08:55
  • Possible duplicate of [Subclassing v.s. Protocols](http://stackoverflow.com/questions/33525160/subclassing-v-s-protocols) –  Oct 03 '16 at 08:57
  • Not quite ColGraff. In my post, checkout the text highlighted in bold. Appreciate any feedback you got. This is definitely a pickle. –  Oct 03 '16 at 08:59
  • 1
    So you are looking for a protocol oriented way to add observers and handle the callbacks ? or it has something to do with UITableView ? Please clarify – OhadM Oct 03 '16 at 09:11
  • So your looking to extend the functionality of UITableView or create an abstract class (protocol oriented) of that logic ? A best practise approach is to use abstract classes but it depends on what you want/need – OhadM Oct 03 '16 at 09:22
  • I need a table view, of which is positioned as a subclass of a view in a view controller, to respond to keyboard movements as described in the sample code. –  Oct 03 '16 at 09:24
  • 1
    You say "_[In Objective-C]_ I could then just cast the table view variable as and when I needed to". If you're willing to cast accesses, you can do the same in Swift: `if let specialTable = self.table as? TableSubclass {`, and you wouldn't need to override the property at all. Is that what you're trying to avoid? – jscs Oct 03 '16 at 17:41
  • There's quite a lot of discussion on this thread. I don't think I can provide any more clarity on what the issue is at the point Josh. –  Oct 03 '16 at 17:54

4 Answers4

2

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)
  }
}
FranMowinckel
  • 4,233
  • 1
  • 30
  • 26
  • I dislike the approach when using computed properties inside a protocol and then having the need to override it inside the implemented class specially for outlets. Do you think that if a different developer will develop a feature in your project, will he understand the need for that ? Instead of just adding your relevant outlet from the storyboard ? – OhadM Oct 03 '16 at 11:29
  • @OhadM The question clearly states that he wants to use different subclasses of UITableView that's the reason for that protocol with the associated type. And also you can just add IBoutlet to that property. – FranMowinckel Oct 03 '16 at 11:34
  • Great post @FranMowinckel it looks like you are on the right track with this. Few concerns: the 'registerKeyboardEvents' function has selectors to a random controller, so presumably you're suggesting we have a generic view controller somewhere for these selectors? Secondly, I just used this code in a playground and couldn't get it working with a custom UITableView subclass. Further, I suppose you would actually have to called 'registerKeyboardEvents' in the init of your view controller example, right? Have you tested this code? –  Oct 03 '16 at 12:10
  • @robdashnash, You are correct. From my experience using Swift 2.2 it is not possible to add an observer using a method from an abstract class and this is why you don't get the callbacks you want. This is why my answer has a different approach. Perhaps in Swift 3.0 it does work ? – OhadM Oct 03 '16 at 13:35
  • @robdashnash Sorry, it was just a quick answer, now it's tested and working. – FranMowinckel Oct 03 '16 at 14:39
  • This is exactly what I was looking for. So glad someone understood what I was trying to say! Thanks @FranMowinckel –  Oct 03 '16 at 18:15
1

If you're happy casting the table view in controller subclasses in Objective-C, you can do so equally in 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
        }
    }
}

This does not require you to override the property at all.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • This is great! I was so sure I did this and got a compiler error once before. Anyway, I must have done something wrong back then. I'm glad I asked the question anyway. I have discovered this approach is legal in Swift, thanks to you, and I have discovered the protocol orientated approach (provided by @FranMowinckel answer), which is also a great solution. Thanks again. –  Oct 03 '16 at 19:42
  • Glad I could help, @robdashnash. – jscs Oct 03 '16 at 19:43
0

To support subclassing, try this:

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

In a general situation, you could do it the other way around, and I'd prefer this one as the general workaround to the subclassing problem you mentioned:

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

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

Although in this particular situation I see keyboardAvoidingTableView (or tableView in your code) not as a variable to store your view reference, but as a "which table view to move with the keyboard". Even more, you could make it a UIScrollView instead to supposed wider range of situations in your app:

func keyboardAvoidingScrollView() -> UIScrollView?
hybridcattt
  • 3,001
  • 20
  • 37
  • Thanks hybridcattt (funny name :D). This is a bit hacky though don't you think? You now have two variables, both of which are different in the type they claim to be, but point at the same reference. –  Oct 03 '16 at 09:07
  • I think you are totally missing the actual request of the OP. – OhadM Oct 03 '16 at 09:32
  • Thanks. You now have two variables, both of which are the same type and point at the same reference. And neither variable gives you access the custom logic within 'MyTableView', because they are both declared as UITableView. Are you sure this doesn't fire compiler warnings when you try to assign MyTableView to UITableView? –  Oct 03 '16 at 09:34
  • @robdashnash Oops that was obviously a silly copy-paste error. Check now :) – hybridcattt Oct 03 '16 at 09:36
  • 1
    Sorry hybridcatt, but there is quite a lot wrong with this. More than I can type into a comment here without exceeding the character limit. –  Oct 03 '16 at 09:38
  • 1
    @OhadM I have read the question and I understand it perfectly. And even though the author asked if there's a protocol-oriented solution here, he also mentioned that he's struggled with finding a solution using subclassing - and that's what my answer is about. – hybridcattt Oct 03 '16 at 09:38
  • @robdashnash Second, general approach works. In the first one, I've forgot to account that you can't override the var like that. Using my another suggestion to use a method works well, so I just left that in the answer. Sorry I didn't check if it was building before replying ;) If you are not happy with the solution that's fine, hope it helps you get more thoughts on the subclassing part. – hybridcattt Oct 03 '16 at 09:47
  • 1
    No worries hybridcatt. It's all community driven so post what you think is a solution and see how things go. –  Oct 03 '16 at 09:48
0

As an alternative to using a generic protocol, you can use generics directly in your class definitions. You still can't override the view property: as with the protocol, you must provide an extra, generically-typed property in the base class, but that's really the only overhead as far as extra code.

These are example base classes (reused from my other answer). The controller class is parameterized, and the second (computed) property provides the table as the correct subclass type.

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

Now subclasses can be defined:

class WishingTable : Table {

    var twinklingStarCount: Int = 0
}

class WishingController : NotifiedController<WishingTable> {

    override init() {

        super.init()

        self.table = WishingTable()
    }

    func makeAWish() {

        self.specializedTable?.twinklingStarCount += 1
    }
}

Note that the controller subclass declares its specialization in the inheritance clause.

The one caveat is that these classes, since they use Swift generics, are now unavailable in Objective-C code. But that's true of the generic protocol solution as well.

jscs
  • 63,694
  • 13
  • 151
  • 195