How to animate from node in ARKit to actual view - ios

I would like to get the rectangular position in the scene view of a tapped node so that I can use that position to animate another UIView from that position to a bigger size (exactly like in the Measure app):
I get the tapped node with this code, though how do I get the rect or the dimensions or whatever to make the smooth transition from the node to the view?
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
if (touch.view == self.sceneView) {
let viewTouchLocation:CGPoint = touch.location(in: sceneView)
guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
return
}
}
}
I've looked into using hitTest or smartHitTest on the scene view given the touch position but I can't really seem to be able to get the size/position of the smaller view.

Not exactly your answer but try it out: (edited to send it back)
var front = false
var boxView = UIView()
#objc func tap(_ sender: UITapGestureRecognizer){
if !front{
let position = sender.location(in: sceneView)
let hitTest = sceneView.hitTest(position, options: nil)
if let hitNode = hitTest.first?.node{
self.boxView = UIView(frame: CGRect(x: position.x, y: position.y, width: 100, height: 40))
self.boxView.backgroundColor = .red
sceneView.addSubview(self.boxView)
UIView.animate(withDuration: 0.2) {
self.boxView.frame = CGRect(x: self.sceneView.frame.width/2 - 150, y: self.sceneView.frame.height/2 - 100, width: 300, height: 200)
}
front = true
}
}else{
let p = plane.convertPosition(plane.position, to: sceneView.scene.rootNode)
print(p, plane.position) // see difference between p and plane.position
let projectedPoint = sceneView.projectPoint(p)
let point = CGPoint(x: CGFloat(projectedPoint.x), y: CGFloat(projectedPoint.y))
UIView.animate(withDuration: 0.2, animations: {
self.boxView.frame = CGRect(x: point.x, y: point.y, width: 100, height: 40)
}) { (completed) in
self.boxView.removeFromSuperview()
}
front = false
}
}
plane here is node in my test case. You might want to look about convertPosition and projectPoint

Related

why when ever I try to tap on my SKSpriteNode it is not interactive

I am trying to have my SKSpriteNode named "slots" be tapped on when ever a user is trying to hit a target in the middle of the sprite. But whenever I Tapp on the slot in does not blink or do anything really.
I was trying to have it blink and print("Tapped on shotSlotNode") before I'll go on to continue creating this game but im still struck on this part of the game.
var slots = [shotSlot]()
var targets = [shotSlot]()
var gameScore: SKLabelNode!
var slotsRed = [targetSlotRed]()
var shotSlotNode: shotSlot? // Define a property for shotSlotNode
override func didMove(to view: SKView) {
let backGround = SKSpriteNode(imageNamed: "background")
backGround.position = CGPoint(x: 512, y: 384)
backGround.blendMode = .replace
backGround.zPosition = -1
// line 20 makes sure things go on top of the background view.
backGround.scale(to: CGSizeMake(1024, 768))
// I used line 21 to strech the image out to fix my scrrens size because I know I set my screen size to 1024 in the GameScene UI
addChild(backGround)
gameScore = SKLabelNode(fontNamed: "Chalkduster")
gameScore.text = "Score: 0"
gameScore.position = CGPoint(x: 480, y: 70)
addChild(gameScore)
self.view?.isMultipleTouchEnabled = true
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 370)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 200)) }
// this code breaks my slots into 3 rows and 3 cloumms of the shotSlots.
for i in 0 ..< 1 { createGreenTarget(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 1 { createRedTarget(at: CGPoint(x: 860 + (i * 370), y: 200)) }
}
func slotTapped(_ slot: shotSlot) {
// Make the slot sprite blink
print("Tapped on slot")
let blinkOut = SKAction.fadeAlpha(to: 0.2, duration: 0.15)
let blinkIn = SKAction.fadeAlpha(to: 1, duration: 0.15)
let blink = SKAction.sequence([blinkOut, blinkIn])
let blinkForever = SKAction.repeatForever(blink)
slot.sprite.run(blinkForever)
print("Started blinking")
// Handle the slot tap logic
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
// now we need to make this greenTarget slide.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
for slot in slots {
if slot.contains(location) {
// The touch is inside the slot node
print("Tapped on shot slot")
slotTapped(slot)
// You can also check if the tapped slot is your `shotSlotNode`
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
}
}
}
func createSlot(at position: CGPoint) {
let slot = shotSlot()
slot.configure(at: position)
slot.zPosition = 1
slot.isUserInteractionEnabled = true
addChild(slot)
targets.append(slot) // Add the slot to the targets array
if position == CGPoint(x: 512, y: 100) {
slot.sprite.name = "shotSlotNode"
slot.isUserInteractionEnabled = true
self.shotSlotNode = slot
print("Set shotSlotNode to sprite with name \(slot.sprite.name)")
}
}
class shotSlot: SKNode {
let sprite = SKSpriteNode(imageNamed: "slots")
func configure(at position: CGPoint) {
self.position = position
sprite.name = "shotSlotNode"
// sprite.position = CGPoint(x: frame.midX, y: frame.midY)
sprite.scale(to: CGSizeMake(220, 140))
sprite.isUserInteractionEnabled = true
sprite.zPosition = 13
addChild(sprite)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if let node = self.childNode(withName: "shotSlotNode") {
let convertedLocation = node.convert(location, from: self)
if node.contains(convertedLocation) {
print("Tapped on shotSlotNode")
}
}
}
}
}

