• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 04/02/2019

Transform views within an application

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 ! :magicien:

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..! :p

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! :zorro:

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:

Cool!
Cool!

Next, experiment with allowing all 3 gestures simultaneously.

Which option to choose? o_O

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. :pirate:

Let's remember to reset all the transformations in startOver method:

creationImageView.transform = .identity

 Now Done! :ange:

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 = .identity
  • Multiple transformations can be concatenated by using  concatenating  method of CGAffineTransform:

    let combinedTransform = transform1.concatenating(transform2)
Exemple de certificat de réussite
Exemple de certificat de réussite