Day 53 - 100 Days of Swift

5 minute read

Project 13 (part 2)

Day 53 is the second part of the thirteenth project. Today you add the ability for the user to filter the chosen photo in various ways and then to save it to their photo library.

First, you import CoreImage , add variables to hold a context and the currently selected filter, and initialize them in viewDidLoad:

1
2
3
4
5
6
7
8
9
10
// Above the class
import CoreImage

// In the class
var context: CIContext!
var currentFilter: CIFilter!

// In viewDidLoad
context = CIContext()
currentFilter = CIFilter(name: "CISepiaTone")

Then you set the filter’s input image at the end of didFinishPickingMediaWithInfo:

1
2
3
4
5
let beginImage = CIImage(image: currentImage)
currentFilter.setValue(beginImage,
                       forKey: kCIInputImageKey)

applyProcessing()

You also call applyProcessing in the slider’s action, so that the processing is re-applied every time the user changes the value of the slider:

1
2
3
@IBAction func intensityChanged(_ sender: Any) {
    applyProcessing()
}

Then you actually write the applyProcessing method:

1
2
3
4
5
6
7
8
9
10
11
func applyProcessing() {
    guard let image = currentFilter.outputImage else { return }
    currentFilter.setValue(intensitySlider.value,
                           forKey: kCIInputIntensityKey)

    if let cgimg = context.createCGImage(image,
                                         from: image.extent) {
        let processedImage = UIImage(cgImage: cgimg)
        imageView.image = processedImage
    }
}

This confirms that there is an image, sets the intensity value of the filter, actually creates a CGImage from the context, makes a UIImage out of that, and then displays it in the imageView.

Next, present an alert with all the filter options when the user taps on the “Change Filter” button. He has you add all the actions to the alert controller individually, but I just wrote a list and used a loop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private let filters = [
               "CIBumpDistortion",
               "CIGaussianBlur",
               "CIPixellate",
               "CISepiaTone",
               "CITwirlDistortion",
               "CIUnsharpMask",
               "CIVignette",
               ]

// In changeFilter
let alertController = UIAlertController(title: "Choose Filter",
                                        message: nil,
                                        preferredStyle: .actionSheet)
for filter in filters {
    let action = UIAlertAction(title: filter,
                               style: .default,
                               handler: setFilter)
    alertController.addAction(action)
}

alertController.add("Cancel")

present(alertController, animated: true)

I also added a simple little extension on UIAlertController because I’m tried of writing multiple lines of code to add a cancel button to an alert:

1
2
3
4
5
6
7
8
extension UIAlertController {
    func add(_ title: String,
             style: UIAlertAction.Style = .cancel) {
        let action = UIAlertAction(title: title,
                                   style: style)
        self.addAction(action)
    }
}

Then you actually add setFilter():

1
2
3
4
5
6
7
8
9
10
11
12
13
func setFilter(action: UIAlertAction) {
    guard currentImage != nil else { return }

    guard let actionTitle = action.title else { return }

    currentFilter = CIFilter(name: actionTitle)

    let beginImage = CIImage(image: currentImage)
    currentFilter.setValue(beginImage,
                           forKey: kCIInputImageKey)

    applyProcessing()
}

This makes sure you have an image, it unwraps the title of the action, it tries to make a filter from the name (it fails if you spelt something wrong), then it sets the input image for the newly created filter and calls applyProcessing.

The only problem now is that not all of the filters have an intensity key, which is what you are setting in applyProcessing, so you have to make some updates to account for that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func applyProcessing() {
    guard let image = currentFilter.outputImage else { return }
    let inputKeys = currentFilter.inputKeys

    if inputKeys.contains(kCIInputIntensityKey) {
        currentFilter.setValue(intensitySlider.value,
                               forKey: kCIInputIntensityKey) }
    if inputKeys.contains(kCIInputRadiusKey) {
        currentFilter.setValue(intensitySlider.value * 200,
                               forKey: kCIInputRadiusKey) }
    if inputKeys.contains(kCIInputScaleKey) {
        currentFilter.setValue(intensitySlider.value * 10,
                               forKey: kCIInputScaleKey) }
    if inputKeys.contains(kCIInputCenterKey) {
        currentFilter.setValue(CIVector(x: currentImage.size.width / 2,
                                        y: currentImage.size.height / 2),
                               forKey: kCIInputCenterKey) }

    if let cgimg = context.createCGImage(image,
                                         from: image.extent) {
        let processedImage = UIImage(cgImage: cgimg)
        imageView.image = processedImage
    }
}

This was all his math, but with a little fiddling around it becomes clear what most of these are doing.

With that, all the filters work. All that is left is to save the image back to the photo library. To do that, you add some stuff to the save method:

1
2
3
4
5
@IBAction func save(_ sender: Any) {
    guard let image = imageView.image else { return }

    UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}

And then write image(_:didFinishSaveWithError:contextInfo:):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@objc func image(_ image: UIImage,
                 didFinishSavingWithError error: Error?,
                 contextInfo: UnsafeRawPointer) {

    if let error = error {
        let alertController = UIAlertController(title: "Save Error",
                                                message: error.localizedDescription,
                                                preferredStyle: .alert)
        alertController.add("Ok",
                            style: .default)
        present(alertController,
                animated: true)
    } else {
        let alertController = UIAlertController(title: "Saved!",
                                                message: "Your altered image has been saved to your photos.",
                                                preferredStyle: .alert)
        alertController.add("Ok",
                            style: .default)
        present(alertController,
                animated: true)
    }
}

That checks for an error and if it finds one, it presents the user with an alert that shows the localized error. If it doesn’t find an error, it lets the user know that their photo was saved successfully. And again, here is an opportunity for me to use my extension on UIAlertController. (Paul really likes alerts, apparently.) Here’s what it looks like:

Screenshots of working app with unedited photo and action sheet
Screenshots of working app with edited photos using sepia, unsharp mask and gaussian blur filters.

You can find my version of this project at the end of day 53 on Github here.