Smooth object rotation in ARSCNView - swift

I'm trying to make my pan gesture to be as smooth as the jigspace app when rotating 3d objects in AR. Here's what I have right now:
#objc func rotateObject(sender: UIPanGestureRecognizer) {
let sceneView = sender.view as! ARSCNView
var currentAngleY: Float = 0.0
let translation = sender.translation(in: sceneView)
var newAngleY = Float(translation.x)*Float(Double.pi)/180
sceneView.scene.rootNode.enumerateChildNodes { (node, stop) in
if sender.state == .changed {
newAngleY -= currentAngleY
node.eulerAngles.y = newAngleY
} else if sender.state == .ended {
currentAngleY = newAngleY
node.removeAllActions()
}
}
}
There seems to be a delay when I'm using it and I'm trying to figure out how to make the rotation as smooth as possible, again, kinda like jigspace or the Ikea app.
I've also noticed that when I try to rotate the object when it's in a certain angle, it could get quite awkward.

Looking at your rotate object function it seems like some of the logic is not quite right.
Firstly, I believe that the var currentAngleY: Float = 0 should be outside of your function body.
Secondly you should be adding the currentAngleY to the newAngleY variable e.g:
/// Rotates The Models On Their YAxis
///
/// - Parameter gesture: UIPanGestureRecognizer
#objc func rotateModels(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: gesture.view!)
var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
newAngleY += currentAngleY
DispatchQueue.main.async {
self.sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
node.eulerAngles.y = newAngleY
}
}
if(gesture.state == .ended) { currentAngleY = newAngleY }
}
An example therefore of this in a working context would be like so:
class ViewController: UIViewController {
#IBOutlet var augmentedRealityView: ARSCNView!
var currentAngleY: Float = 0
//-----------------------
// MARK: - View LifeCycle
//-----------------------
override func viewDidLoad() {
super.viewDidLoad()
//1. Generate Our Three Box Nodes
generateBoxNodes()
//2. Create Our Rotation Gesture
let rotateGesture = UIPanGestureRecognizer(target: self, action: #selector(rotateModels(_:)))
self.view.addGestureRecognizer(rotateGesture)
//3. Run The Session
let configuration = ARWorldTrackingConfiguration()
augmentedRealityView.session.run(configuration)
}
//------------------------
// MARK: - Node Generation
//------------------------
/// Generates Three SCNNodes With An SCNBox Geometry
func generateBoxNodes(){
//1. Create An Array Of Colours For Each Face
let colours: [UIColor] = [.red, .green, .blue, .purple, .cyan, .black]
//2. Create An SCNNode Wih An SCNBox Geometry
let boxNode = SCNNode()
let boxGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.01)
boxNode.geometry = boxGeometry
//3. Create A Different Material For Each Face
var materials = [SCNMaterial]()
for i in 0..<5{
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = colours[i]
materials.append(faceMaterial)
}
//4. Set The Geometries Materials
boxNode.geometry?.materials = materials
//5. Create Two More Nodes By Cloning The First One
let secondBox = boxNode.flattenedClone()
let thirdBox = boxNode.flattenedClone()
//6. Position Them In A Line & Add To The Scene
boxNode.position = SCNVector3(-0.2, 0, -1.5)
secondBox.position = SCNVector3(0, 0, -1.5)
thirdBox.position = SCNVector3(0.2, 0, -1.5)
self.augmentedRealityView.scene.rootNode.addChildNode(boxNode)
self.augmentedRealityView.scene.rootNode.addChildNode(secondBox)
self.augmentedRealityView.scene.rootNode.addChildNode(thirdBox)
}
//----------------------
// MARK: - Node Rotation
//----------------------
/// Rotates The Models On Their YAxis
///
/// - Parameter gesture: UIPanGestureRecognizer
#objc func rotateModels(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: gesture.view!)
var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
newAngleY += currentAngleY
DispatchQueue.main.async {
self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (node, _) in
node.eulerAngles.y = newAngleY
}
}
if(gesture.state == .ended) { currentAngleY = newAngleY }
}
}
Hope it helps...

Related

Shake animation of NSView

