I have what is becoming a fairly complex SpriteKit based app, all made programmatically.
I am moving a number of arrays of SpriteNodes around and have two possible methods to achieve this. Using either Touches Moved function or I can use the UIPanGestureRecognizer to do the same thing in pretty much the same way.
But which is the best way to go? I also have lots of other UI gestures going on like Rotate,Pinch and double tap and I'm worried about conflicting code.
Is it bad practice to combine them ? (I don't mean both methods at the same time - I mean having some touches read by Gestures and others with Touches functions)
Do I have more control or precision with Pan over Touches?
I feel, now that I have it mostly working better to only use Gestures if possible
but what is the best practice?
Some of my code. I have up to maybe 40-50 nodes stored in arrays being moved around.
In touches moved:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let location = touch.location(in: self)
if numberOfBalls >= 0 {
for i in 0...(numberOfBalls) {
if ballBloqArray[i].contains(location) {
ballBloqArray[i].position = location
}
}
}
if numberOfBoxes >= 0 {
for i in 0...(numberOfBoxes) {
if boxBloqArray[i].contains(location) {
boxBloqArray[i].physicsBody?.pinned = false
boxBloqArray[i].position = location
boxBeingMoved = i
}
}
}
}
}
and using PanGesture:
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(GameScene.tappedOnce(sender:)))
view.addGestureRecognizer(panGesture)
panGesture.minimumNumberOfTouches = 1
panGesture.cancelsTouchesInView = true
#objc func tappedOnce(sender:UIPanGestureRecognizer) {
if sender.state == .began{
}
if sender.state == .changed {
var location = sender.location(in: self.view)
location = self.convertPoint(fromView: location)
print("Single tap")
print(location)
if numberOfBalls >= 0 {
for i in 0...(numberOfBalls) {
if ballBloqArray[i].contains(location) {
ballBloqArray[i].position = location
}
}
}
}
}
There is no best, they both have strengths and weaknesses.
The important thing is to stay consistent. Do not combine the two. Pick one method or the other.
Personally, I would go with the Gestures. It allows you to separate the code, and gives you a very clear definition of what is being performed when a touch is moved.
Related
I have a project where I’m adding three UILabels to the view controller’s view. When the user begins moving their finger around the screen, I want to be able to determine when they their finger is moving over any of these UILabels.
I’m assuming a UIPanGestureRecognizer is what I need (for when the user is moving their finger around the screen) but I’m not sure where to add the gesture. (I can add a tap gesture to a UILabel, but this isn’t what I need)
Assuming I add the UIPanGestureRecognizer to the main view, how would I go about accomplishing this?
if gesture.state == .changed {
// if finger moving over UILabelA…
// …do this
// else if finger moving over UILabelB…
// …do something else
}
You can do this with either a UIPanGestureRecognizer or by implementing touchesMoved(...) - which to use depends on what else you might be doing.
For pan gesture, add the recognizer to the view (NOT to the labels):
#objc func handlePan(_ g: UIPanGestureRecognizer) {
if g.state == .changed {
// get the location of the gesture
let loc = g.location(in: view)
// loop through each label to see if its frame contains the gesture point
theLabels.forEach { v in
if v.frame.contains(loc) {
print("Pan Gesture - we're panning over label:", v.text)
}
}
}
}
For using touches, no need to add a gesture recognizer:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let t = touches.first {
// get the location of the touch
let loc = t.location(in: view)
// loop through each label to see if its frame contains the touch point
theLabels.forEach { v in
if v.frame.contains(loc) {
print("Touch - we're dragging the touch over label:", v.text)
}
}
}
}
I have got an UITableView with a custom TableViewCell. I use a pan gesture for recognizing the positions while moving my finger to the left and to the right. On basis of the finger position I change some values in the labels in this TableViewCell. This works really great. But suddenly I can not scroll the TableView up and down. I already read the reason. Swift can not work with two gesture recognizers at the same time. And I found many examples of people how have nearly the same problem. I tried many of them but I can not fix my problem. I use Swift 5. Could you please describe a bit more precise how to fix my problem? Many thanks
import UIKit
class TVCLebensmittel: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
self.addGestureRecognizer(gestureRecognizer)
}
#IBAction func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began {
let translation = gestureRecognizer.translation(in: self)
// Put my finger on the screen
} else if gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self)
// While moving my finger ...
} else if gestureRecognizer.state == .ended {
let translation = gestureRecognizer.translation(in: self)
// Lift finger
}
}
...
}
The solution is to insert the pan gesture to the tableview and not to the tableviewcell. So I can listen to the left and right pan and also the up and down movement of the tableview.
I just share my approach. link It works very well. I needed custom swipe design while perform delete. Try it. If you need more information feel free to ask. Check it if you like.
This gestureRecognizerShouldBegin let you to use scroll while using UIPanGestureRecognizer.
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer.isKind(of: UIPanGestureRecognizer.self)) {
let t = (gestureRecognizer as! UIPanGestureRecognizer).translation(in: contentView)
let verticalness = abs(t.y)
if (verticalness > 0) {
print("ignore vertical motion in the pan ...")
print("the event engine will >pass on the gesture< to the scroll view")
return false
}
}
return true
}
I am creating an app so that user has to Swipe all the boxes from the screen. The goal is to swipe all the boxes until all boxes are swiped like example below.
So my question is:
Is it better to create the boxes using Stack View or rather draw manually by coordinates on the screen?
How to detect if user has swiped through the boxes (using UIGestureRecognizer)?
Note: When user swiped through the boxes, swiped boxes will turn into other color.
Both stack view or manually should work nicely. I would go with manually in this case but this is just a preference because you might have more power over it. But there is a downside that you need to reposition them when screen size changes. A third option is also a collection view.
The gesture recognizer should be pretty straight forward. You just add it on the superview of these cells and check the location when it moves or and when it starts. A pan gesture seems the most appropriate but it will not detect if user just taps the screen. This may be a feature but if you want to handle all touches you should either use a long press gesture with zero press duration (It makes little sense, I know but it works), or you may simply just override touch methods:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
handleDrag(at: touch.location(in: viewWhereAllMiniViewsAre))
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
handleDrag(at: touch.location(in: viewWhereAllMiniViewsAre))
}
}
func handleDrag(at location: CGPoint) {
// TODO: handle the nodes
}
The gesture recognizer procedure would do something like:
func onDrag(_ sender: UIGestureRecognizer) {
switch sender.state {
case .began, .changed, .ended, .cancelled: handleDrag(at: sender.location(in: viewWhereAllMiniViewsAre))
case .possible, .failed: break
}
}
Now all you need is your data source. An array of all of your items should be enough. Like:
static let rows: Int = 10
static let columns: Int = 10
var nodes: [Node] = {
return Array<Node>(repeating: Node(), count: LoginViewController.rows * LoginViewController.columns)
}()
And a list of all of your mini views:
var nodeViews: [UIView] = { ... position them or get them from stack view or from collection view }
Now the implementation on touch handle:
func handleDrag(at location: CGPoint) {
nodeViews.enumerated().forEach { index, view in
if view.frame.contains(location) {
view.backgroundColor = UIColor.green
nodes[index].selected = true
}
}
}
This is just an example. An easy one and rather a bad one from maintenance perspective at least. In general I would rather have a node view of custom UIView subclass with a reference to a node. Also it should hook using delegate to a Node instance so that the node reports when the selection state changes.
This way you have much cleaner solution when handling touches:
func handleDrag(at location: CGPoint) {
nodeViews.first(where: { $0.frame.contains(location) }).node.selected = true
}
Checking if all are green is then just
var allGreen: Bool {
return !nodes.contains(where: { $0.selected == false })
}
I am currently working at a sidescroller game, in which the jump height depends on how long the player presses the right half of the screen.
Everything works just fine, except if the user touches the screen quickly. This causes the jump to be as big as possible.
Am I doing something wrong or is this just a problem with the way SpriteKit works?
How can I solve this problem?
EDIT: Here are all the methods handling touches in my game:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
for touch in touches
{
swiped = false
let location = touch.location(in: cameraNode)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.065)
{
if self.swiped == false
{
if location.x < 0
{
self.changeColor()
}
else
{
self.jump()
}
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
for touch in touches {
let location = touch.location(in: cameraNode)
if location.x > 0
{
// Right
thePlayer.endJump()
}
}
}
And then there is also a gesture recognizer handling swiping left and right, with the following handlers:
#objc func swipedRight()
{
if walkstate != .walkingRight
{
walkstate = .walkingRight
}
else
{
boost(direction: 0)
}
swiped = true
}
#objc func swipedLeft()
{
if walkstate != .walkingLeft
{
walkstate = .walkingLeft
}
else
{
boost(direction: 1)
}
swiped = true
}
Hopefully this is enough to describe the problems. The code above is everything I am doing to handle touches.
Well, the problem was, that I am using a DispatchQueue command to call the jumping method after a short delay, in case the user swiped and not tapped. As a result the touchesEnded method gets called before the jump has even started and thus can not be stopped anymore.
To solve this problem I added a boolean variable which is set to true as soon as the player touches the screen and set to false whenever the users finger leaves the screen. In order to jump this variable has to be set to true and thereby the character will not jump after a quick touch anymore.
I am currently working on an arcade app where the user taps for the sprite to jump over an obstacle and swipes down for it to slide under an obstacle. My problem is that when I begin a swipe the touchesBegan function is called so the sprite jumps instead of sliding. Is there a way to distinguish these two?
You can use a gestures state to fine tune user interaction. Gestures are coordinated, so shouldn't interfere with each other.
func handlePanFrom(recognizer: UIPanGestureRecognizer) {
if recognizer.state != .changed {
return
}
// Handle pan here
}
func handleTapFrom(recognizer: UITapGestureRecognizer) {
if recognizer.state != .ended {
return
}
// Handle tap here
}
How about using a slight delay for your touch controls? I have a game where I do something similar using a SKAction with delay. Optionally you can set a location property to give your self a bit of wiggle room with the touchesMoved method incase someone has a twitchy finger (thanks KnightOfDragon)
let jumpDelayKey = "JumpDelayKey"
var startingTouchLocation: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
// starting touch location
startingTouchLocation = location
// start jump delay
let action1 = SKAction.wait(forDuration: 0.05)
let action2 = SKAction.run(jump)
let sequence = SKAction.sequence([action1, action2])
run(sequence, withKey: jumpDelayKey)
}
}
func jump() {
// your jumping code
}
Just make sure the delay is not too long so that your controls dont feel unresponsive. Play around with the value for your desired result.
Than in your touches moved method you remove the SKAction if your move threshold has been reached
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
guard let startingTouchLocation = startingTouchLocation else { return }
// adjust this value of how much you want to move before continuing
let adjuster: CGFloat = 30
guard location.y < (startingTouchLocation.y - adjuster) ||
location.y > (startingTouchLocation.y + adjuster) ||
location.x < (startingTouchLocation.x - adjuster) ||
location.x > (startingTouchLocation.x + adjuster) else {
return }
// Remove jump action
removeAction(forKey: jumpDelayKey)
// your sliding code
}
}
You could play around with Gesture recognisers although I am not sure that will work and how it affects the responder chain.
Hope this helps