Chapter 23: Dictionaries

You will use UIGesture Recognizers to make images expand, shrink, rotate and move around the screen. You will also be able to tap an image to save a screenshot of the current view to your camera roll on your iPhone.


What you will learn

  • How to add gesture recognizers to your UIImageViews.
  • How to Rendering UIImageView to UIImage
  • How to save an image to the camera roll.

Key Terms

  • UIGestureRecognizer
  • UIPinchGestureRecognizer
  • UIPanGestureRecognizer
  • UIRotateGestureRecognizer
  • UITapGestureRecognizer

Resources

Download here: https://github.com/devslopes/book-assets/wiki

Getting Started

Lets begin with starting a new Single View Application and name it whatever you would like. You should be very familiar with this process by now. For this project we are going to name it Tanstagram.

Once you have created the new project click on the Main.storyboard. This should be your starting point:

Figure 3.5.1 Screenshot 2016-10-27 22.01.03.png

Next click the Assets.xcassets file. Add the .png of each image by dragging and dropping it into the column under the AppIcon folder (Figure 3.5.2). You will know it is the 1x.png because the other images will have either 2x.png or 3x.png.

Figure 3.5.2 Screenshot 2016-10-27 22.10.59.png

Make sure all of the images are selected then proceed to dragging the 2x.png and 3x.png images into their respective boxes (Figure 3.5.3).

Figure 3.5.3 Screenshot 2016-10-27 22.10.59.png

Return to Main.storyboard.

Search for UIImageView in the search bar of the inspector column. Drag and drop an Image View onto the View Controller (Figure 3.5.3).

Figure 3.5.4 Screenshot 2016-10-27 22.19.25.png

With the UIImageView selected, click the Attributes Inspector icon (you should be familiar with this by now), then open the Image drop down menu and choose the first image on the list (Figure 3.5.4).

Change the Content Mode from Scale to Fill to Aspect Fit.

Resize the image view so it fits to the triangles edges (Figure 3.5.4).

Figure 3.5.5 Screenshot 2016-10-27 22.24.42.png

Now you are going to repeat the process for the rest of the triangle images you imported into the project, but the images will be randomly placed on the View Controller. You should have seven total (Figure 3.5.5).

Figure 3.5.6 Screenshot 2016-10-27 22.37.29.png

Open the Assistant Editor while the View Controller with the images selected. Make sure both Main.storyboard and ViewController.swift are side by side (Figure 3.5.6).

Figure 3.5.7 Screenshot 2016-10-27 22.44.35.png

Get rid of any unnecessary code (Figure 3.5.8).

Figure 3.5.8 Screenshot 2016-10-27 22.48.05.png

Now create an @IBOutlet Collection control dragging from the image itself to the ViewController.swift above the viewDidLoad function. Name it images (Figure 3.5.7).

Figure 3.5.9 Screenshot 2016-11-29 23.08.33.png

You will then have an array of UIImageView.

To the left of the IBOutlet you will see a dark circle. Control drag from that to each one of your images. This will add those images to the Collection View you just created.

Figure 3.5.9.B Screenshot 2016-11-29 23.12.36.png

You will see, once you hover over the circle, the circle has been replaced with a plus sign (+) and each image you added will highlight to show it is indeed part of the Collection Outlet (Figure 3.5.9.C).

Figure 3.5.9.C

Screenshot 2016-11-29 23.19.29.png

On each image you are going to enable interaction and multiple touch (Figure 3.5.8).

You do this by selecting an image and checking both boxes in the Interaction section of the Attributes Inspector.

Figure 3.5.10

Screenshot 2016-10-27 23.03.36.png

Selecting these two attributes tells the app the user will be touching, or in our case, accessing gestures on the image. Multiple Touch is telling our app to look out for more than one gesture at a time. Keep in mind an image can have multiple gestures, however a gesture can only be set to one image.

Close the assistant editor and select the ViewController.swift file once more.

We need to tell the View Controller to look for gestures. Which means we need to add... you guest it, the UIGestureRecognizerDelegate after UIViewController at the top of our file. This is preceded by a comma (Figure 3.5.9).

Figure 3.5.11

Screenshot 2016-10-27 23.22.37.png

Now seems like a great time to discuss UIGestureRecognizer. This api houses many different types of gesture recognizers. As stated before we are going to focus on the ability to pinch, or scale, rotate and drag. We'll also be using the tap gesture on one image. These gestures do all the work under the hood that allow the user to interact with the image. All we have to do is set it up and tell the image which gestures we want it to use.

First let's focus on UIPinchGestureRecognizer. Create a function named pinchGesture with a parameter called imageView of type UIImageView that returns UIPinchGestureRecognizer.