I am trying to implement a shaking NSView in my macOS app.
I tried following the example/tutorial from here, but I am not having much success.
I am attempting to not only shake the view left to right, but also up and down, and with a 2 degree positive and negative rotation.
I thought I could nest animations through the completion handler within a loop, but that is not working the way I expected so I am trying to use an if statement to implement the shake, which also is not working.
The intent here is to simulate a shake from an explosion and as the explosions get closer the intensity increases which would cause the shaking to increase in the number of shakes as well as the duration of the view shaking.
Here's example code I am working with to master this task:
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var shakeButton1: NSButton!
#IBOutlet weak var shakeButton2: NSButton!
#IBOutlet weak var shakeButton3: NSButton!
#IBOutlet weak var shakeButton4: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func shakeButtonClicked(button: NSButton) {
switch button.identifier {
case shakeButton1.identifier:
shakeView(on: self.view, shakes: 2)
case shakeButton2.identifier:
shakeView(on: self.view, shakes: 4)
case shakeButton3.identifier:
shakeView(on: self.view, shakes: 6)
case shakeButton4.identifier:
shakeView(on: self.view, shakes: 8)
default: break
}
}
/**
Method simulates a shaking of a view
intensity value is an integer for the number of times the animation is repeated.
*/
func shakeView(on theView: NSView, shakes: Int) {
let originalOrigin = self.view.frame.origin
let rotation: CGFloat = 2
let moveValue: CGFloat = 5
let contextDuration: Double = 0.05
for shake in 0..<shakes {
if shake.isMultiple(of: 2) {
print("Even")
// Perform the first animation
NSAnimationContext.runAnimationGroup({ firstAnimation in
firstAnimation.duration = contextDuration
// Animate to the new origin and angle
theView.animator().frame.origin.x = moveValue // positive origin moves right
theView.animator().frame.origin.y = moveValue // positive origin moves up
theView.animator().rotate(byDegrees: -rotation) // apply a negative rotation
print("First rotation \(shake) of \(shakes) = \(self.view.animator().frameRotation)")
}) { // First completion handler
theView.frame.origin = originalOrigin
theView.rotate(byDegrees: rotation) // apply a positive rotation to rotate the view back to the original position
print("Second rotation \(shake) of \(shakes) = \(self.view.frameRotation)")
}
} else {
print("Odd")
// Perform the opposite action
NSAnimationContext.runAnimationGroup({ secondAnimation in
secondAnimation.duration = contextDuration
theView.animator().frame.origin.x = -moveValue // negative origin moves left
theView.animator().frame.origin.y = -moveValue // negative origin moves down
theView.animator().rotate(byDegrees: rotation) // apply positive rotation
print("Third rotation \(shake) of \(shakes) = \(self.view.frameRotation)")
}) { // Second completion handler
theView.frame.origin = originalOrigin
theView.rotate(byDegrees: -rotation) // apply a negative rotation to rotate the view back to the original position
print("Fourth rotation \(shake) of \(shakes) = \(self.view.frameRotation)")
}
}
}
}
}
I set up four buttons in storyboard and linked them all to the same IBAction method for simplicity. Button identifiers in storyboard are "shake1Button", "shake2Button", "shake3Button", and "shake4Button".
Thank you for any help.
EDIT
I think I nearly have it. The only issue I am now having is when the rotation occurs, the center of rotation does not appear to be centered on the view. I am now using NotificationCenter to handle the animation. It looks pretty good the way it is, but I would really like to get the center of rotation set on the center of the view.
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var shake1Button: NSButton!
#IBOutlet weak var shake2Button: NSButton!
#IBOutlet weak var shake3Button: NSButton!
#IBOutlet weak var shake4Button: NSButton!
let observatory = NotificationCenter.default
var shaken: CGFloat = 0
var intensity: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupObservatory()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func shakeButtonClicked(button: NSButton) {
switch button.identifier {
case shake1Button.identifier:
intensity = 1
case shake2Button.identifier:
intensity = 2
case shake3Button.identifier:
intensity = 3
case shake4Button.identifier:
intensity = 4
default: break
}
observatory.post(name: .shakeView, object: nil)
}
func setupObservatory() {
observatory.addObserver(forName: .shakeView, object: nil, queue: nil, using: shakeView)
observatory.addObserver(forName: .moveDownAnimation, object: nil, queue: nil, using: moveDownAnimation)
observatory.addObserver(forName: .moveLeftAnimation, object: nil, queue: nil, using: moveLeftAnimation)
observatory.addObserver(forName: .moveRightAnimation, object: nil, queue: nil, using: moveRightAnimation)
observatory.addObserver(forName: .moveUpAnimation, object: nil, queue: nil, using: moveUpAnimation)
observatory.addObserver(forName: .rotateLeftAnimation, object: nil, queue: nil, using: rotateLeftAnimation)
observatory.addObserver(forName: .rotateRightAnimation, object: nil, queue: nil, using: rotateRightAnimation)
}
func shakeView(notification: Notification) {
if shaken != intensity {
shaken += 1
observatory.post(name: .moveDownAnimation, object: nil)
} else {
shaken = 0
}
}
func moveDownAnimation(notification: Notification) {
//guard let theView = notification.object as? NSView else { return }
let originalOrigin = self.view.frame.origin
NSAnimationContext.runAnimationGroup({ verticalAnimation in
verticalAnimation.duration = 0.05
// Animate to the new angle
self.view.animator().frame.origin.y -= intensity // subtracting value moves the view down
}) { // Completion handler
self.view.frame.origin = originalOrigin
self.observatory.post(name: .moveLeftAnimation, object: nil)
}
}
func moveLeftAnimation(notification: Notification) {
//guard let theView = notification.object as? NSView else { return }
let originalOrigin = self.view.frame.origin
// Perform the animation
NSAnimationContext.runAnimationGroup({ firstAnimation in
firstAnimation.duration = 0.05
// Animate to the new origin
self.view.animator().frame.origin.x -= intensity // subtracting value moves the view left
}) { // Completion handler
self.view.frame.origin = originalOrigin
self.observatory.post(name: .rotateLeftAnimation, object: nil)
}
}
func moveRightAnimation(notification: Notification) {
//guard let theView = notification.object as? NSView else { return }
let originalOrigin = self.view.frame.origin
// Perform the animation
NSAnimationContext.runAnimationGroup({ horizontalAnimation in
horizontalAnimation.duration = 0.05
// Animate to the new origin
self.view.animator().frame.origin.x += intensity // adding value moves the view right
}) { // Completion handler
self.view.frame.origin = originalOrigin
self.observatory.post(name: .moveUpAnimation, object: nil)
}
}
func moveUpAnimation(notification: Notification) {
//guard let theView = notification.object as? NSView else { return }
let originalOrigin = self.view.frame.origin
NSAnimationContext.runAnimationGroup({ verticalAnimation in
verticalAnimation.duration = 0.05
// Animate to the new angle
self.view.animator().frame.origin.y += intensity // adding value moves the view up
}) { // Completion handler
self.view.frame.origin = originalOrigin
self.observatory.post(name: .rotateRightAnimation, object: nil)
}
}
func rotateLeftAnimation(notification: Notification) {
// Prepare the anchor point to rotate around the center
let newAnchorPoint = CGPoint(x: 0.5, y: 0.5)
view.layer?.anchorPoint = newAnchorPoint
// Prevent the anchor point from modifying views location on screen
guard var position = view.layer?.position else { return }
position.x += view.bounds.maxX * newAnchorPoint.x
position.y += view.bounds.maxY * newAnchorPoint.y
view.layer?.position = position
// Configure the animation
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.byValue = intensity / 180 * CGFloat.pi
rotateAnimation.duration = 0.05;
let unRotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
unRotateAnimation.byValue = -(intensity / 180 * CGFloat.pi)
unRotateAnimation.duration = 0.05;
// Trigger the animation
CATransaction.begin()
view.layer?.add(rotateAnimation, forKey: "rotate")
CATransaction.setCompletionBlock {
// Handle animation completion
self.view.layer?.add(unRotateAnimation, forKey: "rotate")
self.observatory.post(name: .moveRightAnimation, object: nil)
}
CATransaction.commit()
}
func rotateRightAnimation(notification: Notification) {
// Prepare the anchor point to rotate around the center
let newAnchorPoint = CGPoint(x: 0.5, y: 0.5)
view.layer?.anchorPoint = newAnchorPoint
// Prevent the anchor point from modifying views location on screen
guard var position = view.layer?.position else { return }
position.x += view.bounds.maxX * newAnchorPoint.x
position.y += view.bounds.maxY * newAnchorPoint.y
view.layer?.position = position
// Configure the animation
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.byValue = -(intensity / 180 * CGFloat.pi)
rotateAnimation.duration = 0.05;
let unRotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
unRotateAnimation.byValue = intensity / 180 * CGFloat.pi
unRotateAnimation.duration = 0.05;
// Trigger the animation
CATransaction.begin()
view.layer?.add(rotateAnimation, forKey: "rotate")
CATransaction.setCompletionBlock {
// Handle animation completion
self.view.layer?.add(unRotateAnimation, forKey: "rotate")
self.observatory.post(name: .shakeView, object: nil)
}
CATransaction.commit()
}
}
extension Notification.Name {
static var shakeView: Notification.Name {
.init(rawValue: "ViewController.shakeView")
}
static var moveDownAnimation: Notification.Name {
.init(rawValue: "ViewController.moveDownAnimation")
}
static var moveLeftAnimation: Notification.Name {
.init(rawValue: "ViewController.moveLeftAnimation")
}
static var moveRightAnimation: Notification.Name {
.init(rawValue: "ViewController.moveRightAnimation")
}
static var moveUpAnimation: Notification.Name {
.init(rawValue: "ViewController.moveUpAnimation")
}
static var rotateLeftAnimation: Notification.Name {
.init(rawValue: "ViewController.rotateLeftAnimation")
}
static var rotateRightAnimation: Notification.Name {
.init(rawValue: "ViewController.rotateRightAnimation")
}
}

