[iOS7 UI Programming - part 1] Animation Transitioning

Animation Transitioning in iOS7

20130313_osxdock_minimize_genie

많은 개발자들은 UI 개발에 있어서 애니메이션이란 자신이 만든 프로덕에 있어서 선물 보장지와 같은 생각을 가지고 있는 것 같다. 중요하지 않지만 있으면 좋을 것 같고, 유행에 예민할 것 같은 주제라고나 할까? 뭐 사실 소프트웨어 개발에 있어서 트랜드에 예민한 디자인 요소라는 것을 부정하지는 않는다. 하지만 Mac OS X에서는 위 이미지와 같은 요술 램프 지니 애니메이션은 10년이 넘은 효과이기에 위 효과만 보더라도 아직까지 쓰이고 있는 것으로 보아 어찌보면 유행이라는 단어와 어울리지 않을지도 모른다는 생각을 해본다.

이번 주제는 iOS7의 Animation Transitioning에 대한 샘플 코드를 올려보고자 한다. iPhone에 있어서 UINavigationController 같은 앱 전체의 굵은 뼈대가 되는 컴포넌트에서 Push라든지 MadalView를 보여주는 기본 애니메이션이 이제 사용자들에게 슬슬 지루감을 주고 있기 때문일까? iOS7에서 해당 기술을 지원하는 코드가 잘 녹아들어있다. UINavigationController나 UIViewController 클래스만 보더라도 Delegate Protocol들을 통해서 이를 완벽하게 ‘레고 블럭’처럼 잘 설계를 해놓았다. UIViewControllerTransitioningDelegate이나 UINavigationControllerDelegate에 정의된 method에서 따로 구현된 클래스 객체만 잘 리턴해도 멋진 자신만의 애니메이션을 구현할 수 있다.

 

Output

 

Source Code

