How can I end a touch/gesture with a long hold? - swift

I'd like to move a simple, small UIView around the screen and 'drop' it off at any location on the screen, but accurately. Simply lifting your finger from the screen does not have the desired effect as there is always some movement upon lifting your finger, resulting in the object not being in the required location.
What I'm looking for is some way to count down a specified number of milliseconds AFTER holding/pausing at the desired location and then have some mechanism ENDING my touches/gesture so the object is placed EXACTLY where I want it.
I've been reasonably successful with touchesBegan/Moved/Ended, but even though I called the touchesEnded method the touches never really end as I can still drag around the object on the screen and relocate it - not what I want.
import UIKit
class ViewController: UIViewController {
let greenDot : UIView = {
let greenDot = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
greenDot.backgroundColor = .green
greenDot.layer.cornerRadius = greenDot.bounds.height / 2
return greenDot
}()
var timer : Timer?
var lastTouch = Set<UITouch>()
var lastTouchEvent: UIEvent?
override func viewDidLoad() {
super.viewDidLoad()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
print(touch.location(in: view))
greenDot.center = touch.location(in: view)
view.addSubview(greenDot)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
print(touch.location(in: view))
if timer != nil { timer?.invalidate() }
greenDot.center = touch.location(in: view)
view.addSubview(greenDot)
lastTouch = touches
lastTouchEvent = event
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(stopTouches), userInfo: nil, repeats: false)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
timer?.invalidate()
print("Touches Ended #: \(touch.location(in: view))")
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Cancelling touch")
}
#objc func stopTouches() {
touchesEnded(lastTouch, with: lastTouchEvent)
// touchesCancelled(lastTouch, with: lastTouchEvent)
// view.resignFirstResponder()
}
}
With UIGestures I have tried with the UILongPressGestureRecognizer, but I don't know how to 'end' with a long-press. Do I use 2 long-presses in sequence - one to start the movement and another to end the movement? I like the longPress since it is continuous and I can thus pan with it.
import UIKit
class ViewController: UIViewController {
let greenDot : UIView = {
let dot = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
dot.backgroundColor = .green
dot.layer.cornerRadius = dot.frame.width/2
return dot
}()
override func viewDidLoad() {
super.viewDidLoad()
let longPress1 = UILongPressGestureRecognizer(target: self, action: #selector(press1))
longPress1.minimumPressDuration = 0.2
view.addGestureRecognizer(longPress1)
}
#objc func press1(_ sender: UILongPressGestureRecognizer) {
let newLocation = sender.location(in: view)
greenDot.backgroundColor = .green
greenDot.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40)
if sender.state == .ended {
greenDot.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40)
greenDot.backgroundColor = .purple
}
view.addSubview(greenDot)
}
}
So in conclusion: I'm looking for a method to place an object, which I am moving around on the screen with my finger, accurately.
Any help in this regard will be greatly appreciated.

