1

I made drop tile game that tiles fall from the top of screen to one's bottom. This game system is when you touch a tile, the tile will be hidden.

The tiles are custom class (GameTile class), but Touches Began in GameViewController didn't work. How can I solve it?

GameTile.swift

class GameTile: UIImageView {

    init(named: String, frame: CGRect) {
        super.init(frame: frame)
        super.image = (UIImage(named: named))
        super.isUserInteractionEnabled = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class GameTileNormal: GameTile {
    let namedDefault: String
    var frameDefault: CGRect
    let isHiddenDefault: Bool
    var isUserInteractionEnabledDefault: Bool
    let colorName: UIColor

    init(
        named: String,
        frame: CGRect,
        isHidden: Bool = false,
        isUserInteractionEnabled: Bool = true,
        color: UIColor = UIColor.blue) {
        namedDefault = named
        isHiddenDefault = isHidden
        frameDefault = frame
        isUserInteractionEnabledDefault = isUserInteractionEnabled
        colorName = color

        super.init(named: named, frame: frame)
        super.isHidden = isHiddenDefault
        super.isUserInteractionEnabled = isUserInteractionEnabledDefault
        super.backgroundColor = colorName

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

GameView.swift

class GameView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.isUserInteractionEnabled = true

        self.backgroundColor = (UIColor.white)

        self.frame = CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)


            //make tiles
            let tileNormal = GameTileNormal.init(named: "clear",
                                                 frame: CGRect(x:0), y:-60, width:60, height:60),isUserInteractionEnabled: true)
            self.addSubview(tileNormal)

            //move tiles
            moveTile(tile: tileNormal, lane: 1)
    }
}

    func moveTile(tile: GameTile, lane: Int) {

        UIImageView.animate(withDuration: TimeInterval(2.0),
                            delay: 0.0,
                            options: .curveLinear,
                            animations: {
                                tile.frame.origin.y = UIScreen.main.bounds.size.height
        }, completion: {finished in
            tile.removeFromSuperview()

            //make new tile
            self.makeTiles(lane: lane)

        })
    }

GameViewController.swift

class GameViewController: UIViewController {

var gameView: GameView!

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    self.view.isUserInteractionEnabled = true

    gameView = GameView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 568))
    self.view.addSubview(trapView)

}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    let touchEvent = touches.first!

    if let gameView = self.gameView {

        // touchEvent.view is "gameView", not the view whose kind of class is GameTileNormal...
        if let touchedGameTile = touchEvent.view as? GameTileNormal {
            print("Touched normal tile")
            touchEvent.view?.isHidden = true
            touchEvent.view?.isUserInteractionEnabled = false

        }else{
            // other view
        }
    }
}

UPDATE

I changed how to move tiles from UIImageView.animation to Timer. Then If I touched tiles, it didn't through after if (tile.layer.presentation()?.hitTest(location)) != nil { in touchesBegan, GameViewController.....

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touchEvent = touches.first!
        let location = touchEvent.location(in: touchEvent.view)

        if let standardView = self.standardView {

            for tile in standardView.tiles {

             //breakpoint stops here

                if (tile.layer.presentation()?.hitTest(location)) != nil {
                    //breakpoint doesn't through here
                    if tile is GameTileNormal {

                        //normal tile touched

                    }else{

                    }
                    break
                }
            }
        }
    }

moveTiles

makeTileTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateTilesPositionY(timer:)), userInfo: sendArray, repeats: true)

update tile position (drop tiles)

