Let’s create a Tinder-like swipe using NSLayoutAnchors, custom views and protocol extensions.

If you are a junior developer and you are asking yourself what is the best way to handle the UI of your application and you are interested in knowing what can be more valuable to potential employers, layout based on frames and sizes, programmatically auto layout or layout on interface builder, keep reading, if you just want to start the tutorial skip this part…

Let me share my experience and hopefully, this will help you make a decision.  I started, probably as many developers, making apps using interface builder I think it’s great to start using a visual interface that will help you understand how does the UI components work; when I landed my first job at a small startup I was surprised of how they handle the UI, no storyboards or auto layout, instead they used frames and sizes to do it. It took me a while to understand how to do it and a lot of lines of code to do it properly (if you did this before you know what I am talking about), after a couple of weeks I felt confident about it and after a couple of months I was creating great layouts based on frames and sizes for multiple devices.

At that point, I start asking myself if that approach will beneficiate me to get a job in a bigger company or if it can be considered as a bad practice (based on the amount of code needed to handle different devices). After reading a lot of comments on StackOverflow and blog posts I started realizing that my concerns weren’t unjustified. 
At that point, I told myself that for the future I will only build apps using storyboards, mostly because the other option to do layout in code at that moment was using auto layout visual format which I always hated, lucky for me and everyone else iOS 9 released NSLayoutAnchor, an easy API that makes auto layout in code super simple, if you never used it, on this tutorial we will use it by the way.

But, what should you use to make a good impression to a potential employer? well in my own experience it doesn’t matter as long is not layout based on frames and sizes, some employers will value your skills doing programmatically auto layout and others will value your experience with interface builder, my suggestion is to know both because honestly between this two options there is not “the best way to do it”, both are perfectly valid options and what to use on a project will depend on the preference of the team that you potentially will be part of, and what will make you valuable is that you can handle any given requirement, hope this help you, now let’s jump into the code.

My personal preference is programmatically layout using NSLAyoutAnchors, so on this tutorial, we will create a Tinder-like view with no storyboards, the problem with this approach is that you can end with a huge ViewController because you have to “write” the UI, but don’t worry I will show you how to code UI using enums with associated values that will solve this issue, I will also show you how to create custom views and use a protocol extension to keep your ViewController as clean a possible by making your code reusable and easy to abstract.

We will start from scratch step by step so you can start by downloading this starter project that just contains some image assets, but you don’t need to, you can start your own and add your own assets, depends on you.

Now that you have the project go and delete the storyboard from the bundle, there are a couple of steps to be able to run your project after deleting a storyboard.

First, you need to go to the General tab and in the Deployment Info, you need to change the Main Interface to LaunchScreen.storyboard like this…

Screen Shot 2017-07-02 at 6.20.01 PM

Next, you need to go to AppDelegate and add this code inside the didFinishLaunchingWithOptions function like this…

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
     window = UIWindow(frame: UIScreen.main.bounds)
     window?.makeKeyAndVisible()
     window?.rootViewController = ViewController()
     return true
  }

Finally, go to the ViewController file and change the background to white, run the app and you should see an empty white application.

Great, now you have an empty app with no storyboards ready to be customized, this is how it will look by the end of this post.

tinder.gif

As you can see, this view has an Image view, some buttons and labels, I will start showing you how to create a “factory” for UI components using enums with associated values to avoid code repetition. Create a new empty file, call it LabelFactory and import UIKit, copy this code…

enum LabelFactory {
    case standardLabel(text: String, textColor: UIColor, fontStyle: UIFontTextStyle, textAlignment: NSTextAlignment?, sizeToFit: Bool, adjustToFit: Bool)

    var new: UILabel {
        switch self {
        case .standardLabel(let text,let textColor,let fontStyle, let textAlignment,let sizeToFit, let adjustToFit):
            return createStandardLabel(text: text, textColor: textColor, fontStyle: fontStyle, textAlignment: textAlignment, sizeToFit: sizeToFit, adjustToFit: adjustToFit)
        }
    }
    