Since we told the function we were going to return a UIPinchGestureRecognizer, we need to do so.

func pinchGesture(imageView: UIImageView) -> UIPinchGestureRecognizer {
        return UIPinchGestureRecognizer(target: self, action: #selector(ViewController.handlePinch))
    }

After looking at the image above you might be wondering what does target, action and #selector mean. Well, I am going to tell you. target is telling the function you are returning that it needs to look at its self for the UIPinchGestureRecognizer. Action is what will be carried out when it finds the gesture and #selector finds the function the action will carry out.

In this case you have not created a function called handlePinch. So, let's get to it.

Create a function just below the pinchGestesture function called handlePinch.

func handlePinch(sender: UIPinchGestureRecognizer) {
        sender.view?.transform = (sender.view?.transform)!.scaledBy(x: sender.scale, y: sender.scale)
        sender.scale = 1
    }

Give it a parameter named sender of type UIPinchGestureRecognizer. The UIPinchGestureRecognizer has several functions. To access those functions we merely use dot notation.

For example on line 32 we can access transform for the purpose of setting it by stating sender.view?.transform. We are saying "go into sender (which is an instance variable of UIPinchGestureRecognizer), find view (the view the gesture recognizer is attached to), then find transform (the anchor point of the view)."

Then we tell it to scale. What you need to understand about gestures is that they detect when a user pushes down on the screen, then in the case of the pinch gesture, they scale up (larger), or down (smaller) according to where the fingers are moving. Here we set the scale equal to 1 pixel. Meaning it will expand 1 pixel on the x and y plains as long as the user's finger does not lift from the screen.

Once we have created the pinch gesture functions it is time to create a function that both attaches the pinchGesture to the image and adds it. That might sound a bit confusing but you will understand once you see the code.

Create a function called createGestures(). Inside it, create a constant named pinch. Then set it equal to pinchGesture (the function we create which requires an image view) then set the parameter to imageView: shape.

func createGestures() {
        for shape in images {
            let pinch = pinchGesture(imageView: shape)

Now add the constant you just created to the image shape.addGestureRecognizer(pinch) below the pinch constant.

As I said before an image can have several gestures attached to it, but a gesture can not have multiple views assigned to it.

Add createGestures inside the viewDidLoad() after super.viewDidLoad.

Now, run the project and see if you can expand smallTriangleOne.

Good Work!

The following code snippet is how you create both the UIPanGestureRecognizer and UIRotationGestureRecognizer.

They are almost identical in execution but there are slight differences. I would love for you to see if you can figure them out before you look at the following code. It's up to you.

// Setup Gestures
    func pinchGesture(imageView: UIImageView) -> UIPinchGestureRecognizer {
        return UIPinchGestureRecognizer(target: self, action: #selector(ViewController.handlePinch))
    }
    func panGesture(imageView: UIImageView) -> UIPanGestureRecognizer {
        return UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(sender:)))
    }
    func rotateGesture(imageView: UIImageView) -> UIRotationGestureRecognizer {
        return UIRotationGestureRecognizer(target: self, action: #selector(ViewController.handleRotation(sender:)))
    }
    // Handling
    func handlePinch(sender: UIPinchGestureRecognizer) {
        sender.view?.transform = (sender.view?.transform)!.scaledBy(x: sender.scale, y: sender.scale)

        sender.scale = 1
    }
    func handlePan(sender: UIPanGestureRecognizer) {
        let translation = sender.translation(in: self.view)
        if let view = sender.view {
            view.center = CGPoint(x: view.center.x + translation.x, y: view.center.y + translation.y)
        }
        sender.setTranslation(CGPoint.zero, in: self.view)
    }
    func handleRotation(sender: UIRotationGestureRecognizer) {
        sender.view?.transform = (sender.view?.transform)!.rotated(by: sender.rotation)
        sender.rotation = 0
    }
    // Create Gestures
    func createGestures() {
        for shape in images {
            let pinch = pinchGesture(imageView: shape)
            let pan = panGesture(imageView: shape)
            let rotate = rotateGesture(imageView: shape)
            shape.addGestureRecognizer(pinch)
            shape.addGestureRecognizer(pan)
            shape.addGestureRecognizer(rotate)
        }
    }

Don't forget to call your create functions in the viewDidLoad().

Run your project and play with the shapes. Rotate them, move them and pinch them. Once you have had your fun we will move on to capturing your fun and saving it into your Camera Roll.


Rendering UIImageView to UIImage/ saving to Camera Roll

Open the Assets.xcassets file. Drag and drop the savePhotosButton.png. Then place the 2x.png and 3x.png versions into their respective boxes (Figure 3.5.12).

Figure 3.5.12 Screenshot 2016-10-28 02.47.43.png

Now go back to the Storyboard and type UIImage into the filter bar on the bottom right corner. Drag and drop it to the bottom right corner of the View Controller. Change the image to the savePhotosButton image and change the Content Mode to Aspect Fit. Now pin it to the right and the bottom. Also give it a fixed Width and Height.

Figure 3.5.13 Screenshot 2016-10-28 02.56.55.png

This time we are going to add a gesture in a different way. Yes, there are different ways to do things in Xcode. It keeps things interesting. Instead of adding the UITapGestureRecognizer programmatically, we are going to use the Storyboard. The reason we are doing it this way is because we only have one view that requires the tap gesture. Were there's more than one I would do it in code. Otherwise the Storyboard View Controller becomes very crowded.

First lets add a reference (@IBOutlet) for the save image we just created. You remember how to do that, right? Good I am glad to see your confidence is not lacking.

Figure 3.5.14 Screenshot 2016-10-28 03.06.49.png

To add the tap gesture recognizer simply type tap in the filter bar where you find the UIImageView. Drag it directly onto the SaveToPhotos image view and drop it. You will then notice at the top of the View Controller, a new square with a blue icon has appeared.

Control drag from that square into the ViewController.swift you have open next to the Storyboard. Make sure you drop it beneath the viewDidLoad().

Change the Connection to an Action and the Type to UITapGestureRecognizer. I named mine saveToPhotosTapGesture.

Before this works, we need to enable interactions in the Attributes Inspector.

To test it out, type a print statement inside the @IBAction. Run it then tap the image and see if magic happens.

Great! We are moving right along. See what happens when you stick with something and practice. You get better 😀

Now it is time to render the image from the Image View.

Let's create a function called renderImage(). Then we create a constant inside that function called renderer. We then assign it the UIGraphicsImageRenderer. Then we tell it the size is based off the view that houses the images we have sitting in their.

func renderImage() {
        let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
        let image = renderer.image { (goTo) in
            view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
        }
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(ViewController.image(_:didFinishSavingWithError:contextInfo:)), nil)
    }

Under renderer create another constant named image. We assign it renderer.image, give a parameter name of our choosing then we tell it in which order to render the image with view.drawHierarchy. That just means it is copying the image in order for us.

Then we need to tell the image where to go. In this case we want it to go to the camera roll. We do this by using the api UIImageWriteToSavedPhotosAlbum.

You have probably noticed there is a #selector as one of the parameters. That means there is a function being used. In this case it is called image. The purpose of the following function is to handle errors. This exists because it's better for the user to know what is going on than have nothing happen and not be informed. That will get your app deleted off users phones very quickly.

Writing to your phone requires error handling to proceed, so lets just get it over with. The following calls an alert controller to notify the user. Feel free to change the text to whatever you want.

func image(_image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        if let error = error {
            // we got back an error!
            let alert = UIAlertController(title: "Save error", message: error.localizedDescription, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            present(alert, animated: true)
        } else {
            let alert = UIAlertController(title: "Saved!", message: "Your image has been saved to your photos.", preferredStyle: .alert)
  alert.addAction(UIAlertAction(title: "OK", style: .default))
            present(alert, animated: true)
        }
    }

Make sure to call renderImage in saveToPhotosTapGesture.

Figure 3.5.15 Screenshot 2016-10-28 03.58.06.png

Run the project.

Uh oh! A crash, what do we do! Calm down and read the message on the bottom right section.

Figure 3.5.16 Screenshot 2016-10-28 04.00.03.png

All it's saying is that we need to add NSPhotoLibraryUsageDescription key with a string value explaining what we are doing with the screen shot we just made.

Got to the info.plist file in your project.

Figure 3.5.17 Screenshot 2016-10-28 04.02.29.png

Right click on the plist file -> Open As -> Source Code

Figure 3.5.18 Screenshot 2016-10-28 04.04.08.png

Add the following right below <dict>:

<key>NSPhotoLibraryUsageDescription</key>
    <string>Saving photos to camera roll</string>

Figure 3.5.19 Screenshot 2016-10-28 04.08.41.png

Run it and tap the image again. You should see a window pop up asking for permission to access photos. Tap Allow and you will see the alert controller we created telling you it has been saved.

Now go into Photos of your phone and take a look.

Boom! There it is!

IMG_0823.PNG

Wrapping up

As you probably noticed once we finished, gestures do a whole lot for us under the hood. You learned how to make an object bigger or smaller, rotate it, move it around and you even used a tap gesture to save the current photo into your photo library using an image renderer. Just think how far you have come. Keep going and don't give up.