Navigation view transition full screen to a view with corner radius

I am trying to create an app home screen animation from splash, like after launch screen completed (full)screen color transforms into an app logo background color. Currently below code kind of archive what I expected. But, that transformation CAShapeLayer doesn't do with corner radius. Without corner radius it works as normal, when I try to use circle/oval/corner radius animation seems like below gif.
Tried few other StackOverflow answers which create circle animation those are not working. Here one of those.
weak var viewTransitionContext: UIViewControllerContextTransitioning!
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
viewTransitionContext = transitionContext
guard let fromVC = viewTransitionContext.viewController(forKey: .from) else { return }
guard let toVC = viewTransitionContext.viewController(forKey: .to) else { return }
if fromVC.isKind(of: SOGSplashViewController.self) && toVC.isKind(of: SOGHomeViewController.self) {
guard let toVCView = transitionContext.view(forKey: .to) else { return }
guard let fromVCView = transitionContext.view(forKey: .from) else { return }
let containerView = transitionContext.containerView
let labelWidth = UIDevice.width() * 0.75
let labelHeight = labelWidth * 0.7
let xAxis = (UIDevice.width() - labelWidth)/2.0
let yAxis = ((UIDevice.height()/2.0) - labelHeight)/2.0
let labelRect = CGRect(x: xAxis, y: yAxis, width: labelWidth, height: labelHeight)
let radius = (UIDevice.height()/2.0)*0.1
let fromFrame = fromVCView.bounds
let animationTime = transitionDuration(using: transitionContext)
let maskLayer = CAShapeLayer()
maskLayer.isOpaque = false
maskLayer.fillColor = fromVCView.backgroundColor?.cgColor
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.path = toPathValue.cgPath
let maskAnimationLayer = CABasicAnimation(keyPath: "path")
maskAnimationLayer.fromValue = (UIBezierPath(rect: fromFrame)).cgPath
maskAnimationLayer.toValue = toPathValue.cgPath
maskAnimationLayer.duration = animationTime
maskAnimationLayer.delegate = self as? CAAnimationDelegate
containerView.addSubview(fromVCView)
containerView.addSubview(toVCView)
fromVCView.layer.add(maskAnimationLayer, forKey: nil)
maskLayer.add(maskAnimationLayer, forKey: "path")
containerView.layer.addSublayer(maskLayer)
let deadLineTime = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: deadLineTime) {
UIView.animate(withDuration: 0.2, animations: {
maskLayer.opacity = 0
}, completion: { (isSuccess) in
self.viewTransitionContext.completeTransition(true)
})
}
}
}
Transforming a rectangular path to a rounded rectangular path is a very complex operation if you do it through a generic way like Core Animation.. You should better use the cornerRadius property of CALayer which is animatable.
Here is a working example with a constraint based animation:
class ViewController: UIViewController {
#IBOutlet var constraints: [NSLayoutConstraint]!
#IBOutlet var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
self.contentView.layer.cornerRadius = 10.0
self.animate(nil)
}
#IBAction func animate(_ sender: Any?) {
for c in constraints {
c.constant = 40.0
}
UIView.animate(withDuration: 4.0) {
self.view.layoutIfNeeded()
self.contentView.layer.cornerRadius = 40.0
}
}
}
contentView points to the inner view which should be animated, and constraints refer to the four layout constraints defining the distances from the view controller's view to the content view.
This is just a simple, rough example, which can certainly be improved.

