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
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:(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.
(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
‘sdelegate
property allows anyUIScrollViewDelegate
-conforming object to be assigned to it; after all, anything that conforms to the protocol will satisfyUIScrollView
‘s need for information about how to handle events.UITextView
, however, overrides this property, accepting only objects which conform to the more specificUITextViewDelegate
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 objecta: A
, and cast it to its superclassB
, all of its properties should hold exactly as they were… except this is not true forUITextView
. When you cast an instance ofUITextView
toUIScrollView
, itsdelegate
property suddenly becomes more general, meaning that you can assign aUIScrollViewDelegate
which is not aUITextViewDelegate
to thedelegate
property (since, again,UIScrollView
only cares about things thatUIScrollViewDelegate
can tell it). IfUITextViewDelegate
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
but it’s important to stress that this is not true.
UITextView
inherits fromUIScrollView
, and sotextView 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 aUIScrollViewDelegate
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), ifUITextViewDelegate
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:
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.