I'm having trouble retrieving the current position of a view, absolutely in the iPhone screen, when the view is scaled.
Here is a minimal example of what I've done.
I have a single elliptical view on screen. When I tap, it grows and start to shiver to show that it is selected. Then I can drag the view all over the screen.
Here is the code:
ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// The View
let toDrag = MyView(frame: CGRect(x: 0, y: 0, width: 50, height: 40))
toDrag.center = CGPoint(x:100, y:100)
// The gesture recognizer
let panGestRec = UIPanGestureRecognizer(target: self, action:#selector(self.panView(sender:)))
toDrag.addGestureRecognizer(panGestRec)
self.view.addSubview(toDrag)
}
func panView(sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
if sender.state == .recognized { print("Pan Recognized", terminator:"")}
if sender.state == .began { print("Pan began", terminator: "") }
if sender.state == .changed { print("Pan changed", terminator: "") }
if sender.state == .ended { print("Pan ended", terminator: "") }
let point = senderView.convert(senderView.center, to: nil)
print (" point \(point)")
let translation = sender.translation(in: self.view)
senderView.center = CGPoint(x: senderView.center.x + translation.x, y: senderView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
}
MyView
import UIKit
class MyView: UIView {
var isAnimated: Bool = false
override init(frame: CGRect) {
super.init(frame: frame) // calls designated initializer
self.isOpaque = false
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
self.backgroundColor = UIColor.clear
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = true
rotate1()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = false
endRotation()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = false
endRotation()
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.black.cgColor)
context.setFillColor(UIColor.red.cgColor)
context.drawPath(using: .fillStroke)
}
func rotate1() {
guard isAnimated else { return }
UIView.animate(withDuration: 0.05, delay:0.0, options: [], animations: {
self.transform = CGAffineTransform.init(rotationAngle:.pi/16).scaledBy(x: 1.5, y: 1.5)
}, completion: { _ in
self.rotate2()
})
}
func rotate2() {
guard isAnimated else { return }
UIView.animate(withDuration: 0.05, delay:0.0, options: [], animations: {
self.transform = CGAffineTransform.init(rotationAngle:.pi/(-16)).scaledBy(x: 1.5, y: 1.5)
}, completion: { _ in
self.rotate1()
})
}
func endRotation () {
// End existing animation
UIView.animate(withDuration: 0.5, delay:0.0, options: .beginFromCurrentState, animations: {
self.transform = CGAffineTransform.init(rotationAngle:0 ).scaledBy(x: 1, y: 1)
}, completion: nil)
}
}
With this version, here are some displays of the position
Pan began point (233.749182687299, 195.746572421573)
Pan changed point (175.265022520054, 181.332358181369)
Pan changed point (177.265022520054, 184.332358181369)
Pan changed point (177.265022520054, 184.332358181369)
Pan changed point (178.265022520054, 185.332358181369)
Pan changed point (179.265022520054, 186.332358181369)
Pan changed point (180.265022520054, 187.332358181369)
Pan changed point (180.265022520054, 188.332358181369)
Pan changed point (181.265022520054, 188.332358181369)
Pan RecognizedPan ended point (181.265022520054, 189.332358181369)
You can see that the first position (began point) isn't correct: the view was animated at that moment. The other positions are OK, when dragging, the view don't animate.
If I comment the code in rotate1 and rotate2, all the positions are correct, so I assume that the sizing and eventually the rotation of the view are interfering in the result. My question is: how can I retrieve the correct position of the view, when it is scaled? Obviously, the line
senderView.convert(senderView.center, to: nil)
didn't make what I thought : converting the coordinate to the fixed size screen coordinates?
I tried changing the animation from View animation to Core Animation (layer), and the result was the same.
So I tried to imagine a formula, with size, bounds and frame values, that could result in the correct position, but I did not succeed...
The only think that worked, was to stop immediately the animation with a transform, just before trying to retrieve the center, like that in panView
func panView(sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
// Stop the transformation now!
senderView.transform = CGAffineTransform.init(rotationAngle:0 ).scaledBy(x: 1, y: 1)
if sender.state == .recognized { print("Pan Recognized", terminator:"")}
...
It isn't the solution I wanted, but I'll go with this for now...
In fact, I had another problem with view convert (Retrieve the right position of a subView with auto-layout)
and my problem was the same, so the solution is here...
I didn't use the method correctly. If I change using beninho85's solution (and even more cleaner cosyn's method to use just one view), it works! I can have the right coordinates even if the view is animated.
I just had to replace, in ViewController :
let point = senderView.convert(senderView.center, to: nil)
with
let point = senderView.convert(CGPoint(x:senderView.bounds.midX, y:senderView.bounds.midY), to: nil)
Related
Im currently using IOS swift and I'm having trouble rotating the image view that I have on my screen to the point at which I touch the screen, So if tap directly underneath the image, it rotates 180 degrees to point at the touch point.
Here is the Code I have so far, I want to rotate the playerCharacter to wherever I touch the screen
import UIKit
import SpriteKit
class ViewController: UIViewController {
#IBOutlet weak var playerCharacter: UIImageView!
var enemyspeed: Timer?
let imageName = "yourImage.png"
let image = UIImage(named: "")
let imageView = UIImageView(image: nil)
override func viewDidLoad() {
super.viewDidLoad()
imageView.frame = CGRect(x: 1000, y: 700, width: 100, height: 100)
view.addSubview(imageView)
imageView.backgroundColor = .gray
}
#IBAction func beginGame(_ sender: UIButton) {
enemyspeed = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(astroid), userInfo: nil, repeats: true)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: view)
print(position)
UIView.animate(withDuration: 1, animations: {
self.playerCharacter.frame.origin.x = position.x
self.playerCharacter.frame.origin.y = position.y
})
}
}
#objc func astroid(){
let enemyPositionX = playerCharacter.frame.origin.x
let enemyPositionY = playerCharacter.frame.origin.y
UIView.animate(withDuration: 5, animations:{
self.imageView.frame.origin.x = enemyPositionX
self.imageView.frame.origin.y = enemyPositionY
})
}
}
You can use a little math to calculate the angle between the center of the imageView and the touch point:
let pt1 = imgView.center
let pt2 = touch.location(in: view)
let angle = atan2(pt2.y - pt1.y, pt2.x - pt1.x)
then rotate the image view:
imgView.transform = CGAffineTransform(rotationAngle: angle)
Here's a full example that uses a SF Symbol "arrow.right" image (so it starts pointing at Zero degrees), then the arrow tracks the touch location:
class AngleViewController: UIViewController {
let imgView: UIImageView = {
let v = UIImageView()
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// make sure we have an image
guard let img = UIImage(systemName: "arrow.right") else {
fatalError("Could not load image!!!!")
}
// set the image
imgView.image = img
// add the image view to the view
view.addSubview(imgView)
// 100x100 image view frame
imgView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// let's put the image view in the center
imgView.center = view.center
}
func updateArrow(_ pt2: CGPoint) -> Void {
let pt1 = imgView.center
let angle = atan2(pt2.y - pt1.y, pt2.x - pt1.x)
imgView.transform = CGAffineTransform(rotationAngle: angle)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
updateArrow(touch.location(in: view))
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
updateArrow(touch.location(in: view))
}
}
So I have a side menu that is presented when a button is clicked and I would like to know if u guys could help me find how I can detect if a click occurred outside of that side menu view so I can dismiss it.
I have looked around for this and all I see are deprecated things and with errors, and I can't use any.
Here is my animation code :
import UIKit
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else {return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width * 0.8
let finalHeight = toViewController.view.bounds.height
if isPresenting{
containerView.addSubview(toViewController.view)
toViewController.view.frame = CGRect(x: -finalWidth, y: 0, width: finalWidth, height: finalHeight)
}
let transform = {
toViewController.view.transform = CGAffineTransform(translationX: finalWidth, y: 0)
}
let identity = {
fromViewController.view.transform = .identity
}
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}){(_) in
transitionContext.completeTransition(!isCancelled)
}
}
}
I actually have something like this in my app. What you can do is add a UIView() that covers your whole view. Make sure this view is in front of everything but the menu! Set the UIView() userInteraction to false. When the menu is shown, simply set the view to intractable. Then put a touch recognizer so that when its touched the menu goes away!
Something I also like to do with this is set the views background to black, with an alpha of like 0.25! Then when the menu is hidden, alpha is zero, when it shows, animate it to 0.25. it dims the background when the menu is shown so it'll be functional and design nice.
class BackGroundView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
SetUpView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func SetUpView(){
backgroundColor = .black
alpha = 0
isUserInteractionEnabled = false
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Here's where you would hide the menu
}
func MenuIsShown(menuWillShow: Bool)
{
if(menuWillShow){
isUserInteractionEnabled = true
UIView.animate(withDuration: 0.2) {
alpha = 0.45
}
} else{
isUserInteractionEnabled = false
UIView.animate(withDuration: 0.2) {
alpha = 0
}
}
}
func AddViewToScene(view: UIView){
view.addSubview(self)
translatesAutoresizingMaskIntoConstraints = false
topAnchor.constraint(equalTo: view.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
then you can call it doing something like:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let dimView = BackGroundView()
dimView.AddViewToScene(view: view)
}
}
I have some code in my HW Cell as seen below. I am leaving out the extra code that appears to be largely unrelated.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.recognizer = UIPanGestureRecognizer(target: self, action: #selector(HWCell.handlePan(_:)))
self.recognizer.delegate = self
addGestureRecognizer(self.recognizer)
}
override func awakeFromNib() {
super.awakeFromNib()
self.cardView.layer.masksToBounds = false
self.layer.masksToBounds = false
self.contentView.layer.masksToBounds = false
self.layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
self.cardView.layoutIfNeeded() //Needed for iOS10 since layoutSubviews() reports inaccurate frame sizes.
self.cardView.layer.cornerRadius = self.cardView.frame.size.height / 3
self.cardView.layer.masksToBounds = false
}
//MARK: - horizontal pan gesture methods
func handlePan(_ recognizer: UIPanGestureRecognizer) {
// 1
if recognizer.state == .began {
// when the gesture begins, record the current center location
originalCenter = self.center
//originalCenter = self.cardView.center
self.bringSubview(toFront: self.contentView)
self.originalAlpha = self.cardView.alpha
}
// 2
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
self.center = CGPoint(x: originalCenter.x + translation.x, y: originalCenter.y)
// 3 //Remembered, state == .Cancelled when recognizer manually disabled.
if recognizer.state == .ended {
// the frame this cell had before user dragged it
let originalFrame = CGRect(x: 0, y: frame.origin.y,
width: bounds.size.width, height: bounds.size.height)
if (!deleteOnDragRelease && !completeOnDrag) {
// if the item is not being deleted, snap back to the original location
UIView.animate(withDuration: 0.2, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: { self.frame = originalFrame }, completion: nil)
if (self.task?.completed == false) {
UIView.transition(with: self.completionImageView,
duration: 0.2,
options: UIViewAnimationOptions.transitionCrossDissolve,
animations: { self.completionImageView.image = UIImage(named: "Grey Checkmark") },
completion: nil)
}
}
What happens is that, and keep in mind that this ONLY HAPPENS on iPhone X in Landscape mode, is the cell extends itself further by stretching instead of simply moving when I drag my finger across it.
You can see this behaviour in a video I uploaded: https://www.youtube.com/watch?v=qPXZWwnuWhU&feature=youtu.be
This odd behaviour never occurs in iOS10, only in iOS 11, on this particular device and orientation.
My constraints are blue and 0 warnings/errors, I played around with them to see if the safe area was related to this problem. The problem is gone only if I set the leading/trailing constraints of the UITableView to line up with the Safe Area. If they are lined up with the Superview, this error occurs.
Anyone have an idea what could be causing this?
In my application I added a UIButton, but instead of having the standard touch event of the UIButton getting darker then becoming the standard color when touch events have ended, I want to add an animation that when you click on the UIButton the button gets smaller, then when touch events have ended the UIButton becomes back to regular size when touch events have ended. So far I have typed this code, but it does not seem to work. Please let me know how I can accomplish this animation.
// whatsTheGame is the UIButton I have
#IBAction func whatsTheGame(_ sender: UIButton) {
logo.isHighlighted = false
UIView.animate(withDuration: 0.3) {
if let centerLogo = self.logo {
centerLogo.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
UIView.animate(withDuration: 0.3) {
if let centerLogo = self.logo {
centerLogo.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
UIView.animate(withDuration: 0.3) {
if let centerLogo = self.logo {
centerLogo.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
}
Adding Touch Up Inside and Touch Down events
#IBAction func onTouchUpInside(_ sender: Any) {
let button = sender as! UIButton
UIView.animate(withDuration: 0.3) {
button.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
#IBAction func onTouchDown(_ sender: Any) {
let button = sender as! UIButton
UIView.animate(withDuration: 0.3) {
button.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
}
}
So I recently made a swift game with sprite kit and now I am stuck.
I have a character selection screen and I want to show a description for the character if you hold on the character, but if you just touch it you choose it and play with it. I already have the code to play/show the description. I just need to know when to call the corresponding functions and how to differentiate if the node is held or just touched.
Thanks in advance
So, here is how you can do it using SKActions... Also, there is another way using SKAction but I can't really show you all the possibilities :) Anyways, here is the code which does what you want:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let kLongPressDelayActionKey = "longPressDelay"
let kLongPressStartedActionKey = "longPressStarted"
let kStoppingLongPressActionKey = "stoppingLongPressActionKey"
let desc = SKSpriteNode(color: .white, size: CGSize(width: 150, height: 150))
let button = SKSpriteNode(color: .purple, size: CGSize(width: 150, height: 150))
let other = SKSpriteNode(color: .yellow, size: CGSize(width: 150, height: 150))
override func didMove(to view: SKView) {
addChild(desc)
addChild(button)
addChild(other)
button.position.y = -160
button.name = "button"
desc.alpha = 0.0
desc.name = "description"
other.name = "other"
other.alpha = 0.0
other.position.y = -320
}
private func singleTap(onNode:SKNode){
other.alpha = other.alpha == 0.0 ? 1.0 : 0.0
}
private func startLongPress(withDuration duration:TimeInterval){
//How long does it take before long press is fired
//If user moves his finger of the screen within this delay, the single tap will be fired
let delay = SKAction.wait(forDuration: duration)
let completion = SKAction.run({
[unowned self] in
self.desc.removeAction(forKey: self.kLongPressDelayActionKey)
self.desc.run(SKAction.fadeIn(withDuration: 0.5), withKey: self.kLongPressStartedActionKey)
})
self.desc.run(SKAction.sequence([delay,completion]), withKey: kLongPressDelayActionKey)
}
private func stopLongPress(){
//Fire single tap and stop long press
if desc.action(forKey: kLongPressDelayActionKey) != nil{
desc.removeAction(forKey: kLongPressDelayActionKey)
self.singleTap(onNode: self.other)
//or just stop the long press
}else{
desc.removeAction(forKey: kLongPressStartedActionKey)
//Start fade out action if it isn't already started
if desc.action(forKey: kStoppingLongPressActionKey) == nil {
desc.run(SKAction.fadeOut(withDuration: 0.2), withKey: kStoppingLongPressActionKey)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//To stop the long press if we slide a finger of the button
if let touch = touches.first {
let location = touch.location(in: self)
if !button.contains(location) {
stopLongPress()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
if button.contains(location) {
print("Button tapped")
startLongPress( withDuration: 1)
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
stopLongPress()
}
}
You can run the code and you will see that if you tap on the purple button, a yellow box will pop up. If you tap it again, it will hide. Also, if you hold your finger on purple box for 1 second, a white box will fade in. When you release the finger, it will fade out. As an addition, I have implemented touchesMoved, so if you slide your finger of the purple button while blue box is out, it will fade out as well.
So in this example, I have fused long press with single tap. Single tap is considered to be everything that is not long press. Eg, if user holds his finger 0.01 sec, or 0.5 sec, or 0.99 sec, it will be considered as single tap and yellow box will pop out. If you hold your finger for >= 1 second, the long press action will be triggered.
Another way would be to use gesture recognizers...And I will probably make an example for that later :)
And here is how you can do it using gesture recognizers:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var longPressGesture:UILongPressGestureRecognizer!
var singleTapGesture:UITapGestureRecognizer!
let kLongPressStartedActionKey = "longPressStarted"
let kStoppingLongPressActionKey = "stoppingLongPressActionKey"
let desc = SKSpriteNode(color: .white, size: CGSize(width: 150, height: 150))
let button = SKSpriteNode(color: .purple, size: CGSize(width: 150, height: 150))
let other = SKSpriteNode(color: .yellow, size: CGSize(width: 150, height: 150))
override func didMove(to view: SKView) {
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(GameScene.longPress(_:)))
singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(GameScene.singleTap(_:)))
self.view?.addGestureRecognizer(longPressGesture)
self.view?.addGestureRecognizer(singleTapGesture)
addChild(desc)
addChild(button)
addChild(other)
button.position.y = -160
button.name = "button"
desc.alpha = 0.0
desc.name = "description"
other.name = "other"
other.alpha = 0.0
other.position.y = -320
}
private func stopLongPress(){
desc.removeAction(forKey: self.kLongPressStartedActionKey)
//Start fade out action if it isn't already started
if desc.action(forKey: kStoppingLongPressActionKey) == nil {
desc.run(SKAction.fadeOut(withDuration: 0.2), withKey: kStoppingLongPressActionKey)
}
}
func singleTap(_ sender:UITapGestureRecognizer){
if sender.state == .ended {
let location = convertPoint(fromView: sender.location(in: self.view))
if self.button.contains(location) {
self.other.alpha = self.other.alpha == 0.0 ? 1.0 : 0.0
}
}
}
func longPress(_ sender: UILongPressGestureRecognizer) {
let longPressLocation = convertPoint(fromView: sender.location(in: self.view))
if sender.state == .began {
if button.contains(longPressLocation) {
desc.run(SKAction.fadeIn(withDuration: 0.5), withKey: self.kLongPressStartedActionKey)
}
}else if sender.state == .ended {
self.stopLongPress()
}else if sender.state == .changed {
let location = convertPoint(fromView: sender.location(in: self.view))
if !button.contains(location) {
stopLongPress()
}
}
}
}