From 217394abaf0d9a77ffbfc36ad20e60015830b502 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 6 Jan 2021 18:57:57 +0900 Subject: [PATCH] Prevent the potential memory leaks in the modal transition This dismisses the frame 'FloatingPanel Core.move(from:to:animated:completion:)' in the following memory leaks > BoardServices -[BSXPCServiceConnectionEventHandler remoteTarget] > BoardServices __63+[BSXPCServiceConnectionProxy createImplementationForProtocol:]_block_invoke These leaks happens when a panel showes and hides using "Show Multi Panel Modal" in the Samples app. --- Sources/Core.swift | 25 +++++++++++++++++++------ Sources/Transitioning.swift | 30 ++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index f277117b..59b4e01c 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -32,7 +32,8 @@ class Core: NSObject, UIGestureRecognizerDelegate { let panGestureRecognizer: FloatingPanelPanGestureRecognizer var isRemovalInteractionEnabled: Bool = false - fileprivate var animator: UIViewPropertyAnimator? + fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition + fileprivate var transitionAnimator: UIViewPropertyAnimator? fileprivate var moveAnimator: NumericSpringAnimator? private var initialSurfaceLocation: CGPoint = .zero @@ -158,12 +159,15 @@ class Core: NSObject, UIGestureRecognizerDelegate { animator.addCompletion { [weak self] _ in guard let self = self else { return } - self.animator = nil + self.transitionAnimator = nil updateScrollView() self.ownerVC?.notifyDidMove() completion?() } - self.animator = animator + self.transitionAnimator = animator + if isSuspended { + return + } animator.startAnimation() } else { self.state = to @@ -376,7 +380,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { if interactionInProgress { lockScrollView() } else { - if state == layoutAdapter.edgeMostState, self.animator == nil { + if state == layoutAdapter.edgeMostState, self.transitionAnimator == nil { switch layoutAdapter.position { case .top, .left: if offsetDiff < 0 && velocity > 0 { @@ -496,7 +500,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { animator.stopAnimation(true) endAttraction(false) } - if let animator = self.animator { + if let animator = self.transitionAnimator { guard 0 >= layoutAdapter.offsetFromEdgeMost else { return } log.debug("a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!") if animator.isInterruptible { @@ -1037,7 +1041,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer { public override func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) initialLocation = touches.first?.location(in: view) ?? .zero - if floatingPanel?.animator != nil || floatingPanel?.moveAnimator != nil { + if floatingPanel?.transitionAnimator != nil || floatingPanel?.moveAnimator != nil { self.state = .began } } @@ -1198,3 +1202,12 @@ private class NumericSpringAnimator: NSObject { v = (v + h * o2 * (xt - x)) / det } } + +extension FloatingPanelController { + func suspendTransitionAnimator(_ suspended: Bool) { + self.floatingPanel.isSuspended = suspended + } + var transitionAnimator: UIViewPropertyAnimator? { + return self.floatingPanel.transitionAnimator + } +} diff --git a/Sources/Transitioning.swift b/Sources/Transitioning.swift index 677f3b61..bb93321b 100644 --- a/Sources/Transitioning.swift +++ b/Sources/Transitioning.swift @@ -90,14 +90,25 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { return TimeInterval(animator.duration) } - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { guard let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController else { fatalError() } - fpc.show(animated: true) { + if let animator = fpc.transitionAnimator { + return animator + } + + fpc.suspendTransitionAnimator(true) + fpc.show(animated: true) { [weak fpc] in + fpc?.suspendTransitionAnimator(false) transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } + return fpc.transitionAnimator! + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + self.interruptibleAnimator(using: transitionContext).startAnimation() } } @@ -111,14 +122,25 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { return TimeInterval(animator.duration) } - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { guard let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController else { fatalError() } - fpc.hide(animated: true) { + if let animator = fpc.transitionAnimator { + return animator + } + + fpc.suspendTransitionAnimator(true) + fpc.hide(animated: true) { [weak fpc] in + fpc?.suspendTransitionAnimator(false) transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } + return fpc.transitionAnimator! + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + self.interruptibleAnimator(using: transitionContext).startAnimation() } }