Empowering Extensions in Swift 2: Protocols, Types and Subclasses (Xcode 7 beta 2; updated Swift 3, Xcode 8)


Type extensions with generic type parameters

Extensions are a convenient way of separating code, extending pre-existing types and adopting new protocols in Swift. But with the arrival of Swift 2 they've taken on a whole new set of powers.

I've already looked briefly at the extension of protocols (in Xcode 7 beta 1), but with the arrival of Xcode 7 beta 2 we can now perform even more magic.

First up

Consider this extension:
extension Array where T:UIView {
    var backgroundColors:[UIColor] {
        return self.filter{$0.backgroundColor != nil}.map{$0.backgroundColor!}
    }
}

Swift 3, Xcode 8

extension Array where Element:UIView {
    var backgroundColors:[UIColor] {
        return self.filter{$0.backgroundColor != nil}.map{$0.backgroundColor!}
    }
}
It extends arrays with a type of UIView to add a computed variable that is capable of returning all the colours used inside an array of views.

Tipping the scale

A second example here enables an array of views to be scaled.
extension Array where T:UIView {
     func proportionalScale(scale:CGFloat) -> [UIView] {
        let aTran = CGAffineTransformMakeScale(scale, scale)
        return self.map{$0.transform = aTran; return $0}
    }
}
It can be implemented in the following way:
let myView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let myArray = [myView]
let myHalfArray = myArray.proportionalScale(0.5)
myHalfArray[0].frame // {x 50 y 50 w 100 h 100}

// note: x and y changes because layer is anchored at the centre, to adjust centre set i.layer.anchorPoint value or add a translation

Swift 3, Xcode 8

extension Array where Element:UIView {
    func proportionalScale(scale:CGFloat) -> [UIView] {
        let aTran = CGAffineTransform(scaleX: scale, y: scale)
        return self.map{$0.transform = aTran; return $0}
    }
}

let myView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let myArray = [myView]
let myHalfArray = myArray.proportionalScale(scale: 0.5)
myHalfArray[0].frame // {x 50 y 50 w 100 h 100}

Creating order

There are many useful extensions that can be thought of:
extension Dictionary where Value:Comparable {
    var valuesOrdered:[Value] {
        return self.values.sort()
    }
}

extension Dictionary where Key:Comparable {
    var keysOrdered:[Key] {
        return self.keys.sort()
    }
}

["two":"monkey","one":"cat","three":"hamster"].keysOrdered  // ["one", "three", "two"]
In this example an ordered array of keys and values can be extracted from a Dictionary instance, as long as the inner types adopt the Comparable protocol.

Swift 3, Xcode 8

extension Dictionary where Value:Comparable {
    var valuesOrdered:[Value] {
        return self.values.sorted()
    }
}

extension Dictionary where Key:Comparable {
    var keysOrdered:[Key] {
        return self.keys.sorted()
    }
}

["two":"monkey","one":"cat","three":"hamster"].keysOrdered  // ["one", "three", "two"]

Rules of the game

The current rules of extending a generic type in this way is that the type being referenced after the where keyword must be a class or a protocol.
extension Array where T:IntegerType {
    func highestNOTValue() -> T? {
        return self.map{return ~$0}.sort().last
    }
    func lowestNOTValue() -> T? {
        return self.map{return ~$0}.sort().reverse().last
    }
    var NOTValues:[T] {
        return self.map{return ~$0}
    }
}
let bA:[UInt8] = [45,65,45,57]
bA.highestNOTValue() // 210
bA.lowestNOTValue() // 190
bA.NOTValues // [210, 190, 210, 198]
So what we've written here is acceptable but had we specified Int rather than IntegerType it would not be.

Swift 3, Xcode 8

extension Array where Element:Integer {
    func highestNOTValue() -> Element? {
        return self.map{return ~$0}.sorted().last
    }
    func lowestNOTValue() -> Element? {
        return self.map{return ~$0}.sorted().reversed().last
    }
    var NOTValues:[Element] {
        return self.map{return ~$0}
    }
}
let bA:[UInt8] = [45,65,45,57]
bA.highestNOTValue() // 210
bA.lowestNOTValue() // 190
bA.NOTValues // [210, 190, 210, 198]

A way around

There are ways around not being able to include generic types (after the where keyword) that are not classes or protocols:
protocol StringType {
    var characters: String.CharacterView { get }
}

extension String:StringType {
}

extension Array where T:StringType {
    func countArray() -> [Int] {
        return self.map{$0.characters.count}
    }
}