How do you pass a gesture between subviews in UIKit?

I've got a ViewController with three subviews. I'm trying to get them to detect touches in their bounds from a starting point outside their bounds without the user lifting their finger (ie the user dragging into the view). I thought hitTest would do this but it only works for separate taps. I assume this is probably passing a gesture through instead but I've not found out how to implement this.
class SuperViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
self.view = TestView()
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
}
}
Which produces this
And then I've subclassed UIView for the SuperViewController's view.
class TestView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.isUserInteractionEnabled, !isHidden, alpha > 0.01 else {return nil}
if self.point(inside: point, with: event) {
for subview in subviews.reversed() {
let hitView = subview.hitTest(point, with: event)
if hitView != nil {
hitView?.backgroundColor = .red
return hitView
}
}
return self
}
return nil
}
}
So each one turns red when the user taps. But ideally I want them to each respond with one drag from the top left corner of the screen to the other.
You can accomplish this with a UIPanGestureRecognizer.
Here's an example below:
class ViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
self.view.addGestureRecognizer(gestureRecognizer)
}
#objc
private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let view = gestureRecognizer.view else {
return
}
let translation = gestureRecognizer.translation(in: view)
for subview in view.subviews.reversed() {
if let hitView = subview.hitTest(translation, with: nil) {
hitView.backgroundColor = .red
return
}
}
}
}

Tapping a UIImage while it's being animated

I've been trying to be able to tap a UIImage as it animates to the top of my screen and print("Image Tapped"), yet to no success.
override func viewDidLoad() {
super.viewDidLoad()
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
UIView.animate(withDuration: 5, delay: 0, options: UIImageView.AnimationOptions.allowUserInteraction, animations: {
self.redBalloon.frame = CGRect(x: Int(self.xEnding), y: -192, width: 166, height: 192)
}, completion: {(finished:Bool) in
self.endGame()
})
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
}
#objc func imageTapped(_ sender: UITapGestureRecognizer) {
// do something when image tapped
print("image tapped")
}
The problem is that the image view is not in the spot where you see it during animation (it's at the endpoint of the animation). So you are not tapping on the image view at the point where it is, and thus the tap is not detected.
Therefore, either you must hit-test the presentation layer or, if you don't want to do that, you must use a UIViewPropertyAnimator instead of calling UIView.animate.
As an example of the first approach, I'll subclass UIImageView. Make your UIImageView an instance of this subclass:
class TouchableImageView : UIImageView {
override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
let pres = self.layer.presentation()!
let suppt = self.convert(point, to: self.superview!)
let prespt = self.superview!.layer.convert(suppt, to: pres)
return super.hitTest(prespt, with: e)
}
}
However, personally I think it's a lot simpler to use UIViewPropertyAnimator. In that case, do not make your UIImageView a TouchableImageView! You don't want to do extra hit-test munging. Just let the property animator do all the work:
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
let anim = UIViewPropertyAnimator(duration: 5, timingParameters: UICubicTimingParameters(animationCurve: .easeInOut))
anim.addAnimations {
self.redBalloon.frame = CGRect(x: Int(xEnding), y: -192, width: 166, height: 192)
}
anim.addCompletion { _ in
self.endGame()
}
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
anim.startAnimation()

How can I achieve a smooth animation when dragging a UIView