Intriguing how often you end up answering your own question after placing it before others.
So after many days of fumbling around I stumbled across this pointer from Apple:
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/implementing_a_custom_gesture_recognizer/implementing_a_discrete_gesture_recognizer
In short, I have the basics of what I was looking for by creating a custom Gesture Recognizer. Thanks Apple.
So for anyone else who might be interested, following is the 'basic' code for grabbing an object, moving it around and letting a timer release it for you so that the drop is as accurate as needed. Releasing before the timer expires also works, but then some shifting might/will occur during release. Once the timer has fired and your finger is still on the screen, you will be unable to accidentally drag the object out of place.
Obviously there is still a lot of work needed before implementing in an app to mitigate errors. 'Small steps'
import UIKit
import UIKit.UIGestureRecognizerSubclass
class ObjectDropGestureRecognizer: UIGestureRecognizer {
var trackedTouch : UITouch? = nil
var intervalTime : Double = 1.0
var dropTimer : Timer?
var counter : Int = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
print(touches.count)
if touches.count != 1 {
self.state = .failed
}
if self.state == .possible {
self.state = .began
print("began...")
}
if self.trackedTouch == nil {
self.trackedTouch = touches.first
} else {
for touch in touches {
if touch != self.trackedTouch {
self.ignore(touch, for: event)
}
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard let newTouch = touches.first else { return }
self.state = .changed
counter += 1
print("Changed...\(counter)")
if self.dropTimer != nil { dropTimer?.invalidate() }
dropTimer = Timer.scheduledTimer(withTimeInterval: intervalTime, repeats: false) { timer in
print("Timer fired")
print(newTouch.location(in: self.view))
self.state = .ended
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if self.dropTimer != nil {
self.dropTimer?.invalidate()
self.state = .ended
}
self.state = .recognized
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.trackedTouch = nil
self.state = .cancelled
}
override func reset() {
super.reset()
self.trackedTouch = nil
self.counter = 0
}
}
class ViewController: UIViewController {
let greenDot : UIView = {
let dot = UIView(frame: CGRect(x: 50, y: 200, width: 20, height: 20))
dot.backgroundColor = .green
dot.layer.cornerRadius = dot.frame.width/2
return dot
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(greenDot)
let objectMoveGesture = ObjectDropGestureRecognizer(target: self, action: #selector(tapAction(_:)))
objectMoveGesture.intervalTime = 0.8
view.addGestureRecognizer(objectMoveGesture)
}
#objc func tapAction(_ touch: ObjectDropGestureRecognizer){
let newLocation = touch.location(in: view)
greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y)
}
}

Related

remove all drawing in imageview UIGraphicsGetImageFromCurrentImageContext

I want to remove everything written imageview pic. I have tried something like pic.Clear() but its not being recognized. I dont know what else to do. The only other thing I can think off is to somehow create a variable that is like a line because I cant access the lines right now outside of the func they are in now. I am trying to do this is in clearM. Using UIGraphicsGetImageFromCurrentImageContext has something to do with this. I dont want to use pencil kit.
import UIKit
class ViewController: UIViewController {
var startPoint: CGPoint?
var statPoint = CGPoint.zero
var swipe = false
var pic = UIImageView()
var clearBtn = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
[pic,clearBtn].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
pic.backgroundColor = .brown
clearBtn.backgroundColor = .blue
pic.frame = CGRect(x: 100, y: 100, width: 250, height: 250)
clearBtn.frame = CGRect(x: 100, y: 350, width: 50, height: 50)
clearBtn.addTarget(self, action: #selector(clearM), for: .touchDown)
}
#objc func clearM(){
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return}
swipe = false
statPoint = touch.location(in: self.pic)
}
var score = 0
func drawLine(from fromPoint: CGPoint, to toPoint : CGPoint) {
UIGraphicsBeginImageContext( pic.frame.size)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
pic.image?.draw(in: pic.bounds)
context.move(to: fromPoint)
context.addLine(to: toPoint)
context.setLineCap(.round)
context.setLineWidth(5)
context.setStrokeColor(UIColor.black.cgColor)
context.strokePath()
pic.image = UIGraphicsGetImageFromCurrentImageContext()
pic.alpha = 1
UIGraphicsEndImageContext()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
swipe = true
let currentPoint = touch.location(in: pic)
drawLine(from: statPoint, to: currentPoint)
statPoint = currentPoint
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if !swipe {
drawLine(from: statPoint, to: statPoint)
}
UIGraphicsBeginImageContext(pic.frame.size)
pic.image?.draw(in: pic.bounds, blendMode: .normal, alpha: 1)
pic.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}

draw line only in one direction starting with cgpoint

My swifts codes goal below is to be able to draw a straight line. When the uiview is touch the user can only draw 90 degrees above the initial point. The direction has already ben decided. So the user can just draw the line above the point that is touched. You can the gif below the line on the left is what my code does below. The line on the right is what I would like to acheive.
import UIKit
class ViewController: UIViewController{
var draw = Canvas()
override func viewDidLoad() {
super.viewDidLoad()
[draw].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false}
}
override func viewDidLayoutSubviews() {
draw.backgroundColor = .clear
NSLayoutConstraint.activate ([
draw.bottomAnchor.constraint(equalTo: view.bottomAnchor),
draw.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.77, constant: 0),
draw.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1, constant: 0),
draw.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant : 0),
])
}
}
struct ColoredLine {
var color = UIColor.black
var points = [CGPoint]()
var width = 5
}
class Canvas: UIView {
var strokeColor = UIColor.red
var strokeWidth = 5
func undo() {
_ = lines.popLast()
setNeedsDisplay()
}
func clear() {
lines.removeAll()
setNeedsDisplay()
}
var lines = [ColoredLine]()
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
lines.forEach { (line) in
for (i, p) in line.points.enumerated() {
if i == 0 {
context.move(to: p)
} else {
context.addLine(to: p)
}
}
context.setStrokeColor(line.color.cgColor)
context.setLineWidth(CGFloat(line.width))
context.strokePath()
context.beginPath()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var coloredLine = ColoredLine()
coloredLine.color = strokeColor
coloredLine.width = strokeWidth
lines.append(coloredLine)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else { return }
guard var lastLine = lines.popLast() else { return }
lastLine.points.append(point)
lines.append(lastLine)
setNeedsDisplay()
}
}
It is OK to manufacture the coordinates, Points consists of x and y.
Keeping the y, and done.
keep the first point for one touch event
var firstPt: CGPoint?
// get the first point
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let first = touches.first?.location(in: self) else { return }
// store first as property
firstPt = first
}
manufacture the rest points, the x of new get points is irrelevant
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self), let first = firstPt else { return }
let pointNeeded = CGPoint(x: first.x , y: point.y)
// ... , do as usual
}
and in touchesEnd and touchesCancell, firstPt = nil

