In my viewDidLoad function I have a loop creating a 3x6 table of square images (blocks) . The idea is to move and match them eventually.
My question is, how do I assign the current image that is being touched or dragged, as a variable? Here's how I'm adding the image:
let squareImg = UIImageView(image:#imageLiteral(resourceName: "square"))
view.addSubview(squareImg)
And here is what I'm attempting to do:
#IBAction func handlePan(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
if recognizer.state == UIGestureRecognizerState.began {
isDragging = true
let objectDragging = self.view //this is wrong and what I'm trying to correct
print("bg color of block being moved \(objectDragging?.backgroundColor)")
....
Store the images in an array, assign their index to the tag value, screw touchesBagan etc. and simply use gesture recognizers.
Adding images becomes:
var myImages = [UIImage]()
let squareImg = UIImageView(image:#imageLiteral(resourceName: "square"), tag: [//add a distinguished tag here])
view.addSubview(squareImg)
myImages.append(squareImg)
Now that your views are in an array (they don't need to be UIImageViews or even UIImages, just something unique), simply point to the correct thing in your array:
#IBAction func handlePan(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
for view in self.subviews as [UIView] {
if let imageView = view as? UIImageView {
let image = myImages.tag
}
}
}
I'm aware this isn't exactly what you are asking for. But the technique is. I'll adjust my answer based on your feedback.
EDIT:
Based on the comments, it sounds like a good strategy going forward is to at least populate the tag property with "associated pairs in some kind of loop. Additionally, it sounds like some sort of build error is happening. Here's my take on that issue....
Tag properties are available to every object, be they a view or control. Also, they are of Int type. The last comment states the error references an incorrect argument in a function call, expecting an image. I think two separate things are going on.
(1) Populating the tag property for "associated" images in a loop.
If you are loading a "square" and a "circle" n number of times, instantiate things in a loop with an index and populate their tag property.
for index in 0...10 {
// instantiate your images
// associate these images by populating their tag values
imgSquare.tag = index
imgCircle.tag = index
// continue any other set here
}
Loop types in Swift are natively Int, so this is correct.
(2) The incorrect reference build error.
The build error is on line
let circleImg = UIImageView(image: circle, tag: [arrayCounter])
This is not a "canned" initializer for UIImageView, and from the build error it's not (yet) a coded extension to UIImageView. It sounds like there is a coded extension to UIImageView, one that expects two arguments, image and highlightedImage. If you want extend UIImageView for something like this, I'd create an extension with a convenience initializer:
extension UIImageView {
convenience init(image: UIImage, tag:Int) {
self.init()
self.image = image
self.tag = tag
}
}
This is a skeleton. You can add a guard statement and make it a bailable initializer. You can add frames, backgroundColor, whatever. The thing is - you currently do not have an initializer calling for 'image:tag:' but you are coding for one.
just try this
let objectDragging = recognizer.view
You could use the overrides provided by UIImageView.
For Example,
class DragImag: UIImageView {
var draggedView: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?){
//here you get your desired view
draggedView = touches.view
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?){
}
}
Related
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let positionInScene = touch!.location(in: self)
let touchedNode = self.atPoint(positionInScene)
if let name = touchedNode.name {
if name == "leftbutton" {
print("left button stopped")
touchedNode.run(buttonStoppedPressAction)
player?.removeAllActions()
}
if name == "rightbutton" {
print("right button stopped")
touchedNode.run(buttonStoppedPressAction)
player?.removeAllActions()
}
}
}
Here I have code that when the user lifts off their finger from the buttons it stops the action but only if they lift of their finger inside the button. So if they press it and begin to move their finger somewhere else on the screen while continuously pressing down the button will not stop executing its code. Thank you for any help.
Essentially you should check for touch location at touch down and compare to the location at touch up. If the touch is no longer in the area of your button, you cancel all effects.
First, though, a point. It seems like you are handling button logic in the SKScene level, which is what tutorials often tell you to do. However, this may not be the best approach. The risks here, in addition to just a cluttered mess of a SKScene, emerge from handling multiple objects and how they react to touch events, and also additional complexity from multitouch (if allowed).
Years ago when I started with SpriteKit, I felt like this was a huge pain. So I made a button class that handles all the touch logic independently (and sends signals back to the parent when something needs to happen). Benefits: No needless clutter, no trouble distinguishing between objects, the ability to determine multitouch allowances per-node.
What I do in my class to see if the touch hasn't left the button before touch up is that I store the size of the button area (as a parameter of the object) and touch position within it. Simple simple.
In fact, it has baffled me forever that Apple didn't just provide a rudimentary SKButton class by default. Anyhow, I think you might want to think about it. At least for me it saves sooo much time every day. And I've shipped multiple successful apps with the same custom button class.
EDIT: Underneath is my barebones Button class.
import SpriteKit
class Button: SKNode {
private var background: SKSpriteNode?
private var icon: SKNode?
private var tapAction: () -> Void = {}
override init() {
super.init()
isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
}
// MARK: Switches
public func switchButtonBackground(buttonBackgroundSize: CGSize, buttonBackgroundColor: SKColor) {
background = SKSpriteNode(color: buttonBackgroundColor, size: buttonBackgroundSize)
addChild(background!)
}
public func switchButtonIcon(_ buttonIcon: SKNode) {
if icon != nil {
icon = nil
}
icon = buttonIcon
addChild(icon!)
}
public func switchButtonTapAction(_ buttonTapAction: #escaping () -> Void) {
tapAction = buttonTapAction
}
// MARK: Touch events
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
tapAction()
}
}
And then you create the Button object by first initiating it, assigning it a background using a size and color, then assign it an icon, assign it a function to run when tapped and finally add it as a child to the scene.
let icon = SKNode()
let size = CGSize(width: 20.0, height: 20.0)
let button = Button()
button.switchButtonBackground(buttonBackgroundSize: size, buttonBackgroundColor: .clear)
button.switchButtonIcon(icon)
button.switchButtonTapAction(buttonPressed)
addChild(button)
The background defines the touch area for the button, and you can either have a color for it or determine it as .clear. The icon is sort of supposed to hold any text or images you want on top of the button. Just package them into an SKNode and you're good to go. If you want to run a function with a parameter as the tap action, you can just make a code block.
Hope that helps! Let me if you need any further help :).
I have a UITextView embedded in a UITableViewCell.
The text view has scrolling disabled, and grows in height with the text in it.
The text view has a link-like section of text that is attributed with a different color and underlined, and I have a tap gesture recognizer attached to the text view that detects whether the user tapped on the "link" portion of the text or not (This is accomplished using the text view's layoutManager and textContainerInset to detect whether the tap falls within the 'link' or not. It's basically a custom hit test function).
I want the table view cell to receive the tap and become selected when the user "misses" the link portion of the text view, but can't figure out how to do it.
The text view has userInteractionEnabled set to true. However, this does not block the touches from reaching the table view cell when there is no gesture recognizer attached.
Conversely, if I set it to false, for some reason cell selection stops altogether, even when tapping outside of the text view's bounds (but the gesture recognizer still works... WHY?).
What I've Tried
I have tried overriding gestureRecognizer(_ :shouldReceive:), but even when I return false, the table view cell does not get selected...
I have also tried implementing gestureRecognizerShouldBegin(_:), but there too, even if I perform my hit test and return false, the cell does not get the tap.
How can I forward the missed taps back to the cell, to highlight it?
After trying Swapnil Luktuke's answer(to the extent that I understood it, at least) to no avail, and every possible combination of:
Implementing the methods of UIGestureRecognizerDelegate,
Overriding UITapGestureRecognizer,
Conditionally calling ignore(_:for:), etc.
(perhaps in my desperation I missed something obvious, but who knows...)
...I gave up and decided to follow the suggestion by #danyapata in the comments to my question, and subclass UITextView.
Partly based on code found on this Medium post, I came up with this UITextView subclass:
import UIKit
/**
Detects taps on subregions of its attributed text that correspond to custom,
named attributes.
- note: If no tap is detected, the behavior is equivalent to a text view with
`isUserInteractionEnabled` set to `false` (i.e., touches "pass through"). The
same behavior doesn't seem to be easily implemented using just stock
`UITextView` and gesture recognizers (hence the need to subclass).
*/
class LinkTextView: UITextView {
private var tapHandlersByName: [String: [(() -> Void)]] = [:]
/**
Adds a custom block to be executed wjhen a tap is detected on a subregion
of the **attributed** text that contains the attribute named accordingly.
*/
public func addTapHandler(_ handler: #escaping(() -> Void), forAttribute attributeName: String) {
var handlers = tapHandlersByName[attributeName] ?? []
handlers.append(handler)
tapHandlersByName[attributeName] = handlers
}
// MARK: - Initialization
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
commonSetup()
}
private func commonSetup() {
self.delaysContentTouches = false
self.isScrollEnabled = false
self.isEditable = false
self.isUserInteractionEnabled = true
}
// MARK: - UIView
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let attributeName = self.attributeName(at: point), let handlers = tapHandlersByName[attributeName], handlers.count > 0 else {
return nil // Ignore touch
}
return self // Claim touch
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
// find attribute name
guard let touch = touches.first, let attributeName = self.attributeName(at: touch.location(in: self)) else {
return
}
// Execute all handlers for that attribute, once:
tapHandlersByName[attributeName]?.forEach({ (handler) in
handler()
})
}
// MARK: - Internal Support
private func attributeName(at point: CGPoint) -> String? {
let location = CGPoint(
x: point.x - self.textContainerInset.left,
y: point.y - self.textContainerInset.top)
let characterIndex = self.layoutManager.characterIndex(
for: location,
in: self.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
guard characterIndex < self.textStorage.length else {
return nil
}
let firstAttributeName = tapHandlersByName.allKeys.first { (attributeName) -> Bool in
if self.textStorage.attribute(NSAttributedStringKey(rawValue: attributeName), at: characterIndex, effectiveRange: nil) != nil {
return true
}
return false
}
return firstAttributeName
}
}
As ususal, I'll wait a couple of days before accepting my own answer, just in case something better shows up...
Keep all your views active (i.e. user interaction enabled).
Loop through the text view's gestures and disable the ones you do not need.
Loop through the table view's gestureRecognisers array, and make them depend on the text view's custom tap gesture using requireGestureRecognizerToFail.
If its a static table view, you can do this in view did load. For a dynamic table view, do this in 'willDisplayCell' for the text view cell.
Do you know the puzzle game „voi“? That is a game which works with color-XOR-logic. That means: black + black = white.
https://www.youtube.com/watch?v=Aw5BdVcAtII
Is there any way to do the same color logic with two sprite nodes in sprit kit?
Thanks.
Of course, it's possible to do that in Sprite Kit.
Problem:
Let's say you have 2 black squares, squareA and squareB. The user can drag these two squares wherever he wants to. He can drag only one square at a time. You want to color the intersect area to white whenever the two squares intersect.
Initial Setup:
At the top of your scene, there are a few variables that we need to create:
private var squareA: SKSpriteNode?
private var squareB: SKSpriteNode?
private var squares = [SKSpriteNode]()
private var selectedShape: SKSpriteNode?
private var intersectionSquare: SKShapeNode?
squareA and squareB are just the 2 squares that we initially have on screen.
squares is an array and it will store all the squares that are showing on screen.
selectedShape will help us keeping track of the square that is currently being dragged.
intersectionSquare is a white square that represents the intersection area between the two black squares.
Then initialize squareA and squareB, and add them to the squares array like so:
squareA = SKSpriteNode(color: .black, size: CGSize(width: 190.0, height: 190.0))
if let squareA = self.squareA {
squareA.position = CGPoint(x: -200, y: 200)
squareA.name = "Square A"
squares.append(squareA)
self.addChild(squareA)
}
// Do the same for squareB or any other squares that you have on screen..
Note: As you can see, I gave it a name here just to make it easier to differentiate them during the testing phase.
Detect when user is dragging a square:
Now, you need to detect when the user is dragging a square. To do this, you can use:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
These are just helper methods that are going to make our life easier.
Then, you need to setup touchDown, touchMoved and touchUp methods:
func touchDown(atPoint pos : CGPoint) {
let touchedNode = self.nodes(at: pos)
guard let selectedSquare = touchedNode.first as? SKSpriteNode else {
return
}
selectedShape = selectedSquare
}
func touchMoved(toPoint pos : CGPoint) {
guard let selectedSquare = self.selectedShape else {
return
}
selectedSquare.position = pos
checkIntersectionsWith(selectedSquare)
}
func touchUp(atPoint pos : CGPoint) {
selectedShape = nil
}
To explain you in more details what is going on here:
In the touchDown method:
Well, we need the user to be able to drag only one square at a time. Using the nodes(at:) method, it's easy to know which square was touched, and we can know set our selectedShape variable to be equal to the square that was touched.
In the touchMoved method:
Here we are basically just moving the selectedShape to the position the user moves his finger at. We also call the checkIntersectionsWith() method that we will setup in a second.
In the touchUp method:
The user released his finger from the screen, so we can set the selectedShape to nil.
Change the color of the intersection frame:
Now the most important part to make your game actually look like the one you want to make, is how to change the color of the intersection frame to white when two black squares are intersecting ?
Well, you have different possibilities here, and here is one possible way of doing it:
private func checkIntersectionsWith(_ selectedSquare: SKSpriteNode) {
for square in squares {
if selectedSquare != square && square.intersects(selectedSquare) {
let intersectionFrame = square.frame.intersection(selectedSquare.frame)
intersectionSquare?.removeFromParent()
intersectionSquare = nil
intersectionSquare = SKShapeNode(rect: intersectionFrame)
guard let interSquare = self.intersectionSquare else {
return
}
interSquare.fillColor = .white
interSquare.strokeColor = .clear
self.addChild(interSquare)
} else if selectedSquare != square {
intersectionSquare?.removeFromParent()
intersectionSquare = nil
}
}
}
Every time the checkIntersectionsWith() method is called, we are iterating through the nodes that are inside our squares array, and we check, using the frame's intersection() method, if the selected square intersects with any of these (except itself). If it does, then we create a white square, named intersectionSquare, and set its frame to be equal to the intersection frame.
And to save up your memory usage, you can delete the square from the scene and set intersectionSquare to nil if there is no intersection at all.
Final result:
The final result would look like this:
That's just a rapid draft that I made to show you on you could approach the problem, and obviously there are many things that you could add or improve (apply this to a situation where you have not only 2 but many squares on screen, or create a kind of magnetism effect for when your user release his finger from the screen, etc) but I hope at least it will put you on the right track for your project :)
I'm creating a game for ios using swift. I've implemented the component entity system from Apples GameplayKit, very similar to how shown in this tutorial.
I have added a grid of squares which I want to respond differently to tap gestures. I will change a game state using state machine when a UI element is tapped, but I also want to then change how each square reacts to a tap gesture. From my current limited understanding, the best way to do this is change the tap gesture delegate. However, I've not been able to find any simple examples of how this can be done. The sqaures are SKSpriteNodes.
Code is available on request; however, I'm looking for an out of context example of how this could be done in the simplest way.
Does anyone know how this can be done or can suggest a "better" way. To avoid subjectivness, I'm defining better as structured better in terms of architecture. (Multiple if statements in a single gesture handler seems like the wrong way to do this, for example.)
I have found a working solution to my problem based on various bits of help and ideas I've had, however I'm still very much open to alternative solutions.
The main issue this probleme presnted, is how to reference back to the entity from the spritenode.
Through reading the documentation on SKSpriteNode, I discovered that it's parent class SKNode has a userData attribute, which allows you to store data in that node.
Attaching the entity instance to the node needed to happen in the SpriteComponent, as that is where the node is constructed, however it cannot happen in the constructor,so it had to be in a function in the SpriteComponent.
func addToNodeKey() {
self.node.userData = NSMutableDictionary()
self.node.userData?.setObject(self.entity!, forKey: "entity")
}
Then the function is called in the Enity class construction after adding the compoennt to itself.
addComponent(spriteComponent)
spriteComponent.addToNodeKey()
Now the Enity instance can be accessed from the touch event, I needed a way to perform a function on that instance, however you don't know if the instance will be a subclass of GKEntity or not, nor which type.
Therefore, I created another component, TouchableSpriteComponent. After some trying, the init function took a single function as its argument, which is then called from the scenes touch event, which allows the entity which created the TouchableSpriteComponent to define how it handles the event.
I created a gist for this solution, as there's probably too much code to place here. It is a permilink.
I'd really love to know if this is a "good" solution, or if there's a much clearer route which would have the same effect (without looping through all the entities or nodes).
If I understod well, you should have something like:
func entityTouched(touches: Set<UITouch>, withEvent event: UIEvent?)
in your GKEntity subclass.
I do this in my game:
class Square: MainEntity {
override func entityTouched(touches: Set<UITouch>, withEvent event: UIEvent?) {
//DO something
}
}
GameScene:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
let touch = touches.first!
let viewTouchLocation = touch.locationInView(self.view)
let sceneTouchPoint = self.convertPointFromView(viewTouchLocation)
let touchedNode = self.nodeAtPoint(sceneTouchPoint)
let touchedEntity = (touchedNode as? EntityNode)?.entity
if let myVC = touchedEntity as? MainEntity {
myVC.entityTouched(touches, withEvent: event)
}
where:
EntityNode:
class EntityNode: SKSpriteNode {
// MARK: Properties
weak var entity: GKEntity!
}
SpriteComponent:
class SpriteComponent: GKComponent {
let node : EntityNode
init(entity: GKEntity, texture: SKTexture, size: CGSize) {
node = EntityNode(texture: texture, color: SKColor.whiteColor(), size: size)
node.entity = entity
}
}
MainEntity:
class MainEntity: GKEntity {
var entityManager : EntityManager!
var node : SKSpriteNode!
//MARK: INITIALIZE
init(position:CGPoint, entityManager: EntityManager) {
super.init()
self.entityManager = entityManager
let texture = SKTexture(imageNamed: "some image")
let spriteComponent = SpriteComponent(entity: self, texture: texture, size: texture.size())
node = spriteComponent.node
addComponent(spriteComponent)
}
func entityTouched (touches: Set<UITouch>, withEvent event: UIEvent?) {}
}
Let me know if was what you are looking for.
I'm trying to create a subclass of SKLabelNode for a button in SpriteKit. I tried to create a button using SKLabelNode as a parent, so I can use everything I know about labels while creating my buttons (font, text size, text color, position, etc).
I've looked into Swift Spritekit Adding Button Programaticly and I'm using the basis of what that is saying, but rather I'm making a subclass instead of a variable, and I'm creating the button using a label's code. The subclass has the added function that will allow it to be tapped and trigger an action.
class StartScene: SKScene {
class myButton: SKLabelNode {
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if myButton.containsPoint(location) {
// Action code here
}
}
}
}
override func didMoveToView(view: SKView) {
let startGameBtn = myButton(fontNamed: "Copperplate-Light")
startGameBtn.text = "Start Game"
startGameBtn.fontColor = SKColor.blackColor()
startGameBtn.fontSize = 42
startGameBtn.position = CGPointMake(frame.size.width/2, frame.size.height * 0.5)
addChild(startGameBtn)
}
}
My error is in: if myButton.containsPoint(location)
The error reads: Cannot invoke 'containsPoint' with an argument list of type '(CGPoint)'
I know it has to do something with my subclass, but I have no idea what it is specifically.
I also tried putting parentheses around myButton.containsPoint(location) like so:
if (myButton.containsPoint(location))
But then the error reads: 'CGPoint' is not convertible to 'SKNode'
Thanks
I have an alternative solution that would work the same way.
Change the following code
if myButton.containsPoint(location) {
To this and it should compile and have the same functionality
if self.position == location {