Swift: Animating a mask for a UIView

Animated Image

I recently stumbled upon a really excellent article online entitled “UIDynamics, UIKit or OpenGL? 3 Types of iOS Animations For the Star Wars.” A lot of great discussion about performance of interesting techniques in a iOS user interface. I read through it and without going to the Github repository to check out any code I decided to try to replicate that mask animation.

I am not a huge fan of Google’s Android Material Design, but the animated mask aspect of a user interaction is interesting, attention grabbing without being too heavy-handed, and playful. And it was a bit of a challenge as I have never tried masking anything at all in an iOS application.

By the way, I have a handy extension that you might love for use in Swift applications. It allows me to provide hex values to provide UIColor attributes to things.

extension UIColor {
    convenience init(red: Int, green: Int, blue: Int) {
        assert(red >= 0 && red <= 255, "Invalid red component")
        assert(green >= 0 && green <= 255, "Invalid green component")
        assert(blue >= 0 && blue <= 255, "Invalid blue component")
        self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
    }
    convenience init(netHex:Int) {
        self.init(red:(netHex >> 16) & 0xff, green:(netHex >> 8) & 0xff, blue:netHex & 0xff)
    }
}

Anyway, here is the full view controller class. The method close and showUnderView is where the masking action takes place. Make careful consideration of the order in which you add subviews. Here is an example of what I have going on.

import UIKit

extension UIColor {
    convenience init(red: Int, green: Int, blue: Int) {
        assert(red >= 0 && red <= 255, "Invalid red component")
        assert(green >= 0 && green <= 255, "Invalid green component")
        assert(blue >= 0 && blue <= 255, "Invalid blue component")
        self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
    }
    convenience init(netHex:Int) {
        self.init(red:(netHex >> 16) & 0xff, green:(netHex >> 8) & 0xff, blue:netHex & 0xff)
    }
}

class ViewController: UIViewController {

    var underView:UIView!
    var darkSide:Bool = false
    var mySwitch:UISwitch!
    let animSpeed:CFTimeInterval = 0.25
    
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor(netHex: 0xBBBBBB)
        
        let topLayer = UIView(frame: CGRectMake(0, 0, self.view.frame.size.width, 60))
        topLayer.userInteractionEnabled = false
        let topLabel = UILabel(frame: CGRectMake(0, 15, self.view.frame.size.width, 20))
        topLabel.text = "S E T T I N G S"
        topLabel.textColor = UIColor.whiteColor()
        topLabel.font = UIFont(name: "AvenirNext-Regular", size: 16)
        topLabel.textAlignment = NSTextAlignment.Center
        topLayer.addSubview(topLabel)
        
        underView = UIView(frame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height))
        underView.backgroundColor = UIColor.blackColor()
        
        let vader = UIImageView(frame: CGRectMake(0, 50, self.view.frame.size.width, 290))
        vader.image = UIImage(named: "vader.jpg")
        vader.contentMode = .ScaleAspectFill
        
        let closeButton = UIButton(frame: CGRectMake(underView.frame.size.width - 50, 20, 30, 30))
        closeButton.setTitleColor(UIColor.redColor(), forState: .Normal)
        closeButton.setTitle("X", forState: .Normal)
        closeButton.addTarget(self, action: "close", forControlEvents: .TouchUpInside)
        
    //Elements above it all.
        
        let drkSide = UILabel(frame: CGRectMake(20, 360, 200, 20))
        drkSide.textColor = UIColor(netHex: 0x777777)
        drkSide.font = UIFont.systemFontOfSize(13)
        drkSide.text = "DARK SIDE"
        
        let rule = UIView(frame: CGRectMake(20, 390, self.view.frame.size.width - 40, 1))
        rule.backgroundColor = UIColor(netHex: 0x666666)

        let nameHead = UILabel(frame: CGRectMake(20, 410, 200, 20))
        nameHead.textColor = UIColor(netHex: 0x777777)
        nameHead.font = UIFont.systemFontOfSize(13)
        nameHead.text = "FULL NAME"
        
        let darth = UILabel(frame: CGRectMake(20, 450, 200, 20))
        darth.textColor = UIColor(netHex: 0x777777)
        darth.font = UIFont.systemFontOfSize(14)
        darth.text = "Darth Vader"
        
        let anakin = UILabel(frame: CGRectMake(20, 450, 200, 20))
        anakin.textColor = UIColor(netHex: 0x777777)
        anakin.font = UIFont.systemFontOfSize(14)
        anakin.text = "Anakin Skywalker"

        let mask = CALayer()
        mask.contents = UIImage(named:"mask.png")?.CGImage
        mask.frame = CGRectMake(self.view.frame.size.width - 100, 300, 0, 0)
        underView.layer.mask = mask
        
        mySwitch = UISwitch(frame: CGRectMake(self.view.frame.size.width - 70, 350, 0, 0))
        mySwitch.on = false
        mySwitch.enabled = true
        mySwitch.onTintColor = UIColor.redColor()
        mySwitch.addTarget(self, action: "switched:", forControlEvents: .ValueChanged)
        
        let luke = UIImageView(frame: CGRectMake(0, 50, self.view.frame.size.width, 290))
        luke.image = UIImage(named: "luke.png")
        luke.contentMode = .ScaleAspectFill
        
        underView.addSubview(vader)
        underView.addSubview(closeButton)
        self.view.addSubview(luke)
        self.view.addSubview(anakin)
        topLayer.addSubview(drkSide)
        topLayer.addSubview(rule)
        topLayer.addSubview(nameHead)
        self.view.addSubview(underView)
        self.view.addSubview(mySwitch)
        self.view.addSubview(topLayer)
        underView.addSubview(darth)
    }

    func switched(sender:UISwitch){
        if sender.on == true {
            showUnderView()
        } else {
            close()
        }
    }
    
    //Reverse the mask animation.
    func close(){
        darkSide = false
        self.mySwitch.setOn(false, animated: true)
        let mask = underView.layer.mask
        let oldBounds = mask!.bounds
        let newBounds = CGRectMake(mySwitch.center.x, mySwitch.center.y, 0, 0)
        let revealAnimation = CABasicAnimation(keyPath: "bounds")
        revealAnimation.fromValue = NSValue(CGRect: oldBounds)
        revealAnimation.toValue = NSValue(CGRect: newBounds)
        revealAnimation.duration = animSpeed
        //Keep the bounds after the animation completes.
        mask!.bounds = newBounds
        mask!.addAnimation(revealAnimation, forKey: "revealAnimation")
    }
    
    func showUnderView(){
        if darkSide == true {
            return
        }
        darkSide = true
        let mask = CALayer()
        mask.contents = UIImage(named:"mask.png")?.CGImage
        mask.frame = CGRectMake(mySwitch.center.x, mySwitch.center.y, 0, 0)
        underView.layer.mask = mask
        let oldBounds = mask.bounds
        let newBounds = CGRectMake(0, 0, 1600, 1600)
        let revealAnimation = CABasicAnimation(keyPath: "bounds")
        revealAnimation.fromValue = NSValue(CGRect: oldBounds)
        revealAnimation.toValue = NSValue(CGRect: newBounds)
        revealAnimation.duration = animSpeed
        //Keep the bounds after the animation completes.
        mask.bounds = newBounds
        mask.addAnimation(revealAnimation, forKey: "revealAnimation")
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

 

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.