FormationLayout and Protocol-Oriented Programming in Swift

My favourite talk of WWDC 2015 was Protocol-Oriented Programming in Swift. However, when Dave Abrahams says, “Swift is the first Protocol-Oriented Programming language”, I didn’t get it. Isn’t protocol in Swift the same as interface in other languages? Isn’t “Protocol-Oriented Programming” the same as one of the SOLID principles - Dependency inversion principle - “Depend upon Abstractions. Do not depend upon concretions”? It is not new at all.

Then in the later part of the talk I learned from Dave how powerful protocol is in Swift 2, especially Self Requirement and Protocol Extensions, and why we should use protocols instead of classes for abstraction. I felt excited and already Protocol-Oriented myself after watching the talk. I need to do something to check if I really am :)

So here comes FormationLayout, a Protocol-Oriented AutoLayout library in Swift 2. Now I will show you how I made it Protocol-Oriented and why it is good.

There are three main types in FormationLayout:

  • FormationLayout - Top level layout manager.
  • ViewFormation - Layout manager for one view. Created by FormationLayout.view().
  • GroupFormation - Layout manager for a group of views. Created by FormationLayout.group().

My first two goals:

  • The constraint factory methods of “formations” should be chainable.
  • The GroupFormation should be able to do the same things as ViewFormation does on each view in the group.
let layout = FormationLayout(rootView: view)
layout.view(v1).width(100).height(50)
layout.group(v2, v3, v4).width(100).height(50)

It is easy by using an abstract class.

class AbstractFormation {
    func width(value: CGFloat) -> Self { fatalError("abstract") }
    func height(value: CGFloat) -> Self { fatalError("abstract") }
}
class ViewFormation: AbstractFormation {
    // Create constraints in these methods then return self.
    override func width(value: CGFloat) -> Self { return self }
    override func height(value: CGFloat) -> Self { return self }
}
class GroupFormation: AbstractFormation {
    // Call same methods on each view in the group then return self.
    override func width(value: CGFloat) -> Self { return self }
    override func height(value: CGFloat) -> Self { return self }
}

Now I want the formations to make size of a view be the same as another.

let layout = FormationLayout(rootView: view)
layout.view(v1).width(v5).height(v5)
layout.group(v2, v3, v4).width(v5).height(v5)

Still easy. Add another two abstract methods in the abstract class and make the two formation types override them, right? Wait, how about the open/closed principle? I don’t want to modify all my classes every time I want new features!

Let’s try protocols instead.

protocol FormationTakesCGFloat {
    func width(value: CGFloat) -> Self
    func height(value: CGFloat) -> Self
}
extension ViewFormation: FormationTakesCGFloat {
    // Create constraints in these methods then return self.
    func width(value: CGFloat) -> Self { return self }
    func height(value: CGFloat) -> Self { return self }
}
extension GroupFormation: FormationTakesCGFloat {
    // Call same methods on each view in the group then return self.
    func width(value: CGFloat) -> Self { return self }
    func height(value: CGFloat) -> Self { return self }
}

protocol FormationTakesUIView {
    func width(targetView: UIView) -> Self
    func height(targetView: UIView) -> Self
}
//... extension ViewFormation & GroupFormation: FormationTakesUIView ...

Firstly, we now have different protocols and formation extensions that confirm to the protocols. When we need other forms of constraint factory methods just create new protocols and extensions. No changes to any exist code.

Secondly, methods return Self to make them chainable even though they are defined in different protocols or extensions.

That’s good. Now I want more methods that take constant value and UIView. So let’s add top() and bottom() to all the protocols and extensions …

Wait, we have Protocol Extensions in Swift 2. Let’s change our protocols:

protocol FormationTakesCGFloat {
    func attribute(attribute: NSLayoutAttribute, value: CGFloat) -> Self
}
extension ViewFormation: FormationTakesCGFloat {
    // Create constraints in this method then return self.
    attribute(attribute: NSLayoutAttribute, value: CGFloat) -> Self { return self }
}
extension GroupFormation: FormationTakesCGFloat {
    // Call same methods on each view in the group then return self.
    attribute(attribute: NSLayoutAttribute, value: CGFloat) -> Self { return self }
}

extension FormationTakesCGFloat {
    func width(value: CGFloat) -> Self {
        return attribute(.Width, value: value)
    }
    func height(value: CGFloat) -> Self {
        return attribute(.Height, value: value)
    }    
}

When we want more methods take CGFloat/UIView, create more Protocol Extensions.

extension FormationTakesCGFloat {
    func top(value: CGFloat) -> Self {
        return attribute(.Top, value: value)
    }
    func bottom(value: CGFloat) -> Self {
        return attribute(.Bottom, value: value)
    }    
}

Then both ViewFormation and GroupFormatoin will have top() and bottom() methods without any changes to exist code.

So this is how FormationLayout looks like now:

Methods are chainable

Even group only methods like hSpace() and vSpace():

layout.group(v1, v2, v3).size(100).hSpace(10).centerY(view)

Features are protocols

Steps to add a new feature (form of constraint factory methods):

  • Create a protocol.
  • Create extensions of the formations that support the new feature to confirm to the protocol.
  • Create protocol extensions for helper methods. All formations that confirm to the protocol will have the new methods.
  • No changes to any exist code!

Now I feel myself more Protocol-Oriented. Need to be better by watching Building Better Apps with Value Types in Swift :)