위 동영상의 커스텀 Animation Transitioning은 두가지이다. 첫 번째 ModalView가 나올 때 적용된 효과는 공이 튀기는 것과 같은 효과를 주는 바운드 효과이고 살아질 때 나오는 것은 줄어드는 효과인데, 임의대로 만든 효과이다. 먼저 살펴보고자 하는 클래스는 UITableViewController를 상속 받고있는 MasterViewController이다.

  1. #import "MasterViewController.h"
  2. #import "DetailViewController.h"
  3. #import "AppDelegate.h"
  4. #import "Cat.h"
  5. #import "BouncePresentAnimationController.h"
  6. #import "ShrinkDismissAnimationController.h"
  7.  
  8. #pragma mark – Interface of 'MasterViewController'
  9.  
  10. @interface MasterViewController ()<UIViewControllerTransitioningDelegate>
  11.  
  12. @end
  13.  
  14. #pragma mark – Implementation of 'MasterViewController'
  15.  
  16. @implementation MasterViewController
  17. {
  18.     BouncePresentAnimationController *_bounceAnimationController;
  19.     ShrinkDismissAnimationController *_shrinkDismissAnimationController;
  20. }
  21.  
  22. #pragma mark – Getters & Setters
  23.  
  24. - (NSArray *)cats
  25. {
  26.     return ((AppDelegate *)[[UIApplication sharedApplication] delegate]).cats;
  27. }
  28.  
  29. #pragma mark – Life cycle
  30.  
  31. - (instancetype)initWithCoder:(NSCoder *)aDecoder
  32. {
  33.     if (self = [super initWithCoder:aDecoder])
  34.     {
  35.         _bounceAnimationController = [BouncePresentAnimationController new];
  36.         _shrinkDismissAnimationController = [ShrinkDismissAnimationController new];
  37.     }
  38.  
  39.     return self;
  40. }
  41.  
  42. - (void)viewDidLoad
  43. {}
  44.  
  45. - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
  46. {}
  47.  
  48. #pragma mark – Table View
  49.  
  50. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  51. {}
  52.  
  53. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  54. {}
  55.  
  56. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  57. {}
  58.  
  59. #pragma mark – Protocol of 'UIViewControllerTransitioningDelegate'
  60.  
  61. //#1.
  62. - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
  63. {
  64.     return _bounceAnimationController;
  65. }
  66.  
  67. //#2.
  68. - (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
  69. {
  70.     return _shrinkDismissAnimationController;
  71. }

위 코드는 Class extension에 UIViewControllerTransitioningDelegate를 implement한다고 선언한 후, animationControllerForPresentedController:presentingController:sourceController: 메서드와 animationControllerForDismissedController:메서드를 구현하여, 애니메이션이 구현 되어 있는 클래스 객체를 반환만 해주면 된다.

다음은 _bounceAnimationController 객체의 클래스 소스코드이다.

  1. #pragma mark – Interface of 'BouncePresentAnimationController'
  2. @import Foundation;
  3. @interface BouncePresentAnimationController : NSObject <UIViewControllerAnimatedTransitioning>
  4. @end
  5.  
  6. #pragma mark – Implementation of 'BouncePresentAnimationController'
  7. @implementation BouncePresentAnimationController
  8.  
  9. //0.
  10. - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
  11. {
  12.     return 0.5;
  13. }
  14.  
  15. - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
  16. {
  17.     //1. obtain state from the context
  18.     UIViewController *toViewController = nil;
  19.     toViewController = [transitionContext
  20.                         viewControllerForKey:UITransitionContextToViewControllerKey];
  21.     CGRect finalFrame = [transitionContext finalFrameForViewController:toViewController];
  22.     UIViewController *fromViewController = nil;
  23.     fromViewController = [transitionContext
  24.                           viewControllerForKey:UITransitionContextFromViewControllerKey];
  25.  
  26.     //2. obtain the container view
  27.     UIView *containerView = [transitionContext containerView];
  28.  
  29.     //3. set initial state
  30.     CGRect screenBounds = [[UIScreen mainScreen] bounds];
  31.     toViewController.view.frame = CGRectOffset(finalFrame, 0, screenBounds.size.height);
  32.  
  33.     //4. add the view
  34.     [containerView addSubview:toViewController.view];
  35.  
  36.     //5. animate
  37.     NSTimeInterval duration = [self transitionDuration:transitionContext];
  38.  
  39.     [UIView animateWithDuration:duration
  40.                           delay:0.0
  41.          usingSpringWithDamping:0.6
  42.           initialSpringVelocity:0.0
  43.                         options:UIViewAnimationOptionCurveLinear
  44.                      animations:^{
  45.                          fromViewController.view.alpha = 0.2;
  46.                          toViewController.view.frame = finalFrame;
  47.                      }
  48.                      completion:^(BOOL finished) {
  49.                          //6. Set completed! (Important!)
  50.                          [transitionContext completeTransition:YES];
  51.                          fromViewController.view.alpha = 1.0;
  52.                      }];
  53. }
  54.  
  55. @end

 

BouncePresentAnimationController 코드 분석

0. transitionDuration: 메서드는 이름을 보면 알 수 있듯이 transition 타임을 결정 해준다.
1. 새로들어 올 ViewController과 사라지게 될 ViewController를 assign 해준다.
2. transitionContext에는 containerView라는 view가 있다. Transitioning중에 보여질 View 객체들은 ContainerView 안에 있어야 한다.
3. toViewController의 Frame을 세팅한다.
4. ContainerView에 toViewController를 추가한다.
5. UIView의 animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:를 통해 바운스 애니메이션을 구현한다.

다음은 _shrinkDismissAnimationController의 클래스 코드를 살펴보자.

  1. #pragma mark – Interface of 'ShrinkDismissAnimationController'
  2.  
  3. @import Foundation;
  4. @interface ShrinkDismissAnimationController : NSObject<UIViewControllerAnimatedTransitioning>
  5. @end
  6.  
  7. #pragma mark – Implementation of 'ShrinkDismissAnimationController'
  8.  
  9. @implementation ShrinkDismissAnimationController
  10.  
  11. - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
  12. {
  13.     return 0.5;
  14. }
  15.  
  16. - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
  17. {
  18.     //1.
  19.     UIViewController *toViewController = nil;
  20.     toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
  21.     UIViewController *fromViewController = nil;
  22.     fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
  23.     CGRect finalFrame = [transitionContext finalFrameForViewController:toViewController];
  24.     UIView *containerView = [transitionContext containerView];
  25.  
  26.     toViewController.view.frame = finalFrame;
  27.     toViewController.view.alpha = 0.5;
  28.  
  29.     //2
  30.     [containerView addSubview:toViewController.view];
  31.     [containerView sendSubviewToBack:toViewController.view];
  32.  
  33.     //3. Actual animation
  34.     CGRect fromViewFrame = fromViewController.view.frame;
  35.     CGRect screenBounds = [[UIScreen mainScreen] bounds];
  36.     CGRect shrunkenFrame = CGRectInset(fromViewFrame, fromViewFrame.size.width/4, fromViewFrame.size.height/4);
  37.     CGRect fromFinalFrame = CGRectOffset(shrunkenFrame, 0, screenBounds.size.height);
  38.     NSTimeInterval duration = [self transitionDuration:transitionContext];
  39.  
  40.     //4. animate
  41.     [UIView
  42.      animateKeyframesWithDuration:duration
  43.      delay:0.0
  44.      options:UIViewKeyframeAnimationOptionCalculationModeCubic
  45.      animations:^
  46.     {
  47.  
  48.         [UIView addKeyframeWithRelativeStartTime:0.0
  49.                                 relativeDuration:0.5
  50.                                       animations:^{
  51.                                           fromViewController.view.transform = CGAffineTransformMakeScale(0.5, 0.5);
  52.                                           toViewController.view.alpha = 0.5;
  53.                                       }];
  54.  
  55.         [UIView addKeyframeWithRelativeStartTime:0.5
  56.                                 relativeDuration:0.5
  57.                                       animations:^{
  58.                                           fromViewController.view.frame = fromFinalFrame;
  59.                                           toViewController.view.alpha = 1.0;
  60.                                       }];
  61.     }
  62.      completion:^(BOOL finished)
  63.     {
  64.         [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
  65.     }];
  66. }
  67.  
  68. @end

 

ShrinkDismissAnimationController 코드 분석

1. 새로들어 올 ViewController과 사라지게 될 ViewController를 assign 해준다.
2. transitionContext에는 containerView라는 view가 있다. Transitioning중에 보여질 View 객체들은 ContainerView 안에 있어야 한다.
3. 애니메이션에 필요한 값
4. 실제 애니메이션 구현은 UIView의 animateKeyframesWithDuration:delay:options:options:animations:completion: 안에 또 다른 애니메이션 메서드를 구현함을 통해 원하는 애니메이션을 구현한다.

 

마치며..

UI 프로그래밍이 시대의 유행이든 아니든 간에 SDK의 설계를 보며 느끼는 점이 많았던 리서치 같다. 더 공부하고 더 열심히 해야겠다는 생각을 해본다.

[iOS7 DynamicAnimator] 샘플 코드

Screen Shot 2014-03-21 at 11.20.01 AM

iOS7에는 비-게임을 위한 물리엔진이 장착되어 있다. UIDynamicAnimator를 사용하여 간단한 실험을 해볼 수 있다. 코드에 대한 설명은 코드 아래 설명을 참조하라.

  1. #pragma mark – Interface of 'ViewController'
  2.  
  3. @interface ViewController ()<UICollisionBehaviorDelegate>
  4. @end
  5.  
  6. #pragma mark – Implementation of 'ViewController'
  7.  
  8. @implementation ViewController
  9. {
  10.     //#0
  11.     UIDynamicAnimator *_animator;
  12.     UIGravityBehavior *_gravity;
  13. }
  14.  
  15. #pragma mark – Life cycle
  16.  
  17. - (void)viewDidLoad
  18. {
  19.     [super viewDidLoad];
  20.  
  21.     //#1
  22.     UIView *red = [self boostupRedRectangle];
  23.     UICollisionBehavior * collision = [self boostupAnimationAndBehaviorApplyToView:red];
  24.  
  25.     //#2
  26.     UIView *yellow = [self boostupYellowRectangle];
  27.     [self addBarrierOnCollision:collision
  28.                 withBarrierView:yellow
  29.           andBoundaryIdentifier:@"yellow"];
  30.  
  31.     //#3
  32.     UIView *blue = [self boostupBlueRectangle];
  33.     [self addBarrierOnCollision:collision
  34.                 withBarrierView:blue
  35.           andBoundaryIdentifier:@"blue"];
  36. }
  37.  
  38. - (void)didReceiveMemoryWarning
  39. {
  40.     [super didReceiveMemoryWarning];
  41.     // Dispose of any resources that can be recreated.
  42. }
  43.  
  44. #pragma mark – Protocol of 'UICollisionBehaviorDelegate'
  45.  
  46. - (void)collisionBehavior:(UICollisionBehavior*)behavior
  47.       beganContactForItem:(id <UIDynamicItem>)item
  48.    withBoundaryIdentifier:(id <NSCopying>)identifier
  49.                   atPoint:(CGPoint)p
  50. {
  51.  
  52. }
  53.  
  54. #pragma mark – Boostup UIs
  55.  
  56. - (UIView *)boostupRedRectangle
  57. {
  58.     CGRect redFrame = CGRectMake(50, 50, 100, 100);
  59.     UIView *redRectangle = [[UIView alloc] initWithFrame:redFrame];
  60.     [redRectangle setBackgroundColor:[UIColor redColor]];
  61.     [self.view addSubview:redRectangle];
  62.  
  63.     return redRectangle;
  64. }
  65.  
  66. - (UIView *)boostupYellowRectangle
  67. {
  68.     CGRect yellowFrame = CGRectMake(0, 300, 90, 20);
  69.     UIView *yelloRectangle = [[UIView alloc] initWithFrame:yellowFrame];
  70.     [yelloRectangle setBackgroundColor:[UIColor yellowColor]];
  71.     [self.view addSubview:yelloRectangle];
  72.  
  73.     return yelloRectangle;
  74. }
  75.  
  76. - (UIView *)boostupBlueRectangle
  77. {
  78.     CGRect blueFrame = CGRectMake(180, 380, 140, 20);
  79.     UIView *blueRectangle = [[UIView alloc] initWithFrame:blueFrame];
  80.     [blueRectangle setBackgroundColor:[UIColor blueColor]];
  81.     [self.view addSubview:blueRectangle];
  82.  
  83.     return blueRectangle;
  84. }
  85.  
  86. - (UICollisionBehavior *)boostupAnimationAndBehaviorApplyToView:(UIView *)view
  87. {
  88.     //#2-1
  89.     _animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
  90.     _gravity = [[UIGravityBehavior alloc] init];
  91.     [_gravity addItem:view];
  92.     [_animator addBehavior:_gravity];
  93.     _gravity.magnitude = 4.0f;
  94.  
  95.     //#2-2
  96.     UICollisionBehavior *collision = [[UICollisionBehavior alloc]
  97.                                       initWithItems:@[view]];
  98.     collision.collisionDelegate = self;
  99.     collision.translatesReferenceBoundsIntoBoundary = YES;
  100.     [_animator addBehavior:collision];
  101.  
  102.     return collision;
  103. }
  104.  
  105. - (void)addBarrierOnCollision:(UICollisionBehavior *)collision
  106.               withBarrierView:(UIView *)barrier
  107.         andBoundaryIdentifier:(NSString *)identifier
  108. {
  109.     CGPoint startPoint = barrier.frame.origin;
  110.     CGPoint endPoint = CGPointMake(barrier.frame.origin.x + barrier.frame.size.width, barrier.frame.origin.y);
  111.     [collision addBoundaryWithIdentifier:identifier
  112.                                fromPoint:startPoint
  113.                                  toPoint:endPoint];
  114. }
  115.  
  116. @end

코드 설명

  1. ‘boostupAnimationAndBehaviorApplyToView:’ 메서드에서 보면 _animator가 애니메이션을 적용할 뷰를 넘기며 생성하는 것을 볼 수있다. 그리고 난 다음 UIGravityBehavior 클래스의 인스턴스를 애니메이터에 추가하는 것으로 해당 뷰에 중력 효과를 적용할 수 있다. 그리고 UICollisionBehavior 객체를 통하여서 빨강 네모를 화면안에 가둬넣을 수 있다.
  2. addBarrierOnCollision:WithBarrierView:AndBoundaryIdentifer: 메서드를 통하여서 UICollisionBehavior에 보이지 않는 장벽을 만들 수 있다. 실제로 화면에서 보이는 장애물은 그저 자리만 지키고 있을 뿐이며 UICollisionBehavior의 객체에 보이지 않는 장해물을 세팅하는 메서드이다.

[iOS7 Motion-Effect] UIInterpolatingMotionEffect 샘플 코드

UIInterpolatingMotionEffect

출처: google 이미지 검색 결과

iOS7을 보면 잠금 화면이라든지 바탕화면이 시각에 따라 움직이는 효과를 볼 수 있을 것이다. 애플에서 쉽게 라이브러리로 제공을 하고 있다.

  1. #import "ViewController.h"
  2.  
  3. @interface ViewController ()
  4.  
  5. @end
  6.  
  7. @implementation ViewController
  8.  
  9. - (void)viewDidLoad
  10. {
  11.     [super viewDidLoad];
  12.  
  13.     //1. Lower bg
  14.     UIImage *lowerImg = [UIImage imageNamed:@"bethel_in_carseat.jpg"];
  15.     UIImageView *lowerbg = [[UIImageView alloc] initWithImage:lowerImg];
  16.     lowerbg.frame = CGRectInset(self.view.frame, -50.0f, -50.0f);
  17.  
  18.     [self.view addSubview:lowerbg];
  19.     [self addMotionEffectToView:lowerbg magnitude:50.0f];
  20.  
  21.     //2. mid bg
  22.     UIImage *midImg = [UIImage imageNamed:@"Background-MidLayer.png"];
  23.     UIImageView *midbg = [[UIImageView alloc] initWithImage:midImg];
  24.     [self.view addSubview:midbg];
  25.  
  26.     //3. header bg
  27.     UIImage *headerImg = [UIImage imageNamed:@"Sarnie.png"];
  28.     UIImageView *headerBG = [[UIImageView alloc] initWithImage:headerImg];
  29.     headerBG.center = CGPointMake(220, 190);
  30.     [self.view addSubview:headerBG];
  31.     [self addMotionEffectToView:headerBG magnitude:-20.0f];
  32. }
  33.  
  34. - (void)didReceiveMemoryWarning
  35. {
  36.     [super didReceiveMemoryWarning];
  37. }
  38.  
  39. #pragma mark – Methods
  40.  
  41. - (void)addMotionEffectToView:(UIView *)view magnitude:(CGFloat)magnitude
  42. {
  43.     UIInterpolatingMotionEffect *horMoffec = nil;
  44.     horMoffec = [[UIInterpolatingMotionEffect alloc]
  45.                  initWithKeyPath:@"center.x"
  46.                  type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
  47.     horMoffec.maximumRelativeValue = @(magnitude);
  48.     horMoffec.minimumRelativeValue = @(-magnitude);
  49.  
  50.     UIInterpolatingMotionEffect *verMoffec = nil;
  51.     verMoffec = [[UIInterpolatingMotionEffect alloc]
  52.                  initWithKeyPath:@"center.y"
  53.                  type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];
  54.     verMoffec.maximumRelativeValue = @(magnitude);
  55.     verMoffec.minimumRelativeValue = @(-magnitude);
  56.  
  57.     UIMotionEffectGroup *group = [[UIMotionEffectGroup alloc] init];
  58.     group.motionEffects = @[verMoffec, horMoffec];
  59.  
  60.     [view addMotionEffect:group];
  61. }