iOS
UIKit Dynamics mit UICollectionView
Suche…
Einführung
UIKit Dynamics ist eine in UIKit integrierte Physik-Engine. UIKit Dynamics bietet eine Reihe von APIs, die Interoperabilität mit einer UICollectionView
und einem UICollectionView
UICollectionViewLayout
Erstellen eines benutzerdefinierten Ziehverhaltens mit UIDynamicAnimator
In diesem Beispiel wird UIDynamicBehavior
, wie ein benutzerdefiniertes UIDynamicBehavior
durch Unterklassen von UIDynamicBehavior
und Unterklassen von UICollectionViewFlowLayout
. In diesem Beispiel haben wir UICollectionView
, mit dem mehrere Elemente ausgewählt werden können. Mit einer langen UIDynamicAnimator
können diese Elemente dann in einer elastischen, "federnden" Animation gezogen werden, die von einem UIDynamicAnimator
.
Das Verschleppungsverhalten wird durch Kombinieren eines Verhaltens auf niedriger Ebene, das den UIAttachmentBehavior
für die Ecken eines UIDynamicItem
und eines Verhaltens auf hoher Ebene, das das Verhalten auf niedriger Ebene für eine Reihe von UIDynamicItems
.
Wir können damit beginnen, dieses Verhalten auf niedriger Ebene zu erstellen. Wir rufen RectangleAttachmentBehavior
Schnell
final class RectangleAttachmentBehavior: UIDynamicBehavior
{
init(item: UIDynamicItem, point: CGPoint)
{
// Higher frequency more "ridged" formation
let frequency: CGFloat = 8.0
// Lower damping longer animation takes to come to rest
let damping: CGFloat = 0.6
super.init()
// Attachment points are four corners of item
let points = self.attachmentPoints(for: point)
let attachmentBehaviors: [UIAttachmentBehavior] = points.map
{
let attachmentBehavior = UIAttachmentBehavior(item: item, attachedToAnchor: $0)
attachmentBehavior.frequency = frequency
attachmentBehavior.damping = damping
return attachmentBehavior
}
attachmentBehaviors.forEach
{
addChildBehavior($0)
}
}
func updateAttachmentLocation(with point: CGPoint)
{
// Update anchor points to new attachment points
let points = self.attachmentPoints(for: point)
let attachments = self.childBehaviors.flatMap { $0 as? UIAttachmentBehavior }
let pairs = zip(points, attachments)
pairs.forEach { $0.1.anchorPoint = $0.0 }
}
func attachmentPoints(for point: CGPoint) -> [CGPoint]
{
// Width and height should be close to the width and height of the item
let width: CGFloat = 40.0
let height: CGFloat = 40.0
let topLeft = CGPoint(x: point.x - width * 0.5, y: point.y - height * 0.5)
let topRight = CGPoint(x: point.x + width * 0.5, y: point.y - height * 0.5)
let bottomLeft = CGPoint(x: point.x - width * 0.5, y: point.y + height * 0.5)
let bottomRight = CGPoint(x: point.x + width * 0.5, y: point.y + height * 0.5)
let points = [topLeft, topRight, bottomLeft, bottomRight]
return points
}
}
Ziel c
@implementation RectangleAttachmentBehavior
- (instancetype)initWithItem:(id<UIDynamicItem>)item point:(CGPoint)point
{
CGFloat frequency = 8.0f;
CGFloat damping = 0.6f;
self = [super init];
if (self)
{
NSArray <NSValue *> *pointValues = [self attachmentPointValuesForPoint:point];
for (NSValue *value in pointValues)
{
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc]initWithItem:item attachedToAnchor:[value CGPointValue]];
attachment.frequency = frequency;
attachment.damping = damping;
[self addChildBehavior:attachment];
}
}
return self;
}
- (void)updateAttachmentLocationWithPoint:(CGPoint)point
{
NSArray <NSValue *> *pointValues = [self attachmentPointValuesForPoint:point];
for (NSInteger i = 0; i < pointValues.count; i++)
{
NSValue *pointValue = pointValues[i];
UIAttachmentBehavior *attachment = self.childBehaviors[i];
attachment.anchorPoint = [pointValue CGPointValue];
}
}
- (NSArray <NSValue *> *)attachmentPointValuesForPoint:(CGPoint)point
{
CGFloat width = 40.0f;
CGFloat height = 40.0f;
CGPoint topLeft = CGPointMake(point.x - width * 0.5, point.y - height * 0.5);
CGPoint topRight = CGPointMake(point.x + width * 0.5, point.y - height * 0.5);
CGPoint bottomLeft = CGPointMake(point.x - width * 0.5, point.y + height * 0.5);
CGPoint bottomRight = CGPointMake(point.x + width * 0.5, point.y + height * 0.5);
NSArray <NSValue *> *pointValues = @[[NSValue valueWithCGPoint:topLeft], [NSValue valueWithCGPoint:topRight], [NSValue valueWithCGPoint:bottomLeft], [NSValue valueWithCGPoint:bottomRight]];
return pointValues;
}
@end
Als Nächstes können wir das übergeordnete Verhalten erstellen, das eine Reihe von RectangleAttachmentBehavior
kombiniert.
Schnell
final class DragBehavior: UIDynamicBehavior
{
init(items: [UIDynamicItem], point: CGPoint)
{
super.init()
items.forEach
{
let rectAttachment = RectangleAttachmentBehavior(item: $0, point: point)
self.addChildBehavior(rectAttachment)
}
}
func updateDragLocation(with point: CGPoint)
{
// Tell low-level behaviors location has changed
self.childBehaviors.flatMap { $0 as? RectangleAttachmentBehavior }.forEach { $0.updateAttachmentLocation(with: point) }
}
}
Ziel c
@implementation DragBehavior
- (instancetype)initWithItems:(NSArray <id<UIDynamicItem>> *)items point: (CGPoint)point
{
self = [super init];
if (self)
{
for (id<UIDynamicItem> item in items)
{
RectangleAttachmentBehavior *rectAttachment = [[RectangleAttachmentBehavior alloc]initWithItem:item point:point];
[self addChildBehavior:rectAttachment];
}
}
return self;
}
- (void)updateDragLocationWithPoint:(CGPoint)point
{
for (RectangleAttachmentBehavior *rectAttachment in self.childBehaviors)
{
[rectAttachment updateAttachmentLocationWithPoint:point];
}
}
@end
Jetzt, da unsere Verhaltensweisen vorhanden sind, müssen Sie sie als Nächstes in unsere Sammlungsansicht aufnehmen. Da wir normalerweise ein Standardraster-Layout wünschen, können wir UICollectionViewFlowLayout
Unterklasse UICollectionViewFlowLayout
und nur beim Ziehen Attribute ändern. Dies layoutAttributesForElementsInRect
hauptsächlich durch das Überschreiben von layoutAttributesForElementsInRect
und die Verwendung der UIDynamicAnimator's
Convenience-Methode itemsInRect
.
Schnell
final class DraggableLayout: UICollectionViewFlowLayout
{
// Array that holds dragged index paths
var indexPathsForDraggingElements: [IndexPath]?
// The dynamic animator that will animate drag behavior
var animator: UIDynamicAnimator?
// Custom high-level behavior that dictates drag animation
var dragBehavior: DragBehavior?
// Where dragging starts so can return there once dragging ends
var startDragPoint = CGPoint.zero
// Bool to keep track if dragging has ended
var isFinishedDragging = false
// Method to inform layout that dragging has started
func startDragging(indexPaths selectedIndexPaths: [IndexPath], from point: CGPoint)
{
indexPathsForDraggingElements = selectedIndexPaths
animator = UIDynamicAnimator(collectionViewLayout: self)
animator?.delegate = self
// Get all of the draggable attributes but change zIndex so above other cells
let draggableAttributes: [UICollectionViewLayoutAttributes] = selectedIndexPaths.flatMap {
let attribute = super.layoutAttributesForItem(at: $0)
attribute?.zIndex = 1
return attribute
}
startDragPoint = point
// Add them to high-level behavior
dragBehavior = DragBehavior(items: draggableAttributes, point: point)
// Add high-level behavior to animator
animator?.addBehavior(dragBehavior!)
}
func updateDragLocation(_ point: CGPoint)
{
// Tell high-level behavior that point has updated
dragBehavior?.updateDragLocation(with: point)
}
func endDragging()
{
isFinishedDragging = true
// Return high-level behavior to starting point
dragBehavior?.updateDragLocation(with: startDragPoint)
}
func clearDraggedIndexPaths()
{
// Reset state for next drag event
animator = nil
indexPathsForDraggingElements = nil
isFinishedDragging = false
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
let existingAttributes: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElements(in: rect) ?? []
var allAttributes = [UICollectionViewLayoutAttributes]()
// Get normal flow layout attributes for non-drag items
for attributes in existingAttributes
{
if (indexPathsForDraggingElements?.contains(attributes.indexPath) ?? false) == false
{
allAttributes.append(attributes)
}
}
// Add dragged item attributes by asking animator for them
if let animator = self.animator
{
let animatorAttributes: [UICollectionViewLayoutAttributes] = animator.items(in: rect).flatMap { $0 as? UICollectionViewLayoutAttributes }
allAttributes.append(contentsOf: animatorAttributes)
}
return allAttributes
}
}
extension DraggableLayout: UIDynamicAnimatorDelegate
{
func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator)
{
// Animator has paused and done dragging; reset state
guard isFinishedDragging else { return }
clearDraggedIndexPaths()
}
}
Ziel c
@interface DraggableLayout () <UIDynamicAnimatorDelegate>
@property (nonatomic, strong) NSArray <NSIndexPath *> *indexPathsForDraggingElements;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic, assign) CGPoint startDragPoint;
@property (nonatomic, assign) BOOL finishedDragging;
@property (nonatomic, strong) DragBehavior *dragBehavior;
@end
@implementation DraggableLayout
- (void)startDraggingWithIndexPaths:(NSArray <NSIndexPath *> *)selectedIndexPaths fromPoint:(CGPoint)point
{
self.indexPathsForDraggingElements = selectedIndexPaths;
self.animator = [[UIDynamicAnimator alloc]initWithCollectionViewLayout:self];
self.animator.delegate = self;
NSMutableArray *draggableAttributes = [[NSMutableArray alloc]initWithCapacity:selectedIndexPaths.count];
for (NSIndexPath *indexPath in selectedIndexPaths)
{
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForItemAtIndexPath:indexPath];
attributes.zIndex = 1;
[draggableAttributes addObject:attributes];
}
self.startDragPoint = point;
self.dragBehavior = [[DragBehavior alloc]initWithItems:draggableAttributes point:point];
[self.animator addBehavior:self.dragBehavior];
}
- (void)updateDragLoactionWithPoint:(CGPoint)point
{
[self.dragBehavior updateDragLocationWithPoint:point];
}
- (void)endDragging
{
self.finishedDragging = YES;
[self.dragBehavior updateDragLocationWithPoint:self.startDragPoint];
}
- (void)clearDraggedIndexPath
{
self.animator = nil;
self.indexPathsForDraggingElements = nil;
self.finishedDragging = NO;
}
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator
{
if (self.finishedDragging)
{
[self clearDraggedIndexPath];
}
}
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *existingAttributes = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *allAttributes = [[NSMutableArray alloc]initWithCapacity:existingAttributes.count];
for (UICollectionViewLayoutAttributes *attributes in existingAttributes)
{
if (![self.indexPathsForDraggingElements containsObject:attributes.indexPath])
{
[allAttributes addObject:attributes];
}
}
[allAttributes addObjectsFromArray:[self.animator itemsInRect:rect]];
return allAttributes;
}
@end
Zum Schluss erstellen wir einen Ansichts-Controller, der unsere UICollectionView
und unsere lange UICollectionView
.
Schnell
final class ViewController: UIViewController
{
// Collection view that displays cells
lazy var collectionView: UICollectionView =
{
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: DraggableLayout())
collectionView.backgroundColor = .white
collectionView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(collectionView)
collectionView.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: self.bottomLayoutGuide.topAnchor).isActive = true
return collectionView
}()
// Gesture that drives dragging
lazy var longPress: UILongPressGestureRecognizer =
{
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(sender:)))
return longPress
}()
// Array that holds selected index paths
var selectedIndexPaths = [IndexPath]()
override func viewDidLoad()
{
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.addGestureRecognizer(longPress)
}
func handleLongPress(sender: UILongPressGestureRecognizer)
{
guard let draggableLayout = collectionView.collectionViewLayout as? DraggableLayout else { return }
let location = sender.location(in: collectionView)
switch sender.state
{
case .began:
draggableLayout.startDragging(indexPaths: selectedIndexPaths, from: location)
case .changed:
draggableLayout.updateDragLocation(location)
case .ended, .failed, .cancelled:
draggableLayout.endDragging()
case .possible:
break
}
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return 1000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = .gray
if selectedIndexPaths.contains(indexPath) == true
{
cell.backgroundColor = .red
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
// Bool that determines if cell is being selected or unselected
let isSelected = !selectedIndexPaths.contains(indexPath)
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = isSelected ? .red : .gray
if isSelected
{
selectedIndexPaths.append(indexPath)
}
else
{
selectedIndexPaths.remove(at: selectedIndexPaths.index(of: indexPath)!)
}
}
}
Ziel c
@interface ViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) UILongPressGestureRecognizer *longPress;
@property (nonatomic, strong) NSMutableArray <NSIndexPath *> *selectedIndexPaths;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
[self.collectionView addGestureRecognizer:self.longPress];
self.selectedIndexPaths = [[NSMutableArray alloc]init];
}
- (UICollectionView *)collectionView
{
if (!_collectionView)
{
_collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:[[DraggableLayout alloc]init]];
_collectionView.backgroundColor = [UIColor whiteColor];
_collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_collectionView];
[_collectionView.topAnchor constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor].active = YES;
[_collectionView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor].active = YES;
[_collectionView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor].active = YES;
[_collectionView.bottomAnchor constraintEqualToAnchor:self.bottomLayoutGuide.topAnchor].active = YES;
}
return _collectionView;
}
- (UILongPressGestureRecognizer *)longPress
{
if (!_longPress)
{
_longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(handleLongPress:)];
}
return _longPress;
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
DraggableLayout *draggableLayout = (DraggableLayout *)self.collectionView.collectionViewLayout;
CGPoint location = [sender locationInView:self.collectionView];
if (sender.state == UIGestureRecognizerStateBegan)
{
[draggableLayout startDraggingWithIndexPaths:self.selectedIndexPaths fromPoint:location];
}
else if(sender.state == UIGestureRecognizerStateChanged)
{
[draggableLayout updateDragLoactionWithPoint:location];
}
else if(sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateCancelled || sender.state == UIGestureRecognizerStateFailed)
{
[draggableLayout endDragging];
}
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 1000;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor grayColor];
if ([self.selectedIndexPaths containsObject:indexPath])
{
cell.backgroundColor = [UIColor redColor];
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
BOOL isSelected = ![self.selectedIndexPaths containsObject:indexPath];
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
if (isSelected)
{
cell.backgroundColor = [UIColor redColor];
[self.selectedIndexPaths addObject:indexPath];
}
else
{
cell.backgroundColor = [UIColor grayColor];
[self.selectedIndexPaths removeObject:indexPath];
}
}
@end
Weitere Informationen zur WWDC-Sitzung 2013 "Fortgeschrittene Techniken mit UIKit Dynamics"