|
|
@@ -0,0 +1,219 @@ |
|
|
// |
|
|
// ContentView.swift |
|
|
// Instafilter |
|
|
// |
|
|
// Created by Philipp on 18.04.20. |
|
|
// Copyright © 2020 Philipp. All rights reserved. |
|
|
// |
|
|
|
|
|
import SwiftUI |
|
|
import CoreImage |
|
|
import CoreImage.CIFilterBuiltins |
|
|
|
|
|
struct ContentView: View { |
|
|
@State private var inputImage: UIImage? |
|
|
@State private var processedImage: UIImage? |
|
|
@State private var image: Image? |
|
|
@State private var filterIntensity = 0.5 |
|
|
@State private var filterRadius = 100.0 |
|
|
@State private var filterScale = 20.0 |
|
|
@State private var showingImagePicker = false |
|
|
@State private var showingFilterSheet = false |
|
|
|
|
|
@State private var showAlert = false |
|
|
@State private var alertTitle = "" |
|
|
@State private var alertMessage = "" |
|
|
|
|
|
@State private var currentFilter : CIFilter = CIFilter.sepiaTone() |
|
|
let context = CIContext() |
|
|
|
|
|
let operationQueue = OperationQueue() |
|
|
|
|
|
init() { |
|
|
operationQueue.maxConcurrentOperationCount = 1 |
|
|
} |
|
|
|
|
|
var body: some View { |
|
|
let intensity = Binding<Double>( |
|
|
get: { |
|
|
self.filterIntensity |
|
|
}, |
|
|
set: { |
|
|
self.filterIntensity = $0 |
|
|
self.applyProcessing() |
|
|
} |
|
|
) |
|
|
|
|
|
let radius = Binding<Double>( |
|
|
get: { |
|
|
self.filterRadius |
|
|
}, |
|
|
set: { |
|
|
self.filterRadius = $0 |
|
|
self.applyProcessing() |
|
|
} |
|
|
) |
|
|
|
|
|
let scale = Binding<Double>( |
|
|
get: { |
|
|
self.filterScale |
|
|
}, |
|
|
set: { |
|
|
self.filterScale = $0 |
|
|
self.applyProcessing() |
|
|
} |
|
|
) |
|
|
|
|
|
return NavigationView { |
|
|
VStack { |
|
|
ZStack { |
|
|
Rectangle() |
|
|
.fill(Color.secondary) |
|
|
|
|
|
if image != nil { |
|
|
image? |
|
|
.resizable() |
|
|
.scaledToFit() |
|
|
} else { |
|
|
Text("Tap to select a picture") |
|
|
.foregroundColor(.white) |
|
|
.font(.headline) |
|
|
} |
|
|
} |
|
|
.onTapGesture { |
|
|
self.showingImagePicker = true |
|
|
} |
|
|
|
|
|
if self.hasIntensity { |
|
|
HStack { |
|
|
Text("Intensity") |
|
|
Slider(value: intensity) |
|
|
}.padding(.vertical) |
|
|
} |
|
|
|
|
|
if self.hasRadius { |
|
|
HStack { |
|
|
Text("Radius") |
|
|
Slider(value: radius, in: 0...200) |
|
|
}.padding(.vertical) |
|
|
} |
|
|
|
|
|
if self.hasScale { |
|
|
HStack { |
|
|
Text("Scale") |
|
|
Slider(value: scale, in: 0...100) |
|
|
}.padding(.vertical) |
|
|
} |
|
|
|
|
|
HStack { |
|
|
Button("Change Filter") { |
|
|
self.showingFilterSheet = true |
|
|
} |
|
|
|
|
|
Spacer() |
|
|
|
|
|
Button("Save") { |
|
|
guard let processedImage = self.processedImage else { |
|
|
self.showAlert = true |
|
|
self.alertTitle = "Cannot save" |
|
|
self.alertMessage = "No image has been processed yet." |
|
|
return |
|
|
} |
|
|
|
|
|
let imageSaver = ImageSaver() |
|
|
|
|
|
imageSaver.successHandler = { |
|
|
print("Success!") |
|
|
} |
|
|
|
|
|
imageSaver.errorHandler = { |
|
|
print("Oops: \($0.localizedDescription)") |
|
|
} |
|
|
|
|
|
imageSaver.writeToPhotoAlbum(image: processedImage) |
|
|
} |
|
|
} |
|
|
} |
|
|
.padding([.horizontal, .bottom]) |
|
|
.navigationBarTitle("Instafilter") |
|
|
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) { |
|
|
ImagePicker(image: self.$inputImage) |
|
|
} |
|
|
.alert(isPresented: $showAlert) { |
|
|
Alert(title: Text(self.alertTitle), message: Text(self.alertMessage), dismissButton: .default(Text("OK"))) |
|
|
} |
|
|
.actionSheet(isPresented: self.$showingFilterSheet) { |
|
|
ActionSheet(title: Text("Select a filter (current '\(CIFilter.localizedName(forFilterName: self.currentFilter.name) ?? "Unknown Filter"))'"), buttons: [ |
|
|
.default(Text("Crystallize")) { self.setFilter(CIFilter.crystallize()) }, |
|
|
.default(Text("Edges")) { self.setFilter(CIFilter.edges()) }, |
|
|
.default(Text("Gaussian Blur")) { self.setFilter(CIFilter.gaussianBlur()) }, |
|
|
.default(Text("Pixellate")) { self.setFilter(CIFilter.pixellate()) }, |
|
|
.default(Text("Sepia Tone")) { self.setFilter(CIFilter.sepiaTone()) }, |
|
|
.default(Text("Unsharp Mask")) { self.setFilter(CIFilter.unsharpMask()) }, |
|
|
.default(Text("Vignette")) { self.setFilter(CIFilter.vignette()) }, |
|
|
.cancel() |
|
|
]) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
func loadImage() { |
|
|
guard let inputImage = inputImage else { return } |
|
|
|
|
|
let beginImage : CIImage |
|
|
if let ciImage = inputImage.ciImage { |
|
|
beginImage = ciImage |
|
|
} |
|
|
else { |
|
|
beginImage = CIImage(cgImage: inputImage.cgImage!).oriented(CGImagePropertyOrientation(inputImage.imageOrientation)) |
|
|
} |
|
|
|
|
|
currentFilter.setValue(beginImage, forKey: kCIInputImageKey) |
|
|
applyProcessing() |
|
|
} |
|
|
|
|
|
func setFilter(_ filter: CIFilter) { |
|
|
currentFilter = filter |
|
|
loadImage() |
|
|
} |
|
|
|
|
|
func applyProcessing() { |
|
|
if hasIntensity { currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey) } |
|
|
if hasRadius { currentFilter.setValue(filterRadius, forKey: kCIInputRadiusKey) } |
|
|
if hasScale { currentFilter.setValue(filterScale, forKey: kCIInputScaleKey) } |
|
|
|
|
|
guard let outputImage = currentFilter.outputImage else { return } |
|
|
|
|
|
operationQueue.cancelAllOperations() |
|
|
let operation = BlockOperation() |
|
|
operation.addExecutionBlock { |
|
|
if let cgimg = self.context.createCGImage(outputImage, from: outputImage.extent) { |
|
|
let uiImage = UIImage(cgImage: cgimg) |
|
|
DispatchQueue.main.async { |
|
|
self.image = Image(uiImage: uiImage) |
|
|
self.processedImage = uiImage |
|
|
} |
|
|
} |
|
|
} |
|
|
operationQueue.addOperation(operation) |
|
|
} |
|
|
|
|
|
var hasIntensity: Bool { |
|
|
currentFilter.inputKeys.contains(kCIInputIntensityKey) |
|
|
} |
|
|
|
|
|
var hasRadius: Bool { |
|
|
currentFilter.inputKeys.contains(kCIInputRadiusKey) |
|
|
} |
|
|
|
|
|
var hasScale: Bool { |
|
|
currentFilter.inputKeys.contains(kCIInputScaleKey) |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
struct ContentView_Previews: PreviewProvider { |
|
|
static var previews: some View { |
|
|
ContentView() |
|
|
} |
|
|
} |