060Map Dynamic Animation Position Changes to Bounds

suggest change

This example shows how to customize the UIDynamicItem protocol to map position changes of a view being dynamically animated to bounds changes to create a UIButton that expands and contracts in a elastic fashion.

To start we need to create a new protocol that implements UIDynamicItem but that also has a settable and gettable bounds property.

Swift

protocol ResizableDynamicItem: UIDynamicItem
{
    var bounds: CGRect { set get }
}
extension UIView: ResizableDynamicItem {}

Objective-C

@protocol ResizableDynamicItem <UIDynamicItem>
@property (nonatomic, readwrite) CGRect bounds;
@end

We’ll then create a wrapper object that will wrap a UIDynamicItem but will map center changes to the item’s width and height. We will also provide passthroughs for bounds and transform of the underlying item. This will cause any changes the dynamic animator makes to the center x and y values of the underlying item will be applied to the items width and height.

Swift

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
        }
    }
}

Objective-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

Finally, we’ll create a UIViewController that will have a button. When the button is pressed we will create PositionToBoundsMapping with the button as the wrapped dynamic item. We create a UIAttachmentBehavior to it’s current position then add an instantaneous UIPushBehavior to it. However because we have mapped changes its bounds, the button does not move but rather grows and shrinks.

Swift

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
    }
}

Objective-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

For more information see UIKit Dynamics Catalog

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you:


UIKit Dynamics:
* 060Map Dynamic Animation Position Changes to Bounds

Table Of Contents
12 UIView
15 UIColor
26 UIImage
28 CALayer
30 NSDate
40 iBeacon
49 NSTimer
79 NSURL
87 AWS SDK
96 NSData
101 Segues
104 EventKit
105 NSBundle
106 SiriKit
111 StoreKit
117 3D Touch
119 Keychain
122 Block
141 AirDrop
144 UISlider
145 Carthage
146 HealthKit
151 plist
157 MVVM
164 UIPhoenix
166 Simulator
168 NSArray
169 OpenGL
173 UIKit Dynamics
175 Core Data
179 MyLayout
180 UIFont
189 Security
200 Codable