@objc func updateTilesPositionY(timer: Timer){

//tile info
let timerInfo:[Any] = timer.userInfo as! [Any]
let tile:GameTile =  timerInfo[0] as! GameTile
let lane: Int = timerInfo[1] as! Int

//drop tile
tile.frame.origin.y = tile.frame.origin.y+1

//if tile reached on the bottom
if tile.frame.origin.y >= UIScreen.main.bounds.size.height {
    if tile is GameTileNormal {
        self.showGameOverView()
    }
}else{
    //drop tile
}
K.K.D
  • 917
  • 1
  • 12
  • 27

2 Answers2

2

In UIImageView.animate add option .allowUserInteraction:

UIImageView.animate(withDuration: TimeInterval(2.0),
                    delay: 0.0,
                    options: [.curveLinear, .allowUserInteraction],
                    animations: {
                        tile.frame.origin.y = UIScreen.main.bounds.size.height
}, completion: {finished in
    ...

By default the user interaction is disallowed during animations.

UPDATE

However, to test whether the user hit a moving object, you will have a bit harder time. See for example this SO question. Basically, the UIView object does not really move, you can easily test that after firing the animation, the frame of the animated object is set straight to the end position. Just the presentation layer draws the moving view.

You will have to always go over all your moving tiles in the game and test each one if any of them has been touched (here I assume you have a reference to all the tiles in the game):

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    let touchEvent = touches.first!
    let location = touchEvent.location(in: touchEvent.view)

    if let gameView = self.gameView {
        for tile in tiles {
            // go over all the moving objects in your scene and hit test all of them
            if let touchedLayer = tile.layer.presentation()?.hitTest(location) {
                // if a hittest returns a layer, it means that this tile was touched, we can handle it and break out of the loop

                tile.isHidden = true
                tile.isUserInteractionEnabled = false
                tile.removeFromSuperview()
                break
            }
        }
    }
}
Milan Nosáľ
  • 19,169
  • 4
  • 55
  • 90
  • @bao what do you mean it did not work well? what happened? – Milan Nosáľ Mar 28 '18 at 07:55
  • When I touched GameTileNormal, nothing happen. This app through touchedBegan, but didn't through "if let touchedGameTile = touchEvent.view as? GameTileNormal". "touchesEvent.view" is "gameView" in log... – K.K.D Mar 28 '18 at 07:58
  • @bao so the solution works, just the rest of your code does not work as you want it to work.. put there a breakpoint and debug it – Milan Nosáľ Mar 28 '18 at 08:01
  • Originally this code through touchesBegan, but didn't through after if let touchedGameTile = touchEvent.view as? GameTileNormal {}. I'll find this solution but if you find new solution, please let me know. Thanks. – K.K.D Mar 28 '18 at 08:15
  • @bao you have the code that you can run. set a breakpoint and inspect what you get in `touchEvent.view` to see how you should react to it – Milan Nosáľ Mar 28 '18 at 08:16
  • Thank you. I tried to add GameTile and not to add animation, just add subview. Then touchEvent.view was "GameTileNormal"! I think UIImageView.animation has something wrong, but I'm not sure this solution... – K.K.D Mar 28 '18 at 08:39
  • Thank you! It did work well! And how can I completely remove the tile already fall on the bottom from tiles (array)? I could remove tile view by "tile.removeFromSuperview". – K.K.D Mar 28 '18 at 09:08
  • Its **AWFUL** to use other's answers as edit in your current answer @MilanNosáľ – Reinier Melian Mar 28 '18 at 09:10
  • @ReinierMelian I have updated the answer based on SO question that I linked, without looking at your answer here.. the edit was done during you adding your answer, which was incorrect - and I see that you have now updated it - I could also assume that you have updated it based on my correct update. – Milan Nosáľ Mar 28 '18 at 10:29
  • lol @MilanNosáľ did you check that the answer linked in your edit was mine? your edition was 7 minutes after my answer – Reinier Melian Mar 28 '18 at 10:31
  • your original answer was abut the `.allowUserInteraction` option nothing else @MilanNosáľ – Reinier Melian Mar 28 '18 at 10:34
  • @ReinierMelian I know that your answer from that question was yours. I dont know the time difference between you adding the question and my edition - I was focusing on providing a correct solution; it could have been 7 minutes, it could have been even more for all I care. I know what I used as resources, and I have linked the used references. You don't have to believe me, but the fact is that your answer, as you added it at first, was NOT working and was NOT correct, and not surprisingly for me, it now uses my update. Did you used my answer as inspiration? I don't care, cause I won't be petty. – Milan Nosáľ Mar 28 '18 at 10:35
  • @ReinierMelian yes, my original answer was answering the question itself. The update answered the comments OP added to MY answer. But as I said already I don't need to, nor do I have to defend myself from your accusations. In the end only I and God knows if I really just used your answer and in 7 minutes managed to create a correct solution from your incorrect one, and in the end it is only about morality and honesty. I don't have a bad conscience. – Milan Nosáľ Mar 28 '18 at 10:38
  • lol again, I had answered about the way of do it, not exactly answer, in first place, after test the OP code I had updated my answer thats all, but my approach was always right, you had used my answer as "inspiration" I think, we can continue in this forever, but I always refer the name of who I copy or use any answer in my answers, this should be a practice in this community – Reinier Melian Mar 28 '18 at 10:40
  • @bao just add `tile.removeFromSuperview()` directly where you don't need the `tile` anymore. E.g., directly in `touchesBegan`, as I did now in the updated answer – Milan Nosáľ Mar 28 '18 at 10:40
  • @ReinierMelian That's why there is a link to the SO question I used as an inspiration - with yours and Duncans answer as well. If you really think that in 7 minutes from you publishing this answer here I was able to look up your answer there, write the update with the explanation and fix the solution to work in Swift for OP's problem, then I can just say: Thank you for believing in me. – Milan Nosáľ Mar 28 '18 at 10:43
  • @MilanNosáľ by the way if the GameView subviews are userInteractionEnabled = true my approach don't work so you can update your answer now with that, but wait 7 minutes ;) , the downvote is mine BTW – Reinier Melian Mar 28 '18 at 10:54
  • @ReinierMelian thanks. BTW the upvote on https://stackoverflow.com/a/37819789/2912282 is mine ;) – Milan Nosáľ Mar 28 '18 at 11:02
  • Ok @MilanNosáľ seems fair enough for me thanks for that, but anyway I won't delete my answer on this question even with the downvote ;) and your current answer continues wrong because if tiles are `userInteractionEnabled = true` then touchesBegan will be never executed in `GameView` class – Reinier Melian Mar 28 '18 at 11:09
1

Use layer.presentationLayer to run a hitTest if that hitTest return a CALayer then you are touching that titleView, in fact this will only work if your titles are userInteractionEnabled = false

Full Code

import UIKit

class GameTile: UIImageView {

    init(named: String, frame: CGRect) {
        super.init(frame: frame)
        super.image = (UIImage(named: named))
        super.isUserInteractionEnabled = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class GameTileNormal: GameTile {
    let namedDefault: String
    var frameDefault: CGRect
    let isHiddenDefault: Bool
    var isUserInteractionEnabledDefault: Bool
    let colorName: UIColor

    init(
        named: String,
        frame: CGRect,
        isHidden: Bool = false,
        isUserInteractionEnabled: Bool = false,
        color: UIColor = UIColor.blue) {
        namedDefault = named
        isHiddenDefault = isHidden
        frameDefault = frame
        isUserInteractionEnabledDefault = isUserInteractionEnabled
        colorName = color

        super.init(named: named, frame: frame)
        super.isHidden = isHiddenDefault
        super.isUserInteractionEnabled = isUserInteractionEnabledDefault
        super.backgroundColor = colorName

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class GameView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.isUserInteractionEnabled = true

        self.backgroundColor = (UIColor.white)

        self.frame = CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)


        //make tiles
        let tileNormal = GameTileNormal.init(named: "clear",
                                             frame: CGRect(x:0, y:-60, width:60, height:60),isUserInteractionEnabled: false)
        self.addSubview(tileNormal)
        //move tiles
        moveTile(tile: tileNormal, lane: 1)

        self.layer.borderWidth = 1
        self.layer.borderColor = UIColor.blue.cgColor
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func moveTile(tile: GameTile, lane: Int) {

        UIImageView.animate(withDuration: TimeInterval(10),
                            delay: 0.0,
                            options: .curveLinear,
                            animations: {
                                tile.frame.origin.y = UIScreen.main.bounds.size.height
        }, completion: {finished in
            tile.removeFromSuperview()

            //make new tile
            //self.makeTiles(lane: lane)

        })
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touchEvent = touches.first!
        let location = touchEvent.location(in: touchEvent.view)

        for tile in self.subviews {
            // go over all the moving objects in your scene and hit test all of them
            if tile.layer.presentation()?.hitTest(location) != nil {
                // if a hittest returns a layer, it means that this tile was touched, we can handle it and break out of the loop
                tile.isHidden = true
                break
            }
        }
    }

}

class ViewController: UIViewController {

    var gameView: GameView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        self.view.isUserInteractionEnabled = true

        gameView = GameView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 568))
        self.view.addSubview(gameView)

    }

}

enter image description here

Reinier Melian
  • 20,519
  • 3
  • 38
  • 55
  • I would assume this won't work, since `touchEvent.view` would hardly be the touched game tile (unless the touch happens on the final location of the tile) - if that was the case, the original code would have worked – Milan Nosáľ Mar 28 '18 at 09:01
  • I will check with the OP setup, but my answer is about to use hitTest with presentationLayer basically @MilanNosáľ – Reinier Melian Mar 28 '18 at 09:05
  • I see now that you had added basically my answer into you'rs @MilanNosáľ – Reinier Melian Mar 28 '18 at 09:06
  • @bao check my answer – Reinier Melian Mar 28 '18 at 09:35
  • @Reinier Melian Thank you! Now I'm wondering how to stop this animation when I drop many tiles and one of them drop to the bottom but others haven't dropped.... I don't know how to stop UIImageVIew.animation.... – K.K.D Mar 29 '18 at 01:44