< Importing an image into SwiftUI using UIImagePickerController | Customizing our filter using ActionSheet > |
Now that our project has an image the user selected, the next step is to let the user apply varying Core Image filters to it. To start with we’re just going to work with a single filter, but shortly we’ll extend that using an action sheet.
If we want to use Core Image in our apps, we first need to add two imports to the top of ContentView.swift:
import CoreImage
import CoreImage.CIFilterBuiltins
Next we need both a context and a filter. A Core Image context is an object that’s responsible for rendering a CIImage
to a CGImage
, or in more practical terms an object for converting the recipe for an image into an actual series of pixels we can work with. Contexts are expensive to create, so if you intend to render many images it’s a good idea to create a context once and keep it alive. As for the filter, we’ll be using CISepiaTone
as our default but because we’ll make it flexible later we’ll make the filter use @State
so it can be changed.
So, add these two properties to ContentView
:
@State private var currentFilter = CIFilter.sepiaTone()
let context = CIContext()
With those two in place we can now write a method that will process whatever image was imported – that means it will set our sepia filter’s intensity based on the value in filterIntensity
, read the output image back from the filter, ask our CIContext
to render it, then place the result into our image
property so it’s visible on-screen.
func applyProcessing() {
currentFilter.intensity = Float(filterIntensity)
guard let outputImage = currentFilter.outputImage else { return }
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
let uiImage = UIImage(cgImage: cgimg)
image = Image(uiImage: uiImage)
}
}
The next job is to change the way loadImage()
works. Right now that assigns to the image
property, but we don’t want that any more. Instead, it should send whatever image was chosen into the sepia tone filter, then call applyProcessing()
to make the magic happen.
Core Image filters have a dedicated inputImage
property that lets us send in a CIImage
for the filter to work with, but often this is thoroughly broken and will cause your app to crash – it’s much safer to use the filter’s setValue()
method with the key kCIInputImageKey
.
So, replace your existing loadImage()
method with this:
func loadImage() {
guard let inputImage = inputImage else { return }
let beginImage = CIImage(image: inputImage)
currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
applyProcessing()
}
If you run the code now you’ll see our basic app flow works great: we can select an image, then see it with a sepia effect applied. But that intensity slider we added doesn’t do anything, even though it’s bound to the same filterIntensity
value that our filter is reading from.
What’s happening here ought not to be too surprising: even though the slider is changing the value of filterIntensity
, changing that property won’t automatically trigger our applyProcessing()
method again. Instead, we need to do that by hand, and it’s not as easy as just creating a property observer on filterIntensity
because they don’t work well thanks to the @State
property wrapper being used.
Instead, what we need is a custom binding that will return filterIntensity
when it’s read, but when it’s written it will both update filterIntensity
and also call applyProcessing()
so that the latest intensity setting is immediately used in our filter.
Custom bindings that rely on properties of our view need to be created inside the body
property of the view, because Swift doesn’t allow one property to reference another. So, add this just inside the start of the body
property:
let intensity = Binding<Double>(
get: {
self.filterIntensity
},
set: {
self.filterIntensity = $0
self.applyProcessing()
}
)
Important: Now that there is some logic inside the body
property, you must place return
before the NavigationView
, like this: return NavigationView {
.
Now that we have a custom binding, we should attach our slider to that rather than directly to the @State
property, so that changes to the slider will trigger applyProcessing()
.
So, change the slider code to this:
Slider(value: intensity)
Remember, because intensity
is already a binding, we don’t need to use a dollar sign before it – you need to write value: intensity
rather than value: $intensity
.
You can go ahead and run the app now, but be warned: even though Core Image is extremely fast on all iPhones, it’s extremely slow in the simulator. That means you can try it out to make sure everything works, but don’t be surprised if your code runs about as fast as an asthmatic ant carrying a heavy bag of shopping.
SPONSORED Building and maintaining in-app subscription infrastructure is hard. Luckily there's a better way. With RevenueCat, you can implement subscriptions for your app in hours, not months, so you can get back to building your app.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.