skip to Main Content

In the example below, is it reasonable to make TextViewController conform to UIScrollViewDelegate instead of UITextViewDelegate and then cast the text view as a scroll view (or maybe declare it as a UIScrollView)? If not how would you suggest doing this?

I don’t want to make the code look like TextViewController could conform to UITextViewDelegate because that would be confusing. I assume that the cast would fail if a non-optional method were added to UITextViewDelegate. Also, private/fileprivate extensions are not allowed, but that would reduce my concern.


class TextViewController: UIViewController {

    let textView = UITextView()

    override func viewDidLoad() {
        textView.delegate = self // Error
        (textView as? UIScrollView)?.delegate = self // OK
    }
}

extension TextViewController: UIScrollViewDelegate
{

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        doSomethingOnScroll()
    }
}
extension UIScrollViewDelegate where Self: UIViewController {

    func doSomethingOnScroll() {
        // Do something.
    }
}

I did not create my own protocol in this example to make the code more concise.

2

Answers


  1. I do not recommend this. It kind of works (though it throws a warning), but it’s more confusing than what it replaces. TextViewController can definitely still implement UITextViewDelegate methods and they’ll be called; they just have to be marked @objc. That’s asking for subtle bugs as the code evolves. As others (or yourself, as you forgot that you did this), try to add UITextViewDelegate methods, they may or may not be called depending on how their added (in Swift or ObjC, or with or without the @objc). You can assume that’ll never happen, but "this makes future bugs easier to create" is the risk.

    It’s not quite clear why you’re trying to hide the conformance, but there are several ways to do so if it’s important.

    The most straightforward is to make textView itself a UIScrollView:

    let _textView: UIScrollView = UITextView()
    var textView: UITextView { _textView as! UITextView }
    
    ...
    
    _textView.delegate = self
    

    (If you made textView private and don’t need UITextView access, this could be even simpler.)

    Alternately, you can could create an internal private delegate for the UITextView so it’s not exposed. It’s not clear how exposing UITextViewDelegate matters more than exposing UIScrollViewDelegate. But if you want to hide these details, then I’d hide the whole thing in an internal type.

    That said, your question was for advice. The advice is not to do this. If the question is "will this work," the answer is yes, except it throws a warning.

    Login or Signup to reply.
  2. (Adding an answer because this is too long for comments.)

    To add some more explicit detail about why what you’re doing is a bad idea:

    The situation you’re encountering here is an Objective-C-ism that normally isn’t allowed in Swift; specifically, you’re dealing here with a property on a subtype (UITextView : UIScrollView) that is more specific than its supertype’s property (UITextViewDelegate : UIScrollViewDelegate). This violates the Liskov substitution principle, but Objective-C’s type system is weak enough to happily allow this.

    You already implicitly call this out, but briefly for readers: UIScrollView‘s delegate property allows any UIScrollViewDelegate-conforming object to be assigned to it; after all, anything that conforms to the protocol will satisfy UIScrollView‘s need for information about how to handle events. UITextView, however, overrides this property, accepting only objects which conform to the more specific UITextViewDelegate protocol, since it has more questions to ask the delegate about how to handle various scenarios. Under normal subtyping rules (and when the Liskov substitution principle holds), if you take an object a: A, and cast it to its superclass B, all of its properties should hold exactly as they were… except this is not true for UITextView. When you cast an instance of UITextView to UIScrollView, its delegate property suddenly becomes more general, meaning that you can assign a UIScrollViewDelegate which is not a UITextViewDelegate to the delegate property (since, again, UIScrollView only cares about things that UIScrollViewDelegate can tell it). If UITextViewDelegate had required methods, this would be immediately problematic, because at runtime, the text view would attempt to call a method on the delegate that it doesn’t know how to answer.

    You mention that

    I assume that the cast would fail if a non-optional method were added to UITextViewDelegate

    but it’s important to stress that this is not true. UITextView inherits from UIScrollView, and so textView as? UIScrollView will unconditionally succeed — it simply maintains Objective-C’s behavior when accessing the properties on the supertype, as if they were statically-typed properties. What this means is that (textView as? UIScrollView)?.delegate will always allow you to assign a UIScrollViewDelegate object to the property, even if at runtime, UITextView will call methods on it that don’t exist; although this can’t reasonably happen (because existing delegate objects couldn’t implicitly satisfy the requirement), if UITextViewDelegate did get non-optional method requirements, you would simply crash at runtime.

    In your very specific case, this might work, but this is very unsafe to do in the general case.


    For what it’s worth, if you try to recreate this scenario in pure Swift, the compiler won’t let you, for good reason:

    protocol SuperDelegate {}
    protocol SubDelegate: SuperDelegate {}
    
    class SuperClass {
        var delegate: SuperDelegate?
    }
    
    class SubClass: SuperClass {
        override var delegate: SubDelegate?
        // ^ error: property 'delegate' with type '(any SubDelegate)?' cannot override a property with type '(any SuperDelegate)?'
    }
    

    Swift doesn’t allow overriding properties with ones of different types at all to prevent this scenario, and similarly prevents inversions of method parameters and return values.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search