diff --git a/CHANGELOG.md b/CHANGELOG.md index 98420fd..73f4e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ All notable changes to this project will be documented in this file. - Localization is broken, discuss at https://github.com/fulldecent/FDTake/pull/99 +## [2.0.3](https://github.com/fulldecent/FDBarGuage/compare/2.0.3) + +#### Updated + +- Add ImageCrop ViewController (with custom aspect ratio) + ## [2.0.2](https://github.com/fulldecent/FDBarGuage/compare/2.0.2) #### Updated diff --git a/FDTake.podspec b/FDTake.podspec index 625ba97..ccd1c07 100644 --- a/FDTake.podspec +++ b/FDTake.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FDTake" - s.version = "2.0.2" - s.summary = "Easily take a photo or video or choose from library" + s.version = "2.0.3" + s.summary = "Easily take a photo or video or choose from library. Also user can personalize edit cropping" s.description = <<-DESC `FDTake` helps you quickly have the user take or choose an existing photo or video. DESC @@ -13,6 +13,6 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/fulldecent' s.platform = :ios, '10.0' s.requires_arc = true - + s.swift_version = '4.2' s.source_files = 'Source/**/*' end diff --git a/FDTake.xcodeproj/project.pbxproj b/FDTake.xcodeproj/project.pbxproj index d662089..e970fe4 100644 --- a/FDTake.xcodeproj/project.pbxproj +++ b/FDTake.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + D07FC161218E31EF00310E7D /* ImageCropper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07FC15F218E31EE00310E7D /* ImageCropper.swift */; }; + D07FC162218E31EF00310E7D /* UIImage+FixOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07FC160218E31EF00310E7D /* UIImage+FixOrientation.swift */; }; D95E1BE71D88D63400F94976 /* FDTake.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D95E1BDD1D88D63300F94976 /* FDTake.framework */; }; D95E1BEC1D88D63400F94976 /* FDTakeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D95E1BEB1D88D63400F94976 /* FDTakeTests.swift */; }; D95E1BEE1D88D63400F94976 /* FDTake.h in Headers */ = {isa = PBXBuildFile; fileRef = D95E1BE01D88D63300F94976 /* FDTake.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -25,9 +27,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + D07FC15F218E31EE00310E7D /* ImageCropper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCropper.swift; sourceTree = ""; }; + D07FC160218E31EF00310E7D /* UIImage+FixOrientation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+FixOrientation.swift"; sourceTree = ""; }; + D0BA674D2235B5BF003B30D9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D95E1BDD1D88D63300F94976 /* FDTake.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FDTake.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D95E1BE01D88D63300F94976 /* FDTake.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FDTake.h; sourceTree = ""; }; - D95E1BE11D88D63300F94976 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D95E1BE61D88D63400F94976 /* FDTakeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FDTakeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D95E1BEB1D88D63400F94976 /* FDTakeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FDTakeTests.swift; sourceTree = ""; }; D95E1BED1D88D63400F94976 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -72,9 +76,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D0BA674C2235B5BF003B30D9 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D0BA674D2235B5BF003B30D9 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; D95E1BD31D88D63200F94976 = { isa = PBXGroup; children = ( + D0BA674C2235B5BF003B30D9 /* Supporting Files */, D95E1BDF1D88D63300F94976 /* Source */, D95E1BEA1D88D63400F94976 /* Tests */, D95E1BDE1D88D63300F94976 /* Products */, @@ -93,9 +106,10 @@ D95E1BDF1D88D63300F94976 /* Source */ = { isa = PBXGroup; children = ( + D07FC15F218E31EE00310E7D /* ImageCropper.swift */, + D07FC160218E31EF00310E7D /* UIImage+FixOrientation.swift */, D9D7F7B81D8DAC4F00A0DA5B /* FDTakeController.swift */, D95E1BE01D88D63300F94976 /* FDTake.h */, - D95E1BE11D88D63300F94976 /* Info.plist */, D983D7531D8DDA0700E4223A /* Localizable.strings */, ); path = Source; @@ -241,7 +255,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D07FC162218E31EF00310E7D /* UIImage+FixOrientation.swift in Sources */, D9D7F7B91D8DAC4F00A0DA5B /* FDTakeController.swift in Sources */, + D07FC161218E31EF00310E7D /* ImageCropper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -350,6 +366,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -405,6 +422,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -421,10 +439,10 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Source/Info.plist; + INFOPLIST_FILE = "Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "net.phor.--PROJECT-NAME--"; + PRODUCT_BUNDLE_IDENTIFIER = net.phor.FDTake; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -441,10 +459,10 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Source/Info.plist; + INFOPLIST_FILE = "Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "net.phor.--PROJECT-NAME--"; + PRODUCT_BUNDLE_IDENTIFIER = net.phor.FDTake; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 4.2; diff --git a/README.md b/README.md index 3ed053f..5616458 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ Easily take a photo or video or choose from library + +**Also**, you can edit the taken photos and crop them within your needs (defining custom crop aspect ratio). This functionality was taken from **jvk75's** [UIImageCropper](https://github.com/jvk75/UIImageCropper) + **:beer: Author's tip jar: https://amazon.com/hz/wishlist/ls/EE78A23EEGQB** ## Usage @@ -51,6 +54,9 @@ open var allowsSelectFromLibrary: Bool /// Whether to allow editing the media after capturing/selection open var allowsEditing: Bool +/// Aspect ratio when cropping a photo +open var aspectRatio: CGFloat = 4/3 + /// Whether to use full screen camera preview on the iPad open var iPadUsesFullScreenCamera: Bool diff --git a/Source/FDTakeController.swift b/Source/FDTakeController.swift index 9dd2042..52e721b 100644 --- a/Source/FDTakeController.swift +++ b/Source/FDTakeController.swift @@ -53,7 +53,7 @@ open class FDTakeController: NSObject /* , UIImagePickerControllerDelegate, UINa // MARK: - Initializers & Class Convenience Methods /// Convenience method for getting a photo - open class func getPhotoWithCallback(getPhotoWithCallback callback: @escaping (_ photo: UIImage, _ info: [AnyHashable: Any]) -> Void) { + open class func getPhotoWithCallback(getPhotoWithCallback callback: @escaping (_ photo: UIImage, _ info: [AnyHashable: Any]?) -> Void) { let fdTake = FDTakeController() fdTake.allowsVideo = false fdTake.didGetPhoto = callback @@ -109,11 +109,12 @@ open class FDTakeController: NSObject /* , UIImagePickerControllerDelegate, UINa return UIApplication.shared.keyWindow!.rootViewController! }() + open var aspectRatio: CGFloat = 4/3 // MARK: - Callbacks /// A photo was selected - open var didGetPhoto: ((_ photo: UIImage, _ info: [AnyHashable: Any]) -> Void)? + open var didGetPhoto: ((_ photo: UIImage, _ info: [AnyHashable: Any]?) -> Void)? /// A video was selected open var didGetVideo: ((_ video: URL, _ info: [AnyHashable: Any]) -> Void)? @@ -149,15 +150,16 @@ open class FDTakeController: NSObject /* , UIImagePickerControllerDelegate, UINa open var takeVideoText: String? = nil - // MARK: - Private + internal var info: [AnyHashable: Any]? + + internal lazy var imagePicker: UIImagePickerController = { + let picker = UIImagePickerController() + picker.delegate = self + picker.allowsEditing = false + return picker + }() - private lazy var imagePicker: UIImagePickerController = { - [unowned self] in - let retval = UIImagePickerController() - retval.delegate = self - retval.allowsEditing = true - return retval - }() + // MARK: - Private private var alertController: UIAlertController? = nil @@ -206,6 +208,38 @@ open class FDTakeController: NSObject /* , UIImagePickerControllerDelegate, UINa } } + fileprivate func startImagePicker(_ title: FDTakeControllerLocalizableStrings, _ source: UIImagePickerController.SourceType) { + + + self.imagePicker.sourceType = source + if source == .camera && self.defaultsToFrontCamera && UIImagePickerController.isCameraDeviceAvailable(.front) { + self.imagePicker.cameraDevice = .front + } + // set the media type: photo or video + self.imagePicker.allowsEditing = false + var mediaTypes = [String]() + if self.allowsPhoto { + mediaTypes.append(String(kUTTypeImage)) + } + if self.allowsVideo { + mediaTypes.append(String(kUTTypeMovie)) + } + self.imagePicker.mediaTypes = mediaTypes + + //TODO: Need to encapsulate popover code + var popOverPresentRect: CGRect = self.presentingRect ?? CGRect(x: 0, y: 0, width: 1, height: 1) + if popOverPresentRect.size.height == 0 || popOverPresentRect.size.width == 0 { + popOverPresentRect = CGRect(x: 0, y: 0, width: 1, height: 1) + } + + if !(UI_USER_INTERFACE_IDIOM() == .phone || (source == .camera && self.iPadUsesFullScreenCamera)) { + // On iPad use pop-overs. + self.imagePicker.modalPresentationStyle = .popover + self.imagePicker.popoverPresentationController!.sourceView = self.presentingView! + self.imagePicker.popoverPresentationController!.permittedArrowDirections = .any + } + + } /// Presents the user with an option to take a photo or choose a photo from the library open func present() { @@ -239,7 +273,7 @@ open class FDTakeController: NSObject /* , UIImagePickerControllerDelegate, UINa // http://stackoverflow.com/a/34487871/300224 let alertWindow = UIWindow(frame: UIScreen.main.bounds) alertWindow.rootViewController = UIViewController() - alertWindow.windowLevel = UIWindow.Level.alert + 1; + alertWindow.windowLevel = UIWindow.Level.alert + 1 alertWindow.makeKeyAndVisible() alertWindow.rootViewController?.present(alert, animated: true, completion: nil) return @@ -254,36 +288,9 @@ open class FDTakeController: NSObject /* , UIImagePickerControllerDelegate, UINa for (title, source) in titleToSource { let action = UIAlertAction(title: localizeString(title), style: .default) { (UIAlertAction) -> Void in - self.imagePicker.sourceType = source - if source == .camera && self.defaultsToFrontCamera && UIImagePickerController.isCameraDeviceAvailable(.front) { - self.imagePicker.cameraDevice = .front - } - // set the media type: photo or video - self.imagePicker.allowsEditing = self.allowsEditing - var mediaTypes = [String]() - if self.allowsPhoto { - mediaTypes.append(String(kUTTypeImage)) - } - if self.allowsVideo { - mediaTypes.append(String(kUTTypeMovie)) - } - self.imagePicker.mediaTypes = mediaTypes - - //TODO: Need to encapsulate popover code - var popOverPresentRect: CGRect = self.presentingRect ?? CGRect(x: 0, y: 0, width: 1, height: 1) - if popOverPresentRect.size.height == 0 || popOverPresentRect.size.width == 0 { - popOverPresentRect = CGRect(x: 0, y: 0, width: 1, height: 1) - } + self.startImagePicker(title,source) let topVC = self.topViewController(rootViewController: self.presentingViewController) - - if UI_USER_INTERFACE_IDIOM() == .phone || (source == .camera && self.iPadUsesFullScreenCamera) { - topVC.present(self.imagePicker, animated: true, completion: nil) - } else { - // On iPad use pop-overs. - self.imagePicker.modalPresentationStyle = .popover - self.imagePicker.popoverPresentationController?.sourceRect = popOverPresentRect - topVC.present(self.imagePicker, animated: true, completion: nil) - } + topVC.present(self.imagePicker, animated: true, completion: nil) } alertController!.addAction(action) } @@ -315,10 +322,10 @@ open class FDTakeController: NSObject /* , UIImagePickerControllerDelegate, UINa extension FDTakeController : UIImagePickerControllerDelegate, UINavigationControllerDelegate { /// Conformance for ImagePicker delegate - public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info:[UIImagePickerController.InfoKey : Any]) { // Local variable inserted by Swift 4.2 migrator. let info = convertFromUIImagePickerControllerInfoKeyDictionary(info) - + UIApplication.shared.isStatusBarHidden = true let mediaType: String = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaType)] as! String var imageToSave: UIImage @@ -332,15 +339,37 @@ extension FDTakeController : UIImagePickerControllerDelegate, UINavigationContro self.didCancel?() return } - self.didGetPhoto?(imageToSave, info) - if UI_USER_INTERFACE_IDIOM() == .pad { - self.imagePicker.dismiss(animated: true) + + + + if self.allowsEditing { + let cropper = UIImageCropper(cropRatio: self.aspectRatio) + cropper.cropButtonText = "Crop" // button labes can be localised/changed + cropper.cancelButtonText = "Cancel" + + cropper.image = imageToSave.fixOrientation() + cropper.delegate = self + + + picker.present(cropper, animated: true, completion: nil) + + + self.info = info + } else { + self.didGetPhoto?(imageToSave, info) + + if UI_USER_INTERFACE_IDIOM() == .pad { + self.imagePicker.dismiss(animated: true) + } + + picker.dismiss(animated: true, completion: nil) } + + } else if mediaType == kUTTypeMovie as String { self.didGetVideo?(info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaURL)] as! URL, info) } - picker.dismiss(animated: true, completion: nil) } /// Conformance for image picker delegate @@ -349,15 +378,28 @@ extension FDTakeController : UIImagePickerControllerDelegate, UINavigationContro picker.dismiss(animated: true, completion: nil) self.didDeny?() } - // Helper function inserted by Swift 4.2 migrator. private func convertFromUIImagePickerControllerInfoKeyDictionary(_ input: [UIImagePickerController.InfoKey: Any]) -> [String: Any] { return Dictionary(uniqueKeysWithValues: input.map {key, value in (key.rawValue, value)}) } - + // Helper function inserted by Swift 4.2 migrator. private func convertFromUIImagePickerControllerInfoKey(_ input: UIImagePickerController.InfoKey) -> String { return input.rawValue } + +} + +extension FDTakeController: UIImageCropperProtocol { + public func didCropImage(originalImage: UIImage?, croppedImage: UIImage?) { + + if let image = croppedImage { + self.didGetPhoto?(image, self.info) + } else { + self.didFail?() + } + self.dismiss() + } + } diff --git a/Source/ImageCropper.swift b/Source/ImageCropper.swift new file mode 100644 index 0000000..5873db1 --- /dev/null +++ b/Source/ImageCropper.swift @@ -0,0 +1,293 @@ +// +// UIImageCropper.swift +// UIImageCropper +// +// Created by Jari Kalinainen jari@klubitii.com +// +// Licensed under MIT License 2017 +// + +import UIKit + +@objc public protocol UIImageCropperProtocol: class { + /// Called when user presses crop button (or when there is unknown situation (one or both images will be nil)). + /// - parameter originalImage + /// Orginal image from camera/gallery + /// - parameter croppedImage + /// Cropped image in cropRatio aspect ratio + func didCropImage(originalImage: UIImage?, croppedImage: UIImage?) +} + +public class UIImageCropper: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + var presenting = false + /// Aspect ratio of the cropped image + public var cropRatio: CGFloat = 1 + /// delegate that implements UIImageCropperProtocol + public weak var delegate: UIImageCropperProtocol? + + /// Crop button text + public var cropButtonText: String = "Crop" + /// Retake/Cancel button text + public var cancelButtonText: String = "Retake" + + /// original image from camera or gallery + public var image: UIImage? { + didSet { + guard let image = self.image else { + return + } + layoutDone = false + ratio = image.size.height / image.size.width + imageView.image = image + self.view.layoutIfNeeded() + } + } + /// cropped image + public var cropImage: UIImage? { + return crop() + } + + + private let topView = UIView() + private let fadeView = UIView() + private let imageView: UIImageView = UIImageView() + private let cropView: UIView = UIView() + + private var topConst: NSLayoutConstraint? + private var leadConst: NSLayoutConstraint? + + private var imageHeightConst: NSLayoutConstraint? + private var imageWidthConst: NSLayoutConstraint? + + private var ratio: CGFloat = 1 + private var layoutDone: Bool = false + + private var orgHeight: CGFloat = 0 + private var orgWidth: CGFloat = 0 + private var topStart: CGFloat = 0 + private var leadStart: CGFloat = 0 + private var pinchStart: CGPoint = .zero + + private let cropButton = UIButton(type: .custom) + private let cancelButton = UIButton(type: .custom) + + //MARK: - inits + /// initializer + /// - parameter cropRatio + /// Aspect ratio of the cropped image + convenience public init(cropRatio: CGFloat) { + self.init() + self.cropRatio = cropRatio + } + + //MARK: - overrides + override public func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = UIColor.black + + //main views + topView.backgroundColor = UIColor.clear + let bottomView = UIView() + bottomView.backgroundColor = UIColor.black.withAlphaComponent(0.7) + self.view.addSubview(topView) + self.view.addSubview(bottomView) + topView.translatesAutoresizingMaskIntoConstraints = false + bottomView.translatesAutoresizingMaskIntoConstraints = false + let horizontalTopConst = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": topView]) + let horizontalBottomConst = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": bottomView]) + let verticalConst = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(0)-[top]-(0)-[bottom(70)]-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["bottom": bottomView, "top": topView]) + self.view.addConstraints(horizontalTopConst + horizontalBottomConst + verticalConst) + + // image view + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + topView.addSubview(imageView) + topConst = NSLayoutConstraint(item: imageView, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .top, multiplier: 1, constant: 0) + topConst?.priority = .defaultHigh + leadConst = NSLayoutConstraint(item: imageView, attribute: .leading, relatedBy: .equal, toItem: topView, attribute: .leading, multiplier: 1, constant: 0) + leadConst?.priority = .defaultHigh + imageWidthConst = NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 1) + imageWidthConst?.priority = .required + imageHeightConst = NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 1) + imageHeightConst?.priority = .required + imageView.addConstraints([imageHeightConst!, imageWidthConst!]) + topView.addConstraints([topConst!, leadConst!]) + imageView.image = self.image + + // imageView gestures + let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch)) + imageView.addGestureRecognizer(pinchGesture) + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(pan)) + imageView.addGestureRecognizer(panGesture) + imageView.isUserInteractionEnabled = true + + //fade overlay + fadeView.translatesAutoresizingMaskIntoConstraints = false + fadeView.isUserInteractionEnabled = false + fadeView.backgroundColor = UIColor.black.withAlphaComponent(0.3) + topView.addSubview(fadeView) + let horizontalFadeConst = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": fadeView]) + let verticalFadeConst = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": fadeView]) + topView.addConstraints(horizontalFadeConst + verticalFadeConst) + + // crop overlay + cropView.translatesAutoresizingMaskIntoConstraints = false + cropView.isUserInteractionEnabled = false + topView.addSubview(cropView) + let centerXConst = NSLayoutConstraint(item: cropView, attribute: .centerX, relatedBy: .equal, toItem: topView, attribute: .centerX, multiplier: 1, constant: 0) + let centerYConst = NSLayoutConstraint(item: cropView, attribute: .centerY, relatedBy: .equal, toItem: topView, attribute: .centerY, multiplier: 1, constant: 0) + let widthConst = NSLayoutConstraint(item: cropView, attribute: .width, relatedBy: .equal, toItem: topView, attribute: .width, multiplier: 0.9, constant: 0) + widthConst.priority = .defaultHigh + let heightConst = NSLayoutConstraint(item: cropView, attribute: .height, relatedBy: .lessThanOrEqual, toItem: topView, attribute: .height, multiplier: 0.9, constant: 0) + let ratioConst = NSLayoutConstraint(item: cropView, attribute: .width, relatedBy: .equal, toItem: cropView, attribute: .height, multiplier: cropRatio, constant: 0) + cropView.addConstraints([ratioConst]) + topView.addConstraints([widthConst, heightConst, centerXConst, centerYConst]) + cropView.layer.borderWidth = 1 + cropView.layer.borderColor = UIColor.white.cgColor + cropView.backgroundColor = UIColor.clear + + // control buttons + let cropCenterXMultiplier: CGFloat = 1.0 + /* + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.setTitle(cancelButtonText, for: .normal) + cancelButton.addTarget(self, action: #selector(cropCancel), for: .touchUpInside) + bottomView.addSubview(cancelButton) + let centerCancelXConst = NSLayoutConstraint(item: cancelButton, attribute: .centerX, relatedBy: .equal, toItem: bottomView, attribute: .centerX, multiplier: 0.5, constant: 0) + let centerCancelYConst = NSLayoutConstraint(item: cancelButton, attribute: .centerY, relatedBy: .equal, toItem: bottomView, attribute: .centerY, multiplier: 1, constant: 0) + bottomView.addConstraints([centerCancelXConst, centerCancelYConst]) + cropCenterXMultiplier = 1.5 */ + + cropButton.translatesAutoresizingMaskIntoConstraints = false + cropButton.addTarget(self, action: #selector(cropDone), for: .touchUpInside) + bottomView.addSubview(cropButton) + let centerCropXConst = NSLayoutConstraint(item: cropButton, attribute: .centerX, relatedBy: .equal, toItem: bottomView, attribute: .centerX, multiplier: cropCenterXMultiplier, constant: 0) + let centerCropYConst = NSLayoutConstraint(item: cropButton, attribute: .centerY, relatedBy: .equal, toItem: bottomView, attribute: .centerY, multiplier: 1, constant: 0) + bottomView.addConstraints([centerCropXConst, centerCropYConst]) + + self.view.bringSubviewToFront(bottomView) + + bottomView.layoutIfNeeded() + topView.layoutIfNeeded() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.cancelButton.setTitle(cancelButtonText, for: .normal) + self.cropButton.setTitle(cropButtonText, for: .normal) + + if image == nil { + self.dismiss(animated: true, completion: nil) + } + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + guard !layoutDone else { + return + } + layoutDone = true + + if ratio < 1 { + imageWidthConst?.constant = cropView.frame.height / ratio + imageHeightConst?.constant = cropView.frame.height + } else { + imageWidthConst?.constant = cropView.frame.width + imageHeightConst?.constant = cropView.frame.width * ratio + } + + let horizontal = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(<=\(cropView.frame.origin.x))-[view]-(<=\(cropView.frame.origin.x))-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": imageView]) + let vertical = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(<=\(cropView.frame.origin.y))-[view]-(<=\(cropView.frame.origin.y))-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": imageView]) + topView.addConstraints(horizontal + vertical) + + maskFadeView() + orgWidth = imageWidthConst!.constant + orgHeight = imageHeightConst!.constant + } + + private func maskFadeView() { + let path = UIBezierPath(rect: cropView.frame) + path.append(UIBezierPath(rect: fadeView.frame)) + let mask = CAShapeLayer() + mask.fillRule = CAShapeLayerFillRule.evenOdd + mask.path = path.cgPath + fadeView.layer.mask = mask + } + + //MARK: - button actions + @objc func cropDone() { + presenting = false + self.dismiss(animated: false, completion: { + + self.delegate?.didCropImage(originalImage: self.image, croppedImage: self.cropImage) + }) + + } + + @objc func cropCancel() { + presenting = false + self.dismiss(animated: true, completion: nil) + //self.delegate?.didCancelCrop() + + } + + //MARK: - gesture handling + @objc func pinch(_ pinch: UIPinchGestureRecognizer) { + if pinch.state == .began { + orgWidth = imageWidthConst!.constant + orgHeight = imageHeightConst!.constant + pinchStart = pinch.location(in: self.view) + } + let scale = pinch.scale + let height = max(orgHeight * scale, cropView.frame.height) + let width = max(orgWidth * scale, cropView.frame.height / ratio) + imageHeightConst?.constant = height + imageWidthConst?.constant = width + } + + @objc func pan(_ pan: UIPanGestureRecognizer) { + if pan.state == .began { + topStart = topConst!.constant + leadStart = leadConst!.constant + } + let trans = pan.translation(in: self.view) + leadConst?.constant = leadStart + trans.x + topConst?.constant = topStart + trans.y + } + + //MARK: - cropping done here + private func crop() -> UIImage? { + guard let image = self.image else { + return nil + } + let imageSize = image.size + let width = cropView.frame.width / imageView.frame.width + let height = cropView.frame.height / imageView.frame.height + let x = (cropView.frame.origin.x - imageView.frame.origin.x) / imageView.frame.width + let y = (cropView.frame.origin.y - imageView.frame.origin.y) / imageView.frame.height + + let cropFrame = CGRect(x: x * imageSize.width, y: y * imageSize.height, width: imageSize.width * width, height: imageSize.height * height) + if let cropCGImage = image.cgImage?.cropping(to: cropFrame) { + let cropImage = UIImage(cgImage: cropCGImage, scale: 1, orientation: .up) + return cropImage + } + return nil + } + +} + +extension UIView { + func constraintToFill(superView view: UIView?) { + guard let view = view else { + assertionFailure("superview is nil") + return + } + self.translatesAutoresizingMaskIntoConstraints = false + self.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true + self.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true + self.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + self.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + } +} diff --git a/Source/UIImage+FixOrientation.swift b/Source/UIImage+FixOrientation.swift new file mode 100644 index 0000000..4e8d142 --- /dev/null +++ b/Source/UIImage+FixOrientation.swift @@ -0,0 +1,56 @@ +// +// + +import UIKit + +extension UIImage { + + func fixOrientation() -> UIImage { + + if imageOrientation == .up { + return self + } + + var transform: CGAffineTransform = CGAffineTransform.identity + + switch imageOrientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: .pi) + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: .pi/2) + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: -.pi/2) + default: //.up, .upMirrored + break + } + + switch imageOrientation { + case .upMirrored, .downMirrored: + transform.translatedBy(x: size.width, y: 0) + transform.scaledBy(x: -1, y: 1) + case .leftMirrored, .rightMirrored: + transform.translatedBy(x: size.height, y: 0) + transform.scaledBy(x: -1, y: 1) + default: //.up, .down, .left, .right + break + } + + let ctx: CGContext = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: self.cgImage!.bitsPerComponent, bytesPerRow: 0, space: (self.cgImage?.colorSpace)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! + + ctx.concatenate(transform) + + switch imageOrientation { + case .left, .leftMirrored, .right, .rightMirrored: + ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) + default: + ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + } + + let cgImage: CGImage = ctx.makeImage()! + + return UIImage(cgImage: cgImage) + } +} diff --git a/Source/Info.plist b/Supporting Files/Info.plist similarity index 90% rename from Source/Info.plist rename to Supporting Files/Info.plist index fbe1e6b..69c12a5 100644 --- a/Source/Info.plist +++ b/Supporting Files/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion en + CFBundleDisplayName + FDTake CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -17,8 +19,9 @@ CFBundleShortVersionString 1.0 CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 1 NSPrincipalClass + diff --git a/iOS Example/Source/ViewController.swift b/iOS Example/Source/ViewController.swift index d30032f..79ed77f 100644 --- a/iOS Example/Source/ViewController.swift +++ b/iOS Example/Source/ViewController.swift @@ -52,7 +52,7 @@ class ViewController: UIViewController { self.present(alert, animated: true, completion: nil) } fdTakeController.didGetPhoto = { - (photo: UIImage, info: [AnyHashable : Any]) -> Void in + (photo: UIImage, info: [AnyHashable : Any]?) -> Void in let alert = UIAlertController(title: "Got photo", message: "User selected photo", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) @@ -81,12 +81,14 @@ class ViewController: UIViewController { @IBAction func presentFromButton(_ sender: UIButton) { resetFDTakeController() fdTakeController.presentingView = sender + fdTakeController.presentingRect = self.view.frame fdTakeController.present() } @IBAction func presentFromWindow() { resetFDTakeController() fdTakeController.presentingView = self.view + fdTakeController.presentingRect = self.view.frame fdTakeController.present() } } diff --git a/iOS Example/iOS Example.xcodeproj/project.pbxproj b/iOS Example/iOS Example.xcodeproj/project.pbxproj index 518f12a..6e2bd64 100644 --- a/iOS Example/iOS Example.xcodeproj/project.pbxproj +++ b/iOS Example/iOS Example.xcodeproj/project.pbxproj @@ -244,6 +244,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -295,6 +296,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; };