Default constraint behaviors using Swift protocols
When developing an iOS app, you often need to adjust constraints in order to make sure that the keyboard doesn’t obscure any of the elements on screen. This is a common problem that is an easy but annoying fix.
A few days ago I came across this Medium article by Roy McKenzie about a Swift protocol called KeyboardAvoidable
that makes this process so much easier.
To sum it up quickly, any view controller that needs to adjust constraints in response to the keyboard hiding/showing just needs to conform to this protocol and then provide an array of constraints that need to be adjusted. The protocol extension has default methods that can be called when the controller is presented to add keyboard observers, and vice versa when the controller is dismissed.
With this short bit of code (available ion a Gist at the end of the post), all you would need to do to get this behavior is hook up outlets to the constraints of the views that need to be adjusted (probably the bottom constraint of a scroll view), stick them in an array, and implement the requirements of the protocol. When the keyboard is shown, your constraints will all be adjusted and animated. Sweet.
This ideas is so incredibly cool and useful and awesome and I plan to use it in every single project that requires this kind of behavior. What’s better is that the code is very easy to understand and modify for your specific needs.
After seeing this, it got me thinking about other ways protocols can be used to add default behaviors to views by injecting constraints. Every constraint you create in IB is of type NSLayoutConstraint
, which means we can create very generic and reusable code very easily. Natasha the Robot has a great post about protocol-oriented views in Swift that is similar to this, except she isn’t using constraints. In that post, she demonstrated adding animations like shaking to views using protocols so that this functionality can be reused.
In a project I am working on, I have some views inside of a view controller that need to be toggled between being hidden or shown when the user tap’s a button. In addition, I want the view to animate into and off of the screen when it is toggled.
Originally, I was just creating outlets to the constraints on these views that I wanted to collapse upon and then putting all of the toggling logic into a method in my view controller that would get called when a button was tapped. This led to a lot of repeated code. For every collapsible view in my view controller, I was essentially writing the exact same code with slight variations to change which constraint I was collapsing upon. After seeing Roy’s KeyboardAvoidable
protocol, I realized there was a much better way.
I started by making a protocol to represent collapsible views:
protocol Collapsible {
var collapseConstraint: NSLayoutConstraint? { get set }
func collapseView()
func showView()
func isCollapsed() -> Bool
}
The collapseConstraint
variable is the constraint that we want our view to collapse upon. I made this optional because there could be a situation where we want to use one of these views without the collapsing functionality, and in that case we just won’t set this variable and it will default to nil. The collapseView()
and showView()
methods are called when we tap our button, and the isCollapsed()
method just returns a bool letting us know what state we’re in.
Next, I created an extension for my Collapsible
protocol that defined my default implementations of those methods. I constrained my extension to only apply to UIView
objects:
extension Collapsible where Self: UIView {
func collapseView() {
collapseConstraint?.constant = -(self.frame.size.width)
}
func showView() {
collapseConstraint?.constant = 0
}
func isCollapsed() -> Bool {
return !(collapseConstraint?.constant == 0)
}
}
In my app, these view’s will be sliding into and off of the screen from the left or the right, so the collapseView()
method set’s the constant of the collapse constraint to the negative value of the width (that way the view is entirely off screen). The showView()
method sets the constant to 0, so that the view is pinned to the left or right edge. These methods will obviously need to be customized depending on which direction you want your view’s to collapse. If you wanted to get really fancy, you could set the direction as well as the constraint so that you can collapse in any direction with this one protocol.
The next step is just to create a view that conforms to the protocol, and declare our collapseConstraint
variable:
class MyView: UIView, Collapsible {
var collapseConstraint: NSLayoutConstraint?
}
In my app, I am putting my views into my controller using Interface Builder. So inside of my controller I just create an outlet to the view and to the constraint that I want to collapse upon (trailing for right edge or leading for left edge), and in viewDidLoad()
I set the collapseConstraint
variable:
class MyViewController: UIViewController {
@IBOutlet var myView: MyView!
@IBOutlet var myViewLeadingConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
myView.collapseConstraint = myViewLeadingConstraint
}
}
The only thing left to do is put in a method that gets called when you tap a button (or take some other action):
func buttonTapped() {
if myView.isCollapsed() {
myView.showView()
}
else {
myView.collapseView()
}
UIView.animateWithDuration(0.3) {
self.view.layoutIfNeeded()
}
}
I put in the animation block in order to make the constraint change animate over a given time.
Building protocols like these make creating repeated behaviors extremely simple, and IB constraints fit so perfectly into this method. I am using this technique in several places, and I highly recommend it.