    private func createStandardLabel(text: String, textColor: UIColor, fontStyle: UIFontTextStyle, textAlignment: NSTextAlignment?, sizeToFit: Bool, adjustToFit: Bool) -> UILabel {  
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.adjustsFontSizeToFitWidth = adjustToFit\
        label.text = text
        label.font = UIFont.preferredFont(forTextStyle: fontStyle)
        label.textAlignment = textAlignment ?? .left
        label.textColor = textColor
        if sizeToFit {
            label.sizeToFit()
        }
      return label
   }
}

So what is all of this? well, if you had experience creating UI components programmatically you probably experienced a lot of coding repetition in your Views and View Controllers, here we are just using an enum to create a new label based on each case, for this example we just have one case but you can imagine creating different cases for different types of labels that fit in your app, and this is how you will use it…

   let profileImageView = ImageViewFactory.standardImageView(image: #imageLiteral(resourceName: "jynerso"), cornerRadius: 0, interactionEnabled: true, contentMode: .scaleAspectFill, sizeToFit: false).new

One important thing to mention is that in order to be able to use NSLayoutAnchors in views we need to set the translatesAutoresizingMaskIntoConstraints property to false, if not it will not work.

One small tip, if you get a crash on your app that looks like this…

because they have no common ancestor.  Does the constraint or its anchors reference items in different view hierarchies?  That's illegal.'

It means that you probably forgot to add a view to its corresponding parent view.

Let’s do it one more time, now for UIButton ….

enum ButtonFactory {    
    case buttonWithImage(image: UIImage, cornerRadius: CGFloat, target: Any, selector: (Selector), sizeToFit: Bool)
    var new: UIButton {
        switch self {
        case .buttonWithImage(let image,let cornerRadius,let target,let selector, let sizeToFit):
            return createButtonWithImage(image: image, cornerRadius: cornerRadius, target: target, selector: selector, sizeToFit: sizeToFit)
         }
    }
    
    private func createButtonWithImage(image: UIImage, cornerRadius: CGFloat, target: Any, selector: (Selector), sizeToFit: Bool) -> UIButton {
        let button = UIButton()
        button.setImage(image, for: .normal)
        button.addTarget(target, action: selector, for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.layer.cornerRadius = cornerRadius
        button.clipsToBounds = true
        if sizeToFit {
            button.sizeToFit()
        }
        return button       
    }
}

And a factory for ImageView’s…

enum ImageViewFactory {

    case standardImageView(image: UIImage, cornerRadius: CGFloat, interactionEnabled: Bool, contentMode: UIViewContentMode, sizeToFit: Bool)
    
    var new: UIImageView {
        switch self {
        case .standardImageView(let image,let cornerRadius, let interactionEnabled,let contentMode, let sizeToFit):
            return createStandardImageView(image: image, cornerRadius: cornerRadius, interactionEnabled: interactionEnabled,
                                           contentMode: contentMode, sizeToFit: sizeToFit)
        }
    }
    
    private func createStandardImageView(image: UIImage, cornerRadius: CGFloat, interactionEnabled: Bool,contentMode: UIViewContentMode, sizeToFit: Bool) -> UIImageView {
        
        let imageView = UIImageView()
        imageView.image = image
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.layer.cornerRadius = cornerRadius
        imageView.clipsToBounds = true
        imageView.isUserInteractionEnabled = interactionEnabled\
        imageView.contentMode = contentMode
        if sizeToFit {
            imageView.sizeToFit()
        }
        return imageView
    }
}

Now that we have our UI factories, let’s create a base UIView class, we can create new views that inherit from this class and avoid repeating the required initialization every time, create a new file and call it BaseView, import UIKit and copy and paste…

class BaseView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        translatesAutoresizingMaskIntoConstraints = false
        setUpViews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setUpViews() {
        //perform UI configuration in child classes
    }
}

Now, we are going a create our first custom view, create a new file and call it TinderCard, copy and paste…

class TinderCard: BaseView {

    let profileImageView = ImageViewFactory.standardImageView(image: #imageLiteral(resourceName: "jynerso"), cornerRadius: 0, interactionEnabled: true, contentMode: .scaleAspectFill, sizeToFit: false).new
    let friendsIconView = ImageViewFactory.standardImageView(image: #imageLiteral(resourceName: "friendsIcon"), cornerRadius: 0, interactionEnabled: false, contentMode: .scaleAspectFill, sizeToFit: false).new
    let containerView: BaseView = {
        let v = BaseView()
        v.backgroundColor = .white
        v.layer.cornerRadius = 10.0
        v.layer.borderWidth = 0.5
        v.layer.borderColor = UIColor.gray.cgColor
        v.clipsToBounds = true
        return v
    }()
    
    let infoContainerView: BaseView = {
        let v = BaseView()
        return v
    }()
    
    let nameLabel = LabelFactory.standardLabel(text: "Jyn Erso", textColor: .gray, fontStyle: .headline, textAlignment: .left, sizeToFit: true, adjustToFit: true).new 
    let workLabel = LabelFactory.standardLabel(text: "Member of the Alliance to Restore the Republic", textColor: .gray, fontStyle: .subheadline, textAlignment: .left, sizeToFit: true, adjustToFit: true).new
}

Here you can see that this view inherits from our BaseView and that we are using the UI factories, we also add two containers that will help us make things easier, let’s start using NSLayoutanchors, inside this class override the function setUpViews like so…

    override func setUpViews() {
        addSubview(containerView)
        containerView.addSubview(profileImageView)
        containerView.addSubview(infoContainerView)

        infoContainerView.addSubview(nameLabel)
        infoContainerView.addSubview(workLabel)
        infoContainerView.addSubview(friendsIconView)
        
        let infoContainerViewMargins = infoContainerView.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            
            containerView.topAnchor.constraint(equalTo: topAnchor),
            containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
            containerView.widthAnchor.constraint(equalTo: widthAnchor),
            containerView.bottomAnchor.constraint(equalTo: bottomAnchor),

            profileImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            profileImageView.topAnchor.constraint(equalTo: containerView.topAnchor),
            profileImageView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
            profileImageView.heightAnchor.constraint(equalTo: containerView.heightAnchor, multiplier: 0.85),
            
            infoContainerView.topAnchor.constraint(equalTo: profileImageView.bottomAnchor),
            infoContainerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            infoContainerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            infoContainerView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
            
            friendsIconView.centerYAnchor.constraint(equalTo: infoContainerViewMargins.centerYAnchor),
            friendsIconView.heightAnchor.constraint(equalTo: infoContainerViewMargins.heightAnchor, multiplier: 0.7),
            friendsIconView.widthAnchor.constraint(equalTo: friendsIconView.heightAnchor),
            friendsIconView.trailingAnchor.constraint(equalTo: infoContainerViewMargins.trailingAnchor),

            nameLabel.leadingAnchor.constraint(equalTo: infoContainerViewMargins.leadingAnchor),
            nameLabel.topAnchor.constraint(equalTo: infoContainerViewMargins.topAnchor),

            workLabel.leadingAnchor.constraint(equalTo: infoContainerViewMargins.leadingAnchor),
            workLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor),
            workLabel.trailingAnchor.constraint(equalTo: friendsIconView.leadingAnchor, constant: -20)
            ])
    }

