diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index b2af5414..cf708b98 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */; }; 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */; }; 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46463782216FDE4C00E7FF73 /* Assets.xcassets */; }; 46463786216FDE4C00E7FF73 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */; }; @@ -25,6 +26,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterViewController.swift; sourceTree = ""; }; 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LambdaTimeline.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 46463782216FDE4C00E7FF73 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -128,6 +130,7 @@ 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */, 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, + 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -185,7 +188,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1000; - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Lambda School"; TargetAttributes = { 46463777216FDE4B00E7FF73 = { @@ -232,6 +235,7 @@ files = ( 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, + 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */, 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, @@ -285,6 +289,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -310,7 +315,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -346,6 +351,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -365,7 +371,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -382,7 +388,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -401,7 +407,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme index d1c2f96e..8b66f88a 100644 --- a/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme +++ b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme @@ -1,6 +1,6 @@ UIImage? { + guard size.width > 0 && size.height > 0 else { return nil } + + let originalAspectRatio = self.size.width/self.size.height + var correctedSize = size + + if correctedSize.width > correctedSize.width*originalAspectRatio { + correctedSize.width = correctedSize.width*originalAspectRatio + } else { + correctedSize.height = correctedSize.height/originalAspectRatio + } + + return UIGraphicsImageRenderer(size: correctedSize, format: imageRendererFormat).image { context in + draw(in: CGRect(origin: .zero, size: correctedSize)) + } + } + + /// Renders the image if the pixel data was rotated due to orientation of camera + var flattened: UIImage { + if imageOrientation == .up { return self } + return UIGraphicsImageRenderer(size: size, format: imageRendererFormat).image { context in + draw(at: .zero) + } + } +} diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 9e2d25d6..4e1199c4 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -1,10 +1,11 @@ - - + + - + + @@ -13,9 +14,9 @@ - + - + @@ -24,7 +25,7 @@ - + @@ -106,25 +107,25 @@ - + - + - + - + - + - + - + + + + + + + + - + + @@ -170,7 +232,6 @@ - @@ -181,6 +242,9 @@ + + + @@ -191,6 +255,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -198,7 +384,7 @@ - + @@ -215,35 +401,36 @@ - + - - + - + + @@ -257,7 +444,6 @@ - @@ -273,15 +459,15 @@ - + - + - + - + @@ -290,16 +476,16 @@ - + - - + + - + - + + + + diff --git a/LambdaTimeline/View Controllers/ImageFilterViewController.swift b/LambdaTimeline/View Controllers/ImageFilterViewController.swift new file mode 100644 index 00000000..d50a166d --- /dev/null +++ b/LambdaTimeline/View Controllers/ImageFilterViewController.swift @@ -0,0 +1,343 @@ +// +// ImageFilterViewController.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 10/29/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import CoreImage +import CoreImage.CIFilterBuiltins + +enum FilterType: String, CaseIterable { + case none = "No Filter" + case hueAdjust = "Hue Adjust" + case colorMonochrome = "Color Monochrome" + case bumpDistortion = "Bump Distortion" + case twirlDistortion = "Twirl Distortion" + case crystallize = "Crystallize" +} + +protocol FilterVCDelegate: AnyObject { + func updateImage(originalImage: UIImage, filteredImage: UIImage?) +} + +class ImageFilterViewController: UIViewController { + + @IBOutlet private var imageView: UIImageView! + @IBOutlet private var pickerView: UIPickerView! + @IBOutlet private var label1: UILabel! + @IBOutlet private var slider1: UISlider! + @IBOutlet private var label2: UILabel! + @IBOutlet private var slider2: UISlider! + @IBOutlet private var label3: UILabel! + @IBOutlet private var slider3: UISlider! + @IBOutlet private var imageHeightConstraint: NSLayoutConstraint! + + var originalImage: UIImage? + var filteredImage: UIImage? + var scaledImage: CIImage? { + didSet { + updateImage() + } + } + + private let filters = FilterType.allCases.map { $0.rawValue } + private var activeFilter: FilterType = .none + private var touchPoint: CGPoint? + private let context = CIContext() + weak var delegate: FilterVCDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + pickerView.delegate = self + pickerView.dataSource = self + setUpView() + } + + @IBAction func slider1Changed(_ sender: UISlider) { + updateImage() + } + + @IBAction func slider2Changed(_ sender: UISlider) { + updateImage() + } + + @IBAction func slider3Changed(_ sender: UISlider) { + updateImage() + } + + @IBAction func applyFilters(_ sender: UIButton) { + if let originImage = originalImage { + self.delegate?.updateImage(originalImage: originImage, filteredImage: filteredImage) + } + dismiss(animated: true, completion: nil) + } + + private func setUpView() { + if let filteredImage = filteredImage { + imageView.image = filteredImage + var scaledSize = imageView.bounds.size + let scale = imageView.contentScaleFactor + scaledSize.width *= scale + scaledSize.height *= scale + + guard let scaledUIImage = filteredImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + scaledImage = CIImage(image: scaledUIImage) + } else if let originalImage = originalImage { + imageView.image = originalImage + var scaledSize = imageView.bounds.size + let scale = imageView.contentScaleFactor + scaledSize.width *= scale + scaledSize.height *= scale + + guard let scaledUIImage = originalImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + scaledImage = CIImage(image: scaledUIImage) + } + imageView.isUserInteractionEnabled = true + label1.isHidden = true + label2.isHidden = true + label3.isHidden = true + slider1.isHidden = true + slider2.isHidden = true + slider3.isHidden = true + } + + private func setImageViewHeight(with aspectRatio: CGFloat) { + imageHeightConstraint.constant = imageView.frame.size.width * aspectRatio + view.layoutSubviews() + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if let touch = touches.first { + let position = touch.location(in: view) + if imageView.frame.contains(position) { + let xPosition = position.x - imageView.frame.minX + let yPosition = imageView.bounds.height - (position.y - imageView.frame.minY) + touchPoint = CGPoint(x: xPosition, y: yPosition) + updateImage() + } + } + } + + private func updateImage() { + if let scaledImage = scaledImage { + switch activeFilter { + case .hueAdjust: + imageView.image = hueAdjust(byFiltering: scaledImage) + filteredImage = imageView.image + case .colorMonochrome: + imageView.image = colorMonochrome(byFiltering: scaledImage) + filteredImage = imageView.image + case .bumpDistortion: + imageView.image = bumpDistortion(byFiltering: scaledImage) + filteredImage = imageView.image + case .twirlDistortion: + imageView.image = twirlDistortion(byFiltering: scaledImage) + filteredImage = imageView.image + case .crystallize: + imageView.image = crystallize(byFiltering: scaledImage) + filteredImage = imageView.image + default: + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + } + } else { + imageView.image = nil + filteredImage = nil + } + } + + private func noFilterSetUp() { + slider1.isHidden = true + slider2.isHidden = true + slider3.isHidden = true + activeFilter = .none + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + } + + private func hueAdjustSetUp() { + slider1.isHidden = false + slider1.maximumValue = (.pi * 2) + slider1.setValue(.pi, animated: true) + slider2.isHidden = true + slider3.isHidden = true + activeFilter = .hueAdjust + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + updateImage() + } + + private func colorMonochromeSetUp() { + slider1.isHidden = false + slider1.maximumValue = 1 + slider1.setValue(0.75, animated: true) + slider2.isHidden = false + slider2.maximumValue = 1 + slider2.setValue(0, animated: true) + slider3.isHidden = false + slider3.maximumValue = 1 + slider3.setValue(1, animated: true) + activeFilter = .colorMonochrome + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + updateImage() + } + + private func bumpDistortionSetUp() { + slider1.isHidden = false + slider1.maximumValue = 300 + slider1.setValue(150, animated: true) + slider2.isHidden = false + slider2.maximumValue = 1 + slider2.setValue(0.5, animated: true) + slider3.isHidden = true + activeFilter = .bumpDistortion + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + updateImage() + } + + private func twirlDistortionSetUp() { + slider1.isHidden = false + slider1.maximumValue = 200 + slider1.setValue(100, animated: true) + slider2.isHidden = false + slider2.maximumValue = (.pi * 2) + slider2.setValue(.pi, animated: true) + slider3.isHidden = true + activeFilter = .twirlDistortion + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + updateImage() + } + + private func crystallizeSetUp() { + slider1.isHidden = false + slider1.maximumValue = 20 + slider1.setValue(10, animated: true) + slider2.isHidden = true + slider3.isHidden = true + activeFilter = .crystallize + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + updateImage() + } + + private func hueAdjust(byFiltering inputImage: CIImage) -> UIImage? { + let hueAdjustFilter = CIFilter.hueAdjust() + hueAdjustFilter.inputImage = inputImage + hueAdjustFilter.angle = slider1.value + guard let outputImage = hueAdjustFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func colorMonochrome(byFiltering inputImage: CIImage) -> UIImage? { + let colorMonochromeFilter = CIFilter.colorMonochrome() + colorMonochromeFilter.inputImage = inputImage + colorMonochromeFilter.color = CIColor(red: CGFloat(slider1.value), green: CGFloat(slider2.value), blue: CGFloat(slider3.value), alpha: 1) + guard let outputImage = colorMonochromeFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func bumpDistortion(byFiltering inputImage: CIImage) -> UIImage? { + let bumpDistortionFilter = CIFilter.bumpDistortion() + bumpDistortionFilter.inputImage = inputImage + bumpDistortionFilter.radius = slider1.value + bumpDistortionFilter.scale = slider2.value + let centerWidth = CGFloat(inputImage.extent.width / 2) + let centerHeight = CGFloat(inputImage.extent.height / 2) + let center = CGPoint(x: centerWidth, y: centerHeight) + bumpDistortionFilter.center = touchPoint ?? center + guard let outputImage = bumpDistortionFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func twirlDistortion(byFiltering inputImage: CIImage) -> UIImage? { + let twirlDistortionFilter = CIFilter.twirlDistortion() + twirlDistortionFilter.inputImage = inputImage + twirlDistortionFilter.radius = slider1.value + twirlDistortionFilter.angle = slider2.value + let centerWidth = CGFloat(inputImage.extent.width / 2) + let centerHeight = CGFloat(inputImage.extent.height / 2) + let center = CGPoint(x: centerWidth, y: centerHeight) + twirlDistortionFilter.center = touchPoint ?? center + guard let outputImage = twirlDistortionFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func crystallize(byFiltering inputImage: CIImage) -> UIImage? { + let crystallizeFilter = CIFilter.crystallize() + crystallizeFilter.inputImage = inputImage + crystallizeFilter.radius = slider1.value + guard let outputImage = crystallizeFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + +} + +extension ImageFilterViewController: UIPickerViewDelegate, UIPickerViewDataSource { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + filters.count + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + filters[row] + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + switch row { + case 1: + hueAdjustSetUp() + case 2: + colorMonochromeSetUp() + case 3: + bumpDistortionSetUp() + case 4: + twirlDistortionSetUp() + case 5: + crystallizeSetUp() + default: + noFilterSetUp() + } + } + +} diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index 9cffbbbb..9e8899a2 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -11,20 +11,40 @@ import Photos class ImagePostViewController: ShiftableViewController { - @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var titleTextField: UITextField! - @IBOutlet weak var chooseImageButton: UIButton! - @IBOutlet weak var imageHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var postButton: UIBarButtonItem! + @IBOutlet private var imageView: UIImageView! + @IBOutlet private var titleTextField: UITextField! + @IBOutlet private var chooseImageButton: UIButton! + @IBOutlet private var imageHeightConstraint: NSLayoutConstraint! + @IBOutlet private var postButton: UIBarButtonItem! + + @IBOutlet private var filterImageButton: UIButton! + @IBOutlet private var filterAgainButton: UIButton! + @IBOutlet private var clearFiltersButton: UIButton! var postController: PostController! var post: Post? var imageData: Data? + var originalImage: UIImage? { + didSet { + filterImageButton.isHidden = false + } + } + var filteredImage: UIImage? { + didSet { + self.filterImageButton.isHidden = true + self.filterAgainButton.isHidden = false + self.clearFiltersButton.isHidden = false + self.imageView.image = self.filteredImage + } + } + override func viewDidLoad() { super.viewDidLoad() - setImageViewHeight(with: 1.0) + filterImageButton.isHidden = true + filterAgainButton.isHidden = true + clearFiltersButton.isHidden = true } private func presentImagePickerController() { @@ -33,12 +53,12 @@ class ImagePostViewController: ShiftableViewController { presentInformationalAlertController(title: "Error", message: "The photo library is unavailable") return } - - let imagePicker = UIImagePickerController() - imagePicker.delegate = self - imagePicker.sourceType = .photoLibrary - - present(imagePicker, animated: true, completion: nil) + DispatchQueue.main.async { + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.sourceType = .photoLibrary + self.present(imagePicker, animated: true, completion: nil) + } } @IBAction func createPost(_ sender: Any) { @@ -86,8 +106,30 @@ class ImagePostViewController: ShiftableViewController { presentImagePickerController() } - @IBAction func addFilter(_ sender: Any) { - + @IBAction func filterImage(_ sender: UIButton) { + let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let filterVC = storyBoard.instantiateViewController(withIdentifier: "ImageFilterViewController") as! ImageFilterViewController + filterVC.originalImage = originalImage + filterVC.filteredImage = nil + filterVC.delegate = self + self.present(filterVC, animated: true, completion: nil) + } + + @IBAction func filterAgain(_ sender: UIButton) { + let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let filterVC = storyBoard.instantiateViewController(withIdentifier: "ImageFilterViewController") as! ImageFilterViewController + filterVC.originalImage = originalImage + filterVC.filteredImage = filteredImage + filterVC.delegate = self + self.present(filterVC, animated: true, completion: nil) + } + + @IBAction func clearFilters(_ sender: UIButton) { + filteredImage = nil + imageView.image = originalImage + filterImageButton.isHidden = false + filterAgainButton.isHidden = true + clearFiltersButton.isHidden = true } func setImageViewHeight(with aspectRatio: CGFloat) { @@ -109,7 +151,7 @@ extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigation guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } imageView.image = image - + originalImage = image setImageViewHeight(with: image.ratio) } @@ -117,3 +159,10 @@ extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigation picker.dismiss(animated: true, completion: nil) } } + +extension ImagePostViewController: FilterVCDelegate { + func updateImage(originalImage: UIImage, filteredImage: UIImage?) { + self.originalImage = originalImage + self.filteredImage = filteredImage + } +}