["one","two","three"].countArray()  // an array of string lengths
And inspired by Erica Sadun's conundrum (originating from Mike Ash) we could add a further method to the above extension:
func appendAll() -> String  {
   return String(self.map{$0.characters}.reduce(String.CharacterView(), combine: {$0 + $1}))
}
This method takes an array of strings and reduces the array to a single string.
["Hello ","Swift","!"].appendAll()  // Hello Swift!
We could go further and add a method to this extension that joins all strings using another string:
func joinAll(str:String) -> String {
    return String(str.characters.join(self.map{$0.characters}))
}
The possibilities that open up when the restrictions are circumvented appear encouraging, but the restrictions themselves, of not being able to use struct (and enum) types directly, might well be temporary (and be resolved in a future Xcode 7 beta), so I wouldn't try too hard to circumvent them.

Especially in this particular case where there are other options readily available:

Swift 3, Xcode 8

", ".join(["one","two","three"])  // "one, two, three"
"".join(["one","two","three"])  // "onetwothree"

["one","two","three"].reduce("",combine:{$0 + $1}) // "onetwothree"
protocol StringType {
    var characters: String.CharacterView { get }
}

extension String:StringType {
}

extension Array where Element:StringType {
    func countArray() -> [Int] {
        return self.map{$0.characters.count}
    }
    func appendAll() -> String  {
        return String(self.map{$0.characters}.reduce(String.CharacterView(), {$0 + $1}))
    }
}

["one","two","three"].countArray() // an array of string lengths
["Hello ","Swift","!"].appendAll()  // Hello Swift!

Class extensions

The extensions of generic types includes generic Class instances:
class GSuper<T> {
}

extension GSuper where T:IntegerType {
    func num() -> T {
        return 2
    }
}

class GSub:GSuper<Int> {
    func number() -> Int {
        return 4
    }
}

GSuper<Int>().num() // 2

GSub().num() // 2
GSub().number() // 4
And we see above how this plays out when subclassing is added to the mix.

Swift 3, Xcode 8

class GSuper<T> {
}

extension GSuper where T:Integer {
    func num() -> T {
        return 2
    }
}

class GSub:GSuper<Int> {
    func number() -> Int {
        return 4
    }
}

GSuper<Int>().num() // 2

GSub().num() // 2
GSub().number() // 4

Going even further with protocols

We can go even further and employ the Self keyword to help distinguish types that adopt a first protocol in addition to subsequent ones.
protocol FirstProtocol {
    
}

protocol SecondProtocol {
    
}

extension FirstProtocol where Self:SecondProtocol {
    func someMethod() {
        
    }
}

struct FirstStruct:FirstProtocol {
    
}

struct SecondStruct:FirstProtocol,SecondProtocol {
    
}

SecondStruct().someMethod()  // while a SecondStruct instance can access the extension method
FirstStruct()  // FirstStruct cannot because it only adopts FirstProtocol
You'll notice here that because FirstStruct only adopts FirstProtocol it is excluded from the refined requirements of the extension. (Thanks here to Erik Kerber's flying birds example on the Ray Wenderlich site for identifying this.)

Warnings

While there is nothing to prevent you creating extensions like the following:
extension Array where T:CollectionType {
    func myMethod() {
        
    }
}

extension Array where T:ExtensibleCollectionType {
    func myMethod() {
        
    }
}

Swift 3, Xcode 8

extension Array where Element:Collection {
    func myMethod() {
        
    }
}
extension Array where Element:RangeReplaceableCollection {
    func myMethod() {
        
    }
}
When you actually come to implement it, if the type internal to the array adopts both CollectionType and ExtensibleCollectionType protocols then it will raise an error and won't compile because it is unclear which myMethod() you wish to use.

Similarly, it would be almost pointless to write this:
extension Array where T:CollectionType {
    func myMethod() {
        
    }
}

extension Array where T:protocol<CollectionType, ExtensibleCollectionType> {
    func myMethod() {
        
    }
}
The only situation it wouldn't raise an error is if the type adopted ExtensibleCollectionType without adopting CollectionType.

Swift 3, Xcode 8

extension Array where Element:Collection {
    func myMethod() {
        
    }
}
extension Array where Element:Collection & RangeReplaceableCollection {
    func myMethod() {
        
    }
}

Conclusion

This has been a quick overview of what's possible with the new powers gained in Swift 2 extensions. I hope you found it helpful.

If you'd like to explore further, this blogpost is also available as an interactive playground.


Comments

  1. Did this make it into Swift 2 proper (as opposed to the betas). I only ever get "use of undeclared type T" with any of these examples, regardless of whether T is a class or not.

    ReplyDelete
  2. The solution appears to be to replace T with Generator.Element.

    ReplyDelete
    Replies
    1. Yes, in most cases T can be replaced with Element. I'll update the post soon once I've worked back through the code.

      Delete

Post a Comment