Swift: Why does Tap Gesture Recognizer not work every time? - ios

I have weird problem. I have a basic tap gesture recognizer added to my subview, but it works only when it likes to. Sometimes it works on second click, sometimes on the third or even more clicks. But when I added print("whatever") to the function, which is called by the gesture recognizer, it always works on the first time I click the subview.
How do I fix this?
On these two examples occur the same problem:
SubView:
let getGiftGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.getGift))
self.continueButton.addGestureRecognizer(getGiftGestureRecognizer)
DispatchQueue.main.async {
self.coverAnyUIElement(element: self.successView)
let gift = SCNNode()
gift.geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
//nahodne vygenerovat geometry, nebo barvu
let ownedGeometries = UserDefaults.standard.array(forKey: "ownedGeometries")
let ownedColors = UserDefaults.standard.array(forKey: "ownedColors")
let notOwnedGeometriesCount = geometriesCount - ownedGeometries!.count
let notOwnedColorsCount = colorsCount - ownedColors!.count
if notOwnedGeometriesCount == 0{
//mam vsechny geometrie
//vygeneruju barvu
}else if notOwnedColorsCount == 0{
//mam vsechny barvy
//vygeneruju geometrii
}else if(notOwnedGeometriesCount == 0 && notOwnedColorsCount == 0){
//mam uplne vsechno
}else{
//vygeneruj cokoliv
let random = Int.random(in: 0...10)
if random <= 3{
//vygeneruj geometrii
}else{
//vygeneruj barvu
}
}
print(self.generateColor())
print(self.generateGeometry())
self.chest.opacity = 1
self.chest.position.y = -1.8
self.world.addChildNode(self.chest)
let viko = self.chest.childNode(withName: "viko", recursively: false)
self.chest.addChildNode(gift)
print("VLAKNO1: \(Thread.isMainThread)")
viko?.runAction(SCNAction.rotateBy(x: 0, y: 0, z: -90/(180/CGFloat.pi), duration: 1),completionHandler: {
print("VLAKNO2: \(Thread.isMainThread)")
let pohyb = SCNAction.move(by: SCNVector3(0,4,0), duration: 1)
gift.runAction(pohyb,completionHandler: {
print("VLAKNO3: \(Thread.isMainThread)")
self.showConfirmGiftButton()
})
})
}
}
Button:
confirmGiftButton.addTarget(self, action: #selector(acceptedGift), for: .touchUpInside)
#objc func acceptedGift(){
DispatchQueue.main.async {
UIButton.animate(withDuration: 0.2, animations: {
self.confirmGiftButton.alpha = 0.9
}) { (_) in
UIButton.animate(withDuration: 0.2, animations: {
self.confirmGiftButton.alpha = 1
}) { (_) in
//print("VLAKNO1: \(Thread.isMainThread)")
self.chest.runAction(SCNAction.fadeOut(duration: 0.5),completionHandler: {
// print("Vlakno: \(Thread.isMainThread)")
self.chest.removeFromParentNode()
self.moveToMenu()
})
//print("VLAKNO2: \(Thread.isMainThread)")
self.coverAnyUIElement(element: self.confirmGiftButton)
}
}
}
}

So I removed the DispatchQueue.main.async from both methods and tried it for a few times and it seems like it was the problem, because now both the subview gesture recognizer and button target works on first click correctly.
I think that it was caused because when I clicked on the button, the Main thread was already used so it somehow skipped the methods. And worked in the second or more clicks, because in that time it was already empty.

Related

Continuously rendering new child node objects on SceneKit scene in Swift

