The main challenge is to add a number of filters. The problems with that are:
- A filter can have more than one scalar control. This would make it necessary to have more than one slider. How to add these in a generic way? (As opposed to hard coding.)
- A control can have a different name from the three that are hardcoded in the lesson. How to deal with that, preferably in a generic way?
- How to configure a slider for a given filter? One needs a minimum and maximum value, as well as an initial value. Can this be solved in a generic way, or have they to be hard coded for every filter, as seen in the lesson?
- Is there a way to query the system for available filters for use in the "Select A Filter" dialog?
- If so, are all of these useful in the context of this assignment, and if not, what would be a good criterion to include them?
These issues can be addressed as follows.
- This can be solved by replacing the
Slider
with a variable number of SliderView
s, one for each control. A @State
variable is used in every SliderView
to capture the value, a @Binding
variable holding the values for all sliders is used to communicate the value back to the main View, to configure the filter with all control values.
- This point, as well as the following points, can be solved by using the introspection possibilities of
CIFilter
. These can detect many properties of a given filter. For this particular point, there is a way of obtaining the names of all controls for a given filter: the inputKeys
property of the filter. These can be used to label the sliders, and to feed their values back into the filter.
- Some
CIFilters
have properties to show the (suggested) minimum and maximum values for a scalar control, as wel as a default value. These can be used to configure each slider for the filter.
- Yes, one can ask the system for a list of all available filters. But not all of these are appropriate here, see the following point.
- I found that there are controls not only for scalars, but also for point, colors, and many other properties. Only filters with controls of a scalar type are easily configurable by a slider; also, it is not very useful to include filters that have, beside scalar controls, also controls of a non-scalar type, since sliders alone could not fully configure them. There are many of these. So I only consider filters that have scalar controls and no others. In addition, they must have information on minimum, maximum and default values as in point 3 above.
Following is my code. The code to retrieve filter properties is a bit convoluted, so I put many convenience functions on CIFilter in an extension.
All comments and suggestions for improvement are welcome.
ContentView.swift
(Yes I know not a very informative name but for this exercise good enough...):
import SwiftUI
import CoreImage
import CoreImage.CIFilterBuiltins
struct ControlValues: Equatable {
var values = [String : Double]()
}
struct ContentView: View {
@State private var image: Image? {
didSet {
imageProcessEnabled = false
}
}
@State private var controlValues = ControlValues()
@State private var inputImage: UIImage?
@State private var processedImage: UIImage?
@State private var filterIntensity = 0.5
@State private var showingImagePicker = false
@State private var currentFilter: CIFilter = CIFilter.sepiaTone()
@State private var showingFilterSheet = false
@State private var imageProcessEnabled = true
let context = CIContext()
var body: some View {
NavigationView {
VStack {
ZStack {
Rectangle()
.fill(.secondary)
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
image?
.resizable()
.scaledToFit()
}
.onTapGesture {
showingImagePicker = true
}
ForEach(currentFilter.sliderControls, id: \.self) { control in
SliderView(control: control,
displayName: currentFilter.displayName(for: control),
minSliderValue: currentFilter.minSliderValue(for: control),
maxSliderValue: currentFilter.maxSliderValue(for: control),
actualValue: currentFilter.scalarDefaultValue(for: control),
filterValues: $controlValues)
.padding(.trailing)
}
.onChange(of: controlValues.values) { _ in applyProcessing() }
HStack {
Button("Change Filter (now: \(currentFilter.displayName)))") {
showingFilterSheet = true
}
Spacer()
Button("Save", action: save)
.disabled(imageProcessEnabled)
}
}
.padding([.horizontal, .vertical])
.navigationTitle("Instafilter")
.sheet(isPresented: $showingImagePicker) {
ImagePicker(image: $inputImage)
}
.confirmationDialog("Select a filter", isPresented: $showingFilterSheet) {
ForEach(CIFilter.sliderControlledFilterNames, id: \.self) { filterName in
let filter = CIFilter(name: filterName)!
Button(filter.displayName) { setFilter(filter) }
}
}
.onChange(of: inputImage) { _ in loadImage()}
}
}
func save() {
guard let processedImage = processedImage else { return }
let imageSaver = ImageSaver()
imageSaver.successHandler = {
print("Success!")
}
imageSaver.errorHandler = {
print("Oops: \($0.localizedDescription)")
}
imageSaver.writeToPhotoAlbum(image: processedImage)
}
func loadImage() {
guard let inputImage = inputImage else { return }
let beginImage = CIImage(image: inputImage)
currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
applyProcessing()
}
func applyProcessing() {
let controls = currentFilter.sliderControls
print("Controls: \(controls)")
for control in controls {
currentFilter.setValue(controlValues.values[control], forKey: control)
}
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)
processedImage = uiImage
}
}
func setFilter(_ filter: CIFilter) {
currentFilter = filter
loadImage()
}
}
The extension file CIFilterProperties.swift
:
import Foundation
import CoreImage
extension CIFilter {
private static var scalarControlledFilters: [CIFilter] {
var filterNames = CIFilter.filterNames(inCategories: nil)
for name in filterNames {
let filter = CIFilter(name: name)!
let controls = filter.inputKeys.filter {
$0 != "inputImage"
}
for control in controls {
if filter.attributeClass(for: control) != "NSNumber" {
if let index = filterNames.firstIndex(of: name) {
filterNames.remove(at: index)
}
continue
}
}
}
return filterNames.map({ CIFilter(name: $0)! })
}
static var sliderControlledFilters: [CIFilter] {
let filters = scalarControlledFilters
return filters.filter({
$0.sliderControls != []
})
}
static var sliderControlledFilterNames: [String] {
return sliderControlledFilters.map( { $0.fullName } )
}
var fullName: String {
attributes["CIAttributeFilterName"] as! String
}
var displayName: String {
if let name = attributes["CIAttributeFilterDisplayName"] {
return name as! String
} else {
return self.fullName
}
}
var sliderControls: [String] {
inputKeys.filter({ control in
guard controlsDict(for: control)["CIAttributeClass"] as? String? != nil else {
return false
}
return optionalMinSliderValue(for: control) != nil &&
optionalMaxSliderValue(for: control) != nil &&
optionalScalarDefaultValue(for: control) != nil &&
inputKeys.contains("inputImage")
})
}
func displayName(for control: String) -> String {
if let result = controlsDict(for: control)["CIAttributeDisplayName"] {
return result as! String
} else {
return control
}
}
func optionalMinSliderValue(for control: String) -> Double? {
if let result = controlsDict(for: control)["CIAttributeSliderMin"] {
return (result as! Double)
} else {
return nil
}
}
func minSliderValue(for control: String) -> Double {
return (controlsDict(for: control)["CIAttributeSliderMin"] as! Double)
}
func optionalMaxSliderValue(for control: String) -> Double? {
if let result = controlsDict(for: control)["CIAttributeSliderMax"] {
if let result = result as? Double {
return result
} else {
return nil
}
} else {
return nil
}
}
func maxSliderValue(for control: String) -> Double {
return (controlsDict(for: control)["CIAttributeSliderMax"] as! Double)
}
func optionalScalarDefaultValue(for control: String) -> Double? {
let result = controlsDict(for: control)["CIAttributeDefault"]
if result != nil && attributeClass(for: control) == "NSNumber" {
return (result as! Double)
} else {
return nil
}
}
func scalarDefaultValue(for control: String) -> Double {
return (controlsDict(for: control)["CIAttributeDefault"] as! Double)
}
func attributeClass(for control: String) -> String {
if let result = controlsDict(for: control)["CIAttributeClass"] {
return result as! String
} else {
return "No class info"
}
}
private func controlsDict(for control: String) -> [String : Any] {
attributes[control] as! [String : Any]
}
}
The new SliderView.swift
:
import SwiftUI
struct SliderView: View {
let control: String
let displayName: String
let minSliderValue: Double
let maxSliderValue: Double
@State var actualValue: Double
@Binding var filterValues: ControlValues
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(displayName)
.padding(.horizontal)
Slider(value: $actualValue, in: minSliderValue...maxSliderValue)
.onChange(of: actualValue) { _ in
filterValues.values[control] = actualValue
}
}
}
.onAppear {
filterValues.values[control] = actualValue
}
}
}
For the record, ImageSaver.swift
and ImagePicker.swift
, unchanged from the lesson:
import UIKit
class ImageSaver: NSObject {
var successHandler: (() -> Void)?
var errorHandler: ((Error) -> Void)?
func writeToPhotoAlbum(image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveCompleted), nil)
}
@objc func saveCompleted(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
errorHandler?(error)
} else {
successHandler?()
}
}
}
import PhotosUI
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.filter = .images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { image, _ in
self.parent.image = image as? UIImage
}
}
}
}
}