Move SKSpriteNode on Touch to create Button effect

I am currently creating the main menu of a mobile game, and it works fine however To make it look nicer I would like to add an effect when I touch one of my SKSpriteNodes which is an image of a button. I would like it to slightly move down then back up before transitioning to the game, im having some difficulty. Currently when the button is pressed it changed scenes to the game scene.
class MainMenuScene: SKScene{
let startGame = SKSpriteNode(imageNamed: "StartButton")
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "Menu")
background.size = self.size
background.position = CGPoint(x: self.size.width/2, y:self.size.height/2)
background.zPosition = 0
self.addChild(background)
startGame.position = CGPoint(x: self.size.width/2, y: self.size.height*0.6)
startGame.zPosition = 1
startGame.name = "startButton"
self.addChild(startGame)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches{
let pointOfTouch = touch.location(in: self)
let nodeITapped = atPoint(pointOfTouch)
if nodeITapped.name == "startButton" {
let sceneToMoveTo = GameScene(size: self.size)
sceneToMoveTo.scaleMode = self.scaleMode
let myTransition = SKTransition.fade(withDuration: 0.5)
self.view!.presentScene(sceneToMoveTo, transition: myTransition)
}
}
}
}
You could make your logic in the function touchesEnded and then you call a SKAction in your button, passing the moveToScene logic as completion.
Something like:
var shouldMoveToNewScene: Bool = False
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches{
let pointOfTouch = touch.location(in: self)
let nodeITapped = atPoint(pointOfTouch)
shouldMoveToNewScene = nodeITapped.name == "startButton" ? True : False
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard shouldMoveToNewScene, touces.count == 1 else { return }
startButton.run(myAnimation, completion: {
moveToScene()
})
}
private func moveToScene() {
//do your transition scene logic here
{
I haven't tried it yet, but hopefully you could get the idea.
If you're not familiar with SKAction you should consider follow this guide.

Swift 3: How do I pinch to scale and rotate UIImageView?

I am really struggling to find tutorials online as well as already answered questions (I have tried them and they don't seem to work). I have a UIImageView that I have in the centre of my view. I am currently able to tap and drag this wherever I want on screen. I want to be able to pinch to scale and rotate this view. How do I achieve this? I have tried the code for rotation below but it doesn't seem to work? Any help will be a massive help and marked as answer. Thank you guys.
import UIKit
class DraggableImage: UIImageView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.backgroundColor = .blue
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.backgroundColor = .green
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: superview)
center = CGPoint(x: position.x, y: position.y)
}
}
}
class CVController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotateAction(sender:)))
firstImageView.addGestureRecognizer(rotateGesture)
setupViews()
}
func rotateAction(sender: UIRotationGestureRecognizer) {
let rotatePoint = sender.location(in: view)
let firstImageView = view.hitTest(rotatePoint, with: nil)
firstImageView?.transform = (firstImageView?.transform.rotated(by: sender.rotation))!
sender.rotation = 0
}
let firstImageView: DraggableImage = {
let iv = DraggableImage()
iv.backgroundColor = .red
iv.isUserInteractionEnabled = true
return iv
}()
func setupViews() {
view.addSubview(firstImageView)
let firstImageWidth: CGFloat = 50
let firstImageHeight: CGFloat = 50
firstImageView.frame = CGRect(x: (view.frame.width / 2) - firstImageWidth / 2, y: (view.frame.height / 2) - firstImageWidth / 2, width: firstImageWidth, height: firstImageHeight)
}
}
You have a some problems in your code. First you need to add the UIGestureRecognizerDelegate to your view controller and make it your gesture recognizer delegate. You need also to implement shouldRecognizeSimultaneously method and return true. Second when applying the scale you need to save the transform when the pinch begins and apply the scale in top of it:
class DraggableImageView: UIImageView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = .blue
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = .green
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let position = touches.first?.location(in: superview){
center = position
}
}
}
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var identity = CGAffineTransform.identity
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupViews()
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(scale))
let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotate))
pinchGesture.delegate = self
rotationGesture.delegate = self
view.addGestureRecognizer(pinchGesture)
view.addGestureRecognizer(rotationGesture)
}
let firstImageView: DraggableImageView = {
let iv = DraggableImageView()
iv.backgroundColor = .red
iv.isUserInteractionEnabled = true
return iv
}()
func setupViews() {
view.addSubview(firstImageView)
let firstImageWidth: CGFloat = 50
let firstImageHeight: CGFloat = 50
firstImageView.frame = CGRect(x: view.frame.midX - firstImageWidth / 2, y: view.frame.midY - firstImageWidth / 2, width: firstImageWidth, height: firstImageHeight)
}
#objc func scale(_ gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .began:
identity = firstImageView.transform
case .changed,.ended:
firstImageView.transform = identity.scaledBy(x: gesture.scale, y: gesture.scale)
case .cancelled:
break
default:
break
}
}
#objc func rotate(_ gesture: UIRotationGestureRecognizer) {
firstImageView.transform = firstImageView.transform.rotated(by: gesture.rotation)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