I got a question about a Scene Kit project I'm doing. So I have this scene where I randomly spawn cubes and add them to my scene root node, the thing is that they are rendered at the beginning, but quickly they stop being rendered, so I don't see these new objects being spawned, I do see them again when I tap on the screen (so when I make an action on the scene or whatever) OR sometimes some of them are randomly rendered and I don't know why
I have tried setting rendersContinuously to true but this does not change anything.
Here is the cube spawning thread :
DispatchQueue.global(qos: .userInitiated).async {
while true {
self.spawnShape()
sleep(1)
}
}
Here is how I add them to the child nodes :
let geometryNode = SCNNode(geometry: geometry)
geometryNode.simdWorldPosition = simd_float3(Float.random(in: -10..<10), 2, shipLocation+Float.random(in: 20..<30))
self.mainView.scene!.rootNode.addChildNode(geometryNode)
And here's the endless action applied to the camera and my main node :
ship.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
camera.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
The added geometryNode cubes stop being rendered unless I tap on screen
How can I force the rendering of these new child node objects on the scene even when I don't touch the screen?
Thank you
EDIT asked by ZAY:
Here is the beginning of my code basically:
override func viewDidLoad() {
super.viewDidLoad()
guard let scene = SCNScene(named: "art.scnassets/ship.scn")
else { fatalError("Unable to load scene file.") }
let scnView = self.view as! SCNView
self.mainView = scnView
self.mainView.rendersContinuously = true
self.ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
self.camera = scene.rootNode.childNode(withName: "camera", recursively: true)!
self.ship.renderingOrder = 1
self.ship.simdWorldPosition = simd_float3(0, 0, 0)
self.camera.simdWorldPosition = simd_float3(0, 15, -35)
self.mainView.scene = scene
DispatchQueue.global(qos: .userInitiated).async {
while true {
self.spawnShape()
sleep(1)
}
}
ship.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
camera.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
let tap = UILongPressGestureRecognizer(target: self, action: #selector(tapHandler))
tap.minimumPressDuration = 0
self.mainView.addGestureRecognizer(tap)
}
When I tap on screen, this code is called:
func tapHandler(gesture: UITapGestureRecognizer) {
let p = gesture.location(in: self.mainView)
let turnDuration: Double = 0.3
print("x: \(p.x) y: \(p.y)")
if gesture.state == .began {
if p.x >= self.screenSize.width / 2 {
self.delta = 0.5
}
else {
self.delta = -0.5
}
self.ship.runAction(SCNAction.rotateBy(x: 0, y: 0, z: self.delta, duration: turnDuration))
return
}
if gesture.state == .changed {
return
}
self.ship.runAction(SCNAction.rotateBy(x: 0, y: 0, z: -self.delta, duration: turnDuration))
}
Basically it rotates my ship to right or left depending on which side of screen I tap, and it displays the coordinates where I tapped in the console. When I release, the ship goes back to the initial position/rotation.
DispatchQueue.global(qos: .userInitiated).async {
while true {
self.spawnShape()
sleep(1)
}
}
Aren't you blocking that shared queue by doing that? Use a Timer if you want to run something periodically.

if statement to change textLabel SKColor not executing

I am trying to run an if statement to change the color of a text label in SpriteKit where the number of lives has reached a certain number. But the color is not changing from white to red.
In my Game the Lives decrease by 1 every time an Asteroid passes the Spaceship.
Right now the text label code looks like this:
livesLabel.text = "Lives: 3"
livesLabel.fontSize = 70
if livesNumber == 1 {
livesLabel.fontColor = SKColor.red
} else {
livesLabel.fontColor = SKColor.white
}
livesLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.right
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
case 2436: // Checks if device is iPhone X
livesLabel.position = CGPoint(x: self.size.width * 0.79, y: self.size.height * 0.92)
default: // All other iOS devices
livesLabel.position = CGPoint(x: self.size.width * 0.85, y: self.size.height * 0.95)
}
}
livesLabel.zPosition = 100
self.addChild(livesLabel)
I have a function where a livesNumber if statement works fine which looks like this:
func loseALife() {
livesNumber -= 1
livesLabel.text = "Lives: \(livesNumber)"
let scaleUp = SKAction.scale(to: 1.5, duration: 0.2)
let scaleDown = SKAction.scale(to: 1, duration: 0.2)
let scaleSequence = SKAction.sequence([scaleUp, scaleDown])
livesLabel.run(scaleSequence)
if livesNumber == 0{
runGameOver()
}
}
If anyone can shed a light on this that would be great. I thought that it might have something to do with String or Int, but since this works on other functions and also I tried some String to Int conversions and it changed nothing I am no longer sure what the issue is.
As can be seen from the discussion above, the problem was that the "color label" code was run once in the initialization of the view. Therefore, when the livesNumber value was later updated, the "color label" code wasn't executed.
This can be solved in several ways. One would be to add it to the looeALife function:
func loseALife() {
livesNumber -= 1
livesLabel.text = "Lives: \(livesNumber)"
if livesNumber == 1 {
livesLabel.fontColor = SKColor.red
} else {
livesLabel.fontColor = SKColor.white
}
let scaleUp = SKAction.scale(to: 1.5, duration: 0.2)
let scaleDown = SKAction.scale(to: 1, duration: 0.2)
let scaleSequence = SKAction.sequence([scaleUp, scaleDown])
livesLabel.run(scaleSequence)
if livesNumber == 0{
runGameOver()
}
}
(maybe extract it to a separate function)
You could also add a didSet to your livesNumber and then update the value there:
var livesNumber: Int = 3 {
didSet {
livesLabel.color = livesNumber == 1 ? .red : .white
}
}
Hope that helps.

