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.
What if we move these methods into an extension?
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:’)
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:
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.
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