iOS
Dynamique UIKit
Recherche…
Introduction
UIKit Dynamics est un moteur physique réel intégré à UIKit. Il vous permet de créer des interfaces qui se sentent réelles en ajoutant des comportements tels que la gravité, les pièces jointes, les collisions et les forces. Vous définissez les caractéristiques physiques que vous souhaitez que vos éléments d’interface adoptent, et le moteur dynamique prend en charge le reste.
Remarques
Lorsque vous utilisez UIKit Dynamics, vous devez garder à l'esprit que les vues positionnées par l'animateur ne peuvent pas être facilement positionnées par d'autres méthodes de mise en page iOS courantes.
Les nouveaux arrivants à UIKit Dynamics ont souvent du mal avec cette importante mise en garde. Placer des contraintes sur une vue qui est également un élément d'un UIDynamicBehavior
entraînera probablement une confusion à la fois en tant que moteur de présentation automatique et lutte contre le moteur d'animation dynamique sur la position appropriée. De même, si vous tentez de définir directement le cadre d’une vue contrôlée par l’animateur, vous obtenez généralement une animation instable et un placement inattendu. L'ajout d'une vue en tant qu'élément à un UIDynamicBehavior
signifie que l'animateur prendra la responsabilité de positionner une vue et que de tels changements de position de vue devront être implémentés via l'animateur.
Il est possible de définir une image en cours de mise à jour par un animateur dynamique, mais celle-ci doit être immédiatement suivie par l'envoi de messages à l'animateur afin de mettre à jour le modèle interne de l'animateur. Par exemple, si j'ai UILabel
, label
qui est un élément d'un UIGravityBehavior
je peux le déplacer en haut de l'écran pour le regarder retomber en disant:
Rapide
label.frame = CGRect(x: 0.0, y: 0.0, width: label.intrinsicContentSize.width, height: label.intrinsicContentSize.height)
dynamicAnimator.updateItem(usingCurrentState: label)
Objectif c
self.label.frame = CGRectMake(0.0, 0.0, self.label.intrinsicContentSize.width, self.label.intrinsicContentSize.height);
[self.dynamicAnimator updateItemUsingCurrentState: self.label];
Après quoi l'animateur appliquera le comportement de gravité du nouvel emplacement de l'étiquette.
Une autre technique courante consiste à utiliser UIDynamicBehaviors
pour positionner des vues. Par exemple, si vous souhaitez positionner une vue sous un événement tactile, la création d'un UIAttachmentBehavior
et la mise à jour de son anchorPoint
dans les anchorPoint
de touchesMoved
ou d' UIGestureRecognizer
constituent une stratégie efficace.
La place tombante
Permet de tracer un carré au milieu de notre vue et de le faire tomber en bas et d’arrêter au bord inférieur de la limite inférieure de l’écran.
@IBOutlet var animationView: UIView!
var squareView:UIView!
var collision: UICollisionBehavior!
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
override func viewDidLoad() {
super.viewDidLoad()
let squareSize = CGSize(width: 30.0, height: 30.0)
let centerPoint = CGPoint(x: self.animationView.bounds.midX - (squareSize.width/2), y: self.animationView.bounds.midY - (squareSize.height/2))
let frame = CGRect(origin: centerPoint, size: squareSize)
squareView = UIView(frame: frame)
squareView.backgroundColor = UIColor.orangeColor()
animationView.addSubview(squareView)
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [squareView])
animator.addBehavior(gravity)
collision = UICollisionBehavior(items: [square])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
}
Vue panoramique basée sur la vitesse de geste
Cet exemple montre comment faire en sorte qu'une vue suive un geste panoramique et se déclenche de manière physique.
Rapide
class ViewController: UIViewController
{
// Adjust to change speed of view from flick
let magnitudeMultiplier: CGFloat = 0.0008
lazy var dynamicAnimator: UIDynamicAnimator =
{
let dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
return dynamicAnimator
}()
lazy var gravity: UIGravityBehavior =
{
let gravity = UIGravityBehavior(items: [self.orangeView])
return gravity
}()
lazy var collision: UICollisionBehavior =
{
let collision = UICollisionBehavior(items: [self.orangeView])
collision.translatesReferenceBoundsIntoBoundary = true
return collision
}()
lazy var orangeView: UIView =
{
let widthHeight: CGFloat = 40.0
let orangeView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: widthHeight, height: widthHeight))
orangeView.backgroundColor = UIColor.orange
self.view.addSubview(orangeView)
return orangeView
}()
lazy var panGesture: UIPanGestureRecognizer =
{
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(sender:)))
return panGesture
}()
lazy var attachment: UIAttachmentBehavior =
{
let attachment = UIAttachmentBehavior(item: self.orangeView, attachedToAnchor: .zero)
return attachment
}()
override func viewDidLoad()
{
super.viewDidLoad()
dynamicAnimator.addBehavior(gravity)
dynamicAnimator.addBehavior(collision)
orangeView.addGestureRecognizer(panGesture)
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
orangeView.center = view.center
dynamicAnimator.updateItem(usingCurrentState: orangeView)
}
func handlePan(sender: UIPanGestureRecognizer)
{
let location = sender.location(in: view)
let velocity = sender.velocity(in: view)
let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
switch sender.state
{
case .began:
attachment.anchorPoint = location
dynamicAnimator.addBehavior(attachment)
case .changed:
attachment.anchorPoint = location
case .cancelled, .ended, .failed, .possible:
let push = UIPushBehavior(items: [self.orangeView], mode: .instantaneous)
push.pushDirection = CGVector(dx: velocity.x, dy: velocity.y)
push.magnitude = magnitude * magnitudeMultiplier
dynamicAnimator.removeBehavior(attachment)
dynamicAnimator.addBehavior(push)
}
}
}
Objectif c
@interface ViewController ()
@property (nonatomic, assign) CGFloat magnitudeMultiplier;
@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;
@property (nonatomic, strong) UIGravityBehavior *gravity;
@property (nonatomic, strong) UICollisionBehavior *collision;
@property (nonatomic, strong) UIView *orangeView;
@property (nonatomic, strong) UIPanGestureRecognizer *panGesture;
@property (nonatomic, strong) UIAttachmentBehavior *attachment;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.dynamicAnimator addBehavior:self.gravity];
[self.dynamicAnimator addBehavior:self.collision];
[self.orangeView addGestureRecognizer:self.panGesture];
// Adjust to change speed of view from flick
self.magnitudeMultiplier = 0.0008f;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.orangeView.center = self.view.center;
[self.dynamicAnimator updateItemUsingCurrentState:self.orangeView];
}
- (void)handlePan:(UIPanGestureRecognizer *)sender
{
CGPoint location = [sender locationInView:self.view];
CGPoint velocity = [sender velocityInView:self.view];
CGFloat magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y));
if (sender.state == UIGestureRecognizerStateBegan)
{
self.attachment.anchorPoint = location;
[self.dynamicAnimator addBehavior:self.attachment];
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
self.attachment.anchorPoint = location;
}
else if (sender.state == UIGestureRecognizerStateCancelled ||
sender.state == UIGestureRecognizerStateEnded ||
sender.state == UIGestureRecognizerStateFailed ||
sender.state == UIGestureRecognizerStatePossible)
{
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:@[self.orangeView] mode:UIPushBehaviorModeInstantaneous];
push.pushDirection = CGVectorMake(velocity.x, velocity.y);
push.magnitude = magnitude * self.magnitudeMultiplier;
[self.dynamicAnimator removeBehavior:self.attachment];
[self.dynamicAnimator addBehavior:push];
}
}
#pragma mark - Lazy Init
- (UIDynamicAnimator *)dynamicAnimator
{
if (!_dynamicAnimator)
{
_dynamicAnimator = [[UIDynamicAnimator alloc]initWithReferenceView:self.view];
}
return _dynamicAnimator;
}
- (UIGravityBehavior *)gravity
{
if (!_gravity)
{
_gravity = [[UIGravityBehavior alloc]initWithItems:@[self.orangeView]];
}
return _gravity;
}
- (UICollisionBehavior *)collision
{
if (!_collision)
{
_collision = [[UICollisionBehavior alloc]initWithItems:@[self.orangeView]];
_collision.translatesReferenceBoundsIntoBoundary = YES;
}
return _collision;
}
- (UIView *)orangeView
{
if (!_orangeView)
{
CGFloat widthHeight = 40.0f;
_orangeView = [[UIView alloc]initWithFrame:CGRectMake(0.0, 0.0, widthHeight, widthHeight)];
_orangeView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:_orangeView];
}
return _orangeView;
}
- (UIPanGestureRecognizer *)panGesture
{
if (!_panGesture)
{
_panGesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePan:)];
}
return _panGesture;
}
- (UIAttachmentBehavior *)attachment
{
if (!_attachment)
{
_attachment = [[UIAttachmentBehavior alloc]initWithItem:self.orangeView attachedToAnchor:CGPointZero];
}
return _attachment;
}
@end
Effet "Sticky Corners" en utilisant UIFieldBehaviors
Cet exemple montre comment obtenir un effet similaire à FaceTime si une vue est attirée vers le point une fois qu'elle pénètre dans une région particulière, en l'occurrence deux régions en haut et en bas.
Rapide
class ViewController: UIViewController
{
lazy var dynamicAnimator: UIDynamicAnimator =
{
let dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
return dynamicAnimator
}()
lazy var collision: UICollisionBehavior =
{
let collision = UICollisionBehavior(items: [self.orangeView])
collision.translatesReferenceBoundsIntoBoundary = true
return collision
}()
lazy var fieldBehaviors: [UIFieldBehavior] =
{
var fieldBehaviors = [UIFieldBehavior]()
for _ in 0 ..< 2
{
let field = UIFieldBehavior.springField()
field.addItem(self.orangeView)
fieldBehaviors.append(field)
}
return fieldBehaviors
}()
lazy var itemBehavior: UIDynamicItemBehavior =
{
let itemBehavior = UIDynamicItemBehavior(items: [self.orangeView])
// Adjust these values to change the "stickiness" of the view
itemBehavior.density = 0.01
itemBehavior.resistance = 10
itemBehavior.friction = 0.0
itemBehavior.allowsRotation = false
return itemBehavior
}()
lazy var orangeView: UIView =
{
let widthHeight: CGFloat = 40.0
let orangeView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: widthHeight, height: widthHeight))
orangeView.backgroundColor = UIColor.orange
self.view.addSubview(orangeView)
return orangeView
}()
lazy var panGesture: UIPanGestureRecognizer =
{
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(sender:)))
return panGesture
}()
lazy var attachment: UIAttachmentBehavior =
{
let attachment = UIAttachmentBehavior(item: self.orangeView, attachedToAnchor: .zero)
return attachment
}()
override func viewDidLoad()
{
super.viewDidLoad()
dynamicAnimator.addBehavior(collision)
dynamicAnimator.addBehavior(itemBehavior)
for field in fieldBehaviors
{
dynamicAnimator.addBehavior(field)
}
orangeView.addGestureRecognizer(panGesture)
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
orangeView.center = view.center
dynamicAnimator.updateItem(usingCurrentState: orangeView)
for (index, field) in fieldBehaviors.enumerated()
{
field.position = CGPoint(x: view.bounds
.midX, y: view.bounds.height * (0.25 + 0.5 * CGFloat(index)))
field.region = UIRegion(size: CGSize(width: view.bounds.width, height: view.bounds.height * 0.5))
}
}
func handlePan(sender: UIPanGestureRecognizer)
{
let location = sender.location(in: view)
let velocity = sender.velocity(in: view)
switch sender.state
{
case .began:
attachment.anchorPoint = location
dynamicAnimator.addBehavior(attachment)
case .changed:
attachment.anchorPoint = location
case .cancelled, .ended, .failed, .possible:
itemBehavior.addLinearVelocity(velocity, for: self.orangeView)
dynamicAnimator.removeBehavior(attachment)
}
}
}
Objectif c
@interface ViewController ()
@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;
@property (nonatomic, strong) UICollisionBehavior *collision;
@property (nonatomic, strong) UIAttachmentBehavior *attachment;
@property (nonatomic, strong) UIDynamicItemBehavior *itemBehavior;
@property (nonatomic, strong) NSArray <UIFieldBehavior *> *fieldBehaviors;
@property (nonatomic, strong) UIView *orangeView;
@property (nonatomic, strong) UIPanGestureRecognizer *panGesture;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.dynamicAnimator addBehavior:self.collision];
[self.dynamicAnimator addBehavior:self.itemBehavior];
for (UIFieldBehavior *field in self.fieldBehaviors)
{
[self.dynamicAnimator addBehavior:field];
}
[self.orangeView addGestureRecognizer:self.panGesture];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.orangeView.center = self.view.center;
[self.dynamicAnimator updateItemUsingCurrentState:self.orangeView];
for (NSInteger i = 0; i < self.fieldBehaviors.count; i++)
{
UIFieldBehavior *field = self.fieldBehaviors[i];
field.position = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetHeight(self.view.bounds) * (0.25f + 0.5f * i));
field.region = [[UIRegion alloc]initWithSize:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds) * 0.5)];
}
}
- (void)handlePan:(UIPanGestureRecognizer *)sender
{
CGPoint location = [sender locationInView:self.view];
CGPoint velocity = [sender velocityInView:self.view];
if (sender.state == UIGestureRecognizerStateBegan)
{
self.attachment.anchorPoint = location;
[self.dynamicAnimator addBehavior:self.attachment];
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
self.attachment.anchorPoint = location;
}
else if (sender.state == UIGestureRecognizerStateCancelled ||
sender.state == UIGestureRecognizerStateEnded ||
sender.state == UIGestureRecognizerStateFailed ||
sender.state == UIGestureRecognizerStatePossible)
{
[self.itemBehavior addLinearVelocity:velocity forItem:self.orangeView];
[self.dynamicAnimator removeBehavior:self.attachment];
}
}
#pragma mark - Lazy Init
- (UIDynamicAnimator *)dynamicAnimator
{
if (!_dynamicAnimator)
{
_dynamicAnimator = [[UIDynamicAnimator alloc]initWithReferenceView:self.view];
}
return _dynamicAnimator;
}
- (UICollisionBehavior *)collision
{
if (!_collision)
{
_collision = [[UICollisionBehavior alloc]initWithItems:@[self.orangeView]];
_collision.translatesReferenceBoundsIntoBoundary = YES;
}
return _collision;
}
- (NSArray <UIFieldBehavior *> *)fieldBehaviors
{
if (!_fieldBehaviors)
{
NSMutableArray *fields = [[NSMutableArray alloc]init];
for (NSInteger i = 0; i < 2; i++)
{
UIFieldBehavior *field = [UIFieldBehavior springField];
[field addItem:self.orangeView];
[fields addObject:field];
}
_fieldBehaviors = fields;
}
return _fieldBehaviors;
}
- (UIDynamicItemBehavior *)itemBehavior
{
if (!_itemBehavior)
{
_itemBehavior = [[UIDynamicItemBehavior alloc]initWithItems:@[self.orangeView]];
// Adjust these values to change the "stickiness" of the view
_itemBehavior.density = 0.01;
_itemBehavior.resistance = 10;
_itemBehavior.friction = 0.0;
_itemBehavior.allowsRotation = NO;
}
return _itemBehavior;
}
- (UIView *)orangeView
{
if (!_orangeView)
{
CGFloat widthHeight = 40.0f;
_orangeView = [[UIView alloc]initWithFrame:CGRectMake(0.0, 0.0, widthHeight, widthHeight)];
_orangeView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:_orangeView];
}
return _orangeView;
}
- (UIPanGestureRecognizer *)panGesture
{
if (!_panGesture)
{
_panGesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePan:)];
}
return _panGesture;
}
- (UIAttachmentBehavior *)attachment
{
if (!_attachment)
{
_attachment = [[UIAttachmentBehavior alloc]initWithItem:self.orangeView attachedToAnchor:CGPointZero];
}
return _attachment;
}
@end
Pour plus d'informations sur UIFieldBehaviors
consultez la session 2015 WWDC "Nouveautés de la dynamique UIKit et des effets visuels" et un exemple de code UIFieldBehaviors
.
Transition personnalisée UIDynamicBehavior
Cet exemple montre comment créer une transition de présentation personnalisée pilotée par un UIDynamicBehavior
composite. Nous pouvons commencer par créer un contrôleur de vue de présentation qui présentera un modal.
Rapide
class PresentingViewController: UIViewController
{
lazy var button: UIButton =
{
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(button)
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive
= true
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
button.setTitle("Present", for: .normal)
button.setTextColor(UIColor.blue, for: .normal)
return button
}()
override func viewDidLoad()
{
super.viewDidLoad()
button.addTarget(self, action: #selector(self.didPressPresent), for: .touchUpInside)
}
func didPressPresent()
{
let modal = ModalViewController()
modal.view.frame = CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0)
modal.modalPresentationStyle = .custom
modal.transitioningDelegate = modal
self.present(modal, animated: true)
}
}
Objectif c
@interface PresentingViewController ()
@property (nonatomic, strong) UIButton *button;
@end
@implementation PresentingViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.button addTarget:self action:@selector(didPressPresent) forControlEvents:UIControlEventTouchUpInside];
}
- (void)didPressPresent
{
ModalViewController *modal = [[ModalViewController alloc] init];
modal.view.frame = CGRectMake(0.0, 0.0, 200.0, 200.0);
modal.modalPresentationStyle = UIModalPresentationCustom;
modal.transitioningDelegate = modal;
[self presentViewController:modal animated:YES completion:nil];
}
- (UIButton *)button
{
if (!_button)
{
_button = [[UIButton alloc] init];
_button.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_button];
[_button.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
[_button.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
[_button setTitle:@"Present" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
}
return _button;
}
@end
Lorsque vous appuyez sur le bouton actuel, nous créons un ModalViewController
et définissons son style de présentation sur .custom
et définissons son transitionDelegate
sur lui-même. Cela nous permettra de proposer un animateur qui pilotera sa transition modale. Nous définissons également le cadre de la vue modal
afin qu'il soit plus petit que le plein écran.
Regardons maintenant ModalViewController
:
Rapide
class ModalViewController: UIViewController
{
lazy var button: UIButton =
{
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(button)
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive
= true
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
button.setTitle("Dismiss", for: .normal)
button.setTitleColor(.white, for: .normal)
return button
}()
override func viewDidLoad()
{
super.viewDidLoad()
button.addTarget(self, action: #selector(self.didPressDismiss), for: .touchUpInside)
view.backgroundColor = .red
view.layer.cornerRadius = 15.0
}
func didPressDismiss()
{
dismiss(animated: true)
}
}
extension ModalViewController: UIViewControllerTransitioningDelegate
{
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
return DropOutAnimator(duration: 1.5, isAppearing: true)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
return DropOutAnimator(duration: 4.0, isAppearing: false)
}
}
Objectif c
@interface ModalViewController () <UIViewControllerTransitioningDelegate>
@property (nonatomic, strong) UIButton *button;
@end
@implementation ModalViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.button addTarget:self action:@selector(didPressPresent) forControlEvents:UIControlEventTouchUpInside];
self.view.backgroundColor = [UIColor redColor];
self.view.layer.cornerRadius = 15.0f;
}
- (void)didPressPresent
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (UIButton *)button
{
if (!_button)
{
_button = [[UIButton alloc] init];
_button.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_button];
[_button.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
[_button.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
[_button setTitle:@"Dismiss" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
}
return _button;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return [[DropOutAnimator alloc]initWithDuration: 1.5 appearing:YES];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [[DropOutAnimator alloc] initWithDuration:4.0 appearing:NO];
}
@end
Ici, nous créons le contrôleur de vue qui est présenté. De plus, parce que ModalViewController
est son propre transitioningDelegate
il est également chargé de vendre un objet qui gérera son animation de transition. Pour nous, cela signifie transmettre une instance de notre sous-classe composite UIDynamicBehavior
.
Notre animateur aura deux transitions différentes: une pour la présentation et une pour le rejet. Pour la présentation, la vue du contrôleur de vue de présentation apparaîtra d'en haut. Et pour le rejeter, la vue semblera balancer d’une corde puis tomber. Comme DropOutAnimator
est conforme à UIViewControllerAnimatedTransitioning
majeure partie de ce travail sera effectuée dans son implémentation de func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
.
Rapide
class DropOutAnimator: UIDynamicBehavior
{
let duration: TimeInterval
let isAppearing: Bool
var transitionContext: UIViewControllerContextTransitioning?
var hasElapsedTimeExceededDuration = false
var finishTime: TimeInterval = 0.0
var collisionBehavior: UICollisionBehavior?
var attachmentBehavior: UIAttachmentBehavior?
var animator: UIDynamicAnimator?
init(duration: TimeInterval = 1.0, isAppearing: Bool)
{
self.duration = duration
self.isAppearing = isAppearing
super.init()
}
}
extension DropOutAnimator: UIViewControllerAnimatedTransitioning
{
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
// Get relevant views and view controllers from transitionContext
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to),
let fromView = fromVC.view,
let toView = toVC.view else { return }
let containerView = transitionContext.containerView
let duration = self.transitionDuration(using: transitionContext)
// Hold refrence to transitionContext to notify it of completion
self.transitionContext = transitionContext
// Create dynamic animator
let animator = UIDynamicAnimator(referenceView: containerView)
animator.delegate = self
self.animator = animator
// Presenting Animation
if self.isAppearing
{
fromView.isUserInteractionEnabled = false
// Position toView just off-screen
let fromViewInitialFrame = transitionContext.initialFrame(for: fromVC)
var toViewInitialFrame = toView.frame
toViewInitialFrame.origin.y -= toViewInitialFrame.height
toViewInitialFrame.origin.x = fromViewInitialFrame.width * 0.5 - toViewInitialFrame.width * 0.5
toView.frame = toViewInitialFrame
containerView.addSubview(toView)
// Prevent rotation and adjust bounce
let bodyBehavior = UIDynamicItemBehavior(items: [toView])
bodyBehavior.elasticity = 0.7
bodyBehavior.allowsRotation = false
// Add gravity at exaggerated magnitude so animation doesn't seem slow
let gravityBehavior = UIGravityBehavior(items: [toView])
gravityBehavior.magnitude = 10.0
// Set collision bounds to include off-screen view and have collision in center
// where our final view should come to rest
let collisionBehavior = UICollisionBehavior(items: [toView])
let insets = UIEdgeInsets(top: toViewInitialFrame.minY, left: 0.0, bottom: fromViewInitialFrame.height * 0.5 - toViewInitialFrame.height * 0.5, right: 0.0)
collisionBehavior.setTranslatesReferenceBoundsIntoBoundary(with: insets)
self.collisionBehavior = collisionBehavior
// Keep track of finish time in case we need to end the animator befor the animator pauses
self.finishTime = duration + (self.animator?.elapsedTime ?? 0.0)
// Closure that is called after every "tick" of the animator
// Check if we exceed duration
self.action =
{ [weak self] in
guard let strongSelf = self,
(strongSelf.animator?.elapsedTime ?? 0.0) >= strongSelf.finishTime else { return }
strongSelf.hasElapsedTimeExceededDuration = true
strongSelf.animator?.removeBehavior(strongSelf)
}
// `DropOutAnimator` is a composit behavior, so add child behaviors to self
self.addChildBehavior(collisionBehavior)
self.addChildBehavior(bodyBehavior)
self.addChildBehavior(gravityBehavior)
// Add self to dynamic animator
self.animator?.addBehavior(self)
}
// Dismissing Animation
else
{
// Create allow rotation and have a elastic item
let bodyBehavior = UIDynamicItemBehavior(items: [fromView])
bodyBehavior.elasticity = 0.8
bodyBehavior.angularResistance = 5.0
bodyBehavior.allowsRotation = true
// Create gravity with exaggerated magnitude
let gravityBehavior = UIGravityBehavior(items: [fromView])
gravityBehavior.magnitude = 10.0
// Collision boundary is set to have a floor just below the bottom of the screen
let collisionBehavior = UICollisionBehavior(items: [fromView])
let insets = UIEdgeInsets(top: 0.0, left: -1000, bottom: -225, right: -1000)
collisionBehavior.setTranslatesReferenceBoundsIntoBoundary(with: insets)
self.collisionBehavior = collisionBehavior
// Attachment behavior so view will have effect of hanging from a rope
let offset = UIOffset(horizontal: 70.0, vertical: fromView.bounds.height * 0.5)
var anchorPoint = CGPoint(x: fromView.bounds.maxX - 40.0, y: fromView.bounds.minY)
anchorPoint = containerView.convert(anchorPoint, from: fromView)
let attachmentBehavior = UIAttachmentBehavior(item: fromView, offsetFromCenter: offset, attachedToAnchor: anchorPoint)
attachmentBehavior.frequency = 3.0
attachmentBehavior.damping = 3.0
self.attachmentBehavior = attachmentBehavior
// `DropOutAnimator` is a composit behavior, so add child behaviors to self
self.addChildBehavior(collisionBehavior)
self.addChildBehavior(bodyBehavior)
self.addChildBehavior(gravityBehavior)
self.addChildBehavior(attachmentBehavior)
// Add self to dynamic animator
self.animator?.addBehavior(self)
// Animation has two parts part one is hanging from rope.
// Part two is bouncying off-screen
// Divide duration in two
self.finishTime = (2.0 / 3.0) * duration + (self.animator?.elapsedTime ?? 0.0)
// After every "tick" of animator check if past time limit
self.action =
{ [weak self] in
guard let strongSelf = self,
(strongSelf.animator?.elapsedTime ?? 0.0) >= strongSelf.finishTime else { return }
strongSelf.hasElapsedTimeExceededDuration = true
strongSelf.animator?.removeBehavior(strongSelf)
}
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
{
// Return the duration of the animation
return self.duration
}
}
extension DropOutAnimator: UIDynamicAnimatorDelegate
{
func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator)
{
// Animator has reached stasis
if self.isAppearing
{
// Check if we are out of time
if self.hasElapsedTimeExceededDuration
{
// Move to final positions
let toView = self.transitionContext?.viewController(forKey: .to)?.view
let containerView = self.transitionContext?.containerView
toView?.center = containerView?.center ?? .zero
self.hasElapsedTimeExceededDuration = false
}
// Clean up and call completion
self.transitionContext?.completeTransition(!(self.transitionContext?.transitionWasCancelled ?? false))
self.childBehaviors.forEach { self.removeChildBehavior($0) }
animator.removeAllBehaviors()
self.transitionContext = nil
}
else
{
if let attachmentBehavior = self.attachmentBehavior
{
// If we have an attachment, we are at the end of part one and start part two.
self.removeChildBehavior(attachmentBehavior)
self.attachmentBehavior = nil
animator.addBehavior(self)
let duration = self.transitionDuration(using: self.transitionContext)
self.finishTime = 1.0 / 3.0 * duration + animator.elapsedTime
}
else
{
// Clean up and call completion
let fromView = self.transitionContext?.viewController(forKey: .from)?.view
let toView = self.transitionContext?.viewController(forKey: .to)?.view
fromView?.removeFromSuperview()
toView?.isUserInteractionEnabled = true
self.transitionContext?.completeTransition(!(self.transitionContext?.transitionWasCancelled ?? false))
self.childBehaviors.forEach { self.removeChildBehavior($0) }
animator.removeAllBehaviors()
self.transitionContext = nil
}
}
}
}
Objectif c
@interface ObjcDropOutAnimator() <UIDynamicAnimatorDelegate, UIViewControllerAnimatedTransitioning>
@property (nonatomic, strong) id<UIViewControllerContextTransitioning> transitionContext;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic, assign) NSTimeInterval finishTime;
@property (nonatomic, assign) BOOL elapsedTimeExceededDuration;
@property (nonatomic, assign, getter=isAppearing) BOOL appearing;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, strong) UIAttachmentBehavior *attachBehavior;
@property (nonatomic, strong) UICollisionBehavior * collisionBehavior;
@end
@implementation ObjcDropOutAnimator
- (instancetype)initWithDuration:(NSTimeInterval)duration appearing:(BOOL)appearing
{
self = [super init];
if (self)
{
_duration = duration;
_appearing = appearing;
}
return self;
}
- (void) animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
// Get relevant views and view controllers from transitionContext
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
UIView *containerView = transitionContext.containerView;
NSTimeInterval duration = [self transitionDuration:transitionContext];
// Hold refrence to transitionContext to notify it of completion
self.transitionContext = transitionContext;
// Create dynamic animator
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc]initWithReferenceView:containerView];
animator.delegate = self;
self.animator = animator;
// Presenting Animation
if (self.isAppearing)
{
fromView.userInteractionEnabled = NO;
// Position toView just above screen
CGRect fromViewInitialFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect toViewInitialFrame = toView.frame;
toViewInitialFrame.origin.y -= CGRectGetHeight(toViewInitialFrame);
toViewInitialFrame.origin.x = CGRectGetWidth(fromViewInitialFrame) * 0.5 - CGRectGetWidth(toViewInitialFrame) * 0.5;
toView.frame = toViewInitialFrame;
[containerView addSubview:toView];
// Prevent rotation and adjust bounce
UIDynamicItemBehavior *bodyBehavior = [[UIDynamicItemBehavior alloc]initWithItems:@[toView]];
bodyBehavior.elasticity = 0.7;
bodyBehavior.allowsRotation = NO;
// Add gravity at exaggerated magnitude so animation doesn't seem slow
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc]initWithItems:@[toView]];
gravityBehavior.magnitude = 10.0f;
// Set collision bounds to include off-screen view and have collision floor in center
// where our final view should come to rest
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc]initWithItems:@[toView]];
UIEdgeInsets insets = UIEdgeInsetsMake(CGRectGetMinY(toViewInitialFrame), 0.0, CGRectGetHeight(fromViewInitialFrame) * 0.5 - CGRectGetHeight(toViewInitialFrame) * 0.5, 0.0);
[collisionBehavior setTranslatesReferenceBoundsIntoBoundaryWithInsets:insets];
self.collisionBehavior = collisionBehavior;
// Keep track of finish time in case we need to end the animator befor the animator pauses
self.finishTime = duration + self.animator.elapsedTime;
// Closure that is called after every "tick" of the animator
// Check if we exceed duration
__weak ObjcDropOutAnimator *weakSelf = self;
self.action = ^{
__strong ObjcDropOutAnimator *strongSelf = weakSelf;
if (strongSelf)
{
if (strongSelf.animator.elapsedTime >= strongSelf.finishTime)
{
strongSelf.elapsedTimeExceededDuration = YES;
[strongSelf.animator removeBehavior:strongSelf];
}
}
};
// `DropOutAnimator` is a composit behavior, so add child behaviors to self
[self addChildBehavior:collisionBehavior];
[self addChildBehavior:bodyBehavior];
[self addChildBehavior:gravityBehavior];
// Add self to dynamic animator
[self.animator addBehavior:self];
}
// Dismissing Animation
else
{
// Allow rotation and have a elastic item
UIDynamicItemBehavior *bodyBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[fromView]];
bodyBehavior.elasticity = 0.8;
bodyBehavior.angularResistance = 5.0;
bodyBehavior.allowsRotation = YES;
// Create gravity with exaggerated magnitude
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[fromView]];
gravityBehavior.magnitude = 10.0f;
// Collision boundary is set to have a floor just below the bottom of the screen
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[fromView]];
UIEdgeInsets insets = UIEdgeInsetsMake(0, -1000, -225, -1000);
[collisionBehavior setTranslatesReferenceBoundsIntoBoundaryWithInsets:insets];
self.collisionBehavior = collisionBehavior;
// Attachment behavior so view will have effect of hanging from a rope
UIOffset offset = UIOffsetMake(70, -(CGRectGetHeight(fromView.bounds) / 2.0));
CGPoint anchorPoint = CGPointMake(CGRectGetMaxX(fromView.bounds) - 40,
CGRectGetMinY(fromView.bounds));
anchorPoint = [containerView convertPoint:anchorPoint fromView:fromView];
UIAttachmentBehavior *attachBehavior = [[UIAttachmentBehavior alloc] initWithItem:fromView offsetFromCenter:offset attachedToAnchor:anchorPoint];
attachBehavior.frequency = 3.0;
attachBehavior.damping = 0.3;
attachBehavior.length = 40;
self.attachBehavior = attachBehavior;
// `DropOutAnimator` is a composit behavior, so add child behaviors to self
[self addChildBehavior:collisionBehavior];
[self addChildBehavior:bodyBehavior];
[self addChildBehavior:gravityBehavior];
[self addChildBehavior:attachBehavior];
// Add self to dynamic animator
[self.animator addBehavior:self];
// Animation has two parts part one is hanging from rope.
// Part two is bouncying off-screen
// Divide duration in two
self.finishTime = (2./3.) * duration + [self.animator elapsedTime];
// After every "tick" of animator check if past time limit
__weak ObjcDropOutAnimator *weakSelf = self;
self.action = ^{
__strong ObjcDropOutAnimator *strongSelf = weakSelf;
if (strongSelf)
{
if ([strongSelf.animator elapsedTime] >= strongSelf.finishTime)
{
strongSelf.elapsedTimeExceededDuration = YES;
[strongSelf.animator removeBehavior:strongSelf];
}
}
};
}
}
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return self.duration;
}
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator
{
// Animator has reached stasis
if (self.isAppearing)
{
// Check if we are out of time
if (self.elapsedTimeExceededDuration)
{
// Move to final positions
UIView *toView = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
UIView *containerView = [self.transitionContext containerView];
toView.center = containerView.center;
self.elapsedTimeExceededDuration = NO;
}
// Clean up and call completion
[self.transitionContext completeTransition:![self.transitionContext transitionWasCancelled]];
for (UIDynamicBehavior *behavior in self.childBehaviors)
{
[self removeChildBehavior:behavior];
}
[animator removeAllBehaviors];
self.transitionContext = nil;
}
// Dismissing
else
{
if (self.attachBehavior)
{
// If we have an attachment, we are at the end of part one and start part two.
[self removeChildBehavior:self.attachBehavior];
self.attachBehavior = nil;
[animator addBehavior:self];
NSTimeInterval duration = [self transitionDuration:self.transitionContext];
self.finishTime = 1./3. * duration + [animator elapsedTime];
}
else
{
// Clean up and call completion
UIView *fromView = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view;
UIView *toView = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
[fromView removeFromSuperview];
toView.userInteractionEnabled = YES;
[self.transitionContext completeTransition:![self.transitionContext transitionWasCancelled]];
for (UIDynamicBehavior *behavior in self.childBehaviors)
{
[self removeChildBehavior:behavior];
}
[animator removeAllBehaviors];
self.transitionContext = nil;
}
}
}
En tant que comportement composite, DropOutAnimator
peut combiner différents comportements pour effectuer ses animations de présentation et de rejet. DropOutAnimator
montre également comment utiliser le bloc d' action
d'un comportement pour inspecter les emplacements de ses éléments, ainsi que le temps écoulé depuis une technique pouvant être utilisée pour supprimer les vues déplacées ou tronquer les animations qui n'ont pas encore atteint la stase.
Pour plus d'informations Session 2013 de la WWDC "Techniques avancées avec UIKit Dynamics" ainsi que SOLPresentingFun
Transformation de l'ombre avec la physique du monde réel à l'aide de UIDynamicBehaviors
Cet exemple montre comment effectuer une transition de présentation interactive avec une physique "réaliste" similaire à l'écran de notification d'iOS.
Pour commencer, nous avons besoin d'un contrôleur de vue de présentation sur lequel l'ombre apparaîtra. Ce contrôleur de vues fera également office de UIViewControllerTransitioningDelegate
pour notre contrôleur de vue présenté et proposera aux animateurs notre transition. Nous allons donc créer des instances de nos animateurs interactifs (un pour la présentation et un pour le rejet). Nous allons également créer une instance du contrôleur de vue d'ombre, qui, dans cet exemple, n'est qu'un contrôleur de vue avec une étiquette. Parce que nous voulons que le même geste de panoramique dirige l'intégralité de l'interaction, nous transmettons des références au contrôleur de présentation et à la nuance à nos animateurs interactifs.
Rapide
class ViewController: UIViewController
{
var presentingAnimator: ShadeAnimator!
var dismissingAnimator: ShadeAnimator!
let shadeVC = ShadeViewController()
lazy var label: UILabel =
{
let label = UILabel()
label.textColor = .blue
label.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(label)
label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
return label
}()
override func viewDidLoad()
{
super.viewDidLoad()
label.text = "Swipe Down From Top"
presentingAnimator = ShadeAnimator(isAppearing: true, presentingVC: self, presentedVC: shadeVC, transitionDelegate: self)
dismissingAnimator = ShadeAnimator(isAppearing: false, presentingVC: self, presentedVC: shadeVC, transitionDelegate: self)
}
}
extension ViewController: UIViewControllerTransitioningDelegate
{
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
return EmptyAnimator()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
return EmptyAnimator()
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
{
return presentingAnimator
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
{
return dismissingAnimator
}
}
Objectif c
@interface ObjCViewController () <UIViewControllerTransitioningDelegate>
@property (nonatomic, strong) ShadeAnimator *presentingAnimator;
@property (nonatomic, strong) ShadeAnimator *dismissingAnimator;
@property (nonatomic, strong) UILabel *label;
@property (nonatomic, strong) ShadeViewController *shadeVC;
@end
@implementation ObjCViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.label.text = @"Swipe Down From Top";
self.shadeVC = [[ShadeViewController alloc] init];
self.presentingAnimator = [[ShadeAnimator alloc] initWithIsAppearing:YES presentingVC:self presentedVC:self.shadeVC transitionDelegate:self];
self.dismissingAnimator = [[ShadeAnimator alloc] initWithIsAppearing:NO presentingVC:self presentedVC:self.shadeVC transitionDelegate:self];
}
- (UILabel *)label
{
if (!_label)
{
_label = [[UILabel alloc] init];
_label.textColor = [UIColor blueColor];
_label.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_label];
[_label.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
[_label.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
}
return _label;
}
#pragma mark - UIViewControllerTransitioningDelegate
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return [[EmptyAnimator alloc] init];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [[EmptyAnimator alloc] init];
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator
{
return self.presentingAnimator;
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
return self.dismissingAnimator;
}
@end
Nous ne voulons vraiment que vouloir présenter notre nuance à travers une transition interactive, mais à cause de la façon dont UIViewControllerTransitioningDelegate
fonctionne si nous ne retournons pas un contrôleur d'animation régulier, notre contrôleur interactif ne sera jamais utilisé. Pour cette raison, nous créons une classe EmptyAnimator
conforme à UIViewControllerAnimatedTransitioning
.
Rapide
class EmptyAnimator: NSObject
{
}
extension EmptyAnimator: UIViewControllerAnimatedTransitioning
{
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
{
return 0.0
}
}
Objectif c
@implementation EmptyAnimator
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
}
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.0;
}
@end
Enfin, nous devons créer le ShadeAnimator
qui est une sous-classe de UIDynamicBehavior
conforme à UIViewControllerInteractiveTransitioning
.
Rapide
class ShadeAnimator: UIDynamicBehavior
{
// Whether we are presenting or dismissing
let isAppearing: Bool
// The view controller that is not the shade
weak var presentingVC: UIViewController?
// The view controller that is the shade
weak var presentedVC: UIViewController?
// The delegate will vend the animator
weak var transitionDelegate: UIViewControllerTransitioningDelegate?
// Feedback generator for haptics on collisions
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
// The context given to the animator at the start of the transition
var transitionContext: UIViewControllerContextTransitioning?
// Time limit of the dynamic part of the animation
var finishTime: TimeInterval = 4.0
// The Pan Gesture that drives the transition. Not using EdgePan because triggers Notifications screen
lazy var pan: UIPanGestureRecognizer =
{
let pan = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(sender:)))
return pan
}()
// The dynamic animator that we add `ShadeAnimator` to
lazy var animator: UIDynamicAnimator! =
{
let animator = UIDynamicAnimator(referenceView: self.transitionContext!.containerView)
return animator
}()
// init with all of our dependencies
init(isAppearing: Bool, presentingVC: UIViewController, presentedVC: UIViewController, transitionDelegate: UIViewControllerTransitioningDelegate)
{
self.isAppearing = isAppearing
self.presentingVC = presentingVC
self.presentedVC = presentedVC
self.transitionDelegate = transitionDelegate
super.init()
self.impactFeedbackGenerator.prepare()
if isAppearing
{
self.presentingVC?.view.addGestureRecognizer(pan)
}
else
{
self.presentedVC?.view.addGestureRecognizer(pan)
}
}
// Setup and moves shade view controller to just above screen if appearing
func setupViewsForTransition(with transitionContext: UIViewControllerContextTransitioning)
{
// Get relevant views and view controllers from transitionContext
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to),
let toView = toVC.view else { return }
let containerView = transitionContext.containerView
// Hold refrence to transitionContext to notify it of completion
self.transitionContext = transitionContext
if isAppearing
{
// Position toView just off-screen
let fromViewInitialFrame = transitionContext.initialFrame(for: fromVC)
var toViewInitialFrame = toView.frame
toViewInitialFrame.origin.y -= toViewInitialFrame.height
toViewInitialFrame.origin.x = fromViewInitialFrame.width * 0.5 - toViewInitialFrame.width * 0.5
toView.frame = toViewInitialFrame
containerView.addSubview(toView)
}
else
{
fromVC.view.addGestureRecognizer(pan)
}
}
// Handles the entire interaction from presenting/dismissing to completion
func handlePan(sender: UIPanGestureRecognizer)
{
let location = sender.location(in: transitionContext?.containerView)
let velocity = sender.velocity(in: transitionContext?.containerView)
let fromVC = transitionContext?.viewController(forKey: .from)
let toVC = transitionContext?.viewController(forKey: .to)
let touchStartHeight: CGFloat = 90.0
let touchLocationFromBottom: CGFloat = 20.0
switch sender.state
{
case .began:
let beginLocation = sender.location(in: sender.view)
if isAppearing
{
guard beginLocation.y <= touchStartHeight,
let presentedVC = self.presentedVC else { break }
presentedVC.modalPresentationStyle = .custom
presentedVC.transitioningDelegate = transitionDelegate
presentingVC?.present(presentedVC, animated: true)
}
else
{
guard beginLocation.y >= (sender.view?.frame.height ?? 0.0) - touchStartHeight else { break }
presentedVC?.dismiss(animated: true)
}
case .changed:
guard let view = isAppearing ? toVC?.view : fromVC?.view else { return }
UIView.animate(withDuration: 0.2)
{
view.frame.origin.y = location.y - view.bounds.height + touchLocationFromBottom
}
transitionContext?.updateInteractiveTransition(view.frame.maxY / view.frame.height
)
case .ended, .cancelled:
guard let view = isAppearing ? toVC?.view : fromVC?.view else { return }
let isCancelled = isAppearing ? (velocity.y < 0.5 || view.center.y < 0.0) : (velocity.y > 0.5 || view.center.y > 0.0)
addAttachmentBehavior(with: view, isCancelled: isCancelled)
addCollisionBehavior(with: view)
addItemBehavior(with: view)
animator.addBehavior(self)
animator.delegate = self
self.action =
{ [weak self] in
guard let strongSelf = self else { return }
if strongSelf.animator.elapsedTime > strongSelf.finishTime
{
strongSelf.animator.removeAllBehaviors()
}
else
{
strongSelf.transitionContext?.updateInteractiveTransition(view.frame.maxY / view.frame.height
)
}
}
default:
break
}
}
// Add collision behavior that causes bounce when finished
func addCollisionBehavior(with view: UIView)
{
let collisionBehavior = UICollisionBehavior(items: [view])
let insets = UIEdgeInsets(top: -view.bounds.height, left: 0.0, bottom: 0.0, right: 0.0)
collisionBehavior.setTranslatesReferenceBoundsIntoBoundary(with: insets)
collisionBehavior.collisionDelegate = self
self.addChildBehavior(collisionBehavior)
}
// Add attachment behavior that pulls shade either to top or bottom
func addAttachmentBehavior(with view: UIView, isCancelled: Bool)
{
let anchor: CGPoint
switch (isAppearing, isCancelled)
{
case (true, true), (false, false):
anchor = CGPoint(x: view.center.x, y: -view.frame.height)
case (true, false), (false, true):
anchor = CGPoint(x: view.center.x, y: view.frame.height)
}
let attachmentBehavior = UIAttachmentBehavior(item: view, attachedToAnchor: anchor)
attachmentBehavior.damping = 0.1
attachmentBehavior.frequency = 3.0
attachmentBehavior.length = 0.5 * view.frame.height
self.addChildBehavior(attachmentBehavior)
}
// Makes view more bouncy
func addItemBehavior(with view: UIView)
{
let itemBehavior = UIDynamicItemBehavior(items: [view])
itemBehavior.allowsRotation = false
itemBehavior.elasticity = 0.6
self.addChildBehavior(itemBehavior)
}
}
extension ShadeAnimator: UIDynamicAnimatorDelegate
{
// Determines transition has ended
func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator)
{
guard let transitionContext = self.transitionContext else { return }
let fromVC = transitionContext.viewController(forKey: .from)
let toVC = transitionContext.viewController(forKey: .to)
guard let view = isAppearing ? toVC?.view : fromVC?.view else { return }
switch (view.center.y < 0.0, isAppearing)
{
case (true, true), (true, false):
view.removeFromSuperview()
transitionContext.finishInteractiveTransition()
transitionContext.completeTransition(!isAppearing)
case (false, true):
toVC?.view.frame = transitionContext.finalFrame(for: toVC!)
transitionContext.finishInteractiveTransition()
transitionContext.completeTransition(true)
case (false, false):
fromVC?.view.frame = transitionContext.initialFrame(for: fromVC!)
transitionContext.cancelInteractiveTransition()
transitionContext.completeTransition(false)
}
childBehaviors.forEach { removeChildBehavior($0) }
animator.removeAllBehaviors()
self.animator = nil
self.transitionContext = nil
}
}
extension ShadeAnimator: UICollisionBehaviorDelegate
{
// Triggers haptics
func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying?, at p: CGPoint)
{
guard p.y > 0.0 else { return }
impactFeedbackGenerator.impactOccurred()
}
}
extension ShadeAnimator: UIViewControllerInteractiveTransitioning
{
// Starts transition
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning)
{
setupViewsForTransition(with: transitionContext)
}
}
Objectif c
@interface ShadeAnimator() <UIDynamicAnimatorDelegate, UICollisionBehaviorDelegate>
@property (nonatomic, assign) BOOL isAppearing;
@property (nonatomic, weak) UIViewController *presentingVC;
@property (nonatomic, weak) UIViewController *presentedVC;
@property (nonatomic, weak) NSObject<UIViewControllerTransitioningDelegate> *transitionDelegate;
@property (nonatomic, strong) UIImpactFeedbackGenerator *impactFeedbackGenerator;
@property (nonatomic, strong) id<UIViewControllerContextTransitioning> transitionContext;
@property (nonatomic, assign) NSTimeInterval finishTime;
@property (nonatomic, strong) UIPanGestureRecognizer *pan;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@end
@implementation ShadeAnimator
- (instancetype)initWithIsAppearing:(BOOL)isAppearing presentingVC:(UIViewController *)presentingVC presentedVC:(UIViewController *)presentedVC transitionDelegate:(id<UIViewControllerTransitioningDelegate>)transitionDelegate
{
self = [super init];
if (self)
{
_isAppearing = isAppearing;
_presentingVC = presentingVC;
_presentedVC = presentedVC;
_transitionDelegate = transitionDelegate;
_impactFeedbackGenerator = [[UIImpactFeedbackGenerator alloc]initWithStyle:UIImpactFeedbackStyleLight];
[_impactFeedbackGenerator prepare];
if (_isAppearing)
{
[_presentingVC.view addGestureRecognizer:self.pan];
}
else
{
[_presentedVC.view addGestureRecognizer:self.pan];
}
}
return self;
}
#pragma mark - Lazy Init
- (UIPanGestureRecognizer *)pan
{
if (!_pan)
{
_pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePan:)];
}
return _pan;
}
- (UIDynamicAnimator *)animator
{
if (!_animator)
{
_animator = [[UIDynamicAnimator alloc]initWithReferenceView:self.transitionContext.containerView];
}
return _animator;
}
#pragma mark - Setup
- (void)setupViewForTransitionWithContext:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = toVC.view;
UIView *containerView = transitionContext.containerView;
self.transitionContext = transitionContext;
if (self.isAppearing)
{
CGRect fromViewInitialFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect toViewInitialFrame = toView.frame;
toViewInitialFrame.origin.y -= CGRectGetHeight(toViewInitialFrame);
toViewInitialFrame.origin.x = CGRectGetWidth(fromViewInitialFrame) * 0.5 - CGRectGetWidth(toViewInitialFrame) * 0.5;
[containerView addSubview:toView];
}
else
{
[fromVC.view addGestureRecognizer:self.pan];
}
}
#pragma mark - Gesture
- (void)handlePan:(UIPanGestureRecognizer *)sender
{
CGPoint location = [sender locationInView:self.transitionContext.containerView];
CGPoint velocity = [sender velocityInView:self.transitionContext.containerView];
UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGFloat touchStartHeight = 90.0;
CGFloat touchLocationFromBottom = 20.0;
if (sender.state == UIGestureRecognizerStateBegan)
{
CGPoint beginLocation = [sender locationInView:sender.view];
if (self.isAppearing)
{
if (beginLocation.y <= touchStartHeight)
{
self.presentedVC.modalPresentationStyle = UIModalPresentationCustom;
self.presentedVC.transitioningDelegate = self.transitionDelegate;
[self.presentingVC presentViewController:self.presentedVC animated:YES completion:nil];
}
}
else
{
if (beginLocation.y >= [sender locationInView:sender.view].y - touchStartHeight)
{
[self.presentedVC dismissViewControllerAnimated:true completion:nil];
}
}
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
UIView *view = self.isAppearing ? toVC.view : fromVC.view;
[UIView animateWithDuration:0.2 animations:^{
CGRect frame = view.frame;
frame.origin.y = location.y - CGRectGetHeight(view.bounds) + touchLocationFromBottom;
view.frame = frame;
}];
[self.transitionContext updateInteractiveTransition:CGRectGetMaxY(view.frame) / CGRectGetHeight(view.frame)];
}
else if (sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateCancelled)
{
UIView *view = self.isAppearing ? toVC.view : fromVC.view;
BOOL isCancelled = self.isAppearing ? (velocity.y < 0.5 || view.center.y < 0.0) : (velocity.y > 0.5 || view.center.y > 0.0);
[self addAttachmentBehaviorWithView:view isCancelled:isCancelled];
[self addCollisionBehaviorWithView:view];
[self addItemBehaviorWithView:view];
[self.animator addBehavior:self];
self.animator.delegate = self;
__weak ShadeAnimator *weakSelf = self;
self.action =
^{
if (weakSelf.animator.elapsedTime > weakSelf.finishTime)
{
[weakSelf.animator removeAllBehaviors];
}
else
{
[weakSelf.transitionContext updateInteractiveTransition:CGRectGetMaxY(view.frame) / CGRectGetHeight(view.frame)];
}
};
}
}
#pragma mark - UIViewControllerInteractiveTransitioning
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
[self setupViewForTransitionWithContext:transitionContext];
}
#pragma mark - Behaviors
- (void)addCollisionBehaviorWithView:(UIView *)view
{
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc]initWithItems:@[view]];
UIEdgeInsets insets = UIEdgeInsetsMake(-CGRectGetHeight(view.bounds), 0.0, 0.0, 0.0);
[collisionBehavior setTranslatesReferenceBoundsIntoBoundaryWithInsets:insets];
collisionBehavior.collisionDelegate = self;
[self addChildBehavior:collisionBehavior];
}
- (void)addItemBehaviorWithView:(UIView *)view
{
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc]initWithItems:@[view]];
itemBehavior.allowsRotation = NO;
itemBehavior.elasticity = 0.6;
[self addChildBehavior:itemBehavior];
}
- (void)addAttachmentBehaviorWithView:(UIView *)view isCancelled:(BOOL)isCancelled
{
CGPoint anchor;
if ((self.isAppearing && isCancelled) || (!self.isAppearing && isCancelled))
{
anchor = CGPointMake(view.center.x, -CGRectGetHeight(view.frame));
}
else
{
anchor = CGPointMake(view.center.x, -CGRectGetHeight(view.frame));
}
UIAttachmentBehavior *attachmentBehavior = [[UIAttachmentBehavior alloc]initWithItem:view attachedToAnchor:anchor];
attachmentBehavior.damping = 0.1;
attachmentBehavior.frequency = 3.0;
attachmentBehavior.length = 0.5 * CGRectGetHeight(view.frame);
[self addChildBehavior:attachmentBehavior];
}
#pragma mark - UICollisionBehaviorDelegate
- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p
{
if (p.y > 0.0)
{
[self.impactFeedbackGenerator impactOccurred];
}
}
#pragma mark - UIDynamicAnimatorDelegate
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator
{
UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *view = self.isAppearing ? toVC.view : fromVC.view;
if (view.center.y < 0.0 && (self.isAppearing || !self.isAppearing))
{
[view removeFromSuperview];
[self.transitionContext finishInteractiveTransition];
[self.transitionContext completeTransition:!self.isAppearing];
}
else if (view.center.y >= 0.0 && self.isAppearing)
{
toVC.view.frame = [self.transitionContext finalFrameForViewController:toVC];
[self.transitionContext finishInteractiveTransition];
[self.transitionContext completeTransition:YES];
}
else
{
fromVC.view.frame = [self.transitionContext initialFrameForViewController:fromVC];
[self.transitionContext cancelInteractiveTransition];
[self.transitionContext completeTransition:NO];
}
for (UIDynamicBehavior *behavior in self.childBehaviors)
{
[self removeChildBehavior:behavior];
}
[animator removeAllBehaviors];
self.animator = nil;
self.transitionContext = nil;
}
@end
L'animateur déclenche le début de la transition lorsque le geste panoramique commence. Et déplace simplement la vue au fur et à mesure que le geste change. Mais lorsque le geste se termine, UIDynamicBehaviors
détermine si la transition doit être terminée ou annulée. Pour ce faire, il utilise un comportement d'attachement et de collision. Pour plus d'informations, consultez la session 2013 de WWDC "Techniques avancées avec UIKit Dynamics .
Mapper la position de l'animation dynamique sur les limites
Cet exemple montre comment personnaliser le protocole UIDynamicItem
pour mapper les modifications de position d'une vue animée dynamiquement aux modifications de limites pour créer un UIButton
qui se développe et se contracte de manière élastique.
Pour commencer, nous devons créer un nouveau protocole qui implémente UIDynamicItem
mais qui possède également une propriété de bounds
configurables et personnalisables.
Rapide
protocol ResizableDynamicItem: UIDynamicItem
{
var bounds: CGRect { set get }
}
extension UIView: ResizableDynamicItem {}
Objectif c
@protocol ResizableDynamicItem <UIDynamicItem>
@property (nonatomic, readwrite) CGRect bounds;
@end
Nous allons ensuite créer un objet wrapper qui UIDynamicItem
un UIDynamicItem
mais qui UIDynamicItem
modifications du centre sur la largeur et la hauteur de l'élément. Nous allons également fournir des relais pour les bounds
et la transform
de l'élément sous-jacent. Cela entraînera toute modification apportée par l'animateur dynamique aux valeurs centrales x et y de l'élément sous-jacent sera appliquée à la largeur et à la hauteur des éléments.
Rapide
final class PositionToBoundsMapping: NSObject, UIDynamicItem
{
var target: ResizableDynamicItem
init(target: ResizableDynamicItem)
{
self.target = target
super.init()
}
var bounds: CGRect
{
get
{
return self.target.bounds
}
}
var center: CGPoint
{
get
{
return CGPoint(x: self.target.bounds.width, y: self.target.bounds.height)
}
set
{
self.target.bounds = CGRect(x: 0.0, y: 0.0, width: newValue.x, height: newValue.y)
}
}
var transform: CGAffineTransform
{
get
{
return self.target.transform
}
set
{
self.target.transform = newValue
}
}
}
Objectif c
@interface PositionToBoundsMapping ()
@property (nonatomic, strong) id<ResizableDynamicItem> target;
@end
@implementation PositionToBoundsMapping
- (instancetype)initWithTarget:(id<ResizableDynamicItem>)target
{
self = [super init];
if (self)
{
_target = target;
}
return self;
}
- (CGRect)bounds
{
return self.target.bounds;
}
- (CGPoint)center
{
return CGPointMake(self.target.bounds.size.width, self.target.bounds.size.height);
}
- (void)setCenter:(CGPoint)center
{
self.target.bounds = CGRectMake(0, 0, center.x, center.y);
}
- (CGAffineTransform)transform
{
return self.target.transform;
}
- (void)setTransform:(CGAffineTransform)transform
{
self.target.transform = transform;
}
@end
Enfin, nous allons créer un UIViewController
qui aura un bouton. Lorsque le bouton est pressé, nous allons créer PositionToBoundsMapping
avec le bouton comme élément dynamique enveloppé. Nous créons un UIAttachmentBehavior
à sa position actuelle puis y ajoutons un UIPushBehavior
instantané. Cependant, parce que nous avons mappé change ses limites, le bouton ne bouge pas mais plutôt grandit et rétrécit.
Rapide
final class ViewController: UIViewController
{
lazy var button: UIButton =
{
let button = UIButton(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 200.0))
button.backgroundColor = .red
button.layer.cornerRadius = 15.0
button.setTitle("Tap Me", for: .normal)
self.view.addSubview(button)
return button
}()
var buttonBounds = CGRect.zero
var animator: UIDynamicAnimator?
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
button.addTarget(self, action: #selector(self.didPressButton(sender:)), for: .touchUpInside)
buttonBounds = button.bounds
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
button.center = view.center
}
func didPressButton(sender: UIButton)
{
// Reset bounds so if button is press twice in a row, previous changes don't propogate
button.bounds = buttonBounds
let animator = UIDynamicAnimator(referenceView: view)
// Create mapping
let buttonBoundsDynamicItem = PositionToBoundsMapping(target: button)
// Add Attachment behavior
let attachmentBehavior = UIAttachmentBehavior(item: buttonBoundsDynamicItem, attachedToAnchor: buttonBoundsDynamicItem.center)
// Higher frequency faster oscillation
attachmentBehavior.frequency = 2.0
// Lower damping longer oscillation lasts
attachmentBehavior.damping = 0.1
animator.addBehavior(attachmentBehavior)
let pushBehavior = UIPushBehavior(items: [buttonBoundsDynamicItem], mode: .instantaneous)
// Change angle to determine how much height/ width should change 45° means heigh:width is 1:1
pushBehavior.angle = .pi / 4.0
// Larger magnitude means bigger change
pushBehavior.magnitude = 30.0
animator.addBehavior(pushBehavior)
pushBehavior.active = true
// Hold refrence so animator is not released
self.animator = animator
}
}
Objectif c
@interface ViewController ()
@property (nonatomic, strong) UIButton *button;
@property (nonatomic, assign) CGRect buttonBounds;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self.button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
self.buttonBounds = self.button.bounds;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.button.center = self.view.center;
}
- (UIButton *)button
{
if (!_button)
{
_button = [[UIButton alloc]initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
_button.backgroundColor = [UIColor redColor];
_button.layer.cornerRadius = 15.0;
[_button setTitle:@"Tap Me" forState:UIControlStateNormal];
[self.view addSubview:_button];
}
return _button;
}
- (void)didTapButton:(id)sender
{
self.button.bounds = self.buttonBounds;
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
PositionToBoundsMapping *buttonBoundsDynamicItem = [[PositionToBoundsMapping alloc]initWithTarget:sender];
UIAttachmentBehavior *attachmentBehavior = [[UIAttachmentBehavior alloc]initWithItem:buttonBoundsDynamicItem attachedToAnchor:buttonBoundsDynamicItem.center];
[attachmentBehavior setFrequency:2.0];
[attachmentBehavior setDamping:0.3];
[animator addBehavior:attachmentBehavior];
UIPushBehavior *pushBehavior = [[UIPushBehavior alloc] initWithItems:@[buttonBoundsDynamicItem] mode:UIPushBehaviorModeInstantaneous];
pushBehavior.angle = M_PI_4;
pushBehavior.magnitude = 2.0;
[animator addBehavior:pushBehavior];
[pushBehavior setActive:TRUE];
self.animator = animator;
}
@end
Pour plus d'informations, voir Catalogue dynamique UIKit