I have the below code from a project available at: https://github.com/FlexMonkey/SmoothScribble/tree/master/SmoothScribble
Currently this project only runs on iOS9 as the project uses UIStackView and coalescedTouchesForTouch which are only available for iOS9. However I wish to convert the project to make it available for iOS7 and iOS8 as well. How do I go about doing that?
I think I might have a handle on how I might go about replacing UIStackView with a UIView. However, how do I handle sections that refer to coalescedTouches?
For instance, how do I convert the below code snippet so that it conforms to a traditional touchesMoved that works on iOS8 and iOS9?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
{
guard let
touch = touches.first,
coalescedTouches = event?.coalescedTouchesForTouch(touch),
touchOrigin = touchOrigin
else
{
return
}
coalescedTouches.forEach
{
hermiteScribbleView.appendScribble($0.locationInView(touchOrigin))
simpleScribbleView.appendScribble($0.locationInView(touchOrigin))
}
}
import UIKit
class ViewController: UIViewController
{
let stackView = UIStackView()
let hermiteScribbleView = HermiteScribbleView(title: "Hermite")
let simpleScribbleView = SimpleScribbleView(title: "Simple")
var touchOrigin: ScribbleView?
override func viewDidLoad()
{
super.viewDidLoad()
view.addSubview(stackView)
stackView.addArrangedSubview(hermiteScribbleView)
stackView.addArrangedSubview(simpleScribbleView)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
guard let
location = touches.first?.locationInView(self.view) else
{
return
}
if(hermiteScribbleView.frame.contains(location))
{
touchOrigin = hermiteScribbleView
}
else if (simpleScribbleView.frame.contains(location))
{
touchOrigin = simpleScribbleView
}
else
{
touchOrigin = nil
return
}
if let adjustedLocationInView = touches.first?.locationInView(touchOrigin)
{
hermiteScribbleView.beginScribble(adjustedLocationInView)
simpleScribbleView.beginScribble(adjustedLocationInView)
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
{
guard let
touch = touches.first,
coalescedTouches = event?.coalescedTouchesForTouch(touch),
touchOrigin = touchOrigin
else
{
return
}
coalescedTouches.forEach
{
hermiteScribbleView.appendScribble($0.locationInView(touchOrigin))
simpleScribbleView.appendScribble($0.locationInView(touchOrigin))
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?)
{
hermiteScribbleView.endScribble()
simpleScribbleView.endScribble()
}
override func motionBegan(motion: UIEventSubtype, withEvent event: UIEvent?)
{
if motion == UIEventSubtype.MotionShake
{
hermiteScribbleView.clearScribble()
simpleScribbleView.clearScribble()
}
}
override func viewDidLayoutSubviews()
{
stackView.frame = CGRect(x: 0,
y: topLayoutGuide.length,
width: view.frame.width,
height: view.frame.height - topLayoutGuide.length).insetBy(dx: 10, dy: 10)
stackView.axis = view.frame.width > view.frame.height
? UILayoutConstraintAxis.Horizontal
: UILayoutConstraintAxis.Vertical
stackView.spacing = 10
stackView.distribution = UIStackViewDistribution.FillEqually
}
}
Full project code available at: project available at: https://github.com/FlexMonkey/SmoothScribble/tree/master/SmoothScribble
Yesterday I also had a question on compatibility and it turned out that it could be solved easily by Availability Condition, like this:
if #available(iOS 9.0, *) {
let store = CNContactStore()
} else {
// Fallback on earlier versions
}
You can read this article to know more about it.
http://useyourloaf.com/blog/checking-api-availability-with-swift.html
I do not know if we have the same problem. But, in my view, I could have two snippets of code comforming to iOS 9 and iOS 8 respectively, and use Availability Condition or #available Attribute to resolve compatibility problem.
Related
Update[11/1/2018]:
I try to test the code step by step and found that:
The method touchesEnded was never called for the leftover UIView.
Same for the method touchesCancelled
I am following the Apple Documentation on implementing a multitouch app.
The url is :
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/handling_touches_in_your_view/implementing_a_multitouch_app
I am using the 4 methods:
- touchesBegan(:with:),
- touchesMoved(:with:),
- touchesEnded(:with:),
- touchesCancelled(:with:)
to create a UIView on the touching location when touchesBegan is called. Update the UIView location when touchesMoved is called. And finally, remove the UIView when touchesEnded and touchedCancelled is called.
The problem is when I am tapping really fast on the screen, the UIView doesn't remove from the superview. The code is basically the same as the code provided in the URL with different graphics. I add another view which offset a little bit from the touch location to have a better look on the touch point.
Example of the leftover UIView
Another Image, same code as the comment below
I don't understand what's wrong and why is this happening. I suspect that touchesEnded() was never called but I don't know why. And I would like to ask is there any method to prevent this from happening (no leftover UIView)?
I am using XCode Version 9.2 (9C40b) and iPad Air 2.
I have try the app on different iPad model and same thing happen.
Part of the codes:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
createViewForTouch(touch: touch)
}
}
func createViewForTouch( touch : UITouch ) {
let newView = TouchSpotView() //TouchSportView is a custom UIView
newView.bounds = CGRect(x: 0, y: 0, width: 1, height: 1)
newView.center = touch.location(in: self)
// Add the view and animate it to a new size.
addSubview(newView)
UIView.animate(withDuration: 0.2) {
newView.bounds.size = CGSize(width: 100, height: 100)
}
// Save the views internally
touchViews[touch] = newView //touchViews is a dict, i.e. touchView[UITouch:TouchSportView]
}
func removeViewForTouch (touch : UITouch ) {
if let view = touchViews[touch] {
view.removeFromSuperview()
touchViews.removeValue(forKey: touch)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
For the full code, please see the attached URL for apple documentation.
Many thanks for your generous help!!!!
Tried following code (copy pasted from the link you provided) and it works perfectly both on actual device and simulator:
import UIKit
class ViewController: UIViewController {
override func loadView() {
view = TouchableView()
view.backgroundColor = .white
}
}
class TouchableView: UIView {
var touchViews = [UITouch:TouchSpotView]()
override init(frame: CGRect) {
super.init(frame: frame)
isMultipleTouchEnabled = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isMultipleTouchEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
createViewForTouch(touch: touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let view = viewForTouch(touch: touch)
// Move the view to the new location.
let newLocation = touch.location(in: self)
view?.center = newLocation
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
func createViewForTouch( touch : UITouch ) {
let newView = TouchSpotView()
newView.bounds = CGRect(x: 0, y: 0, width: 1, height: 1)
newView.center = touch.location(in: self)
// Add the view and animate it to a new size.
addSubview(newView)
UIView.animate(withDuration: 0.2) {
newView.bounds.size = CGSize(width: 100, height: 100)
}
// Save the views internally
touchViews[touch] = newView
}
func viewForTouch (touch : UITouch) -> TouchSpotView? {
return touchViews[touch]
}
func removeViewForTouch (touch : UITouch ) {
if let view = touchViews[touch] {
view.removeFromSuperview()
touchViews.removeValue(forKey: touch)
}
}
}
class TouchSpotView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.lightGray
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Update the corner radius when the bounds change.
override var bounds: CGRect {
get { return super.bounds }
set(newBounds) {
super.bounds = newBounds
layer.cornerRadius = newBounds.size.width / 2.0
}
}
}
So, if you are doing something more with it, add that additional code to the question, or if you removed something from the code, let us know. Otherwise, it should work.
EDIT
For completeness I am including a GitHub link to a minimal project, that works for me.
Instead of finding why touchesCancelled and touchesEnded were not called, using event?.allTouches method to check all touch which UITouch is not being handled.
I am trying to implement moving to another scene when a wheel stops rotating. The code I have is shown below. I cannot figure out how to detect when the velocity has reached 0.0?
import SpriteKit
import GameplayKit
class GameplayScene: SKScene {
var player: Player?;
override func didMove(to view: SKView) {
player = self.childNode(withName: "spinner") as! Player?;
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if atPoint(location).name == "play_button" {
spin()
}
}
}
func spin () {
let random = GKRandomDistribution(lowestValue: 20, highestValue: 90)
let r = random.nextInt()
player?.physicsBody = SKPhysicsBody(circleOfRadius: CGFloat(self.frame.width))
player?.physicsBody?.affectedByGravity = false
player?.physicsBody?.isDynamic = true
player?.physicsBody?.allowsRotation = true
player?.physicsBody?.angularVelocity = CGFloat(r)
player?.physicsBody?.angularDamping = 1.0
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
}
So from here, I would like to execute the following when the wheel has stopped spinning:
let play_scene = Questions(fileNamed: "QuestionsScene")
play_scene?.scaleMode = .aspectFill
self.view?.presentScene(play_scene!, transition: SKTransition.doorsOpenVertical(withDuration: 1))
I have now edited the class and it looks as follows:
import SpriteKit
import GameplayKit
class GameplayScene: SKScene, SKSceneDelegate {
var player: Player?
override func didMove(to view: SKView) {
self.delegate = self
player = self.childNode(withName: "spinner") as! Player?;
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if atPoint(location).name == "play_button" {
spin()
}
}
}
func spin () {
let random = GKRandomDistribution(lowestValue: 20, highestValue: 90)
let r = random.nextInt()
player?.physicsBody = SKPhysicsBody(circleOfRadius: CGFloat(self.frame.width))
player?.physicsBody?.affectedByGravity = false
player?.physicsBody?.isDynamic = true
player?.physicsBody?.allowsRotation = true
player?.physicsBody?.pinned = true
player?.physicsBody?.angularVelocity = CGFloat(r)
player?.physicsBody?.angularDamping = 1.0
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func didSimulatePhysics() {
if ((player?.physicsBody?.angularVelocity)! <= CGFloat(0.01))
{
print("Got it")
}
}
}
Problem is I still receive an error on the if statement within didSimulatePhysics function. The error I receive is "Thread 1: EXC_BAD_INSTRUCTION...."
Your wheel's SKPhysicsBody has a built-in property, angularVelocity, that tells you how fast it's spinning. You're already using it when you set it to r to start the wheel spinning.
To watch angularVelocity you can use didSimulatePhysics(). It gets called once every frame, right after the physics calculations are done. That will look something like this:
func didSimulatePhysics() {
if wheelIsSpinning && angularVelocity != nil && angularVelocity! <= CGFloat(0.001) {
wheelIsSpinning = false
// wheel has stopped
// add your code here
}
}
Due to the vagaries of physics modeling, the angularVelocity might never be exactly zero. So instead we watch for it to be less than some arbitrary threshold, in this case 0.001.
You don't want it to execute every frame when the wheel isn't moving, so I added a property wheelIsSpinning to keep track. You'll need to add it as an instance property to GameplayScene, and set it to true in spin().
I have a single UIButton on a storyboard. The UIButton is connected to a Touch Drag Inside action myTouchDragInsideAction. The action is triggered when a user drags the button from inside (UIControlEventTouchDragInside).
The problem is that the action is triggered after 1 pixel of inside dragging. However 1 pixel is too sensitive and can be triggered with just the slightest finger movement.
#IBAction func myTouchDragInsideAction(sender: UIButton) {
print("Button dragged inside")
}
Question:
How can I extend this action to trigger the inside dragging action only after at least 5 pixels of movement?
You have to create custom button for it. Following CustomButton may help you.
let DelayPoint:CGFloat = 5
class CustomButton: UIButton {
var startPoint:CGPoint?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.startPoint == nil {
self.startPoint = touches.first?.previousLocationInView(self)
}
if self.shouldAllowForSendActionForPoint((touches.first?.locationInView(self))!) {
super.touchesMoved(touches, withEvent: event)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.startPoint = nil
super.touchesEnded(touches, withEvent: event)
}
func shouldAllowForSendActionForPoint(location:CGPoint) -> Bool {
if self.startPoint != nil {
let xDiff = (self.startPoint?.x)! - location.x
let yDiff = (self.startPoint?.y)! - location.y
if (xDiff > DelayPoint || xDiff < -DelayPoint || yDiff > DelayPoint || yDiff < -DelayPoint) {
return true
}
}
return false
}
}
You change the "DelayPoint" as per your req.
Hope this will help you.
My implementation of solution for Swift 3
final class DraggedButton: UIButton {
// MARK: - Public Properties
#IBInspectable var sensitivityOfDrag: CGFloat = 5
// MARK: - Private Properties
private var startDragPoint: CGPoint?
// MARK: - UIResponder
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let firstTouch = touches.first else {
return
}
let location = firstTouch.location(in: self)
let previousLocation = firstTouch.previousLocation(in: self)
if startDragPoint == nil {
startDragPoint = previousLocation
}
if shouldAllowForSendActionForPoint(location: location) {
super.touchesMoved(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startDragPoint = nil
super.touchesEnded(touches, with: event)
}
// MARK: - Private methods
private func shouldAllowForSendActionForPoint(location: CGPoint) -> Bool {
guard let startDragPoint = startDragPoint else {
return false
}
let xDifferent = abs(startDragPoint.x - location.x)
let yDifferent = abs(startDragPoint.y - location.y)
return xDifferent > sensitivityOfDrag || yDifferent > sensitivityOfDrag
}
}
Im using Xcode 7, Swift, and SpriteKit and I'm attempting to allow the user to use two fingers at once in my app.
Basically I have two halves to my screen, and I want separate touch-recognition for each side, and simultaneously.
Here is my current code below :
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
{
guard let touch = touches.first else {
return;
}
let location = touch.locationInNode(self)
let touchedNode = self.nodeAtPoint(location)
if (touchedNode == touch1){
//code1
}
else if (touchedNode == touch2){
//code2
}
}
touch1 and touch2 are SkSpriteNodes that each take up a different half of the screen.
This code works well, as long as you only have 1 finger on the screen at a time.
However if there are two(1 for each half), which ever one was placed on the screen first is the one that is registered.
How do I make it so that both are being registered, and therefore code1 and code2 are being run?
You need multipleTouchEnabled property set to true. From the docs about this property :
When set to YES, the receiver receives all touches associated with a
multi-touch sequence. When set to NO, the receiver receives only the
first touch event in a multi-touch sequence. The default value of this
property is NO.
EDIT:
Based on your comments, you might try this (making sprites responsive to touches):
class Button:SKSpriteNode {
init(size:CGSize, color:SKColor) {
super.init(texture: nil, color: color, size: size)
userInteractionEnabled = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let name = self.name {
print("Button with \(name) pressed")
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let name = self.name {
print("Button with \(name) pressed")
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let name = self.name {
print("Button with \(name) released")
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
let left = Button(size: CGSize(width: frame.size.width/2.0, height: frame.size.height), color: .blackColor())
left.name = "left"
left.position = CGPoint(x: left.size.width/2.0, y: frame.midY)
let right = Button(size: CGSize(width: frame.size.width/2.0, height: frame.size.height), color: .whiteColor())
right.name = "right"
right.position = CGPoint(x:frame.maxX-right.size.width/2.0, y: frame.midY)
addChild(left)
addChild(right)
}
}
Does anybody know why this won't work anymore in Xcode 7 GM?
I have debugged and verified, but even though the nodes are appearing on the scene, they no longer pass touch events to parent nodes.
Below is a fully working example. If you replace the default GameScene from the "New Sprite Kit Game" you can reproduce it yourself.
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let card = Card(imageNamed: "card_background_blank")
card.name = "Card"
let stack = Stack(imageNamed: "stack_background")
stack.name = "Stack"
addChild(stack)
stack.addChild(card)
card.position = CGPointMake(100,100)
stack.zPosition = 1
card.zPosition = 1
stack.position = CGPointMake(506,428)
card.userInteractionEnabled = false
stack.userInteractionEnabled = true
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
print("Touch")
moveAllCardsAndReturn()
}
func moveAllCardsAndReturn () {
var wait:NSTimeInterval = 1.0
let waitIncrement:NSTimeInterval = 0.05
let duration:NSTimeInterval = 0.15
func removeAndMove(card:Card) {
let oldParent = card.parent!
card.removeFromParent()
addChild(card)
card.position = CGPointMake(500,48)
var sequence = [SKAction]()
sequence.append(SKAction.waitForDuration(wait))
sequence.append(SKAction.runBlock({
card.removeFromParent()
oldParent.addChild(card)
card.position = CGPointMake(100,100)
}))
card.runAction(SKAction.sequence(sequence))
wait += waitIncrement
}
let stack = childNodeWithName("Stack")
let card = stack?.childNodeWithName("Card")
removeAndMove(card as! Card)
}
}
class Stack:SKSpriteNode {
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
print("Stack Touch")
}
}
class Card:SKSpriteNode {
}
What happens is the scene works correctly, but touches no longer pass to the parent node, even though userInteractionEnabled is false for the card and true for the parent node of the card (a Stack:SKSpriteNode)
So, I have updated this with a working test case. It is a bug, as it works in iOS 8.4 sim, but not in 9 GM. I have submitted it to apple.
This is a bug.
I have created a tester class to test for the presence of the bug and act accordingly...
Can be found here