This short piece is about interactive slide-out menu in iOS. The demo is using an embedded container view for a menu view controller, and that controller handles animations and interaction.
The following implementation will be used for interaction-animation control where view hierarchy remains mostly static.
1. Menu view consists of two container views - a full screen dim view and an overlapping menu view. Interaction is registered by a pan gesture recognizer and is converted to a normalized “progress” property value. Setting this property on a menu view controller updates both the alpha property of the dim view and an origin of a menu view (via a horizontal leading constraint).
import UIKit class RootViewController: UIViewController, UIGestureRecognizerDelegate{ @IBOutlet weak var menuContainerView: UIView! @IBOutlet weak var mainContainerView: UIView! weak var menuVC: MenuViewController!; override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // Instead of fishing for an embedded controller in a prepareForSegue. for vc in childViewControllers { if let mvc = vc as? MenuViewController { menuVC = mvc; } } } // MARK: - Gesture Recognizer var initialProgress: Float = 0.0; var ignoreGesture = false; @IBAction func handlePanGesture(recognizer: UIPanGestureRecognizer) { let vel = recognizer.velocityInView(view); let velThreshold = menuVC.velocityThreshold; switch(recognizer.state) { case .Began: ignoreGesture = abs(vel.y) > abs(vel.x); if (ignoreGesture) { return; } initialProgress = menuVC.progress; recognizer.setTranslation(CGPointZero, inView: view) case .Changed: if (ignoreGesture) { return; } let translation = recognizer.translationInView(view); let tX = Float(translation.x); let progress = initialProgress + tX / velThreshold; // The following can be used to prevent menu progress overshot (not a good "responsive UI"). // progress = min(1.0, max(0.0, progress)); menuVC.progress = progress; case .Ended: if (ignoreGesture) { return; } let velX = Float(vel.x); let expand = ((menuVC.progress > 0.5 && velX > -velThreshold) || (menuVC.progress < 0.5 && velX > velThreshold)); menuVC.animateMenu(expand); default: break } } }
2. Progress property setter does a few things differently if the value is to be animated. Because menu works in conjunction with the embedding superview, the progress will enable/disable the superview as needed so that collapsed menu stays out of way when user interacts with the main content. To animate this property from its current value - there is a function animateMenu:.
import UIKit class MenuViewController: UIViewController { @IBOutlet weak var dimView: UIView! @IBOutlet weak var menuView: UIView! @IBOutlet weak var menuLeadingConstraint: NSLayoutConstraint! var _useAnimation = false; var _progress: Float = 0.0; // To animate to a value set the _useAnimation true. var progress: Float { get { return _progress; } set { if (abs(newValue) > 1.0) { _progress = min(1.5, pow(abs(newValue), 0.2) * (newValue >= 0.0 ? 1.0 : -1.0)); } else { _progress = newValue; } if (newValue > 1e-5 && !menuEnabled) { // Don't just set to condition via assignment, "false" will break animation (0.0 will collapse the menu before animation begins). menuEnabled = true; } let animations = { self.dimView.alpha = CGFloat(min(1.0, self._progress) / 2); let menuOriginX = CGFloat(self.velocityThreshold * self._progress) - self.menuView.frame.width; self.menuLeadingConstraint.constant = menuOriginX; self.view.layoutIfNeeded(); }; let completion: (Bool -> Void) = { finished in if (newValue <= 1e-5) { self.menuEnabled = false; } self._useAnimation = false; }; if (_useAnimation) { UIView.animateWithDuration(0.25, delay: 0.0, options: .CurveEaseOut, animations: animations, completion: completion); } else { animations(); completion(true); } } } private(set) var velocityThreshold: Float = 100.0; // Enables/disables container view. var menuEnabled: Bool { get { return !view.superview!.hidden; } set { view.superview!.hidden = !newValue; } } func animateMenu(expand: Bool) { _useAnimation = true; if (expand) { progress = 1; } else { progress = 0; } } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // Initial menu view offset is a design-time position for an "expanded" state. velocityThreshold = Float(menuView.frame.width + menuView.frame.origin.x); } override func viewDidAppear(animated: Bool) { menuEnabled = true; progress = 1.0; animateMenu(false); } @IBAction func tapDimView(sender: AnyObject) { animateMenu(false); } }
Other ways to implement this, such as using UIScrollView or custom transition animators, are more appropriate if standard transitions (on navigation controller) are desired. I find them either lacking in brevity of code or having tight dependence on standard view controllers.
No comments:
Post a Comment