수색…
소개
UIKit Dynamics는 UIKit에 통합 된 완전한 실제 물리 엔진입니다. 중력, 부착물, 충돌 및 힘과 같은 행동을 추가하여 실제와 같은 느낌을주는 인터페이스를 만들 수 있습니다. 인터페이스 요소에 적용 할 물리적 특성을 정의하면 나머지는 동적 엔진이 처리합니다.
비고
UIKit Dynamics를 사용할 때 유의할 점은 애니메이터가 배치 한 뷰가 다른 일반적인 iOS 레이아웃 방법으로 쉽게 배치 될 수 없다는 것입니다.
UIKit 다이나믹스를 처음 접하게 된 사람들은 종종이 중요한주의 사항으로 어려움을 겪습니다. UIDynamicBehavior
의 항목이기도 한 뷰에 제약 조건을 UIDynamicBehavior
자동 레이아웃 엔진과 동적 애니메이터 엔진이 모두 적절한 위치에서 UIDynamicBehavior
하므로 혼란을 야기 할 수 있습니다. 마찬가지로 애니메이터에 의해 제어되는 뷰의 프레임을 직접 설정하려고하면 일반적으로 불안정한 애니메이션과 예기치 않은 배치가 발생합니다. 뷰를 항목으로 UIDynamicBehavior
추가하면 애니메이터가 뷰 배치의 책임을 맡게되므로 뷰어 위치 변경은 애니메이터를 통해 구현되어야합니다.
동적 애니메이터에 의해 업데이트되는 뷰의 프레임을 설정할 수는 있지만 애니메이터에게 메시징하여 뷰 계층의 애니메이터의 내부 모델을 업데이트해야합니다. 예를 들어, UIGravityBehavior
의 항목 인 UILabel
label
이있는 경우 화면 맨 위로 이동하여 다시 떨어지는 것을 볼 수 있습니다.
빠른
label.frame = CGRect(x: 0.0, y: 0.0, width: label.intrinsicContentSize.width, height: label.intrinsicContentSize.height)
dynamicAnimator.updateItem(usingCurrentState: label)
목표 -C
self.label.frame = CGRectMake(0.0, 0.0, self.label.intrinsicContentSize.width, self.label.intrinsicContentSize.height);
[self.dynamicAnimator updateItemUsingCurrentState: self.label];
그 후에 애니메이터는 레이블의 새 위치에서 중력 동작을 적용합니다.
또 다른 일반적인 기술은 UIDynamicBehaviors
를 사용하여 뷰를 배치하는 것입니다. 예를 들어 터치 이벤트 아래 도면을 위치하는 경우 생성 바람직한 UIAttachmentBehavior
그 갱신 anchorPoint
하나로 touchesMoved
또는 UIGestureRecognizer
효과적인 전략이다의 행동.
떨어지는 광장
우리보기의 중간에 정사각형을 그리고 그것을 바닥으로 떨어 뜨리고 화면 하단 경계와 부딪히는 아래쪽 가장자리에서 멈추도록하십시오.
@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)
}
제스처 속도에 따른 플릭보기
이보기는 뷰가 팬 제스처를 추적하고 물리 기반 방식으로 출발하는 방법을 보여줍니다.
빠른
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)
}
}
}
목표 -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
UIFieldBehaviors를 사용하는 "끈적한 코너"효과
이 예제는 뷰가 특정 영역 (이 경우에는 위쪽과 아래쪽의 2 개 영역)에 들어가면 뷰를 끌어 당기는 FaceTime과 유사한 효과를 얻는 방법을 보여줍니다.
빠른
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)
}
}
}
목표 -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
UIFieldBehaviors
대한 자세한 내용은 2015 WWDC 세션 "UIKit Dynamics 및 Visual Effects의 새로운 기능" 및 샘플 코드를 참조하십시오 .
UIDynamicBehavior 기반 사용자 지정 전환
이 예제에서는 복합 UIDynamicBehavior
에 의해 구동되는 사용자 지정 프레젠테이션 전환을 만드는 방법을 보여줍니다. 우리는 모달을 표현할 프리젠 테이션 뷰 컨트롤러를 생성함으로써 시작할 수 있습니다.
빠른
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)
}
}
목표 -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
현재 버튼을 두드리면 ModalViewController
만들고 프리젠 테이션 스타일을 .custom
로 설정하고 transitionDelegate
를 자체로 설정합니다. 이렇게하면 모달 전환을 유도하는 애니메이터를 판매 할 수 있습니다. 또한 modal
의 뷰 프레임을 전체 화면보다 작게 설정합니다.
ModalViewController
살펴 ModalViewController
.
빠른
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)
}
}
목표 -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
여기에 제시된 뷰 컨트롤러를 생성합니다. 또한 ModalViewController
는 자신의 transitioningDelegate
이므로 전환 애니메이션을 관리 할 객체를 판매하는 책임도 있습니다. 우리는 복합 UIDynamicBehavior
서브 클래스의 인스턴스를 전달한다는 것을 의미합니다.
우리의 애니메이터는 프리젠 테이션을위한 것 하나, 해산을위한 것 하나의 두 가지 다른 트랜지션을 갖습니다. 프리젠 테이션을 위해 프리젠 테이션 뷰 컨트롤러의 뷰는 위로부터 떨어집니다. 그리고 해산을 위해,보기는 밧줄에서 흔들고 그 후에 떨어지는 것처럼 보일 것입니다. DropOutAnimator
가 UIViewControllerAnimatedTransitioning
DropOutAnimator
때문에이 작업의 대부분은 func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
의 구현에서 수행됩니다.
빠른
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
}
}
}
}
목표 -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;
}
}
}
복합 동작 인 DropOutAnimator
는 여러 가지 동작을 결합하여 애니메이션을 표시하거나 해제 할 수 있습니다. 또한 DropOutAnimator
는 비헤이비어의 action
블록을 사용하여 아이템의 위치를 검사하는 방법과 시간 경과와 함께 화면 밖으로 이동하거나 아직 스테이 시스에 도달하지 않은 애니메이션을 잘라내는 데 사용할 수있는 기법을 보여줍니다.
자세한 내용은 2013 WWDC 세션 "UIKit Dynamics를 사용한 고급 기법" 및 SOLPresentingFun
UIDynamicBehaviors를 사용하여 실제 물리를 이용한 음영 전환
이 예는 iOS의 알림 화면과 비슷한 "실제"물리학을 사용하여 대화 형 프레젠테이션을 전환하는 방법을 보여줍니다.
우선, 그늘이 나타날 프리젠 테이션 컨트롤러가 필요합니다. 이 뷰 컨트롤러는 우리의 제시된 뷰 컨트롤러를위한 UIViewControllerTransitioningDelegate
역할을 할 것이고 우리의 전환을위한 애니메이터를 판매 할 것입니다. 따라서 우리는 대화 형 애니메이터의 인스턴스를 생성합니다 (하나는 발표 용이고 하나는 해제 용). 또한 셰이드 뷰 컨트롤러의 인스턴스를 생성합니다.이 예제에서는 레이블이있는 뷰 컨트롤러입니다. 동일한 팬 제스처가 전체 상호 작용을 유도하기를 원하기 때문에 우리는 제시된 뷰 컨트롤러에 대한 참조와 대화 형 애니메이터로의 음영을 전달합니다.
빠른
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
}
}
목표 -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
대화식 전환을 통해 우리의 그늘을 표현하기를 정말로 원하지만, 일반적인 애니메이션 컨트롤러를 반환하지 않으면 UIViewControllerTransitioningDelegate
작동하는 방식 때문에 대화 형 컨트롤러가 절대 사용되지 않습니다. 따라서 UIViewControllerAnimatedTransitioning
을 준수하는 EmptyAnimator
클래스를 만듭니다.
빠른
class EmptyAnimator: NSObject
{
}
extension EmptyAnimator: UIViewControllerAnimatedTransitioning
{
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
{
return 0.0
}
}
목표 -C
@implementation EmptyAnimator
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
}
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.0;
}
@end
마지막으로 실제로 UIViewControllerInteractiveTransitioning
을 준수하는 UIDynamicBehavior
의 하위 클래스 인 ShadeAnimator
를 생성해야합니다.
빠른
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)
}
}
목표 -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
팬 제스처가 시작되면 애니메이터가 전환 시작을 트리거합니다. 제스처가 변경되면보기가 움직입니다. 하지만 제스처가 끝나는 시점은 UIDynamicBehaviors
가 전환을 완료해야하는지 또는 취소해야 하는지를 결정할 때입니다. 이렇게하려면 첨부 및 충돌 동작을 사용합니다. 자세한 내용은 2013 WWDC 세션 "UIKit Dynamics를 사용한 고급 기법"을 참조하십시오.
동적 애니메이션 위치 변경을 경계로 매핑
이 예제는 UIDynamicItem
프로토콜을 사용자 정의하여 동적으로 뷰의 위치 변경을 바운드 변경에 매핑하여 탄성 방식으로 확장 및 축소하는 UIButton
을 생성하는 방법을 보여줍니다.
시작하려면 UIDynamicItem
을 구현하는 새로운 프로토콜을 만들어야하지만 settable 및 gettable bounds
속성도 있습니다.
빠른
protocol ResizableDynamicItem: UIDynamicItem
{
var bounds: CGRect { set get }
}
extension UIView: ResizableDynamicItem {}
목표 -C
@protocol ResizableDynamicItem <UIDynamicItem>
@property (nonatomic, readwrite) CGRect bounds;
@end
그런 다음 UIDynamicItem
을 래핑 할 래퍼 객체를 만들지 만 가운데 변경 사항을 항목의 너비와 높이에 매핑합니다. 또한 기본 항목의 bounds
및 transform
을위한 패스 스루를 제공합니다. 이렇게하면 동적 애니메이터가 가운데에하는 모든 변경 사항이 기본 항목의 x 및 y 값이 항목 너비 및 높이에 적용됩니다.
빠른
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
}
}
}
목표 -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
마지막으로 버튼이있는 UIViewController
를 만듭니다. 버튼을 누르면 버튼이 PositionToBoundsMapping
된 동적 항목으로 PositionToBoundsMapping
이 생성됩니다. UIAttachmentBehavior
를 만들어 현재 위치에 UIPushBehavior
다음 즉각적인 UIPushBehavior
를 추가합니다. 그러나 변경 사항의 경계를 매핑했기 때문에 버튼은 움직이지 않고 오히려 커지고 축소됩니다.
빠른
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
}
}
목표 -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
자세한 내용은 UIKit 다이나믹 카탈로그를 참조하십시오.