Inside this function, we are defining the TinderCard hierarchy by adding each subview to a corresponding container and add the main container to the view, let me try to explain what’s going on…

Screen Shot 2017-07-03 at 3.27.46 PM

This is the TinderCard, it has a container view constrained to fill the same dimensions of the Super View, it has two sub-Views, a profileImageView with the image of the user and below it, the infoContainerView, in code we can say that the topAnchor of the infoContainerview is constrained to the bottom anchor of the profileImageView, make sense? this is the line of code…

 infoContainerView.topAnchor.constraint(equalTo: profileImageView.bottomAnchor)

As simple as that, clean and expressive, that’s why I love NSLayoutAnchors.

Let’s explore now the infoContainerView, it has three subViews; two labels, and one imageView.

Screen Shot 2017-07-03 at 3.37.13 PM

To make things easier UIView’s provides a property called layoutMarginsGuide, that we can use as a reference for layouts. (If you are reading this article after ios11 is released check this link).

For this “container” we are constraining the subViews to its margins that’s why we declared a constant that will hold the view’s layoutMarginsGuide to make things easier for us.

Now that we have the TinderCard View implemented, let’s build the rest, this time I will show you how to use a UIStackview programmatically, create a new empty file and call it ButtonsView, import UIKit, copy and paste…

