f09c4d0986
The presentationController variable is declared as an implicitly unwrapped optional (line 38): private var presentationController: SideMenuPresentationController! There are situations where this will cause a crash, as the property could be null while transitioning: Crashed: com.apple.main-thread 0 MyApp 0x102c57398 closure #1 in SideMenuNavigationController.viewWillTransition(to:with:) + 107 (SideMenuAnimationController.swift:107) 1 MyApp 0x102c573d0 thunk for @escaping @callee_guaranteed (@guaranteed UIViewControllerTransitionCoordinatorContext) -> () + 4336055248 (<compiler-generated>:4336055248) 2 UIKitCore 0x1b67c58f4 -[_UIViewControllerTransitionCoordinator _applyBlocks:releaseBlocks:] + 276 3 UIKitCore 0x1b67c21b4 -[_UIViewControllerTransitionContext __runAlongsideAnimations] + 284 4 UIKitCore 0x1b67d9b38 -[_UIWindowAnimationController animateTransition:] + 344 5 UIKitCore 0x1b6e48044 -[UIWindow _adjustSizeClassesAndResizeWindowToFrame:] + 964 6 UIKitCore 0x1b6e546b4 -[UIWindow _resizeWindowFrameToSceneBoundsIfNecessary] + 252 7 UIKitCore 0x1b6e4a26c __78-[UIWindow _rotateWindowToOrientation:updateStatusBar:duration:skipCallbacks:]_block_invoke + 408 8 UIKitCore 0x1b67da1e8 __58-[_UIWindowRotationAnimationController animateTransition:]_block_invoke_2 + 180 9 UIKitCore 0x1b72d0e48 +[UIView(Internal) _performBlockDelayingTriggeringResponderEvents:forScene:] + 212 10 UIKitCore 0x1b67d9fd4 __58-[_UIWindowRotationAnimationController animateTransition:]_block_invoke + 180 11 UIKitCore 0x1b67d9e98 -[_UIWindowRotationAnimationController animateTransition:] + 524 12 UIKitCore 0x1b6e478c4 -[UIWindow _rotateToBounds:withAnimator:transitionContext:] + 624 13 UIKitCore 0x1b6e49f74 -[UIWindow _rotateWindowToOrientation:updateStatusBar:duration:skipCallbacks:] + 1436 14 UIKitCore 0x1b6e4a4b0 -[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:isRotating:] + 404 15 UIKitCore 0x1b6e49844 -[UIWindow _setRotatableViewOrientation:updateStatusBar:duration:force:] + 132 16 UIKitCore 0x1b66bf2dc -[_UIFullscreenPresentationController _placeCounterRotationViewWithView:inWindow:fromOrientation:toOrientation:force:] + 520 17 UIKitCore 0x1b66bf010 -[_UIFullscreenPresentationController _adjustOrientationIfNecessaryInWindow:forViewController:preservingViewController:] + 908 18 UIKitCore 0x1b66b45ec -[UIPresentationController _presentWithAnimationController:interactionController:target:didEndSelector:] + 900 19 UIKitCore 0x1b67a8590 -[UIViewController _presentViewController:modalSourceViewController:presentationController:animationController:interactionController:completion:] + 1416 20 UIKitCore 0x1b67a9f74 -[UIViewController _presentViewController:withAnimationController:completion:] + 4212 21 UIKitCore 0x1b67ac540 __63-[UIViewController _presentViewController:animated:completion:]_block_invoke + 108 22 UIKitCore 0x1b67aca94 -[UIViewController _performCoordinatedPresentOrDismiss:animated:] + 528 23 UIKitCore 0x1b67ac48c -[UIViewController _presentViewController:animated:completion:] + 212 24 UIKitCore 0x1b67ac720 -[UIViewController presentViewController:animated:completion:] + 176 25 UIKitCore 0x1b6f6f9b8 __74-[UIStoryboardPresentationSegueTemplate newDefaultPerformHandlerForSegue:]_block_invoke + 148 26 UIKitCore 0x1b6f75a18 -[UIStoryboardSegueTemplate _performWithDestinationViewController:sender:] + 296 27 UIKitCore 0x1b6f758bc -[UIStoryboardSegueTemplate _perform:] + 96 28 UIKitCore 0x1b6f75b98 -[UIStoryboardSegueTemplate perform:] + 164 29 UIKitCore 0x1b6e086c0 -[UIApplication sendAction:to:from:forEvent:] + 100 30 UIKitCore 0x1b6479858 __45-[_UIButtonBarTargetAction _invoke:forEvent:]_block_invoke + 84 31 UIKitCore 0x1b64796e4 -[_UIButtonBarTargetAction _invoke:forEvent:] + 256 32 UIKitCore 0x1b6e086c0 -[UIApplication sendAction:to:from:forEvent:] + 100 33 UIKitCore 0x1b67f0b30 -[UIControl sendAction:to:forEvent:] + 208 34 UIKitCore 0x1b67f0e98 -[UIControl _sendActionsForEvents:withEvent:] + 400 35 UIKitCore 0x1b67efeb0 -[UIControl touchesEnded:withEvent:] + 520 36 UIKitCore 0x1b6e43b08 -[UIWindow _sendTouchesForEvent:] + 1024 37 UIKitCore 0x1b6e452f0 -[UIWindow sendEvent:] + 3548 38 UIKitCore 0x1b6e2045c -[UIApplication sendEvent:] + 348 39 UIKitCore 0x1b6ea3a54 __dispatchPreprocessedEventFromEventQueue + 6688 40 UIKitCore 0x1b6ea6648 __handleEventQueueInternal + 5368 41 UIKitCore 0x1b6e9e578 __handleHIDEventFetcherDrain + 144 42 CoreFoundation 0x1b2c39af4 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 28 43 CoreFoundation 0x1b2c39a48 __CFRunLoopDoSource0 + 84 44 CoreFoundation 0x1b2c39198 __CFRunLoopDoSources0 + 196 45 CoreFoundation 0x1b2c33f38 __CFRunLoopRun + 796 46 CoreFoundation 0x1b2c338f4 CFRunLoopRunSpecific + 480 47 GraphicsServices 0x1bd04a604 GSEventRunModal + 164 48 UIKitCore 0x1b6e07358 UIApplicationMain + 1944 49 MyApp 0x102921628 main + 15 (AppDelegate.swift:15) 50 libdyld.dylib 0x1b2aaf2dc start + 4 By making it a normal optional, we can avoid such crashes.
233 lines
9.4 KiB
Swift
233 lines
9.4 KiB
Swift
//
|
|
// SideMenuAnimationController.swift
|
|
// SideMenu
|
|
//
|
|
// Created by Jon Kent on 10/24/18.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
internal protocol AnimationModel {
|
|
/// The animation options when a menu is displayed. Ignored when displayed with a gesture.
|
|
var animationOptions: UIView.AnimationOptions { get }
|
|
/// Duration of the remaining animation when the menu is partially dismissed with gestures. Default is 0.35 seconds.
|
|
var completeGestureDuration: Double { get }
|
|
/// Duration of the animation when the menu is dismissed without gestures. Default is 0.35 seconds.
|
|
var dismissDuration: Double { get }
|
|
/// The animation initial spring velocity when a menu is displayed. Ignored when displayed with a gesture.
|
|
var initialSpringVelocity: CGFloat { get }
|
|
/// Duration of the animation when the menu is presented without gestures. Default is 0.35 seconds.
|
|
var presentDuration: Double { get }
|
|
/// The animation spring damping when a menu is displayed. Ignored when displayed with a gesture.
|
|
var usingSpringWithDamping: CGFloat { get }
|
|
}
|
|
|
|
internal protocol SideMenuAnimationControllerDelegate: class {
|
|
func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didDismiss viewController: UIViewController)
|
|
func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didPresent viewController: UIViewController)
|
|
}
|
|
|
|
internal final class SideMenuAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
|
|
|
typealias Model = AnimationModel & PresentationModel
|
|
|
|
private var config: Model
|
|
private weak var containerView: UIView?
|
|
private let leftSide: Bool
|
|
private weak var originalSuperview: UIView?
|
|
private var presentationController: SideMenuPresentationController?
|
|
private unowned var presentedViewController: UIViewController?
|
|
private unowned var presentingViewController: UIViewController?
|
|
weak var delegate: SideMenuAnimationControllerDelegate?
|
|
|
|
init(config: Model, leftSide: Bool, delegate: SideMenuAnimationControllerDelegate? = nil) {
|
|
self.config = config
|
|
self.leftSide = leftSide
|
|
self.delegate = delegate
|
|
}
|
|
|
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
guard
|
|
let presentedViewController = transitionContext.presentedViewController,
|
|
let presentingViewController = transitionContext.presentingViewController
|
|
else { return }
|
|
|
|
if transitionContext.isPresenting {
|
|
self.containerView = transitionContext.containerView
|
|
self.presentedViewController = presentedViewController
|
|
self.presentingViewController = presentingViewController
|
|
self.presentationController = SideMenuPresentationController(
|
|
config: config,
|
|
leftSide: leftSide,
|
|
presentedViewController: presentedViewController,
|
|
presentingViewController: presentingViewController,
|
|
containerView: transitionContext.containerView
|
|
)
|
|
}
|
|
|
|
transition(using: transitionContext)
|
|
}
|
|
|
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
guard let transitionContext = transitionContext else { return 0 }
|
|
return duration(presenting: transitionContext.isPresenting, interactive: transitionContext.isInteractive)
|
|
}
|
|
|
|
func animationEnded(_ transitionCompleted: Bool) {
|
|
guard let presentedViewController = presentedViewController else { return }
|
|
if presentedViewController.isHidden {
|
|
delegate?.sideMenuAnimationController(self, didDismiss: presentedViewController)
|
|
} else {
|
|
delegate?.sideMenuAnimationController(self, didPresent: presentedViewController)
|
|
}
|
|
}
|
|
|
|
func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, alongsideTransition: (() -> Void)? = nil, complete: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
|
prepare(presenting: presenting)
|
|
transitionWillBegin(presenting: presenting)
|
|
transition(
|
|
presenting: presenting,
|
|
animated: animated,
|
|
interactive: interactive,
|
|
animations: { [weak self] in
|
|
guard let self = self else { return }
|
|
self.transition(presenting: presenting)
|
|
alongsideTransition?()
|
|
}, completion: { [weak self] _ in
|
|
guard let self = self else { return }
|
|
if complete {
|
|
self.transitionDidEnd(presenting: presenting, completed: true)
|
|
self.finish(presenting: presenting, completed: true)
|
|
}
|
|
completion?(true)
|
|
})
|
|
}
|
|
|
|
func layout() {
|
|
presentationController?.containerViewWillLayoutSubviews()
|
|
}
|
|
}
|
|
|
|
private extension SideMenuAnimationController {
|
|
|
|
func duration(presenting: Bool, interactive: Bool) -> Double {
|
|
if interactive { return config.completeGestureDuration }
|
|
return presenting ? config.presentDuration : config.dismissDuration
|
|
}
|
|
|
|
func prepare(presenting: Bool) {
|
|
guard
|
|
presenting,
|
|
let presentingViewController = presentingViewController,
|
|
let presentedViewController = presentedViewController
|
|
else { return }
|
|
|
|
originalSuperview = presentingViewController.view.superview
|
|
containerView?.addSubview(presentingViewController.view)
|
|
containerView?.addSubview(presentedViewController.view)
|
|
}
|
|
|
|
func transitionWillBegin(presenting: Bool) {
|
|
// prevent any other menu gestures from firing
|
|
containerView?.isUserInteractionEnabled = false
|
|
if presenting {
|
|
presentationController?.presentationTransitionWillBegin()
|
|
} else {
|
|
presentationController?.dismissalTransitionWillBegin()
|
|
}
|
|
}
|
|
|
|
func transition(presenting: Bool) {
|
|
if presenting {
|
|
presentationController?.presentationTransition()
|
|
} else {
|
|
presentationController?.dismissalTransition()
|
|
}
|
|
}
|
|
|
|
func transitionDidEnd(presenting: Bool, completed: Bool) {
|
|
if presenting {
|
|
presentationController?.presentationTransitionDidEnd(completed)
|
|
} else {
|
|
presentationController?.dismissalTransitionDidEnd(completed)
|
|
}
|
|
containerView?.isUserInteractionEnabled = true
|
|
}
|
|
|
|
func finish(presenting: Bool, completed: Bool) {
|
|
guard
|
|
presenting != completed,
|
|
let presentingViewController = self.presentingViewController
|
|
else { return }
|
|
presentedViewController?.view.removeFromSuperview()
|
|
originalSuperview?.addSubview(presentingViewController.view)
|
|
}
|
|
|
|
func transition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
prepare(presenting: transitionContext.isPresenting)
|
|
transitionWillBegin(presenting: transitionContext.isPresenting)
|
|
transition(
|
|
presenting: transitionContext.isPresenting,
|
|
animated: transitionContext.isAnimated,
|
|
interactive: transitionContext.isInteractive,
|
|
animations: { [weak self] in
|
|
guard let self = self else { return }
|
|
self.transition(presenting: transitionContext.isPresenting)
|
|
}, completion: { [weak self] _ in
|
|
guard let self = self else { return }
|
|
let completed = !transitionContext.transitionWasCancelled
|
|
self.transitionDidEnd(presenting: transitionContext.isPresenting, completed: completed)
|
|
self.finish(presenting: transitionContext.isPresenting, completed: completed)
|
|
|
|
// Called last. This causes the transition container to be removed and animationEnded() to be called.
|
|
transitionContext.completeTransition(completed)
|
|
})
|
|
}
|
|
|
|
func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, animations: @escaping (() -> Void) = {}, completion: @escaping ((Bool) -> Void) = { _ in }) {
|
|
if !animated {
|
|
animations()
|
|
completion(true)
|
|
return
|
|
}
|
|
|
|
let duration = self.duration(presenting: presenting, interactive: interactive)
|
|
if interactive {
|
|
// IMPORTANT: The non-interactive animation block will not complete if adapted for interactive. The below animation block must be used!
|
|
UIView.animate(
|
|
withDuration: duration,
|
|
delay: duration, // HACK: If zero, the animation briefly flashes in iOS 11.
|
|
options: .curveLinear,
|
|
animations: animations,
|
|
completion: completion
|
|
)
|
|
return
|
|
}
|
|
|
|
UIView.animate(
|
|
withDuration: duration,
|
|
delay: 0,
|
|
usingSpringWithDamping: config.usingSpringWithDamping,
|
|
initialSpringVelocity: config.initialSpringVelocity,
|
|
options: config.animationOptions,
|
|
animations: animations,
|
|
completion: completion
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension UIViewControllerContextTransitioning {
|
|
|
|
var isPresenting: Bool {
|
|
return viewController(forKey: .from)?.presentedViewController === viewController(forKey: .to)
|
|
}
|
|
|
|
var presentingViewController: UIViewController? {
|
|
return viewController(forKey: isPresenting ? .from : .to)
|
|
}
|
|
|
|
var presentedViewController: UIViewController? {
|
|
return viewController(forKey: isPresenting ? .to : .from)
|
|
}
|
|
}
|