I have been trying to implement a Snapchat-like edit text on an image.
What I did so far is implement a UILabel in the center of the UIImageView and I added 3 gestures to this UILabel: UIPanGestureRecognizer, UIPinchGestureRecognizer & UIRotationGestureRecognizer.
I have managed to implement the Pan method, but I am having hard time to make the Pinch + Rotation as smooth as they do, I get horrible results T_T
How do you guys think this was made? which components are involved in this & if you have any reading / watching material I could use to accomplish this.
Thanks :)
EDIT:
These are the methods I implemented to handle Pinch & Rotation:
func handlePinch(recognizer: UIPinchGestureRecognizer) {
if let view = recognizer.view as? UILabel {
view.transform = CGAffineTransformScale(view.transform, recognizer.scale, recognizer.scale)
}
}
func handleRotate(recognizer: UIRotationGestureRecognizer) {
if let view = recognizer.view as? UILabel {
view.transform = CGAffineTransformRotate(view.transform, recognizer.rotation)
}
}
Preview video of how the pinch I implemented works:
https://drive.google.com/file/d/0B-AVM51jxsvUY2RUUHdWbGo5QlU/view?usp=sharing
Found a solution, apparently all I needed to do in the Rotation & Pinch is to always reset the view's rotation / scale.
For instance, setting my recognizer.scale to 1.0 and recognizer.rotation to 0.0.
Here is an example of my code:
func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(view)
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
recognizer.setTranslation(CGPointZero, inView: view)
}
func handlePinch(recognizer: UIPinchGestureRecognizer) {
if let view = recognizer.view as? UILabel {
let pinchScale: CGFloat = recognizer.scale
view.transform = CGAffineTransformScale(view.transform, pinchScale, pinchScale)
recognizer.scale = 1.0
}
}
func handleRotate(recognizer: UIRotationGestureRecognizer) {
if let view = recognizer.view as? UILabel {
let rotation: CGFloat = recognizer.rotation
view.transform = CGAffineTransformRotate(view.transform, rotation)
recognizer.rotation = 0.0
}
}
For "it scales up / down way too much in a very aggressive way":
You need to handle the pinch gesture scale value according to your need.
From your recogniser method, you get the scale value as:
var pinchScale: CGFloat = recogniser.scale
You need to modify this value either like decrease it by /10 or /100 as per your need, and use this value in the label transformation instead of using the pangesture scale.
The issue you have is that your code takes the current transform and adds another transform based on the current "movement", so you accumulate changes (compound them, really) as you move during a single gesture.
Keep instance variables for rotation, scale, and movement, update the relevant one in each of your gesture recognizer's actions (you'll also need to store the state of each at the beginning of each gesture, so you can apply the delta to the initial state), and create the transform from scratch using those three variables. The transform creating should of course be factorized in a separate function, since you're going to use it several times.
I'm not sure if this is exactly what you're looking for (I've never used snapchat), but this could give you some ideas
https://github.com/danielinoa/DIImageView
I'm not sure this one has a pinch gesture, but I'm not entirely sure how you mean it to be used anyway.
Here is a demo of that project:
https://www.cocoacontrols.com/controls/diimageview
In general, I recommend checking cocoacontrols every time you venture to implement something like this. There are usually solid examples to get you started, and sometimes, you'll find exactly what you need.
Related
Hy,
I'm trying to implement a custom UISlider from scratch based on UIGestureRecognizer because I have to make the thumb view a custom one with a clean design.
The UIGestureRecognizer usage is a request from technical lead.
What I have until now it's a sample where I made tests:
func setupSlider() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(dragged(gestureRecognizer:)))
sliderView.isUserInteractionEnabled = true
sliderView.addGestureRecognizer(panGesture)
}
#objc func dragged(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizer.State.began || gestureRecognizer.state == UIGestureRecognizer.State.changed {
let translation = gestureRecognizer.translation(in: self.sliderContainer)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
In this point I have twoproblems:
to keep the slider view in the middle of container
to be able to get "curent value" of the slider based on the min and max value of the interval
Can you help with any suggestion, please ?
Kind regards.
Not technically an answer to your question but....
I'm trying to implement a custom UISlider from scratch based on UIGestureRecognizer because I have to make the thumb view a custom one with a clean design.
Are you aware that you can customize the thumb image?
See for example this article:
https://zeitschlag.net/customizing-uislider/
This is the API: setThumbImage(_:for:)
The UIGestureRecognizer usage is a request from technical lead.
If you can manage to get the result by using UISlider after all, I am sure your lead will welcome it.
I created an SCNSphere so now it looks like a planet kind of. This is exactly what I want. My next goal is to allow users to rotate the sphere using a pan gesture recognizer. They are allowed to rotate it around the X or Y axis. I was just wondering how I can do that. This is what I have so far.
origin = sceneView.frame.origin
node.geometry = SCNSphere(radius: 1)
node.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "world.jpg")
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(CategoryViewController.panGlobe(sender:)))
sceneView.addGestureRecognizer(panGestureRecognizer)
func panGlobe(sender: UIPanGestureRecognizer) {
// What should i put inside this method to allow them to rotate the sphere/ball
}
We have a ViewController that contains a node sphereNode that contains our sphere.
To rotate the sphere we could use a UIPanGestureRecognizer.
Since the recognizer reports the total distance our finger has traveled on the screen we cache the last point that was reported to us.
var previousPanPoint: CGPoint?
let pixelToAngleConstant: Float = .pi / 180
func handlePan(_ newPoint: CGPoint) {
if let previousPoint = previousPanPoint {
let dx = Float(newPoint.x - previousPoint.x)
let dy = Float(newPoint.y - previousPoint.y)
rotateUp(by: dy * pixelToAngleConstant)
rotateRight(by: dx * pixelToAngleConstant)
}
previousPanPoint = newPoint
}
We calculate dx and dy with how much pixel our finger has traveled in each direction since we last called the recognizer.
With the pixelToAngleConstant we convert our pixel value in an angle (in randians) to rotate our sphere. Use a bigger constant for a faster rotation.
The gesture recognizer returns a state that we can use to determine if the gesture has started, ended, or the finger has been moved.
When the gesture starts we save the fingers location in previousPanPoint.
When our finger moves we call the function above.
When the gesture is ended or canceled we clear our previousPanPoint.
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
previousPanPoint = gestureRecognizer.location(in: view)
case .changed:
handlePan(gestureRecognizer.location(in: view))
default:
previousPanPoint = nil
}
}
How do we rotate our sphere?
The functions rotateUp and rotateRight just call our more general function, rotate(by: around:) which accepts not only the angle but also the axis to rotate around.
rotateUp rotates around the x-axis, rotateRight around the y-axis.
func rotateUp(by angle: Float) {
let axis = SCNVector3(1, 0, 0) // x-axis
rotate(by: angle, around: axis)
}
func rotateRight(by angle: Float) {
let axis = SCNVector3(0, 1, 0) // y-axis
rotate(by: angle, around: axis)
}
The rotate(by:around:) is in this case relative simple because we assume that the node is not translated/ we want to rotate around the origin of the nodes local coordinate system.
Everything is a little more complicated when we look at a general case but this answer is only a small starting point.
func rotate(by angle: Float, around axis: SCNVector3) {
let transform = SCNMatrix4MakeRotation(angle, axis.x, axis.y, axis.z)
sphereNode.transform = SCNMatrix4Mult(sphereNode.transform, transform)
}
We create a rotation matrix from the angle and the axis and multiply the old transform of our sphere with the calculated one to get the new transform.
This is the little demo I created:
This approach has two major downsides.
It only rotates around the nodes coordinate origin and only works properly if the node's position is SCNVector3Zero
It does takes neither the speed of the gesture into account nor does the sphere continue to rotate when the gesture stops.
An effect similar to a table view where you can flip your finger and the table view scrolls fast and then slows down can't be easily achieved with this approach.
One solution would be to use the physics system for that.
Below is what I tried, not sure whether it is accurate with respect to angles but...it sufficed most of my needs....
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!)
let x = Float(translation.x)
let y = Float(-translation.y)
let anglePan = (sqrt(pow(x,2)+pow(y,2)))*(Float)(Double.pi)/180.0
var rotationVector = SCNVector4()
rotationVector.x = x
rotationVector.y = y
rotationVector.z = 0.0
rotationVector.w = anglePan
self.earthNode.rotation = rotationVector
}
Sample Github-EarthRotate
I am new to Swift and SpriteKit and am learning to understand the control in the game "Fish & Trip". The sprite node is always at the center of the view and it will rotate according to moving your touch, no matter where you touch and move (hold) it will rotate correspondingly.
The difficulty here is that it is different from the Pan Gesture and simple touch location as I noted in the picture 1 and 2.
For the 1st pic, the touch location is processed by atan2f and then sent to SKAction.rotate and it is done, I can make this working.
For the 2nd pic, I can get this by setup a UIPanGestureRecognizer and it works, but you can only rotate the node when you move your finger around the initial point (touchesBegan).
My question is for the 3rd pic, which is the same as the Fish & Trip game, you can touch anywhere on the screen and then move (hold) to anywhere and the node still rotate as you move, you don't have to move your finger around the initial point to let the node rotate and the rotation is smooth and accurate.
My code is as follow, it doesn't work very well and it is with some jittering, my question is how can I implement this in a better way? and How can I make the rotation smooth?
Is there a way to filter the previousLocation in the touchesMoved function? I always encountered jittering when I use this property, I think it reports too fast. I haven't had any issue when I used UIPanGestureRecoginzer and it is very smooth, so I guess I must did something wrong with the previousLocation.
func mtoRad(x: CGFloat, y: CGFloat) -> CGFloat {
let Radian3 = atan2f(Float(y), Float(x))
return CGFloat(Radian3)
}
func moveplayer(radian: CGFloat){
let rotateaction = SKAction.rotate(toAngle: radian, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
var touchpoint = CGPoint.zero
var R2 : CGFloat? = 0.0
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches{
let previousPointOfTouch = t.previousLocation(in: self)
touchpoint = t.location(in: self)
if touchpoint.x != previousPointOfTouch.x && touchpoint.y != previousPointOfTouch.y {
let delta_y = touchpoint.y - previousPointOfTouch.y
let delta_x = touchpoint.x - previousPointOfTouch.x
let R1 = mtoRad(x: delta_x, y: delta_y)
if R2! != R1 {
moveplayer(radiant: R1)
}
R2 = R1
}
}
}
This is not an answer (yet - hoping to post one/edit this into one later), but you can make your code a bit more 'Swifty' by changing the definition for movePlayer() from:
func moveplayer(radian: CGFloat)
to
rotatePlayerTo(angle targetAngle: CGFloat) {
let rotateaction = SKAction.rotate(toAngle: targetAngle, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
then, to call it, instead of:
moveplayer(radiant: R1)
use
rotatePlayerTo(angle: R1)
which is more readable as it describes what you are doing better.
Also, your rotation to the new angle is constant at 0.1s - so if the player has to rotate further, it will rotate faster. it would be better to keep the rotational speed constant (in terms of radians per second). we can do this as follows:
Add the following property:
let playerRotationSpeed = CGFloat((2 *Double.pi) / 2.0) //Radian per second; 2 second for full rotation
change your moveShip to:
func rotatePlayerTo(angle targetAngle: CGFloat) {
let angleToRotateBy = abs(targetAngle - thePlayer.zRotation)
let rotationTime = TimeInterval(angleToRotateBy / shipRotationSpeed)
let rotateAction = SKAction.rotate(toAngle: targetAngle, duration: rotationTime , shortestUnitArc: true)
thePlayer.run(rotateAction)
}
this may help smooth the rotation too.
I'm try to implement this in my project. But I have some troubles with it. I plan to use UiPanGestureRecognizer for change size of rectangle. As I understand, I should be use UIVIew and custom drawRect method?
Add an action for your UIPanGestureRecognizer and use translation:
func wasDragged(gesture: UIPanGestureRecognizer) {
let translation = gesture.translationInView(self.view)
// Do your resizing here, e.g. from a
customView.frame.size.width = currentFrame.width + translation.x
customView.frame.size.height = currentFrame.height + translation.y
if gesture.state == .Ended {
currentFrame = customView.frame
}
}
Using this method, add a CGRect variable to store the currentFrame.
I’m trying to create an application which duplicates the ability of Apple’s Photos app (iPhone) to zoom, pan and scroll through photographic images. (I also want to use the same controls when viewing pdfs and other documents.) I got the tap gesture to show/hide the navigation bar and the swipe gesture to scroll through the images from left to right & vice versa. Then I got the pinch gesture to zoom in and out, but when I added the pan gesture to move around within a zoomed image, the swipe gesture quit working.
I found potential solutions elsewhere on StackOverflow including the use of shouldRecognizeSimultaneouslyWithGestureRecognizer, but so far I have not been able to resolve the conflict. Any suggestions?
Here's the code:
func gestureRecognizer(UIPanGestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer UISwipeGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
#IBAction func handlePinch(sender: UIPinchGestureRecognizer) {
sender.view!.transform = CGAffineTransformScale(sender.view!.transform, sender.scale, sender.scale)
sender.scale = 1
}
#IBAction func handlePan(sender: UIPanGestureRecognizer) {
self.view.bringSubviewToFront(sender.view!)
var translation = sender.translationInView(self.view)
sender.view!.center = CGPointMake(sender.view!.center.x + translation.x, sender.view!.center.y + translation.y)
sender.setTranslation(CGPointZero, inView: self.view)
}
#IBAction func handleSwipeRight(sender: UISwipeGestureRecognizer) {
if (self.index == 0) {
self.index = ((photos.count) - 1);
}
else
{
self.index--;
}
// requireGestureRecognizerToFail(panGesture)
setImage()
}
You do not want shouldRecognizeSimultaneouslyWithGestureRecognizer: (which allows two gestures to happen simultaneously). That's useful if you want to, for example, simultaneously pinch and pan. But the simultaneous gestures will not help in this scenario where you are panning and swiping at the same time. (If anything, recognizing those simultaneously probably confuses the situation.)
Instead, you might want to establish precedence of swipe and pan gestures (e.g. only pan if swipe fails) with requireGestureRecognizerToFail:.
Or better, retire the swipe gesture entirely and use solely the pan gesture, which, if you're zoomed out will be an interactive gesture to navigate from one image to the next, and if zoomed in, pans the image. Interactive pan gestures generally a more satisfying UX, anyway; e.g., if swiping from one photo to the next, be able to stop mid pan gesture and go back. If you look at the Photos.app, it's actually using a pan gesture to swipe from one image to another, not a swipe gesture.
I discovered a tutorial at http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift that does a great job of introducing UIScrollView as a way of combining zooming, panning and paging in Swift. I recommend it for anyone trying to learn how to make these gestures work well together.
In similar case I've used another approach: extended pan gesture to support swipe:
// in handlePan()
switch recognizer.state {
struct Holder {
static var lastTranslate : CGFloat = 0
static var prevTranslate : CGFloat = 0
static var lastTime : TimeInterval = 0
static var prevTime : TimeInterval = 0
}
case .began:
Holder.lastTime = Date.timeIntervalSinceReferenceDate
Holder.lastTranslate = translation.y
Holder.prevTime = Holder.lastTime
Holder.prevTranslate = Holder.lastTranslate
//perform appropriate pan action
case .changed:
Holder.prevTime = Holder.lastTime
Holder.prevTranslate = Holder.lastTranslate
Holder.lastTime = Date.timeIntervalSinceReferenceDate
Holder.lastTranslate = translation.y
//perform appropriate pan action
case .ended ,.cancelled:
let seconds = CGFloat(Date.timeIntervalSinceReferenceDate) - CGFloat(Holder.prevTime)
var swipeVelocity : CGFloat = 0
if seconds > 0 {
swipeVelocity = (translation.y - Holder.prevTranslate)/seconds
}
var shouldSwipe : Bool = false
if Swift.abs(swipeVelocity) > velocityThreshold {
shouldSwipe = swipeVelocity < 0
}
if shouldSwipe {
// perform swipe action
} else {
// perform appropriate pan action
}
default:
print("Unsupported")
}
All you need to do is to find appropriate velocityTreshold for your swipe gesture