Created
September 14, 2017 07:33
-
-
Save HarshilShah/a2faf44f5c9b5a0cf0e904f3e27c6cd6 to your computer and use it in GitHub Desktop.
Control Center-like grabber view
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// GrabberView.swift | |
// | |
// Created by Harshil Shah on 23/07/17. | |
// Copyright © 2017 Harshil Shah. All rights reserved. | |
// | |
import UIKit | |
final class GrabberView: UIView { | |
// MARK:- Private types | |
enum State { | |
case arrowUp | |
case flat | |
case arrowDown | |
} | |
// MARK:- Public variables | |
var state: GrabberView.State = .flat { | |
didSet { if oldValue != state { setMaskPath(animated: true) } } | |
} | |
// MARK:- Private variables | |
private var requiresUpdate = false | |
private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) | |
private let maskLayer = CALayer() | |
private let leftLayer = CAShapeLayer() | |
private let rightLayer = CAShapeLayer() | |
// MARK:- Initialization | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
setup() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
setup() | |
} | |
override func awakeFromNib() { | |
super.awakeFromNib() | |
setup() | |
} | |
private func setup() { | |
leftLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 3, width: 18, height: 4), byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(width: 2, height: 2)).cgPath | |
maskLayer.addSublayer(leftLayer) | |
rightLayer.path = UIBezierPath(roundedRect: CGRect(x: 18, y: 3, width: 18, height: 4), byRoundingCorners: [.topRight, .bottomRight], cornerRadii: CGSize(width: 2, height: 2)).cgPath | |
maskLayer.addSublayer(rightLayer) | |
blurView.layer.mask = maskLayer | |
blurView.translatesAutoresizingMaskIntoConstraints = false | |
addSubview(blurView) | |
NSLayoutConstraint.activate([ | |
blurView.topAnchor.constraint(equalTo: topAnchor), | |
blurView.bottomAnchor.constraint(equalTo: bottomAnchor), | |
blurView.leadingAnchor.constraint(equalTo: leadingAnchor), | |
blurView.trailingAnchor.constraint(equalTo: trailingAnchor) | |
]) | |
setMaskPath(animated: false) | |
} | |
// MARK:- UIView methods | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
maskLayer.frame = bounds | |
leftLayer.frame = maskLayer.bounds | |
rightLayer.frame = maskLayer.bounds | |
if requiresUpdate { | |
setMaskPath(animated: false) | |
} | |
} | |
override var intrinsicContentSize : CGSize { | |
return CGSize(width: 36, height: 10) | |
} | |
// MARK:- Private methods | |
private func setMaskPath(forState targetState: GrabberView.State? = nil, animated: Bool) { | |
guard maskLayer.frame != .zero else { | |
requiresUpdate = true | |
return | |
} | |
requiresUpdate = false | |
let state = targetState ?? self.state | |
let rotationInRad: CGFloat = 0.35 | |
let duration: TimeInterval = animated ? 0.2 : 0 | |
let translation: CGFloat = { | |
switch state { | |
case .arrowUp: return -5 | |
case .flat: return 0 | |
case .arrowDown: return 5 | |
} | |
}() | |
let rotationLeft: CGFloat = { | |
switch state { | |
case .arrowUp: return -rotationInRad | |
case .flat: return 0 | |
case .arrowDown: return rotationInRad | |
} | |
}() | |
let rotationRight: CGFloat = { | |
switch state { | |
case .arrowUp: return rotationInRad | |
case .flat: return 0 | |
case .arrowDown: return -rotationInRad | |
} | |
}() | |
let anchorPoint: CGPoint = { | |
switch state { | |
case .arrowUp: return CGPoint(x: 0.5, y: 0.3) | |
case .flat: return CGPoint(x: 0.5, y: 0.5) | |
case .arrowDown: return CGPoint(x: 0.5, y: 0.7) | |
} | |
}() | |
leftLayer.anchorPoint = anchorPoint | |
rightLayer.anchorPoint = anchorPoint | |
animateLayer(leftLayer, translation: translation, rotation: rotationLeft, duration: duration) | |
animateLayer(rightLayer, translation: translation, rotation: rotationRight, duration: duration) | |
} | |
private func animateLayer(_ layer: CAShapeLayer, translation: CGFloat, rotation: CGFloat, duration: TimeInterval) { | |
let initialTransform = CATransform3DMakeTranslation(0, translation, 0) | |
let finalTransform = CATransform3DRotate(initialTransform, rotation, 0, 0, 1) | |
let originalTransform = layer.transform | |
layer.transform = finalTransform | |
let animation = CABasicAnimation(keyPath: "transform") | |
animation.fromValue = originalTransform | |
animation.duration = duration | |
layer.add(animation, forKey: "transform") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment