Search code examples
iosswiftuidynamicanimator

How to attach multiple UIDynamicItems to each other


I am trying to implement circles attached to each other like in Apple's Music App via UIDynamicAnimator. I need to attach circles to each other and to view center. I was trying to implement this via UIAttachmentBehavior, but seems to it's not supporting multiple attachments. In result, circles overlaps on each other :)

let attachment = UIAttachmentBehavior(item: circle, attachedToAnchor: CGPoint(x: view.center.x, y: view.center.y))
attachment.length = 10
animator?.addBehavior(attachment)

let push = UIPushBehavior(items: [circle], mode: .continuous)

collision.addItem(circle)

animator?.addBehavior(push)

Apple Music enter image description here

What I am doing wrong?


Solution

  • I don't think the apple music genre picker thing uses UIAttachmentBehavior which is closer to attaching two views with a pole or a rope. But, it seems like the problem you're experiencing might be that all of the views are added at the same location which has the effect of placing them on top of each other and with the collision behavior causes them to be essentially be stuck together. One thing to do is to turn on UIDynamicAnimator debugging by calling animator.setValue(true, forKey: "debugEnabled").

    For recreating the above circle picker design, I would look into using UIFieldBehavior.springField().

    For example:

    class ViewController: UIViewController {
    
        lazy var animator: UIDynamicAnimator = {
            let animator = UIDynamicAnimator(referenceView: view)
            return animator
        }()
        lazy var collision: UICollisionBehavior = {
            let collision = UICollisionBehavior()
            collision.collisionMode = .items
            return collision
        }()
        lazy var behavior: UIDynamicItemBehavior = {
            let behavior = UIDynamicItemBehavior()
            behavior.allowsRotation = false
            behavior.elasticity = 0.5
            behavior.resistance = 5.0
            behavior.density = 0.01
            return behavior
        }()
        lazy var gravity: UIFieldBehavior = {
            let gravity = UIFieldBehavior.springField()
            gravity.strength = 0.008
            return gravity
        }()
        lazy var panGesture: UIPanGestureRecognizer = {
            let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.didPan(_:)))
            return panGesture
        }()
    
        var snaps = [UISnapBehavior]()
        var circles = [CircleView]()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addGestureRecognizer(panGesture)
            animator.setValue(true, forKey: "debugEnabled")
            addCircles()
            addBehaviors()
        }
    
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            gravity.position = view.center
            snaps.forEach {
                $0.snapPoint = view.center
            }
        }
    
        func addCircles() {
            (1...30).forEach { index in
                let xIndex = index % 2
                let yIndex: Int = index / 3
                let circle = CircleView(frame: CGRect(origin: CGPoint(x: xIndex == 0 ? CGFloat.random(in: (-300.0 ... -100)) : CGFloat.random(in: (500 ... 800)), y: CGFloat(yIndex) * 200.0), size: CGSize(width: 100, height: 100)))
                circle.backgroundColor = .red
                circle.text = "\(index)"
                circle.textAlignment = .center
                view.addSubview(circle)
                gravity.addItem(circle)
                collision.addItem(circle)
                behavior.addItem(circle)
                circles.append(circle)
            }
        }
    
        func addBehaviors() {
            animator.addBehavior(collision)
            animator.addBehavior(behavior)
            animator.addBehavior(gravity)
        }
    
        @objc
        private func didPan(_ sender: UIPanGestureRecognizer) {
            let translation = sender.translation(in: sender.view)
            switch sender.state {
            case .began:
                animator.removeAllBehaviors()
                fallthrough
            case .changed:
                circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
            case .possible, .cancelled, .failed:
                break
            case .ended:
                circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
                addBehaviors()
            @unknown default:
                break
            }
            sender.setTranslation(.zero, in: sender.view)
        }
    }
    
    final class CircleView: UILabel {
    
        override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
            return .ellipse
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            layer.cornerRadius = bounds.height * 0.5
            layer.masksToBounds = true
        }
    }
    

    screenshot

    For more information I would watch What's New in UIKit Dynamics and Visual Effects from WWDC 2015