We now know how to assign gestures to our views and can now recognize user’s manipulations.
If a user touches something on a screen that appears to be zoomable, movable or rotatable, but nothing happens in response? - Our app would look, somewhat, useless.
So, next thing to accomplish is to translate those manipulations into view transformations and provide visual feedback to the user accordingly.
Discovering View Transformations
The basic transformations are:
Rotate
Scale
Move (a.k.a. Translate)
Any more sophisticated transformations can be achieved using a composite approach of the above.
The transformation are available to us through the CoreGraphics module - you can identify its components by CG
prefix.
Transform property
View transformations are performed using transform
property of CGAffineTransform
type.
There are helper initializers available to us for each type of transformations:
CGAffineTransform(rotationAngle: CGFloat)
- generates a transformation object with rotation configuration.CGAffineTransform(scaleX: CGFloat, y: CGFloat)
- generates a transformation object with scaling configuration.CGAffineTransform(translationX: CGFloat, y: CGFloat)
- generates a transformation object with translation configuration.
Transformation can also be continues and built on top of the existing transformation. For that, following methods can be used:
view.transform.rotated(by: CGFloat)
view.transform.scaledBy(x: CGFloat, y: CGFloat)
view.transform.translatedBy(x: CGFloat, y: CGFloat)
Removing all transformations is done by assigning .identity
value to the transform property:
view.transform = .identity
Let's understand how each of them works. Handy enough, we can make use of each of them in FrameIT !
Transforming FrameIT
We've got to finish the work we started in the previous chapter. We promised the user zooming, moving and rotating. They are still waiting..!
Let's remind ourselves of the placeholder methods we've created:
@objc func moveImageView(_ sender: UIPanGestureRecognizer) {
print("moving")
}
@objc func rotateImageView(_ sender: UIRotationGestureRecognizer) {
print("rotating")
}
@objc func scaleImageView(_ sender: UIPinchGestureRecognizer) {
print("scaling")
}
Let's get going!
Rotating
@objc func rotateImageView(_ sender: UIRotationGestureRecognizer) {
creationImageView.transform = creationImageView.transform.rotated(by: sender.rotation)
sender.rotation = 0
}
Let's review the above:
We are assigning a transform value to creation image view taking the rotation gesture value of the rotation gesture and adding it to however the view has been already transformed.
Then, resetting the rotation property of the gesture to 0. Wo that when the next change occurs, we are only apply the new delta to support adequate transformation. Why 0? - Because it's a neutral/default value for the rotation.
Scaling
@objc func scaleImageView(_ sender: UIPinchGestureRecognizer) {
creationImageView.transform = creationImageView.transform.scaledBy(x: sender.scale, y: sender.scale)
sender.scale = 1
}
Let's see what's done here:
Similarly to the rotation, we are assigning a transform value to creation image view taking the scale gesture value of the pinch gesture and adding it to the existing transformations.
Then, resetting the scale property of the gesture to 1, so that when the next change occurs, we are only apply the new delta to support adequate transformation. This time we are using 1, as it's a neutral/default value for scaling.
Moving
Moving is a fancy one. We need to take care of an additional aspect - remember the position of a view every time a new gesture begins. We need to hold this value somewhere while user is continuing moving. So, let's declare a variable for that amongst outlets and other variable - at the top of class declaration:
var initialImageViewOffset = CGPoint()
And compete the moving action:
@objc func moveImageView(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: creationImageView.superview)
if sender.state == .began {
initialImageViewOffset = creationImageView.frame.origin
}
let position = CGPoint(x: translation.x + initialImageViewOffset.x - creationImageView.frame.origin.x, y: translation.y + initialImageViewOffset.y - creationImageView.frame.origin.y)
creationImageView.transform = creationImageView.transform.translatedBy(x: position.x, y: position.y)
}
This looks similar to both previous implementations with a couple additions:
We've got the translation variable that holds the position of the movement relative to the image view superview.
Next, we are making sure to capture the correct position (frame origin) of the image view when the gesture begins. We're identifying the beginning of gesture by comparing gesture's state to .began value.
We've created an assisting position variable to hold the adjusted position of current translation with the offset adjustment.
And, finally, similarly to rotation and scaling, we're assigning a position value to creation image view to the existing transformations.
Hm.. looks like we're all set!
Let's test it! Run the app, tap on the placeholder image to load a photo. Use 'Random' option for the quickest load, and try manipulating the image. Make it cool:

Next, experiment with allowing all 3 gestures simultaneously.
Which option to choose?
After giving both options a try, the one that only allows rotation and scaling function together and separates the movement appears more appealing.
Fine, but how about the requirements?
If requirements are not explicit enough - chose the best version that appeals to you and then discuss with designers if they provide you feedback against your choice.
If requirements clearly state that something needs to be implemented and your tests show it's not the best option - discuss with designers demonstrating the results for available options.
Done now?
Nope.
Let's remember to reset all the transformations in startOver method:
creationImageView.transform = .identity
Now Done!
Let's Recap!
A view
translation(in: UIView)
method is used to obtain the translation of the finger on the screen while gesture is being performed.UIView has a
transform
property that allows to perform transformations: move, rotate and scale.To create a transformations for key actions, the following CGAffineTransform initializers can be used:
CGAffineTransform(rotationAngle: CGFloat)CGAffineTransform(scaleX: CGFloat, y: CGFloat)CGAffineTransform(translationX: CGFloat, y: CGFloat)To add to existing transformation dedicated methods are used:
view.transform.rotated(by: CGFloat)view.transform.scaledBy(x: CGFloat, y: CGFloat)view.transform.translatedBy(x: CGFloat, y: CGFloat)Reseting view's transformations is done by assigning .identity value to transform property:
view.transform = .identityMultiple transformations can be concatenated by using
concatenating
method of CGAffineTransform:let combinedTransform = transform1.concatenating(transform2)