This is the code I have written so far to change the position of the view when dragged.
The view changes its center point when the UIPanGestureRecognizer is either changed or began and that happen when I let go the gesture. I do not want that. I want it to go along with my drag like what the Notification Center and Control Center does.
Thanks in advance for the help.
class ViewController: UIViewController {
let maxY = UIScreen.main.bounds.height
lazy var slideUpView: UIView = {
let view = UIView(frame: CGRect(x: 0, y: maxY - 295, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 3 ))
view.backgroundColor = .green
return view
}()
lazy var redImageView: UIView = {
let view = UIView(frame: CGRect(x: 0, y: maxY - 295, width: UIScreen.main.bounds.width, height: slideUpView.frame.height / 2 ))
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
print("MaxX: \(UIScreen.main.bounds.maxX)")
print("Max Hight: \(UIScreen.main.bounds.height)")
print("MaxY: \(UIScreen.main.bounds.maxY)")
print("Max width: \(UIScreen.main.bounds.width)")
// MARK: add the views as subview
let viewsArray = [slideUpView, redImageView]
viewsArray.forEach{view.addSubview($0)}
// Add a UIPanGestureRecognizer to it
viewsArray.forEach {createPanGestureRecognizer(targetView: $0)}
}
// The Pan Gesture
func createPanGestureRecognizer(targetView: UIView) {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(recognizer:)))
panGesture.maximumNumberOfTouches = 1
targetView.addGestureRecognizer(panGesture)
}
#objc func handlePanGesture(recognizer: UIPanGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.began || recognizer.state == UIGestureRecognizerState.changed {
let translation = recognizer.translation(in: self.view)
print(recognizer.view!.center.y)
let newPos = CGPoint(x:recognizer.view!.center.x + translation.x, y: recognizer.view!.center.y + translation.y)
if insideDraggableArea(newPos) {
guard let targetedView = recognizer.view else {
print("Error: No View to handle")
return
}
targetedView.center.y = newPos.y
recognizer.setTranslation(.zero, in: targetedView)
}
}
}
private func insideDraggableArea(_ point : CGPoint) -> Bool {
return // point.x > 50 && point.x < 200 &&
point.y > (maxY * 0.27) && point.y <= maxY
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: view)
print(position)
}
}
}
I also had this issue, at least in a playgrounds prototype...
I put all of the translation (that you have in the changed state) inside of a UIViewProperty animator that I had defined within the handlePanGesture function and then called .startAnimation() within the began/changed state.
It takes a bit of tweaking on the animation but its a lot smoother for me..
This code works fine. The issues was in the simulator I used yesterday. Everything works as I wanted it to be when tested in an iPhone.

How to rotate an UIImageView using TouchesMoved

I am trying to rotate an UIImageView using Touch events, I want the image to rotate with my finger, I want to do the same as this: dropbox link (best to see it, so u understand what I mean)
After researching on how to rotate an UIImageView: How can I rotate an UIImageView by 20 degrees. I have tried the following code with my simple approach to developing iPhone apps:
class ViewController: UIViewController {
var startpoint:CGPoint!
var endpoint:CGPoint!
#IBOutlet var yello:UIImageView
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
let t:UITouch = touches.anyObject() as UITouch
startpoint = t.locationInView(self.view)
endpoint = t.previousLocationInView(self.view)
if t.view == yello {
var startX = startpoint.x
var endX = endpoint.x
yello.center = CGPointMake(yello.center.x, yello.center.y)
UIView.animateWithDuration(1, animations: { self.yello.transform = CGAffineTransformMakeRotation(startX-endX)})
}
}
}
I can see clearly that my code has a lot of mistakes and bad practices, and it also doesn't behave correctly as I want. (see dropbox link above)
So maybe there is a better way to do this perhaps by using CoreAnimation, and I would appreciate if code sample would do the same as I want.
I just wrote this real quick. I think it is what you are looking for. Instead of using images I used colored-views but you could easily replace that with an image. You may want to detect if the view was dragged so that the user must drag the view in-order to rotate, because currently the user can drag anywhere to rotate the view. (The code below now checks for this case) Let me know if you have any questions about it.
class ViewController: UIViewController {
var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
myView.center = self.view.center
myView.backgroundColor = UIColor.redColor()
self.view.addSubview(myView)
myView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
let box = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
box.backgroundColor = UIColor.blueColor()
myView.addSubview(box)
}
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
let touch = touches.anyObject() as UITouch
if touch.view === myView.subviews[0] {
let position = touch.locationInView(self.view)
let target = myView.center
let angle = atan2(target.y-position.y, target.x-position.x)
myView.transform = CGAffineTransformMakeRotation(angle)
}
}
}
//updated code for swift 4
class ViewController: UIViewController {
var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
myView.center = self.view.center
myView.isUserInteractionEnabled = true
myView.backgroundColor = UIColor.red
self.view.addSubview(myView)
myView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
let box = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
box.backgroundColor = UIColor.blue
myView.addSubview(box)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch:UITouch = touches.first!
if touch.view === myView.subviews[0] {
let position = touch.location(in: self.view)
let target = myView.center
let angle = atan2(target.y-position.y, target.x-position.x)
myView.transform = CGAffineTransform(rotationAngle: angle)
}
}

Resources