Using UIPercentDrivenInteractiveTransition with CABasicAnimation has weird glitch

I'm implementing custom transition using CABasicAnimation and UIView.animate both. Also need to implement a custom interactive transition using UIPercentDrivenInteractiveTransition which exactly copies the behavior of the native iOS swipe back. Animation without a back swipe gesture (when I'm pushing and popping by the back arrow) works fine and smoothly. Moreover, swipe back also works smoothly, except when the gesture velocity is more than 900
Gesture Recognition function:
#objc func handleBackGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
guard animationTransition != nil else { return }
switch gesture.state {
case .began:
interactionController = TransparentNavigationControllerTransitionInteractor(duration: anumationDuration)
popViewController(animated: true)
case .changed:
guard let view = gesture.view?.superview else { return }
let translation = gesture.translation(in: view)
var percentage = translation.x / view.bounds.size.width
percentage = min(1.0, max(0.0, percentage))
shouldCompleteTransition = percentage > 0.5
interactionController?.update(percentage)
case .cancelled, .failed, .possible:
if let interactionController = self.interactionController {
isInteractiveStarted = false
interactionController.cancel()
}
case .ended:
interactionController?.completionSpeed = 0.999
let greaterThanMaxVelocity = gesture.velocity(in: view).x > 800
let canFinish = shouldCompleteTransition || greaterThanMaxVelocity
canFinish ? interactionController?.finish() : interactionController?.cancel()
interactionController = nil
#unknown default: assertionFailure()
}
}
UIPercentDrivenInteractiveTransition class. Here I'm synchronizing layer animation.
final class TransparentNavigationControllerTransitionInteractor: UIPercentDrivenInteractiveTransition {
// MARK: - Private Properties
private var context: UIViewControllerContextTransitioning?
private var pausedTime: CFTimeInterval = 0
private let animationDuration: TimeInterval
// MARK: - Initialization
init(duration: TimeInterval) {
self.animationDuration = duration * 0.4 // I dk why but layer duration should be less
super.init()
}
// MARK: - Public Methods
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
super.startInteractiveTransition(transitionContext)
context = transitionContext
pausedTime = transitionContext.containerView.layer.convertTime(CACurrentMediaTime(), from: nil)
transitionContext.containerView.layer.speed = 0
transitionContext.containerView.layer.timeOffset = pausedTime
}
override func finish() {
restart(isFinishing: true)
super.finish()
}
override func cancel() {
restart(isFinishing: false)
super.cancel()
}
override func update(_ percentComplete: CGFloat) {
super.update(percentComplete)
guard let transitionContext = context else { return }
let progress = CGFloat(animationDuration) * percentComplete
transitionContext.containerView.layer.timeOffset = pausedTime + Double(progress)
}
// MARK: - Private Methods
private func restart(isFinishing: Bool) {
guard let transitionLayer = context?.containerView.layer else { return }
transitionLayer.beginTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
transitionLayer.speed = isFinishing ? 1 : -1
}
}
And here is my Dismissal animation function in UIViewControllerAnimatedTransitioning class
private func runDismissAnimationFrom(
_ fromView: UIView,
to toView: UIView,
in transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) else { return }
toView.frame = toView.frame.offsetBy(dx: -fromView.frame.width / 3, dy: 0)
let toViewFinalFrame = transitionContext.finalFrame(for: toViewController)
let fromViewFinalFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0)
// Create mask to hide bottom view with sliding
let slidingMask = CAShapeLayer()
let initialMaskPath = UIBezierPath(rect: CGRect(
x: fromView.frame.width / 3,
y: 0,
width: 0,
height: toView.frame.height)
)
let finalMaskPath = UIBezierPath(rect: toViewFinalFrame)
slidingMask.path = initialMaskPath.cgPath
toView.layer.mask = slidingMask
toView.alpha = 0
let slidingAnimation = CABasicAnimation(keyPath: "path")
slidingAnimation.fromValue = initialMaskPath.cgPath
slidingAnimation.toValue = finalMaskPath.cgPath
slidingAnimation.timingFunction = .init(name: .linear)
slidingMask.path = finalMaskPath.cgPath
slidingMask.add(slidingAnimation, forKey: slidingAnimation.keyPath)
UIView.animate(
withDuration: duration,
delay: 0,
options: animationOptions,
animations: {
fromView.frame = fromViewFinalFrame
toView.frame = toViewFinalFrame
toView.alpha = 1
},
completion: { _ in
toView.layer.mask = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
I note that glitch occurs only when a swipe has a grand velocity.
Here a video with the result of smooth animation at normal speed and not smooth at high speed - https://youtu.be/1d-kTPlhNvE
UPD:
I've already tried to use UIViewPropertyAnimator combine with
interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating
But the result is another type of glitching.
I've solved the issue, just change a part of restart function:
transitionLayer.beginTime =
transitionLayer.convertTime(CACurrentMediaTime(), from: nil) - transitionLayer.timeOffset
transitionLayer.speed = 1
I don't really understand why, but looks like timeOffset subtraction works!

Detect intersect with masked image (not rectangular)

How to not get detected with intersects func when moving around masked image in UIImageView frame?
explanation image
Code that I am using to detect collision:
movingPerson.frame.intersects(camera.frame)
Where movingPerson is just a regular UIImage that I am moving around with touchesmoved func and camera is a masked Image.
I've tried .bounds instead of .frame but it's not working.
Is there any easy way?
If you want the "Very easy way" then UIDynamicItem and provide a path that bounds your image and let UIDynamicAnimator handle the collisions for you. If you do not know how to get the path for your artwork (yours is pretty easy and you should be able to extract the Bezier coordinates automatically in Paintcode, by hand in Illustrator, or from a SVG file) you can use SpriteKit instead since it will actually generate the bounding polygon for your sprite automatically.
EDIT Sample (note I didn't want to write a whole app for this so I took an existing playground and just added the colliders. the collision works, but the reset after collision doesn't):
import UIKit
import PlaygroundSupport
class ObstacleView: UIView {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .path
}
override var collisionBoundingPath: UIBezierPath {
return UIBezierPath(ovalIn: bounds)
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.width / 2
}
}
class ViewController: UIViewController {
var dynamicAnimator: UIDynamicAnimator!
var gravityBehavior: UIGravityBehavior!
var pushBehavior: UIPushBehavior!
var collisionBehavior: UICollisionBehavior!
lazy var player: UILabel = {
let label = UILabel()
label.text = "🐥"
label.sizeToFit()
label.isUserInteractionEnabled = true
label.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(label)
return label
}()
var isPanning = false
var anchor: CGPoint = .zero
let maxDistance = CGFloat(100)
lazy var obstacles: [UIView] = {
return (0..<2).map { index in
let view = ObstacleView(frame: CGRect(x:100 + CGFloat(index)*200,y:200, width: 40, height: 40))
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(view)
return view
}
}()
override func viewDidLoad() {
super.viewDidLoad()
dynamicAnimator = UIDynamicAnimator(referenceView: view)
dynamicAnimator.delegate = self
gravityBehavior = UIGravityBehavior(items: [player])
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
_ = obstacles
view.addGestureRecognizer(panGestureRecognizer)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
anchor = view.center
player.center = anchor
}
#objc private func pan(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
guard player.bounds.contains(recognizer.location(in: player)) else {
isPanning = false
return
}
isPanning = true
case .changed:
guard isPanning else {
return
}
player.center = recognizer.location(in: view)
let dx = player.center.x - anchor.x
let dy = player.center.y - anchor.y
let distance = CGFloat(hypotf(Float(dx), Float(dy)))
if distance > maxDistance {
player.center.x = anchor.x + dx / distance * maxDistance
player.center.y = anchor.y + dy / distance * maxDistance
}
case .ended:
guard isPanning else {
return
}
isPanning = false
let dx = player.center.x - anchor.x
let dy = player.center.y - anchor.y
let distance = CGFloat(hypotf(Float(dx), Float(dy)))
guard distance > 10 else {
player.center = anchor
return
}
if pushBehavior != nil {
dynamicAnimator.removeBehavior(pushBehavior)
}
pushBehavior = UIPushBehavior(items: [player], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: -dx, dy: -dy)
pushBehavior.magnitude = distance / maxDistance * 0.75
dynamicAnimator.addBehavior(pushBehavior)
dynamicAnimator.addBehavior(gravityBehavior)
collisionBehavior = UICollisionBehavior(items: [player] + obstacles)
collisionBehavior.translatesReferenceBoundsIntoBoundary = true
collisionBehavior.collisionDelegate = self
dynamicAnimator.addBehavior(collisionBehavior)
case .cancelled:
isPanning = false
player.center = anchor
default:
break
}
}
}
extension ViewController: UIDynamicAnimatorDelegate {
func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator) {
dynamicAnimator.removeAllBehaviors()
player.center = anchor
}
}
extension ViewController: UICollisionBehaviorDelegate {
func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item1: UIDynamicItem, with item2: UIDynamicItem, at p: CGPoint) {
print("contact")
}
}
PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true