class ButtonsView: BaseView {    

    lazy var likeButton: UIButton = {
        let b = ButtonFactory.buttonWithImage(image: #imageLiteral(resourceName: "like"), cornerRadius: 0, target: self, selector: #selector(like), sizeToFit: true).new
        return b
    }()
    
    lazy var passButton: UIButton = {
        let b = ButtonFactory.buttonWithImage(image: #imageLiteral(resourceName: "pass"), cornerRadius: 0, target: self, selector: #selector(pass), sizeToFit: true).new
        return b
    }()
    
    lazy var superLikeButton: UIButton = {
        let b = ButtonFactory.buttonWithImage(image: #imageLiteral(resourceName: "superlike"), cornerRadius: 0, target: self, selector: #selector(superLike), sizeToFit: true).new
        return b
    }()
    
    lazy var container: UIStackView = {
        let c = UIStackView(arrangedSubviews: [
            self.likeButton, self.passButton, self.superLikeButton
            ])
        c.translatesAutoresizingMaskIntoConstraints = false
        c.spacing = 20
        c.distribution = .fillEqually
        return c
    }()
}

Like with the TinderCard class we start by defining the UI elements, we have some buttons built with our UIButton factory, and a subclass of UIStackView called container, inside the closure we are initializing an instance of UISTackView defining it’s arranged subviews (the elements that we want inside the stack, in this case, the three buttons). We can customize the appearance of the elements inside a UIStackView very easily by setting the spacing, alignment and distribution properties, here we are giving a 20 for spacing between the buttons and distribute them to fill equally, this is what we want to accomplish…

Screen Shot 2017-07-03 at 4.33.40 PM

Let’s finish thie ButtonsView by adding the stackView to it, copy and paste…

    override func setUpViews() {

       addSubview(container)
            
        NSLayoutConstraint.activate([
            container.leadingAnchor.constraint(equalTo: leadingAnchor),
            container.topAnchor.constraint(equalTo: topAnchor),
            container.widthAnchor.constraint(equalTo: widthAnchor),
            container.heightAnchor.constraint(equalTo: heightAnchor)
            ])
    }
    
    func like() {
        print("like print")
    }

    func pass() {
        print("pass print")
    }
    
    func superLike() {
        print("super like print")
    }

Again, we are overriding the setUpViews method of BaseView, and constraining the stackView to fill the view, then we have some methods just to satisfy the compiler.

Great, now we have all the tools that we need to build the app, let’s open the ViewController file, copy and paste…

class ViewController: UIViewController {

    lazy var tinderCard: TinderCard = {
        let tc = TinderCard()
       //tc.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(swipeCard(sender:))))
        return tc
    }()
    
    let buttonsContainer: ButtonsView = {
        let c = ButtonsView()
        return c
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        setUpViews()
    }
    
    func setUpViews() {
        
        view.addSubview(tinderCard)
        view.addSubview(buttonsContainer)
        
        NSLayoutConstraint.activate([
            tinderCard.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor, constant: 20),
            tinderCard.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.85),
            tinderCard.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            tinderCard.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.70),
            
            buttonsContainer.topAnchor.constraint(equalTo: tinderCard.bottomAnchor, constant: 50),
            buttonsContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            buttonsContainer.widthAnchor.constraint(equalTo: tinderCard.widthAnchor)
            ])
    }
}