Stop UIDynamicAnimator Whit view Limits

I have been trying to create a custom UIClass that implements an horizontal scrolling for its inner content by using UIPanGestureRecognizer.
My first success approach was:
#IBAction func dragAction(_ sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: sender.view)
if velocity.x > 0{
//print("\(logClassName) Dragging Right ...")
if innerView!.frame.origin.x < CGFloat(0){
offSetTotal += 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
else{
//print("\(logClassName) Dragging Left ...")
if totalWidth - innerView!.bounds.maxX < innerView!.frame.origin.x{
offSetTotal -= 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
}
That way, althought is a little bit clunky, I am able to scroll from left to right (and reverse) and the innerview is always covering in the holderView.
Then, and because i wanted to get the scrolling effect that you would get with a UIScrolling view i tried the following approach
#objc func handleTap(_ pan: UIPanGestureRecognizer){
let translation = pan.translation(in: innerView)
let recogView = pan.view
let curX = recogView?.frame.origin.x
if pan.state == UIGestureRecognizerState.began {
self.animator.removeAllBehaviors()
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.changed {
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.ended {
let itemBehavior = UIDynamicItemBehavior(items: [innerView!]);
var velocity = pan.velocity(in: self)
print(velocity.y)
velocity.y = 0
itemBehavior.addLinearVelocity(velocity, for: innerView!);
itemBehavior.friction = 1
itemBehavior.resistance = 1;
//itemBehavior.elasticity = 0.8;
self.collision = UICollisionBehavior(items: [innerView!]);
self.collision!.collisionMode = .boundaries
self.animator.addBehavior(collision!)
self.animator.addBehavior(itemBehavior);
}
Now the problem i face is that it moves horizontally but it goes away and gets lose.
Is that the right approach? Is there a delegate?
The question was posted due to a lack of knowledge of UIScrollViews. I Knew how to do with Storyboard and only scroll vertically.
I ended up creating a programatically UIScrollView and setting up its content size with the inner view.
scrollView = UIScrollView(frame: bounds)
scrollView?.backgroundColor = UIColor.yellow
scrollView?.contentSize = innerView!.bounds.size
scrollView?.addSubview(innerView!)
addSubview(scrollView!)
But first I have defined the innerView.

Position of UIImageView during an animation

Today I make a shoot em up game without SpriteKit
And I have a problem for check UIImageView position during an animation.
Code I have tried:
#IBAction func MoveTDButton(_ sender: UIButton) {
animationForPlane = UIViewPropertyAnimator(duration: 0.5, curve: .easeIn, animations: {
if sender.tag == 1 {
self.ship.center.x = sender.frame.origin.x + sender.frame.size.width - (self.ship.frame.size.width / 2)
self.ship.image = #imageLiteral(resourceName: "shipright")
print(self.ship.center.x)
}else{
self.ship.center.x = sender.frame.origin.x + (self.ship.frame.size.width / 2)
self.ship.image = #imageLiteral(resourceName: "shipleft")
print(self.ship.center.x)
}
})
animationForPlane.startAnimation()
}
private func autoshoot(_ imgShip :UIImageView, _ imgBullet :UIImageView){
var t: TimeInterval!
t = 0.2
//var hauteur = self.view.frame.maxY
if let duration = t {
UIView.animate(withDuration: duration, animations: {
imgBullet.center.y = 10
imgBullet.center.x = self.ship.center.x
}, completion: { (true) in
imgBullet.center.x = self.ship.center.x
imgBullet.center.y = imgShip.center.y
//checkPosEnemy(img, hauteur: hauteur)
self.autoshoot(imgShip, imgBullet)
})
}
}
The function MoveTDButton it's for move my ship by button (left and right) with animation for the slide.
And, auto shoot function for shoot ^^
Problem :
When I click on a button for move my ship, position is set at the end of animation and not during.
So my bullet is not shoot at the ship's position but at the end of animation.
It will be better if you Use SpriteKit or similar frameworks.
However to answer to your question you can try :
imgBullet.center.x = self.ship.layer.presentation()!.position
instead of
imgBullet.center.x = self.ship.center.x

slide transition using UIView.transition() method

I have and table view and when the user clicks a button I want it to reload it's data and change numerous other things. Sadly, my users have not been satisfied with the available options for the transitions/animations, and wanted a sliding transition. I couldn't find anything online to do this, however. Here is my code:
UIView.transition(with: view, duration: 0.5,options: .transitionCurlUp,animations:
{ () -> Void in
self.schedule.reloadData()
self.formatter.dateFormat = "yyyy-MM-dd"
let dayOfWeek = self.week[self.getDayOfWeek(self.currentDate)!-1]
var forReturn = dayOfWeek.getCourseNumber()
if forReturn>3{
forReturn = forReturn + 1
self.refreshButton.isHidden = false
self.noClasses.isHidden = true
}else{
self.refreshButton.isHidden = true
self.noClasses.isHidden = false
}
self.changeWeek()
let startDate = self.formatter.string(from: self.startDateBase)
if self.currentDate == startDate{
self.other1.text = "Today's Schedule"
}else{
self.formatter.dateFormat = "yyyy-MM-dd"
self.currentDate = self.formatter.string(from: self.date)
self.other1.text = dayNames[self.getDayOfWeek(self.currentDate)!-1] + " Schedule"
}
}, completion: nil
);
Thank you.
You can go old school on this:
take a snapshot of the view;
add it to the view hierarchy;
apply offsetting transforms to the snapshot view and the view, itself;
animate the restoration of the main view's transform back to .identity; and
removing the snapshot view when done with the animation.
Thus:
let snapshotView = snapshot()
view.addSubview(snapshotView)
snapshotView.transform = CGAffineTransform(translationX: -view.bounds.size.width, y: 0)
view.transform = CGAffineTransform(translationX: view.frame.size.width, y: 0)
// do whatever updates to the views you want here
tableView.reloadData()
UIView.animate(withDuration: 0.5, animations: {
self.view.transform = .identity
}, completion: { _ in
snapshotView.removeFromSuperview()
})
For the snapshot, I used:
func snapshot() -> UIView {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let imageView = UIImageView(image: image)
imageView.frame = view.bounds
return imageView
}
By the way, you can also use snapshotView(afterScreenupdates:), which is faster than drawHierarchy, but it doesn't work in 7+ simulator for some reason (see https://forums.developer.apple.com/thread/63438), so I'm not 100% comfortable with it. But if you wanted to do that, you'd do replace the shapshotView declaration with:
let snapshotView = view.snapshotView(afterScreenUpdates: false)!

Resources