Camera follow falling ball

I am trying to implement a scene which consists of a ball falling from a height and bouncing off the floor, i would also like the camera to follow the ball throughout its journey. Bellow is an attempt however the ball does not seem to be affected by gravity as println near the bottom of the code always outputs 100. however in reality the ball is on screen for a brief moment quickly falls off screen. What have i done wrong?
class obj {
var sn_node: SCNNode
init() {
let geom = SCNSphere(radius: 2.0)
sn_node = SCNNode(geometry: geom)
sn_node.position = SCNVector3Make(0, 100, 0)
sn_node.physicsBody = SCNPhysicsBody.dynamicBody()
}
}
var obj = obj()
class SceneKitController: NSViewController,SCNSceneRendererDelegate {
var _scene:SCNScene!
var _cameraNode:SCNNode!
var _floorNode:SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
let sceneView = view as SCNView
sceneView.backgroundColor = NSColor.blackColor()
setupScene()
sceneView.scene = _scene
sceneView.delegate = self
sceneView.jitteringEnabled = true
sceneView.autoenablesDefaultLighting = true
var newPos = obj.sn_node.position
newPos.z += 10
_cameraNode.position = newPos
sceneView.pointOfView = _cameraNode
}
func setupScene() {
_scene = SCNScene()
setupEnviroment()
setupSceneElements()
setupInitial()
}
func setupEnviroment() {
//create main camera
let camera = SCNCamera()
_cameraNode = SCNNode()
_cameraNode.camera = camera
//floor
var floor = SCNFloor()
floor.reflectivity = 0;
_floorNode = SCNNode(geometry: floor)
_floorNode.physicsBody = SCNPhysicsBody.staticBody()
_scene.rootNode.addChildNode(_floorNode)
}
func setupSceneElements() {
}
func setupInitial() {
_scene.rootNode.addChildNode(obj.sn_node)
}
func renderer(aRenderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: NSTimeInterval){
var newPos = obj.sn_node.position
println(newPos.y)
}
}
obj.sn_node.presentatioNode.position will give you what you want.