You can see now the benefits of creating custom views, the views encapsulate its implementation, make them reusable and make your view controller cleaner, this VC has an instance of TinderCard and ButtonsView.

NSLayoutconstraint has also a method that let you use a multiplier that will make your view resize based on it…

 tinderCard.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.70),

This will make the tinderCard height will be always the 70% of the super view, ending in an adaptive UI for any device! run the app and you should see the final design ready.

There is so much to talk about programmatically layout and I will post a tutorial about it in the future but enough of layout, for now. Let’s jump into the implementation of Swiping left/right. Create a new empty file and call it Swipeable, we are going to use protocol extensions for this task, copy and paste…

//MARK: step 1 protocol
protocol Swipeable { }

//MARK: step 2 Protocol extension constrained to UIPanGestureRecognizer
extension Swipeable where Self: UIPanGestureRecognizer {

    //MARK function available for any UIPanGestureRecongnizer instance
    func swipeView(_ view: UIView) {
        
        switch state {
        case .changed:
            let translation = self.translation(in: view.superview)
            view.transform = transform(view: view, for: translation)
        case .ended:
            UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 1.0, options: [], animations: {
                view.transform = .identity
            }, completion: nil)
            
        default:
            break
        }
    }
    
    //MARK: Helper method that handles transformation
    private func transform(view: UIView, for translation: CGPoint) -> CGAffineTransform {
        
        let moveBy = CGAffineTransform(translationX: translation.x, y: translation.y)
        let rotation = -sin(translation.x / (view.frame.width * 4.0))
        return moveBy.rotated(by: rotation)
    }    
}

//MARK: step 4 UIPanGestureRecognizer conforming to Swipeable
extension UIPanGestureRecognizer: Swipeable {}

If you don’t have experience with protocol extensions that’s ok, let me explain it step by step using this example, the first step is to create a protocol, why is it empty? well if you declare properties or methods inside the protocol every instance that conforms to it will MUST also implement those methods and properties and sometimes, like in this example, that’s not what we want, instead, we will leave it empty and create a protocol extension.

The cool thing about protocol extensions is that you can declare functions inside of them without obligating the instance that conforms to it to use them, you can even implement the functionality, I like this because it “encapsulates” the logic and just extend the capability of the instance,  we can also constrain the protocol extension methods to be available only to a certain class, like the UIPanGestureRecognizer on this example.

By the way if you are thinking how to use a protocol extension constrained to a struct instead of a class this is how you do it…

Declare a struct and make it conform to Swipeable like so…

stuct MyStruct: Swipeable {}

Next, extend the protocol and constrain it to be able only to instances of MyStruct…

extension Swipeable where Self == Test {
    func sayHello() {
        print("hello")
    }
}

ok, let’s go back to our implementation, I won’t go into detail about the transformation code, that can have its own tutorial but for now, the takeaway is how we can encapsulate logic inside protocol extensions and extend the capabilities of any instance if needed.

Let’s now use this protocol, go to ViewController and follow this two steps…

First, uncomment this line inside the tinderCard property…

tc.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(swipeCard(sender:))))

Second, copy and paste this function inside the Viewcontroller…

  func swipeCard(sender: UIPanGestureRecognizer) {
        sender.swipeView(tinderCard)
   }

There is nothing special about this function declaration, is a selector handled by the UIPanGestureRecognizer that we just uncomment, the cool part is what’s inside of it, you can see that in this case, the sender is an instance of UIPanGestureRecognizer and that this instance is using the function that we declared before in the protocol, it is passing as a parameter the view that we want to be able to swipe, I want you to take a close look to the ViewController and realize the amount of code that we extract from it by creating reusable custom views and by abstracting code logic in protocol extensions.

That’s it, remember that keeping your code clean and your ViewController’s instances as small and easy to handle as possible are always considered good practices. (check this tutorial if you want to avoid massive view controllers).

Now run the app and start swiping!

You can download the final project here.

Happy swiping.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s