Control Center-like sliding view implementation in Swift

I'm trying to create an interactive sliding transition on App.
The goal is to present a uiview when view controller that will be dragged from the bottom(not from the bottom edge so it won't conflict with the control center) and will partially cover the main view controller (just like the iOS control center). The transition should be interactive, i.e according to the dragging of the user.
Would be happy to hear ideas regarding available APIs.
Following code will do the simple slideView animation code tested in Xcode 8 and worked..
import UIKit
class ViewController: UIViewController,UIGestureRecognizerDelegate {
// I am using VisualView as a slideView
#IBOutlet weak var slideView: UIVisualEffectView!
//Button to open slideView
#IBOutlet weak var button: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// setting background for sideView
slideView.backgroundColor = UIColor(patternImage: UIImage(named: "original.jpg")!)
//Initial hide so it won't show.when the mainView loads...
slideView.isHidden = true
// DownSwipe to hide slideView
let downSwipe = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.handleSwipes(_:)))
downSwipe.direction = .down
view.addGestureRecognizer(downSwipe)
}
// set UIButton to open slideVie...(I am using UIBarButton)
#IBAction func sdrawButton(_ sender: AnyObject) {
self.slideView.isHidden = false
slideView.center.y += view.frame.height
UIView.animate(withDuration: 0.7, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations:{
self.slideView.center.y -= self.view.frame.height
self.button.isEnabled = false
}, completion: nil)
}
func handleSwipes(_ sender:UISwipeGestureRecognizer) {
if (sender.direction == .down) {
print("Swipe Down")
self.slideView.isHidden = true
self.slideView.center.y -= self.view.frame.height
UIView.animate(withDuration: 0.7, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations:{
self.slideView.center.y += self.view.frame.height
self.button.isEnabled = true
}, completion: nil)
}
}
}
Let me know the code works for you...
Sorry if this question is a bit old.
You can subclass a UIView and override touchesBegan(_ , with) to save the original touch location
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { print("No touch"); return }
startPoint = touch.location(in: self)
startHeight = heightConstraint.constant
slideDirection = .none
super.touchesBegan(touches, with: event)
}
}
and touchesMoved(_,with) to update the Height constraint while a finger is sliding the view.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { print("touchesMovedno touches"); return }
let endPoint = touch.location(in: self)
let diff = endPoint.y - startPoint.y
// This causes the view to move along the drag
switch anchorLocation {
case .top :
heightConstraint.constant = startHeight + diff
case .bottom :
heightConstraint.constant = startHeight - diff
}
self.layoutIfNeeded()
// Update direction
if diff == 0.0 {
self.slideDirection = .none
} else {
self.slideDirection = (diff > 0) ? .down : .up
}
super.touchesMoved(touches, with: event)
}
Override touchesEnded(_, with) to add animation if you wish
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.modifyHeightConstraints()
self.animateViewTransition(duration: animDuration, delay: animDelay, clearance: animClearance, springDampingRatio: animSpringDampingRatio, initialSpringVelocity: animInitialSpringVelocity, complete: {
self.slideDirection = .none
})
super.touchesEnded(touches, with: event)
}
I have created a component that might have the exact feature you wanted. The component includes adjustable animated bouncing. You can layout and design subviews on the view in storyboard.
Check out the link in GitHub
https://github.com/narumolp/NMPAnchorOverlayView