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() } }