Your Delegation Methods Might Not Be Called In Swift 3

One of the most obvious changes in Swift 3 is its naming convention. Apple also renamed lots of methods when releasing Xcode 8.

To make new Swift APIs to be compatible with existing Objective-C APIs, Swift compiler converts Swift 3 methods to corresponding Objective-C selectors, but sometimes such conversion does not happen.

The worst case is, you implement a delegation method, but it is never be called, and you do not know why.

Naming in Swift 3

Swift 3 tries to reduce redundancy. In Objective-C or Swift earlier than Swift 3, we used to include the names of parameters in a method’s body, but now lots of them are removed.

Apple updated its API guideline and renamed lots of methods in the Swift 3 interface of Cocoa and Cocoa Touch framework, to follow the convention of Swift 3. Apple changed not only basic methods, but also delegation methods defined in protocols.

For example, we may a delegation method in Objective-C whose selector is “collectionView:layout:sizeForItemAtIndexPath:”, and in Swift 2, it is

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize

“IndexPath” appears for three times. Swift 3 sees it as a redundancy and should be removed. So, in Swift 3, it becomes to:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize

If you implement a protocol in the body of a class, the conversion between naming of Swift 3 and Objective-C bridge works perfectly. However, if you declare that a class confronting a protocol, but you implement the protocol in a Swift extension, Swift compiler does not do such conversion.

Delegation Methods in Swift Extensions

Let us take a look. We have a UIViewController subclass here, it has a UIColllectionView, and it needs to confront to UICollectionViewDataSource and UICollectionViewDelegateFlowLayout protocols. It looks perfect right now.

source1

What if we move these methods into an extension?

source2

There are several errors raised. It says:

Objective-C method ‘collectionView:layout:sizeForItemAt:’ provided by method ‘collectionView(_:layout:sizeForItemAt:)’ does not match the requirement’s selector (‘collectionView:layout:sizeForItemAtIndexPath:’)

and

Objective-C method ‘collectionView:cellForItemAt:’ provided by method ‘collectionView(_:cellForItemAt:)’ does not match the requirement’s selector (‘collectionView:cellForItemAtIndexPath:’

To make Swift compiler happy, you have to use “@objc” annotations to specify Objective-C selectors:

source3

Subclassing and Extensions

Here comes the case we met a couple weeks ago. We created a superclass KKViewController, and we declared that it confront to both UICollectionViewDataSource and UICollectionViewDelegateFlowLayout protocols. We did not implement any UICollectionViewDelegateFlowLayout methods in KKViewController, since most of UICollectionViewDelegateFlowLayout’s methods are optional.

Then, we subclassed it, and the subclass was ViewController. We implements a delegation method in an extension of ViewController. The method’s name is following the convention of Swift 3, but is was never be called.

source4

We found two ways to solve the issue. One is to use “@objc” annotation once again, while another one is to rename it into

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize

Wait! It works, but it does not follow the API guideline, right?

One More Thing

Let us take a look at another delegation method of UICollection view. It is

func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool

If you implement the method in your app, your app may crash occasionally. Why? Because the indexPath parameter is defined non-null, but UIKit may pass nil to it. If you force unwrapping an variable whose value is actually nil, there will be an exception raised. There is a post on Swift.org about the topic.

To prevent your app from crashing, you should make the variable optional by naming the method as:

@objc(collectionView:canFocusItemAtIndexPath:) func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath?) -> Bool

Sigh.

About zonble

iOS Developer at KKBOX.
This entry was posted in Code, iOS, Mac and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *