Can a UIView assign a UITapGestureRecognizer to itself? - ios

I'm trying to make a UIView that recognizes tap gestures, however taps are not ever correctly registered by the UIView. Here's the code for the UIView subclass itself:
import UIKit
class ActionCell: SignalTableCell, UIGestureRecognizerDelegate {
var icon: UIImageView!
var actionType: UILabel!
var actionTitle: UILabel!
var a:Action?
var tap:UITapGestureRecognizer?
required init(frame: CGRect) {
//
super.init(frame:frame)
tap = UITapGestureRecognizer(target: self, action: #selector(self.touchTapped(_:)))
tap?.delegate = self
addGestureRecognizer(tap!)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
tap = UITapGestureRecognizer(target: self, action: #selector(self.touchTapped(_:)))
tap?.delegate = self
addGestureRecognizer(tap!)
}
#objc func touchTapped(_ sender: UITapGestureRecognizer) {
print("OK")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.isUserInteractionEnabled = true
}
override func layoutSubviews() {
if(icon == nil) {
let rect = CGRect(origin: CGPoint(x: 10,y :20), size: CGSize(width: 64, height: 64))
icon = UIImageView(frame: rect)
addSubview(icon)
}
icon.image = UIImage(named:(a?.icon)!)
if(actionType == nil) {
let rect = CGRect(origin: CGPoint(x: 100,y :20), size: CGSize(width: 200, height: 16))
actionType = UILabel(frame: rect)
addSubview(actionType)
}
actionType.text = a.type
if(actionTitle == nil) {
let rect = CGRect(origin: CGPoint(x: 100,y :80), size: CGSize(width: 200, height: 16))
actionTitle = UILabel(frame: rect)
addSubview(actionTitle)
}
actionTitle.text = a?.title
}
func configure( a:Action ) {
self.a = a
}
override func setData( type:SignalData ) {
a = (type as! Action)
}
}
I'm simply trying to make it so that this UIView can, you know, know when it's tapped. Is this possible without adding a separate UIViewController? This seems as though it should be fairly simple but it doesn't appear to be, confusingly.
I've stepped through the code and the init method is called and the Gesture Recognizer is added but it doesn't trigger.

If its a table view cell, I'd recommend not using a tap gesture, since it might interfere with the didSelectRowAtIndexPath: and other delegate methods. But if you still wanna keep the tap gesture, try adding tap?.cancelsTouchesInView = false before addGestureRecognizer(tap!) and see if that works.
If you just want to know when its tapped, you could also override the following UIResponder method:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
// do your stuff
}

I think, it is easier to override touchesBegan method. Something like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touched")
super.touchesBegan(touches, with: event)
}

Most likely the actionType, ActionTitle, and icon are being tapped and the tap is not falling through because user interaction is disabled by default for labels and images. Set isUserInteractionEnabled = true for each of those fields that are subviews of the main view.
override func layoutSubviews() {
if(icon == nil) {
let rect = CGRect(origin: CGPoint(x: 10,y :20), size: CGSize(width: 64, height: 64))
icon = UIImageView(frame: rect)
icon.isUserInteractionEnabled = true
addSubview(icon)
}
icon.image = UIImage(named:(a?.icon)!)
if(actionType == nil) {
let rect = CGRect(origin: CGPoint(x: 100,y :20), size: CGSize(width: 200, height: 16))
actionType = UILabel(frame: rect)
actionType.isUserInteractionEnabled = true
addSubview(actionType)
}
actionType.text = a.type
if(actionTitle == nil) {
let rect = CGRect(origin: CGPoint(x: 100,y :80), size: CGSize(width: 200, height: 16))
actionTitle = UILabel(frame: rect)
actionTitle.isUserInteractionEnabled = true
addSubview(actionTitle)
}
actionTitle.text = a?.title
}

Related

Swift hitTest:point withEvent to detect subview

I'm trying to detect when subView is tap using hitTest. Here is my implementation:
class MyViews:UIView {
override init (frame : CGRect) {
super.init(frame : frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
let subPoint = subview.convert(point, from:self);
if let result = subview.hitTest(subPoint, with:event) {
return result;
}
}
return nil
}
}
Here is how I'm adding the subViews:
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UITapGestureRecognizer(target: self, action: #selector(doSomethingHere(_:)))
let blueView = MyViews(frame: CGRect(origin: CGPoint(x: 38.0, y: 231.0), size: CGSize(width: 335.0, height: 444.0)))
blueView.tag = 100
blueView.isUserInteractionEnabled = true
blueView.backgroundColor = .blue
blueView.addGestureRecognizer(gesture)
view.addSubview(blueView)
let grey = MyViews(frame: CGRect(origin: CGPoint(x: 38.0, y: 20.0 ), size: CGSize(width: 279, height: 203.0)))
grey.tag = 200
grey.backgroundColor = .gray
grey.isUserInteractionEnabled = true
grey.addGestureRecognizer(gesture)
blueView.addSubview(grey)
let white = MyViews(frame: CGRect(origin: CGPoint(x: 56.0 , y: 17.0 ), size: CGSize(width: 182.0, height: 92)))
white.tag = 300
white.isUserInteractionEnabled = true
white.addGestureRecognizer(gesture)
grey.addSubview(white)
let result = blueView.hitTest(CGPoint(x: 157.5, y: 149.0), with: nil)
print(result ?? "no result")
}
I'm simulating a touch on this line let result = blueView.hitTest(CGPoint(x: 157.5, y: 149.0), with: nil)
But the response from this function:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
let subPoint = subview.convert(point, from:self);
if let result = subview.hitTest(subPoint, with:event) {
return result;
}
}
return nil
}
It always is nil. I don't understand why it should be returning the white view.
Any of you knows why this function is not returning the correct view?
I'll really appreciate your help.
It looks like you hitTest is never checking to see if the point is in the view. Give this a whirl:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if there are subviews, step through them
// if the subview contains the point, call hitTest on the subview to see if the subview's subviews contain the point
// if they do, return the result,
// if they don't, return the subview that does contain the point.
for subview in self.subviews.reversed() {
let subPoint = subview.convert(point, from:self)
if subview.point(inside: subPoint, with: event) == true {
if let result = subview.hitTest(subPoint, with: event) {
return result
} else {
return subview
}
}
}
// none of the subviews contain the point,(or there are no subviews) does the current view contain the point?
// if yes, return self. otherwise, return nil
if self.point(inside: point, with: event) == true {
return self
} else {
return nil
}
}

Can't touch UIImageView that is moving in Swift

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
}
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
}
}
}
}
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)
}
}

Zoom on UIView contained in UIScrollView

I have some trouble handling zoom on UIScrollView that contains many subviews. I already saw many answers on SO, but none of them helped me.
So, basically what I'm doing is simple : I have a class called UIFreeView :
import UIKit
class UIFreeView: UIView, UIScrollViewDelegate {
var mainUIView: UIView!
var scrollView: UIScrollView!
var arrayOfView: [UIView]?
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setupView() {
// MainUiView
self.mainUIView = UIView(frame: self.frame)
self.addSubview(mainUIView)
// ScrollView
self.scrollView = UIScrollView(frame: self.frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
func reloadViews(postArray:[Post]?) {
if let postArray = postArray {
print("UIFreeView::reloadVIew.postArraySize = \(postArray.count)")
let size: CGFloat = 80.0
let margin: CGFloat = 20.0
scrollView.contentSize.width = (size * CGFloat(postArray.count))+(margin*CGFloat(postArray.count))
scrollView.contentSize.height = (size * CGFloat(postArray.count))+(margin*CGFloat(postArray.count))
for item in postArray {
let view = buildPostView(item)
self.scrollView.addSubview(view)
}
}
}
fileprivate func buildPostView(_ item:Post) -> UIView {
// Const
let size: CGFloat = 80.0
let margin: CGFloat = 5.0
// Var
let view = UIView()
let textView = UITextView()
let backgroundImageView = UIImageView()
// Setup view
let x = CGFloat(UInt64.random(lower: UInt64(0), upper: UInt64(self.scrollView.contentSize.width)))
let y = CGFloat(UInt64.random(lower: UInt64(0), upper: UInt64(self.scrollView.contentSize.height)))
view.frame = CGRect(x: x,
y: y,
width: size,
height: size)
// Setup background view
backgroundImageView.frame = CGRect(x: 0,
y: 0,
width: view.frame.size.width,
height: view.frame.size.height)
var bgName = ""
if (item.isFromCurrentUser) {
bgName = "post-it-orange"
} else {
bgName = "post-it-white"
}
backgroundImageView.contentMode = .scaleAspectFit
backgroundImageView.image = UIImage(named: bgName)
view.addSubview(backgroundImageView)
// Setup text view
textView.frame = CGRect(x: margin,
y: margin,
width: view.frame.size.width - margin*2,
height: view.frame.size.height - margin*2)
textView.backgroundColor = UIColor.clear
textView.text = item.content
textView.isEditable = false
textView.isSelectable = false
textView.isUserInteractionEnabled = false
view.addSubview(textView)
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
view.addGestureRecognizer(gestureRecognizer)
return view
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.scrollView
}
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.scrollView)
// note: 'view' is optional and need to be unwrapped
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.scrollView)
}
}
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
}
This class is working perfectly, I can scroll through all my views, but I can't zoom-in and zoom-out in order to see less/more views on my screen.
I think the solution is simple, but I can't seem to find a real solution for my problem ..
Thanks !

SpriteKit device orientation change causes SKLabelNode distortion Swift

I am using SpriteKit to write a game for iOS in Swift. I am still fairly new to SpriteKit.
I would like to support both orientations for both iPhone and iPad, and have found this:
multiple orientations in SpriteKit
This works as expected in the simulator and device, however on device I notice some SKSpriteNodes distort to their new size slightly before the device rotation animation.
This is very noticeable especially with SKLabelNodes where the text distorts either slightly squashed or stretched depending on the orientation change.
I have an idea why the distortion is happening, but confirmation and a fix would be fantastic.
This occurs on device with the code described in the link, but I have updated for swift 3
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size:self.view.bounds.size)
scene.scaleMode = .resizeFill
(self.view as! SKView).presentScene(scene)
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override var prefersStatusBarHidden: Bool {
return true
}
}
class GameScene: SKScene {
var currentNode: CustomNode!
override func didMove(to view: SKView) {
self.backgroundColor = SKColor.white
transitionToScene(sceneType: .Menu)
}
override func didChangeSize(_ oldSize: CGSize) {
currentNode?.layout()
}
func transitionToScene(sceneType: SceneTransition) {
switch sceneType {
case .Menu:
currentNode?.dismissWithAnimation(animation: .Right)
currentNode = MenuNode(gameScene: self)
currentNode.presentWithAnimation(animation: .Right)
case .Scores:
currentNode?.dismissWithAnimation(animation: .Left)
currentNode = ScoresNode(gameScene: self)
currentNode.presentWithAnimation(animation: .Left)
default: fatalError("Unknown scene transition.")
}
}
}
class CustomNode: SKNode {
weak var gameScene: GameScene!
init(gameScene: GameScene) {
self.gameScene = gameScene
super.init()
}
func layout() {}
func presentWithAnimation(animation:Animation) {
layout()
let invert: CGFloat = animation == .Left ? 1 : -1
self.position = CGPoint(x: invert*gameScene.size.width, y: 0)
gameScene.addChild(self)
let action = SKAction.move(to: CGPoint(x: 0, y: 0), duration: 0.3)
action.timingMode = SKActionTimingMode.easeInEaseOut
self.run(action)
}
func dismissWithAnimation(animation:Animation) {
let invert: CGFloat = animation == .Left ? 1 : -1
self.position = CGPoint(x: 0, y: 0)
let action = SKAction.move(to: CGPoint(x: invert*(-gameScene.size.width), y: 0), duration: 0.3)
action.timingMode = SKActionTimingMode.easeInEaseOut
self.run(action, completion: {self.removeFromParent()})
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MenuNode: CustomNode {
var label: SKLabelNode
var container: SKSpriteNode
override func layout() {
container.position = CGPoint(x: gameScene.size.width/2.0, y: gameScene.size.height/2.0)
}
override init(gameScene: GameScene) {
label = SKLabelNode(text: "Menu Scene")
label.horizontalAlignmentMode = .center
label.verticalAlignmentMode = .center
container = SKSpriteNode(color: UIColor.black, size: CGSize(width: 200, height: 200))
container.addChild(label)
super.init(gameScene: gameScene)
self.addChild(container)
self.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.gameScene.transitionToScene(sceneType: .Scores)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ScoresNode: CustomNode {
var label: SKLabelNode
var container: SKSpriteNode
override func layout() {
container.position = CGPoint(x: gameScene.size.width/2.0, y: gameScene.size.height/2.0)
}
override init(gameScene: GameScene) {
label = SKLabelNode(text: "Scores Scene")
label.horizontalAlignmentMode = .center
label.verticalAlignmentMode = .center
container = SKSpriteNode(color: UIColor.black, size: CGSize(width: 200, height: 200))
container.addChild(label)
super.init(gameScene: gameScene)
self.addChild(container)
self.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.gameScene.transitionToScene(sceneType: .Menu)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
enum SceneTransition{
case Menu, Scores
}
enum Animation {
case Left, Right, None
}
credit: Epic Byte
I have also tried using
viewWillTransitionToSize...
as this handles device orientation changes, I see that didChangeSize... is called multiple times in a device rotation therefore I prefer the viewWillTransitionToSize...
Thanks in advance.
Leon
Go into your Storyboard file, Select your ViewController's view and on the right hand bar, look for the slider image, it should be the 4th one from the left. This is called the attributes inspector. Change Content Mode to Center. This will give you black bars but stop the squishing.
This was the code created from the suggestion by #Knight0fDragon, producing SKLabelNodes that do not squash or stretch when rotated with scaleMode .resizeFill. Note the size of the view in this is for an iPad with 1024x768 resolution, creating a square skview of size 1024x1024. This would be easy to replicate with the largest value either width or height when the app loads depending on the orientation.
class GameViewController: UIViewController {
var scene : GameScene!
var newView: SKView!
override func viewDidLoad() {
super.viewDidLoad()
self.newView = SKView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 1024, height: 1024)))
(self.view as? SKView)?.contentMode = .center
self.newView.contentMode = .center
self.newView.autoresizesSubviews = false
self.newView.autoresizingMask = [.flexibleBottomMargin, .flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin]
(self.view as? SKView)?.addSubview(newView)
self.newView.center = view.center
self.scene = GameScene(size: CGSize(width: 1024, height: 1024))
self.scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.scene.scaleMode = .resizeFill
self.newView.presentScene(scene)
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override var prefersStatusBarHidden: Bool {
return true
}
}
class GameScene: SKScene {
var labelNode: SKLabelNode!
var container: SKSpriteNode!
override func didMove(to view: SKView) {
self.backgroundColor = SKColor.darkGray
container = SKSpriteNode(color: .black, size: CGSize(width: 300, height: 300))
self.addChild(container)
labelNode = SKLabelNode(text: "hello")
labelNode.fontColor = .white
container.addChild(labelNode)
}
}

How to recognise which image was touched

I am developing an application which the user will be able to drag and drop items on a canvas and when he releases the image it is drawn on the canvas.
This is my DragImage class which handle the touches:
class DragImages: UIImageView {
var originalPos : CGPoint!
var dropTarget: UIView?
override init (frame : CGRect){
super.init(frame: frame)
}
required init?(coder aDecoder : NSCoder){
super.init(coder : aDecoder)
}
override func touchesBegan(_ touches : Set<UITouch>,with event: UIEvent?){
originalPos = self.center
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let position = touch.location(in: self.superview)
self.center = CGPoint(x : position.x, y : position.y)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let target = dropTarget{
let position = touch.location(in: self.superview)
if target.frame.contains(position){
NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: "onTargetDropped"), object: nil))
}else {
self.center = originalPos
}
}
print(self.center.x, self.center.y)
self.center = originalPos
}
func getEndPosX() -> CGFloat{
return self.center.x
}
func getEndPosY() -> CGFloat {
return self.center.y
}
}
In my ViewController class I added this piece of code to handle the touches etc:
ornament1.dropTarget = xmasTree
ornament2.dropTarget = xmasTree
ornament3.dropTarget = xmasTree
ornament4.dropTarget = xmasTree
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.itemDroppedOnTree(_:)), name: NSNotification.Name(rawValue: "onTargetDropped"), object: nil)
}
func itemDroppedOnTree(_ notif : AnyObject){
}
I managed to get the X and Y position when the image is dragged on the canvas but i cant find a way to recognise which of the 4 images is being dropped in order for me to draw that specific one!
You could add the sender to your notification (and also the position):
NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: "onTargetDropped"), object: self, userInfo: ["position":position]))
and get it later in itemDroppedOnTree:
func itemDroppedOnTree(_ notif : NSNotification){
let position = notif.userInfo["position"]
let sender = notif.object as! DragImage
if sender === dragImage1 {
//...
} else if sender === dragImage2 {
//...
}
}
I recommend against it though and plead to use a delegate to inform the ViewController instead. (Opinion based: In general, use Notifications for to-many broadcasts only.)
The delegate function should have the sender as first parameter. According to func tableView: tableView:UITableView, cellForRowAt indexPath:IndexPath).
This way you know which image is sending its new position and can compare it to your property like in the above example:
if dragImage === dragImage1 {...
Your code plus working delegate to paste to Playground:
import UIKit
import PlaygroundSupport
protocol DragImageDelegate: class {
func dragimage(_ dragImage:DragImage, didDropAt position:CGPoint)
}
class DragImage: UIImageView {
weak var delegate: DragImageDelegate?
var originalPos : CGPoint!
var dropTarget: UIView?
override init (frame : CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches : Set<UITouch>,with event: UIEvent?){
originalPos = self.center
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let position = touch.location(in: self.superview)
self.center = CGPoint(x : position.x, y : position.y)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let target = dropTarget {
let position = touch.location(in: self.superview)
if target.frame.contains(position){
print(self.center.x, self.center.y)
guard let delegate = self.delegate else {
print("delegate not set")
return
}
print(self.center.x, self.center.y)
delegate.dragimage(self, didDropAt: position)
return
}
}
self.center = originalPos
}
}
class MyVC: UIViewController, DragImageDelegate {
let dragImage1 = DragImage(frame: CGRect(x: 0.0, y: 0.0, width: 30.0, height: 30.0))
let dragImage2 = DragImage(frame: CGRect(x: 0.0, y: 100.0, width: 30.0, height: 30.0))
override func viewDidLoad() {
let target = UIView(frame: CGRect(x: 200.0, y: 400.0, width: 30.0, height: 30.0))
target.backgroundColor = .black
view.addSubview(target)
dragImage1.backgroundColor = .white
dragImage2.backgroundColor = .white
dragImage1.dropTarget = target
dragImage2.dropTarget = target
view.addSubview(dragImage1)
view.addSubview(dragImage2)
dragImage1.delegate = self
dragImage2.delegate = self
}
private func move(_ view:UIView, to position:CGPoint) {
view.frame = CGRect(x: position.x, y: position.y, width: view.frame.size.width, height: view.frame.size.height)
}
// MARK: - DragImageDelegate
func dragimage(_ dragImage: DragImage, didDropAt position: CGPoint) {
if dragImage === dragImage1 {
move(dragImage1, to: position)
} else if dragImage === dragImage2 {
move(dragImage2, to: position)
}
}
}
var container = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 600.0))
let myVc = MyVC()
myVc.view.frame = CGRect(x: 0.0, y: 0.0, width: 300.0, height: 600.0)
myVc.view.backgroundColor = .green
container.addSubview(myVc.view)
PlaygroundPage.current.liveView = container
Result:

Resources