diff --git a/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomInAnimationController.swift b/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomInAnimationController.swift index 4365d17..9a06c20 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomInAnimationController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomInAnimationController.swift @@ -27,7 +27,13 @@ class FMZoomInAnimationController: NSObject, UIViewControllerAnimatedTransitioni let bgView = UIView(frame: containerView.frame) containerView.addSubview(bgView) containerView.addSubview(snapshot) - snapshot.frame = self.realDestinationFrame(scaledFrame: self.getOriginFrame(), realSize: snapshot.frame.size) + + let originalSnapshotCornerRadius = snapshot.layer.cornerRadius + let originalSnapshotSize = snapshot.frame.size + let startFrame = self.realDestinationFrame(scaledFrame: self.getOriginFrame(), realSize: snapshot.frame.size) + + snapshot.layer.cornerRadius = 0 + snapshot.frame = startFrame containerView.addSubview(toVC.view) containerView.addSubview(snapshot) @@ -49,8 +55,9 @@ class FMZoomInAnimationController: NSObject, UIViewControllerAnimatedTransitioni } UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.9) { - snapshot.frame = CGRect(x: 0, y: 0, width: photoVC.viewToSnapshot().frame.width, height: photoVC.viewToSnapshot().frame.height) + snapshot.frame = CGRect(origin: .zero, size: originalSnapshotSize) snapshot.center = containerView.center + snapshot.layer.cornerRadius = originalSnapshotCornerRadius } UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) { diff --git a/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomOutAnimationController.swift b/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomOutAnimationController.swift index 5e71293..1e9ce36 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomOutAnimationController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Animated Trasitioning/FMZoomOutAnimationController.swift @@ -31,18 +31,24 @@ class FMZoomOutAnimationController: NSObject, UIViewControllerAnimatedTransition return } - let snapshot = photoVC.viewToSnapshot().ins_snapshotView() let containerView = transitionContext.containerView let pannedVector = fromVC.pageViewController.view.frame.origin + + let snapshot = photoVC.viewToSnapshot().ins_snapshotView() containerView.addSubview(snapshot) - snapshot.frame = CGRect(x: 0, y: 0, width: photoVC.viewToSnapshot().frame.width, height: photoVC.viewToSnapshot().frame.height) - snapshot.center = containerView.center + + let originSnapshotSize = snapshot.frame.size + + snapshot.frame = CGRect(origin: .zero, size: originSnapshotSize) + snapshot.center = containerView.center + snapshot.frame = CGRect(origin: CGPoint(x: snapshot.frame.origin.x + pannedVector.x, y: snapshot.frame.origin.y + pannedVector.y), - size: snapshot.frame.size) + size: originSnapshotSize) fromVC.view.isHidden = true + let duration = transitionDuration(using: transitionContext) UIView.animateKeyframes( @@ -52,6 +58,7 @@ class FMZoomOutAnimationController: NSObject, UIViewControllerAnimatedTransition animations: { UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.9) { snapshot.frame = self.realDestinationFrame(scaledFrame: self.getDestFrame(), realSize: snapshot.frame.size) + snapshot.layer.cornerRadius = 0 } UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1) { diff --git a/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotoAsset.swift b/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotoAsset.swift index 85097ba..0a2486b 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotoAsset.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotoAsset.swift @@ -10,19 +10,25 @@ import Foundation import Photos public class FMPhotoAsset { - var asset: PHAsset + let asset: PHAsset? + let sourceImage: UIImage? + var mediaType: FMMediaType - var thumb: UIImage? - var thumbRequestId: PHImageRequestID? - var originalThumb: UIImage? + // a fully edited thumbnail version of the image + var editedThumb: UIImage? + + // a filterd-only thumbnail version of the image + var filterdThumb: UIImage? + + var thumbRequestId: PHImageRequestID? var videoFrames: [CGImage]? var thumbChanged: (UIImage) -> Void = { _ in } private var fullSizePhotoRequestId: PHImageRequestID? - private var editor = FMImageEditor() + private var editor: FMImageEditor! /** Indicates whether the request for the full size image was canceled. A workaround for this issue: @@ -30,57 +36,116 @@ public class FMPhotoAsset { */ private var canceledFullSizeRequest = false - init(asset: PHAsset) { + init(asset: PHAsset, forceCropType: FMCroppable?) { self.asset = asset self.mediaType = FMMediaType(withPHAssetMediaType: asset.mediaType) + self.sourceImage = nil + + self.editor = self.initializeEditor(for: forceCropType, imageSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight)) + } + + init(sourceImage: UIImage, forceCropType: FMCroppable?) { + self.sourceImage = sourceImage + self.mediaType = .image + self.asset = nil + + self.editor = self.initializeEditor(for: forceCropType, imageSize: sourceImage.size) + } + + private func initializeEditor(for forceCropType: FMCroppable?, imageSize: CGSize) -> FMImageEditor { + guard let forceCropType = forceCropType, let fmCropRatio = forceCropType.ratio() else { + return FMImageEditor() + } + + let imageRatio = CGFloat(imageSize.width) / CGFloat(imageSize.height) + let cropRatio = fmCropRatio.width / fmCropRatio.height + var scaleW, scaleH: CGFloat + if imageRatio > cropRatio { + scaleH = 1.0 + scaleW = cropRatio / imageRatio + } else { + scaleW = 1.0 + scaleH = imageRatio / cropRatio + } + let cropArea = FMCropArea(scaleX: (1 - scaleW) / 2, + scaleY: (1 - scaleH) / 2, + scaleW: scaleW, + scaleH: scaleH) + return FMImageEditor(filter: kDefaultFilter, crop: forceCropType, cropArea: cropArea) } func requestVideoFrames(_ complete: @escaping ([CGImage]) -> Void) { if let videoFrames = self.videoFrames { complete(videoFrames) } else { - Helper.generateVideoFrames(from: self.asset) { cgImages in - self.videoFrames = cgImages - complete(cgImages) + if let asset = asset { + Helper.generateVideoFrames(from: asset) { cgImages in + self.videoFrames = cgImages + complete(cgImages) + } + } else { + complete([]) } } } - func requestThumb(_ complete: @escaping (UIImage?) -> Void) { - if let thumb = self.thumb { - complete(thumb) + func requestThumb(refresh: Bool=false, _ complete: @escaping (UIImage?) -> Void) { + if let editedThumb = self.editedThumb, !refresh { + complete(editedThumb) } else { - self.thumbRequestId = Helper.getPhoto(by: self.asset, in: CGSize(width: 150, height: 150)) { image in - self.thumbRequestId = nil - self.thumb = image - self.originalThumb = image - - guard let image = image else { return complete(nil) } - let edited = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited) + // It is not absolutely right but it gives much better performance in most cases + let cropScale = (editor.cropArea.scaleW + editor.cropArea.scaleH) / 2 + let size = CGSize(width: 200 / cropScale, height: 200 / cropScale) + if let asset = asset { + self.thumbRequestId = Helper.getPhoto(by: asset, in: size) { image in + self.thumbRequestId = nil + + guard let image = image else { return complete(nil) } + + self.editedThumb = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited) + self.filterdThumb = self.editor.reproduce(source: image, cropState: .edited, filterState: .original) + + complete(self.editedThumb) + } + } else { + guard let image = sourceImage else { return complete(nil) } + let edited = self.editor.reproduce(source: image.resize(toSizeInPixel: size), cropState: .edited, filterState: .edited) complete(edited) } } } func requestImage(in desireSize: CGSize, _ complete: @escaping (UIImage?) -> Void) { - _ = Helper.getPhoto(by: self.asset, in: desireSize) { image in - guard let image = image else { return complete(nil) } + if let asset = asset { + _ = Helper.getPhoto(by: asset, in: desireSize) { image in + guard let image = image else { return complete(nil) } + let edited = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited) + complete(edited) + } + } else { + guard let image = sourceImage?.resize(toSizeInPixel: desireSize) else { return complete(nil) } let edited = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited) complete(edited) } } func requestFullSizePhoto(cropState: FMImageEditState, filterState: FMImageEditState, complete: @escaping (UIImage?) -> Void) { - self.fullSizePhotoRequestId = Helper.getFullSizePhoto(by: self.asset) { image in - self.fullSizePhotoRequestId = nil - if self.canceledFullSizeRequest { - self.canceledFullSizeRequest = false - complete(nil) - } else { - guard let image = image else { return complete(nil) } - let result = self.editor.reproduce(source: image, cropState: cropState, filterState: filterState) - complete(result) + if let asset = asset { + self.fullSizePhotoRequestId = Helper.getFullSizePhoto(by: asset) { image in + self.fullSizePhotoRequestId = nil + if self.canceledFullSizeRequest { + self.canceledFullSizeRequest = false + complete(nil) + } else { + guard let image = image else { return complete(nil) } + let result = self.editor.reproduce(source: image, cropState: cropState, filterState: filterState) + complete(result) + } } + } else { + guard let image = sourceImage else { return complete(nil) } + let result = self.editor.reproduce(source: image, cropState: cropState, filterState: filterState) + complete(result) } } @@ -114,28 +179,19 @@ public class FMPhotoAsset { return editor.cropArea } - public func getAppliedZoomScale() -> CGFloat? { - return editor.zoomScale - } - - public func apply(filter: FMFilterable, crop: FMCroppable, cropArea: FMCropArea, zoomScale: CGFloat) { + public func apply(filter: FMFilterable, crop: FMCroppable, cropArea: FMCropArea) { editor.filter = filter editor.crop = crop editor.cropArea = cropArea - editor.zoomScale = zoomScale - if let source = originalThumb { - thumb = editor.reproduce(source: source, cropState: .edited, filterState: .edited) - if thumb != nil { - thumbChanged(thumb!) + requestThumb(refresh: true) { image in + if let image = image { + self.thumbChanged(image) } } } public func isEdited() -> Bool { - if editor.filter as? FMFilter != FMFilter.None { return true } - if !editor.cropArea.isApproximatelyEqualToOriginal() { return true } - - return false + return editor.isEdited() } } diff --git a/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotosDataSource.swift b/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotosDataSource.swift index c0e7a85..3dc89e7 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotosDataSource.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotosDataSource.swift @@ -38,12 +38,12 @@ class FMPhotosDataSource { return self.selectedPhotoIndexes.count } - public func mediaTypeForPhoto(atIndex index: Int) -> PHAssetMediaType? { - return self.photo(atIndex: index)?.asset.mediaType + public func mediaTypeForPhoto(atIndex index: Int) -> FMMediaType? { + return self.photo(atIndex: index)?.mediaType } - public func countSelectedPhoto(byType: PHAssetMediaType) -> Int { - return self.getSelectedPhotos().filter { $0.asset.mediaType == byType }.count + public func countSelectedPhoto(byType: FMMediaType) -> Int { + return self.getSelectedPhotos().filter { $0.mediaType == byType }.count } public func affectedSelectedIndexs(changedIndex: Int) -> [Int] { diff --git a/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/Crop/FMCrop.swift b/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/Crop/FMCrop.swift index 4bc64a8..9fc2777 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/Crop/FMCrop.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/Crop/FMCrop.swift @@ -40,22 +40,25 @@ public enum FMCrop: FMCroppable { switch orientation { case .down: - targetRect.origin.x = image.size.width - rect.maxX * scale - targetRect.origin.y = image.size.height - rect.maxY * scale + targetRect.origin.x = (image.size.width - rect.maxX) * scale + targetRect.origin.y = (image.size.height - rect.maxY) * scale targetRect.size.width = rect.width * scale targetRect.size.height = rect.height * scale case .right: targetRect.origin.x = rect.minY * scale - targetRect.origin.y = image.size.width - rect.maxX * scale - targetRect.size.width = rect.height - targetRect.size.height = rect.width + targetRect.origin.y = (image.size.width - rect.maxX) * scale + targetRect.size.width = rect.height * scale + targetRect.size.height = rect.width * scale case .left: targetRect.origin.x = image.size.height - rect.maxY * scale targetRect.origin.y = rect.minX * scale - targetRect.size.width = rect.height - targetRect.size.height = rect.width + targetRect.size.width = rect.height * scale + targetRect.size.height = rect.width * scale default: - targetRect = rect + targetRect = CGRect(x: rect.origin.x * scale, + y: rect.origin.y * scale, + width: rect.width * scale, + height: rect.height * scale) } if let croppedCGImage = image.cgImage?.cropping(to: targetRect) { diff --git a/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/FMImageEditor.swift b/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/FMImageEditor.swift index 40d37b9..7d16d57 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/FMImageEditor.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/ImageEditor/FMImageEditor.swift @@ -44,10 +44,24 @@ public struct FMCropArea { } struct FMImageEditor { - var filter: FMFilterable = kDefaultFilter - var crop: FMCroppable = kDefaultCrop - var cropArea: FMCropArea = FMCropArea() - var zoomScale: CGFloat? + var filter: FMFilterable + var crop: FMCroppable + var cropArea: FMCropArea + + let initFilter: FMFilterable + let initCrop: FMCroppable + let initCropArea: FMCropArea + + init(filter: FMFilterable=kDefaultFilter, crop: FMCroppable=kDefaultCrop, cropArea: FMCropArea=FMCropArea()) { + initFilter = filter + self.filter = filter + + initCrop = crop + self.crop = crop + + initCropArea = cropArea + self.cropArea = cropArea + } func reproduce(source image: UIImage, cropState: FMImageEditState, filterState: FMImageEditState) -> UIImage { var result = image @@ -75,4 +89,8 @@ struct FMImageEditor { return crop.crop(image: image, toRect: cropArea.area(forSize: image.size)) } + + public func isEdited() -> Bool { + return filter.filterName() != initFilter.filterName() || crop.name() != initCrop.name() || !cropArea.isApproximatelyEqual(to: initCropArea) + } } diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.swift index f1babbc..97f5351 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.swift @@ -21,6 +21,7 @@ class FMPhotoPickerImageCollectionViewCell: UICollectionViewCell { @IBOutlet weak var videoInfoView: UIView! @IBOutlet weak var videoLengthLabel: UILabel! @IBOutlet weak var editedMarkImageView: UIImageView! + @IBOutlet weak var editedMarkImageViewTopConstraint: NSLayoutConstraint! private weak var photoAsset: FMPhotoAsset? @@ -48,21 +49,27 @@ class FMPhotoPickerImageCollectionViewCell: UICollectionViewCell { public func loadView(photoAsset: FMPhotoAsset, selectMode: FMSelectMode, selectedIndex: Int?) { self.selectMode = selectMode + if selectMode == .single { + self.selectedIndex.isHidden = true + self.selectButton.isHidden = true + self.editedMarkImageViewTopConstraint.constant = 10 + } + self.photoAsset = photoAsset photoAsset.requestThumb() { image in self.imageView.image = image } - photoAsset.thumbChanged = { [weak self] image in - guard let strongSelf = self else { return } + photoAsset.thumbChanged = { [weak self, weak photoAsset] image in + guard let strongSelf = self, let strongPhotoAsset = photoAsset else { return } strongSelf.imageView.image = image - strongSelf.editedMarkImageView.isHidden = !photoAsset.isEdited() + strongSelf.editedMarkImageView.isHidden = !strongPhotoAsset.isEdited() } if photoAsset.mediaType == .video { self.videoInfoView.isHidden = false - self.videoLengthLabel.text = photoAsset.asset.duration.stringTime + self.videoLengthLabel.text = photoAsset.asset?.duration.stringTime } self.editedMarkImageView.isHidden = !photoAsset.isEdited() diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.xib b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.xib index 36d31ac..656ed53 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.xib +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerImageCollectionViewCell.xib @@ -65,7 +65,7 @@ - + @@ -108,7 +108,7 @@ - + @@ -118,6 +118,7 @@ + diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.swift index 9377da9..153b71d 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.swift @@ -26,6 +26,8 @@ internal let kDefaultCrop = FMCrop.ratioCustom internal let kEpsilon: CGFloat = 0.01 +internal let kFilterPreviewImageSize = CGSize(width: 90, height: 90) + internal let kDefaultAvailableFilters = [ FMFilter.None, FMFilter.CIPhotoEffectChrome, @@ -66,7 +68,7 @@ public class FMPhotoPickerViewController: UIViewController { @IBOutlet weak var imageCollectionView: UICollectionView! @IBOutlet weak var numberOfSelectedPhotoContainer: UIView! @IBOutlet weak var numberOfSelectedPhoto: UILabel! - @IBOutlet weak var doneButton: UIButton! + @IBOutlet weak var determineButton: UIButton! @IBOutlet weak var controlBarTopConstrant: NSLayoutConstraint! // MARK: - Public @@ -128,7 +130,7 @@ public class FMPhotoPickerViewController: UIViewController { self.numberOfSelectedPhotoContainer.layer.cornerRadius = self.numberOfSelectedPhotoContainer.frame.size.width / 2 self.numberOfSelectedPhotoContainer.isHidden = true - self.doneButton.isHidden = true + self.determineButton.isHidden = true if #available(iOS 11.0, *) { guard let window = UIApplication.shared.keyWindow else { return } @@ -140,33 +142,12 @@ public class FMPhotoPickerViewController: UIViewController { } // MARK: - Target Actions - @IBAction func onTapDismiss(_ sender: Any) { + @IBAction func onTapCancel(_ sender: Any) { self.dismiss(animated: true) } - @IBAction func onTapNextStep(_ sender: Any) { - FMLoadingView.shared.show() - - var dict = [Int:UIImage]() - - DispatchQueue.global(qos: .userInitiated).async { - let multiTask = DispatchGroup() - for (index, element) in self.dataSource.getSelectedPhotos().enumerated() { - multiTask.enter() - element.requestFullSizePhoto(cropState: .edited, filterState: .edited) { - guard let image = $0 else { return } - dict[index] = image - multiTask.leave() - } - } - multiTask.wait() - - let result = dict.sorted(by: { $0.key < $1.key }).map { $0.value } - DispatchQueue.main.async { - FMLoadingView.shared.hide() - self.delegate?.fmPhotoPickerController(self, didFinishPickingPhotoWith: result) - } - } + @IBAction func onTapDetermine(_ sender: Any) { + processDetermination() } // MARK: - Logic @@ -182,7 +163,8 @@ public class FMPhotoPickerViewController: UIViewController { private func fetchPhotos() { let photoAssets = Helper.getAssets(allowMediaTypes: self.config.mediaTypes) - let fmPhotoAssets = photoAssets.map { FMPhotoAsset(asset: $0) } + let forceCropType = config.forceCropEnabled ? config.availableCrops.first! : nil + let fmPhotoAssets = photoAssets.map { FMPhotoAsset(asset: $0, forceCropType: forceCropType) } self.dataSource = FMPhotosDataSource(photoAssets: fmPhotoAssets) if self.dataSource.numberOfPhotos > 0 { @@ -195,16 +177,41 @@ public class FMPhotoPickerViewController: UIViewController { public func updateControlBar() { if self.dataSource.numberOfSelectedPhoto() > 0 { - self.doneButton.isHidden = false + self.determineButton.isHidden = false if self.config.selectMode == .multiple { self.numberOfSelectedPhotoContainer.isHidden = false self.numberOfSelectedPhoto.text = "\(self.dataSource.numberOfSelectedPhoto())" } } else { - self.doneButton.isHidden = true + self.determineButton.isHidden = true self.numberOfSelectedPhotoContainer.isHidden = true } } + + private func processDetermination() { + FMLoadingView.shared.show() + + var dict = [Int:UIImage]() + + DispatchQueue.global(qos: .userInitiated).async { + let multiTask = DispatchGroup() + for (index, element) in self.dataSource.getSelectedPhotos().enumerated() { + multiTask.enter() + element.requestFullSizePhoto(cropState: .edited, filterState: .edited) { + guard let image = $0 else { return } + dict[index] = image + multiTask.leave() + } + } + multiTask.wait() + + let result = dict.sorted(by: { $0.key < $1.key }).map { $0.value } + DispatchQueue.main.async { + FMLoadingView.shared.hide() + self.delegate?.fmPhotoPickerController(self, didFinishPickingPhotoWith: result) + } + } + } } // MARK: - UICollectionViewDataSource @@ -258,9 +265,8 @@ extension FMPhotoPickerViewController: UICollectionViewDataSource { */ public func tryToAddPhotoToSelectedList(photoIndex index: Int) { if self.config.selectMode == .multiple { - guard let phMediaType = self.dataSource.mediaTypeForPhoto(atIndex: index) else { return } - - let fmMediaType = FMMediaType(withPHAssetMediaType: phMediaType) + guard let fmMediaType = self.dataSource.mediaTypeForPhoto(atIndex: index) else { return } + var canBeAdded = true switch fmMediaType { @@ -324,6 +330,9 @@ extension FMPhotoPickerViewController: UICollectionViewDelegate { vc.didMoveToViewControllerHandler = { vc, photoIndex in self.presentedPhotoIndex = photoIndex } + vc.didTapDetermine = { + self.processDetermination() + } vc.view.frame = self.view.frame vc.transitioningDelegate = self diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.xib b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.xib index 1d5b016..202ae0b 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.xib +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Picker/FMPhotoPickerViewController.xib @@ -13,7 +13,7 @@ - + @@ -36,7 +36,7 @@ - + diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/FMImageEditorViewController.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/FMImageEditorViewController.swift index 84bf9d5..98b8f78 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/FMImageEditorViewController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/FMImageEditorViewController.swift @@ -10,7 +10,12 @@ import UIKit let kContentFrameSpacing: CGFloat = 22.0 -class FMImageEditorViewController: UIViewController { +// MARK: - Delegate protocol +public protocol FMImageEditorViewControllerDelegate: class { + func fmImageEditorViewController(_ editor: FMImageEditorViewController, didFinishEdittingPhotoWith photo: UIImage) +} + +public class FMImageEditorViewController: UIViewController { @IBOutlet weak var topMenuTopConstraint: NSLayoutConstraint! @IBOutlet weak var bottomMenuBottomConstraint: NSLayoutConstraint! @@ -24,11 +29,15 @@ class FMImageEditorViewController: UIViewController { @IBOutlet weak var cropMenuButton: UIButton! @IBOutlet weak var cropMenuIcon: UIImageView! - public var didEndEditting: () -> Void = {} + public var didEndEditting: (@escaping () -> Void) -> Void = { _ in } + public var delegate: FMImageEditorViewControllerDelegate? + private let isAnimatedPresent: Bool lazy private var filterSubMenuView: FMFiltersMenuView = { - let filterSubMenuView = FMFiltersMenuView(withImage: originalThumb, appliedFilter: fmPhotoAsset.getAppliedFilter(), availableFilters: config.availableFilters) + let filterSubMenuView = FMFiltersMenuView(withImage: originalThumb.resize(toSizeInPixel: kFilterPreviewImageSize), + appliedFilter: fmPhotoAsset.getAppliedFilter(), + availableFilters: config.availableFilters) filterSubMenuView.didSelectFilter = { [unowned self] filter in self.selectedFilter = filter FMLoadingView.shared.show() @@ -44,16 +53,19 @@ class FMImageEditorViewController: UIViewController { }() lazy private var cropSubMenuView: FMCropMenuView = { - let cropSubMenuView = FMCropMenuView(appliedCrop: selectedCrop, availableCrops: config.availableCrops) + let cropSubMenuView = FMCropMenuView(appliedCrop: selectedCrop, availableCrops: config.availableCrops, forceCropEnabled: config.forceCropEnabled) cropSubMenuView.didSelectCrop = { [unowned self] crop in self.selectedCrop = crop self.cropView.crop = crop } cropSubMenuView.didReceiveCropControl = { [unowned self] cropControl in - if cropControl == .reset { - self.cropView.reset() - } else if cropControl == .rotate { + switch cropControl { + case .resetAll: + self.cropView.resetAll() + case .rotate: self.cropView.rotate() + case .resetFrameWithoutChangeRatio: + self.cropView.resetFrameWithoutChangeRatio() } } return cropSubMenuView @@ -94,27 +106,56 @@ class FMImageEditorViewController: UIViewController { selectedFilter = fmPhotoAsset.getAppliedFilter() selectedCrop = fmPhotoAsset.getAppliedCrop() + isAnimatedPresent = false + super.init(nibName: "FMImageEditorViewController", bundle: Bundle(for: FMImageEditorViewController.self)) self.view.backgroundColor = kBackgroundColor } - required init?(coder aDecoder: NSCoder) { + public init(config: FMPhotoPickerConfig, sourceImage: UIImage) { + self.config = config + + let forceCropType = config.forceCropEnabled ? config.availableCrops.first! : nil + let fmPhotoAsset = FMPhotoAsset(sourceImage: sourceImage, forceCropType: forceCropType) + self.fmPhotoAsset = fmPhotoAsset + + self.originalThumb = sourceImage + + self.originalImage = sourceImage + self.filteredImage = sourceImage + + selectedFilter = fmPhotoAsset.getAppliedFilter() + selectedCrop = fmPhotoAsset.getAppliedCrop() + + isAnimatedPresent = true + + super.init(nibName: "FMImageEditorViewController", bundle: Bundle(for: FMImageEditorViewController.self)) + + fmPhotoAsset.requestThumb { image in + self.originalThumb = image! + } + + self.view.backgroundColor = kBackgroundColor + } + + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK - Life cycle - override func viewDidLoad() { + override public func viewDidLoad() { super.viewDidLoad() + topMenuContainter.isHidden = true subMenuContainer.isHidden = true filterSubMenuView.isHidden = true cropSubMenuView.isHidden = true cropView = FMCropView(image: filteredImage, appliedCrop: fmPhotoAsset.getAppliedCrop(), - appliedCropArea: fmPhotoAsset.getAppliedCropArea(), - zoomScale: fmPhotoAsset.getAppliedZoomScale()) + appliedCropArea: fmPhotoAsset.getAppliedCropArea()) + cropView.foregroundView.eclipsePreviewEnabled = self.config.eclipsePreviewEnabled self.view.addSubview(self.cropView) self.view.sendSubview(toBack: self.cropView) @@ -146,11 +187,16 @@ class FMImageEditorViewController: UIViewController { } } - // hide the view until the crop view image is located - view.isHidden = true + if !isAnimatedPresent { + // Hide entire view view until the crop view image is located + // Because the crop view's frame is restore when view did appear + // It's neccssary to hide the initial view until the view's position restore is completed + // It will make the transition become more natural and smooth + view.isHidden = true + } } - override func viewWillAppear(_ animated: Bool) { + override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // hide top and bottom menu @@ -163,8 +209,12 @@ class FMImageEditorViewController: UIViewController { // so we need call again in viewDidAppear to dissable them cropView.isCropping = false } - override func viewDidAppear(_ animated: Bool) { + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + + // show top menu before animation + topMenuContainter.isHidden = false + showAnimatedMenu() // show filter menu by default @@ -178,22 +228,24 @@ class FMImageEditorViewController: UIViewController { cropView.contentFrame = contentFrameFilter() cropView.moveCropBoxToAspectFillContentFrame() - // show the view when the crop view image is located - view.isHidden = false + // show view the crop view image is re-located + if !isAnimatedPresent { + view.isHidden = false + } // dissable pan and pinch gestures cropView.isCropping = false } - override func viewDidLayoutSubviews() { + override public func viewDidLayoutSubviews() { cropView.frame = view.frame } - override func viewDidDisappear(_ animated: Bool) { + override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) } - override func didReceiveMemoryWarning() { + override public func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } @@ -208,28 +260,35 @@ class FMImageEditorViewController: UIViewController { self.fmPhotoAsset.apply(filter: self.selectedFilter, crop: self.selectedCrop, - cropArea: cropArea, - zoomScale: self.cropView.scrollView.zoomScale) - - - // notify PresenterViewController to update it's image - self.didEndEditting() - - self.hideAnimatedMenu { - self.dismiss(animated: false, completion: nil) + cropArea: cropArea) + + if let delegate = self.delegate { + // In case that FMImageEditorViewController is used as standard-alone tool + self.fmPhotoAsset.requestFullSizePhoto(cropState: .edited, filterState: .edited) { image in + if let image = image { + delegate.fmImageEditorViewController(self, didFinishEdittingPhotoWith: image) + } + } + } else { + // notify PresenterViewController to update it's image + self.didEndEditting() { + self.dismiss(animated: self.isAnimatedPresent) + } } } + + self.hideAnimatedMenu() {} } @IBAction func onTapCancel(_ sender: Any) { let doCancelBlock = { - self.hideAnimatedMenu { - self.dismiss(animated: false, completion: nil) - } + self.cropView.isCropping = false self.cropView.contentFrame = self.contentFrameFullScreen() self.cropView.moveCropBoxToAspectFillContentFrame() - self.cropView.isCropping = false + self.hideAnimatedMenu { + self.dismiss(animated: self.isAnimatedPresent, completion: nil) + } } if fmPhotoAsset.getAppliedFilter().filterName() == selectedFilter.filterName() && @@ -254,7 +313,7 @@ class FMImageEditorViewController: UIViewController { // enable foreground touches to control show/hide compareView cropView.foregroundView.isEnabledTouches = true - filterSubMenuView.image = cropView.getCroppedImage() + filterSubMenuView.image = cropView.getCroppedThumbImage() } @IBAction func onTapOpenCrop(_ sender: Any) { cropMenuIcon.tintColor = kRedColor diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropForegroundView.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropForegroundView.swift index 40d8d47..d43dc75 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropForegroundView.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropForegroundView.swift @@ -16,10 +16,14 @@ class FMCropForegroundView: UIView { isUserInteractionEnabled = isEnabledTouches } } + + public var eclipsePreviewEnabled = false override var frame: CGRect { didSet { -// self.imageView.frame = self.frame + if eclipsePreviewEnabled { + layer.cornerRadius = frame.width / 2 + } } } @@ -37,6 +41,16 @@ class FMCropForegroundView: UIView { addSubview(compareView) clipsToBounds = true + layer.masksToBounds = true + } + + internal func getViewableCompareView() -> UIImage { + compareView.frame = imageView.frame + compareView.isHidden = false + let image = UIImage(view: self) + compareView.isHidden = true + + return image } private func showCompareView() { diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropView.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropView.swift index c04126a..d36b4ac 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropView.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/Crop/FMCropView.swift @@ -30,7 +30,17 @@ class FMCropView: UIView { } private var cropArea: FMCropArea? - private var zoomScale: CGFloat? + lazy private var zoomScale: CGFloat? = { + guard let cropArea = cropArea else { return nil } + let contentFrameSizeRatio = contentFrame.width / contentFrame.height + let cropBoxSizeRatio = cropArea.scaleW / cropArea.scaleH * (image.size.width / image.size.height) + + if contentFrameSizeRatio > cropBoxSizeRatio { + return contentFrame.height / (cropArea.scaleH * image.size.height) + } else { + return contentFrame.width / (cropArea.scaleW * image.size.width) + } + }() public var isCropping: Bool = false { didSet { @@ -65,12 +75,11 @@ class FMCropView: UIView { } } - init(image: UIImage, appliedCrop: FMCroppable, appliedCropArea: FMCropArea?, zoomScale: CGFloat?) { + init(image: UIImage, appliedCrop: FMCroppable, appliedCropArea: FMCropArea?) { self.image = image crop = appliedCrop cropArea = appliedCropArea - self.zoomScale = zoomScale scrollView = FMCropScrollView(image: image) @@ -145,32 +154,32 @@ class FMCropView: UIView { } public func restoreFromPreviousEdittingSection() { + guard let cropArea = cropArea, let zoomScale = zoomScale else { return } + // check for previous section data - if let zoomScale = zoomScale, let cropArea = cropArea { - var cropFrame = cropBoxView.frame - - // use for first time only - let scrollViewScale = zoomScale / scrollView.zoomScale - - cropFrame.size.width = ceil(scrollView.imageView.frame.width / scrollView.zoomScale * cropArea.scaleW * zoomScale) - cropFrame.size.height = ceil(scrollView.imageView.frame.height / scrollView.zoomScale * cropArea.scaleH * zoomScale) - - //The scale we need to scale up the crop box to fit full screen - let cropBoxScale = min(contentFrame.width / cropFrame.width, contentFrame.height / cropFrame.height) - cropFrame.size.width = ceil(cropFrame.size.width * cropBoxScale) - cropFrame.size.height = ceil(cropFrame.size.height * cropBoxScale) - - cropFrame.origin.x = contentFrame.origin.x + ceil(contentFrame.size.width - cropFrame.size.width) * 0.5 - cropFrame.origin.y = contentFrame.origin.y + ceil(contentFrame.size.height - cropFrame.size.height) * 0.5 - - let targetOffset = CGPoint(x: ceil(scrollView.imageView.frame.width / scrollView.zoomScale * zoomScale * cropArea.scaleX) - cropFrame.minX, - y: ceil(scrollView.imageView.frame.height / scrollView.zoomScale * zoomScale * cropArea.scaleY) - cropFrame.minY) - - scrollView.zoomScale *= scrollViewScale - scrollView.contentOffset = targetOffset - cropBoxView.frame = cropFrame - cropboxViewFrameDidChange(rect: cropFrame) - } + var cropFrame = cropBoxView.frame + + // use for first time only + let scrollViewScale = zoomScale / scrollView.zoomScale + + cropFrame.size.width = ceil(scrollView.imageView.frame.width / scrollView.zoomScale * cropArea.scaleW * zoomScale) + cropFrame.size.height = ceil(scrollView.imageView.frame.height / scrollView.zoomScale * cropArea.scaleH * zoomScale) + + //The scale we need to scale up the crop box to fit full screen + let cropBoxScale = min(contentFrame.width / cropFrame.width, contentFrame.height / cropFrame.height) + cropFrame.size.width = ceil(cropFrame.size.width * cropBoxScale) + cropFrame.size.height = ceil(cropFrame.size.height * cropBoxScale) + + cropFrame.origin.x = contentFrame.origin.x + ceil(contentFrame.size.width - cropFrame.size.width) * 0.5 + cropFrame.origin.y = contentFrame.origin.y + ceil(contentFrame.size.height - cropFrame.size.height) * 0.5 + + let targetOffset = CGPoint(x: ceil(scrollView.imageView.frame.width / scrollView.zoomScale * zoomScale * cropArea.scaleX) - cropFrame.minX, + y: ceil(scrollView.imageView.frame.height / scrollView.zoomScale * zoomScale * cropArea.scaleY) - cropFrame.minY) + + scrollView.zoomScale *= scrollViewScale + scrollView.contentOffset = targetOffset + cropBoxView.frame = cropFrame + cropboxViewFrameDidChange(rect: cropFrame) } private func moveCroppedContentToCenterAnimated(complete: (() -> Void)? = nil) { @@ -289,25 +298,40 @@ class FMCropView: UIView { return crop.ratio() } - public func getCroppedImage() -> UIImage { - return UIImage(view: foregroundView) + public func getCroppedThumbImage() -> UIImage { + return foregroundView.getViewableCompareView().resize(toSizeInPixel: kFilterPreviewImageSize) } - public func reset() { + public func resetAll() { let imageRatio = image.size.width / image.size.height + resetCropFrame(to: imageRatio) + } + + public func resetFrameWithoutChangeRatio() { + guard let fmCropRatio = crop.ratio() else { return } + let cropRatio = fmCropRatio.width / fmCropRatio.height + + resetCropFrame(to: cropRatio) + } + + private func resetCropFrame(to cropRatio: CGFloat) { let contentFrameRatio = contentFrame.width / contentFrame.height var cropFrame: CGRect = .zero - if imageRatio > contentFrameRatio { + if cropRatio > contentFrameRatio { cropFrame.size.width = contentFrame.width - cropFrame.size.height = ceil(cropFrame.width / imageRatio) + cropFrame.size.height = ceil(cropFrame.width / cropRatio) } else { cropFrame.size.height = contentFrame.height - cropFrame.size.width = ceil(cropFrame.height * imageRatio) + cropFrame.size.width = ceil(cropFrame.height * cropRatio) } cropFrame.origin = CGPoint(x: (contentFrame.width - cropFrame.width) / 2 + contentFrame.origin.x, y: (contentFrame.height - cropFrame.height) / 2 + contentFrame.origin.y) - self.scrollView.minimumZoomScale = max(cropFrame.width / image.size.width, cropFrame.height / image.size.height) + let targetZoomScale = max(cropFrame.width / image.size.width, cropFrame.height / image.size.height) + let targetContentOffset = CGPoint(x: -cropFrame.origin.x + (image.size.width * targetZoomScale - cropFrame.width) / 2, + y: -cropFrame.origin.y + (image.size.height * targetZoomScale - cropFrame.height) / 2) + + scrollView.minimumZoomScale = targetZoomScale UIView.animate(withDuration: kComplexAnimationDuration, delay: 0, @@ -315,8 +339,8 @@ class FMCropView: UIView { initialSpringVelocity: 1.0, options: .beginFromCurrentState, animations: { - self.scrollView.zoomScale = self.scrollView.minimumZoomScale - self.scrollView.contentOffset = CGPoint(x: -cropFrame.origin.x, y: -cropFrame.origin.y) + self.scrollView.zoomScale = targetZoomScale + self.scrollView.contentOffset = targetContentOffset self.cropBoxView.frame = cropFrame self.cropboxViewFrameDidChange(rect: cropFrame) }, diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/FMCropMenuView.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/FMCropMenuView.swift index 09e854f..db6de9e 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/FMCropMenuView.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Editor/Views/FMCropMenuView.swift @@ -9,19 +9,21 @@ import UIKit enum FMCropControl { - case reset + case resetAll + case resetFrameWithoutChangeRatio case rotate func name() -> String { switch self { - case .reset: return "リセット" + case .resetFrameWithoutChangeRatio: return "リセット" + case .resetAll: return "リセット" case .rotate: return "回転" } } func icon() -> UIImage? { switch self { - case .reset: + case .resetAll, .resetFrameWithoutChangeRatio: return UIImage(named: "icon_crop_reset", in: Bundle(for: FMPhotoPickerViewController.self), compatibleWith: nil) case .rotate: return UIImage(named: "icon_crop_rotation", in: Bundle(for: FMPhotoPickerViewController.self), compatibleWith: nil) @@ -31,7 +33,7 @@ enum FMCropControl { class FMCropMenuView: UIView { private let collectionView: UICollectionView - private let menuItems: [FMCropControl] = [.reset] // TODO: support rotation function + private let menuItems: [FMCropControl] private let cropItems: [FMCroppable] public var didSelectCrop: (FMCroppable) -> Void = { _ in } @@ -45,9 +47,25 @@ class FMCropMenuView: UIView { } } - init(appliedCrop: FMCroppable?, availableCrops: [FMCroppable]) { + init(appliedCrop: FMCroppable?, availableCrops: [FMCroppable], forceCropEnabled: Bool) { selectedCrop = appliedCrop - cropItems = availableCrops.count == 0 ? kDefaultAvailableCrops : availableCrops + + var tAvailableCrops = availableCrops + tAvailableCrops = tAvailableCrops.count == 0 ? kDefaultAvailableCrops : tAvailableCrops + + // if the force crop mode is enabled + // then only the first crop type in the avaiableCrops will be used + if forceCropEnabled { + tAvailableCrops = [tAvailableCrops.first!] + } + + cropItems = tAvailableCrops + + if forceCropEnabled { + menuItems = [.resetFrameWithoutChangeRatio] + } else { + menuItems = [.resetAll] + } let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: 52, height: 64) @@ -128,13 +146,16 @@ extension FMCropMenuView: UICollectionViewDataSource { extension FMCropMenuView: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 0 { - if indexPath.row == 0 { - didReceiveCropControl(.reset) + let selectedCropControl = menuItems[indexPath.row] + + switch selectedCropControl { + case .resetAll: selectedCrop = kDefaultCrop collectionView.reloadData() - } else if indexPath.row == 1 { - didReceiveCropControl(.rotate) + default: break } + + didReceiveCropControl(selectedCropControl) } else if indexPath.section == 1 { if let cell = collectionView.cellForItem(at: indexPath) as? FMCropCell { let prevCropItem = selectedCrop diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.swift index db2d2eb..0901057 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.swift @@ -15,8 +15,10 @@ class FMPhotoPresenterViewController: UIViewController { @IBOutlet weak var selectedContainer: UIView! @IBOutlet weak var selectedIcon: UIImageView! @IBOutlet weak var selectedIndex: UILabel! - @IBOutlet weak var selectButton: UIButton! @IBOutlet weak var controlBarHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var numberOfSelectedPhotoContainer: UIView! + @IBOutlet weak var numberOfSelectedPhoto: UILabel! + @IBOutlet weak var determineButton: UIButton! // MARK: - Public public var swipeInteractionController: FMPhotoInteractionAnimator? @@ -27,6 +29,8 @@ class FMPhotoPresenterViewController: UIViewController { public var didMoveToViewControllerHandler: ((FMPhotoViewController, Int) -> Void)? + public var didTapDetermine: (() -> Void)? + public var bottomView: FMPresenterBottomView! // MARK: - Private @@ -79,10 +83,6 @@ class FMPhotoPresenterViewController: UIViewController { } } - deinit { - print("deinit FMPhotoPresenterViewController") - } - // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() @@ -117,16 +117,16 @@ class FMPhotoPresenterViewController: UIViewController { self.bottomView.onTapEditButton = { [unowned self] in guard let photo = self.dataSource.photo(atIndex: self.currentPhotoIndex), let vc = self.pageViewController.viewControllers?.first as? FMPhotoViewController, - let originalThumb = photo.originalThumb, + let originalThumb = photo.filterdThumb, let filteredImage = vc.getFilteredImage() else { return } let editorVC = FMImageEditorViewController(config: self.config, fmPhotoAsset: photo, filteredImage: filteredImage, originalThumb: originalThumb) - editorVC.didEndEditting = { [unowned self] in + editorVC.didEndEditting = { [unowned self] viewDidUpdate in if let photoVC = self.pageViewController.viewControllers?.first as? FMPhotoViewController { - photoVC.reloadPhoto() + photoVC.reloadPhoto(complete: viewDidUpdate) } } self.present(editorVC, animated: false, completion: nil) @@ -137,12 +137,25 @@ class FMPhotoPresenterViewController: UIViewController { self.bottomView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true self.bottomView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true self.bottomView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true - self.bottomView.heightAnchor.constraint(equalToConstant: 46).isActive = true + self.bottomView.heightAnchor.constraint(equalToConstant: 90).isActive = true self.pageViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.pageViewController.didMove(toParentViewController: self) self.view.backgroundColor = kBackgroundColor + + self.numberOfSelectedPhotoContainer.layer.cornerRadius = self.numberOfSelectedPhotoContainer.frame.size.width / 2 + self.numberOfSelectedPhotoContainer.isHidden = true + + if config.selectMode == .single { + selectedContainer.isHidden = true + + // alway show done button + self.determineButton.isHidden = false + } else { + // in multiple mode done button only appear when at least one image has beem selected + self.determineButton.isHidden = true + } } override func viewDidAppear(_ animated: Bool) { @@ -163,33 +176,43 @@ class FMPhotoPresenterViewController: UIViewController { // MARK: - Update Views private func updateInfoBar() { + let n = dataSource.numberOfSelectedPhoto() + if self.config.selectMode == .multiple { + if n > 0 { + determineButton.isHidden = false + numberOfSelectedPhotoContainer.isHidden = false + numberOfSelectedPhoto.isHidden = false + numberOfSelectedPhoto.text = "\(n)" + } else { + determineButton.isHidden = true + numberOfSelectedPhotoContainer.isHidden = true + numberOfSelectedPhoto.isHidden = true + } + } else { + numberOfSelectedPhotoContainer.isHidden = true + numberOfSelectedPhoto.isHidden = true + + determineButton.isHidden = false + } + // Update selection status if let selectedIndex = self.dataSource.selectedIndexOfPhoto(atIndex: self.currentPhotoIndex) { - - self.selectedContainer.isHidden = false if self.config.selectMode == .multiple { + self.selectedIndex.isHidden = false self.selectedIndex.text = "\(selectedIndex + 1)" self.selectedIcon.image = UIImage(named: "check_on", in: Bundle(for: self.classForCoder), compatibleWith: nil) } else { self.selectedIndex.isHidden = true self.selectedIcon.image = UIImage(named: "single_check_on", in: Bundle(for: self.classForCoder), compatibleWith: nil) } - - UIView.performWithoutAnimation { - self.selectButton.setTitle("選択解除", for: .normal) - self.selectButton.layoutIfNeeded() - } } else { - self.selectedContainer.isHidden = true - UIView.performWithoutAnimation { - self.selectButton.setTitle("選択", for: .normal) - self.selectButton.layoutIfNeeded() - } + self.selectedIndex.isHidden = true + self.selectedIcon.image = UIImage(named: "check_off", in: Bundle(for: self.classForCoder), compatibleWith: nil) } // Update photo title if let photoAsset = self.dataSource.photo(atIndex: self.currentPhotoIndex), - let creationDate = photoAsset.asset.creationDate { + let creationDate = photoAsset.asset?.creationDate { self.photoTitle.text = self.formatter.string(from: creationDate) } } @@ -222,7 +245,9 @@ class FMPhotoPresenterViewController: UIViewController { if fmAsset.mediaType == .video { bottomView.videoMode() fmAsset.requestVideoFrames { cgImages in - self.bottomView.resetPlaybackControl(cgImages: cgImages, duration: fmAsset.asset.duration) + if let asset = fmAsset.asset { + self.bottomView.resetPlaybackControl(cgImages: cgImages, duration: asset.duration) + } } } else { bottomView.imageMode() @@ -233,6 +258,7 @@ class FMPhotoPresenterViewController: UIViewController { @IBAction func onTapClose(_ sender: Any) { self.dismiss(animated: true) } + @IBAction func onTapSelection(_ sender: Any) { if self.dataSource.selectedIndexOfPhoto(atIndex: self.currentPhotoIndex) == nil { self.didSelectPhotoHandler?(self.currentPhotoIndex) @@ -241,6 +267,16 @@ class FMPhotoPresenterViewController: UIViewController { } self.updateInfoBar() } + + @IBAction func onTapDetermine(_ sender: Any) { + if config.selectMode == .single { + // in single selection mode, tap on done button mean the current displaying image will be selected + self.didSelectPhotoHandler?(self.currentPhotoIndex) + } + + didTapDetermine?() + } + } // MARK: - UIPageViewControllerDataSource / UIPageViewControllerDelegate diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.xib b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.xib index 0b0b3b1..b542bec 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.xib +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/FMPhotoPresenterViewController.xib @@ -13,8 +13,10 @@ + + + - @@ -60,9 +62,9 @@ - + - + @@ -79,17 +81,39 @@ + + + + + + + + + + + + + + + + + diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMImageViewController.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMImageViewController.swift index 0a64353..ec1bf7c 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMImageViewController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMImageViewController.swift @@ -36,6 +36,7 @@ class FMImageViewController: FMPhotoViewController { self.scalingImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.scalingImageView.clipsToBounds = true + self.scalingImageView.eclipsePreviewEnabled = config.eclipsePreviewEnabled self.view.addSubview(self.scalingImageView) @@ -48,7 +49,7 @@ class FMImageViewController: FMPhotoViewController { } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - reloadPhoto() + reloadPhoto() {} } override func viewWillDisappear(_ animated: Bool) { @@ -109,11 +110,12 @@ class FMImageViewController: FMPhotoViewController { return self.smallImage } - override func reloadPhoto() { + override func reloadPhoto(complete: @escaping () -> Void) { self.photo.requestFullSizePhoto(cropState: .edited, filterState: .edited) { [weak self] image in guard let strongSelf = self, - let image = image else { return } + let image = image else { return complete() } strongSelf.scalingImageView.image = image + complete() } // get filtered image diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMPhotoViewController.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMPhotoViewController.swift index 94b80e3..06f9ca4 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMPhotoViewController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMPhotoViewController.swift @@ -48,7 +48,7 @@ class FMPhotoViewController: UIViewController { return nil } - public func reloadPhoto() { + public func reloadPhoto(complete: @escaping () -> Void) { } } diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMScalingImageView.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMScalingImageView.swift index 5ce9cb8..ef3e94b 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMScalingImageView.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMScalingImageView.swift @@ -28,10 +28,14 @@ class FMScalingImageView: UIScrollView { var image: UIImage? { didSet { - updateImage(image) + if let image = image { + updateImage(image) + } } } + var eclipsePreviewEnabled = false + override var frame: CGRect { didSet { updateZoomScale() @@ -41,6 +45,7 @@ class FMScalingImageView: UIScrollView { override init(frame: CGRect) { super.init(frame: frame) + imageView.layer.masksToBounds = true setupImageScrollView() updateZoomScale() } @@ -84,16 +89,18 @@ class FMScalingImageView: UIScrollView { self.contentInset = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset); } - private func updateImage(_ image: UIImage?) { - let size = image?.size ?? CGSize.zero - + private func updateImage(_ image: UIImage) { imageView.transform = CGAffineTransform.identity imageView.image = image - imageView.frame = CGRect(origin: CGPoint.zero, size: size) - self.contentSize = size + imageView.frame = CGRect(origin: CGPoint.zero, size: image.size) + self.contentSize = image.size updateZoomScale() centerScrollViewContents() + + if eclipsePreviewEnabled { + imageView.layer.cornerRadius = image.size.width / 2 + } } private func updateZoomScale() { diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMVideoViewController.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMVideoViewController.swift index 31f9704..ee94453 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMVideoViewController.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Viewer/FMVideoViewController.swift @@ -81,9 +81,10 @@ class FMVideoViewController: FMPhotoViewController { } private func loadVideoIfNeeded() { - guard (playerController == nil) else { return } + guard (playerController == nil), + let asset = photo.asset else { return } - Helper.requestAVAsset(asset: photo.asset) { avAsset in + Helper.requestAVAsset(asset: asset) { avAsset in // Do not run on main thread for better perf DispatchQueue.global(qos: .userInitiated).async { guard self.shouldUpdateView == true, diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPlaybackControlView.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPlaybackControlView.swift index 11be955..0bc3f5c 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPlaybackControlView.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPlaybackControlView.swift @@ -93,6 +93,17 @@ class FMPlaybackControlView: UIView { self.backgroundColor = kTransparentBackgroundColor addPlayerObservers() + + // top border view + let topBorder = UIView(frame: .zero) + topBorder.backgroundColor = kBorderColor + addSubview(topBorder) + + topBorder.translatesAutoresizingMaskIntoConstraints = false + topBorder.topAnchor.constraint(equalTo: topAnchor).isActive = true + topBorder.leftAnchor.constraint(equalTo: leftAnchor).isActive = true + topBorder.rightAnchor.constraint(equalTo: rightAnchor).isActive = true + topBorder.heightAnchor.constraint(equalToConstant: 1).isActive = true } required init?(coder aDecoder: NSCoder) { diff --git a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPresenterBottomView.swift b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPresenterBottomView.swift index c1b9b5d..7a3e79f 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPresenterBottomView.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Scene/Presenter/Views/FMPresenterBottomView.swift @@ -40,7 +40,7 @@ class FMPresenterBottomView: UIView { self.addSubview(playbackControlView) playbackControlView.translatesAutoresizingMaskIntoConstraints = false - playbackControlView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true + playbackControlView.heightAnchor.constraint(equalToConstant: 100).isActive = true playbackControlView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true playbackControlView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true playbackControlView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true @@ -51,7 +51,7 @@ class FMPresenterBottomView: UIView { self.addSubview(editMenuView) editMenuView.translatesAutoresizingMaskIntoConstraints = false - editMenuView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true + editMenuView.heightAnchor.constraint(equalToConstant: 46).isActive = true editMenuView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true editMenuView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true editMenuView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true diff --git a/FMPhotoPicker/FMPhotoPicker/source/Utilities/FMPhotoPickerConfig.swift b/FMPhotoPicker/FMPhotoPicker/source/Utilities/FMPhotoPickerConfig.swift index 8306541..20d9bb7 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Utilities/FMPhotoPickerConfig.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Utilities/FMPhotoPickerConfig.swift @@ -51,6 +51,9 @@ public struct FMPhotoPickerConfig { public var availableCrops: [FMCroppable] = kDefaultAvailableCrops public var alertController: FMAlertable = FMAlert() + public var forceCropEnabled = false + public var eclipsePreviewEnabled = false + public init() { } diff --git a/FMPhotoPicker/FMPhotoPicker/source/Utilities/UIImage+Extensions.swift b/FMPhotoPicker/FMPhotoPicker/source/Utilities/UIImage+Extensions.swift index a36d181..0fbbb80 100644 --- a/FMPhotoPicker/FMPhotoPicker/source/Utilities/UIImage+Extensions.swift +++ b/FMPhotoPicker/FMPhotoPicker/source/Utilities/UIImage+Extensions.swift @@ -16,4 +16,43 @@ extension UIImage { UIGraphicsEndImageContext() self.init(cgImage: image!.cgImage!) } + + func resize(toSizeInPixel: CGSize) -> UIImage { + let screenScale = UIScreen.main.scale + let sizeInPoint = CGSize(width: toSizeInPixel.width / screenScale, + height: toSizeInPixel.height / screenScale) + return resize(toSizeInPoint: sizeInPoint) + } + + + func resize(toSizeInPoint: CGSize) -> UIImage { + let size = self.size + var newImage: UIImage + + let widthRatio = toSizeInPoint.width / size.width + let heightRatio = toSizeInPoint.height / size.height + + var newSize: CGSize + if(widthRatio > heightRatio) { + newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio) + } else { + newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio) + } + + if #available(iOS 10.0, *) { + let renderFormat = UIGraphicsImageRendererFormat.default() + let renderer = UIGraphicsImageRenderer(size: CGSize(width: newSize.width, height: newSize.height), format: renderFormat) + newImage = renderer.image { + (context) in + self.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + } + } else { + UIGraphicsBeginImageContextWithOptions(newSize, false, 0) + self.draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: newSize)) + newImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + } + + return newImage + } } diff --git a/FMPhotoPickerExample/FMPhotoPickerExample.xcodeproj/project.pbxproj b/FMPhotoPickerExample/FMPhotoPickerExample.xcodeproj/project.pbxproj index e68cef6..6dafe27 100644 --- a/FMPhotoPickerExample/FMPhotoPickerExample.xcodeproj/project.pbxproj +++ b/FMPhotoPickerExample/FMPhotoPickerExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + CED345E92069E563000ED8F7 /* file0001608482449.jpg in Resources */ = {isa = PBXBuildFile; fileRef = CED345E72069E563000ED8F7 /* file0001608482449.jpg */; }; CEE50D492019745C005AE708 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE50D482019745C005AE708 /* AppDelegate.swift */; }; CEE50D4B2019745C005AE708 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE50D4A2019745C005AE708 /* ViewController.swift */; }; CEE50D4E2019745C005AE708 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CEE50D4C2019745C005AE708 /* Main.storyboard */; }; @@ -55,6 +56,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + CED345E72069E563000ED8F7 /* file0001608482449.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = file0001608482449.jpg; sourceTree = ""; }; CEE50D452019745C005AE708 /* FMPhotoPickerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FMPhotoPickerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; CEE50D482019745C005AE708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CEE50D4A2019745C005AE708 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -97,6 +99,7 @@ CEE50D472019745C005AE708 /* FMPhotoPickerExample */ = { isa = PBXGroup; children = ( + CED345E72069E563000ED8F7 /* file0001608482449.jpg */, CEE50D482019745C005AE708 /* AppDelegate.swift */, CEE50D4A2019745C005AE708 /* ViewController.swift */, CEE50D4C2019745C005AE708 /* Main.storyboard */, @@ -202,6 +205,7 @@ files = ( CEE50D532019745C005AE708 /* LaunchScreen.storyboard in Resources */, CEE50D502019745C005AE708 /* Assets.xcassets in Resources */, + CED345E92069E563000ED8F7 /* file0001608482449.jpg in Resources */, CEE50D4E2019745C005AE708 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/FMPhotoPickerExample/FMPhotoPickerExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/FMPhotoPickerExample/FMPhotoPickerExample/Assets.xcassets/AppIcon.appiconset/Contents.json index 1d060ed..d8db8d6 100644 --- a/FMPhotoPickerExample/FMPhotoPickerExample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/FMPhotoPickerExample/FMPhotoPickerExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -84,6 +84,11 @@ "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/FMPhotoPickerExample/FMPhotoPickerExample/Base.lproj/Main.storyboard b/FMPhotoPickerExample/FMPhotoPickerExample/Base.lproj/Main.storyboard index 2a6bf22..8182728 100644 --- a/FMPhotoPickerExample/FMPhotoPickerExample/Base.lproj/Main.storyboard +++ b/FMPhotoPickerExample/FMPhotoPickerExample/Base.lproj/Main.storyboard @@ -18,84 +18,79 @@ - - + - + - + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + - + + + - + + + + - - - + + + + @@ -162,13 +217,20 @@ + + + + + + + diff --git a/FMPhotoPickerExample/FMPhotoPickerExample/ViewController.swift b/FMPhotoPickerExample/FMPhotoPickerExample/ViewController.swift index f16f317..9baeeeb 100644 --- a/FMPhotoPickerExample/FMPhotoPickerExample/ViewController.swift +++ b/FMPhotoPickerExample/FMPhotoPickerExample/ViewController.swift @@ -9,9 +9,14 @@ import UIKit import FMPhotoPicker -class ViewController: UIViewController, FMPhotoPickerViewControllerDelegate { +class ViewController: UIViewController, FMPhotoPickerViewControllerDelegate, FMImageEditorViewControllerDelegate { + func fmImageEditorViewController(_ editor: FMImageEditorViewController, didFinishEdittingPhotoWith photo: UIImage) { + self.dismiss(animated: true, completion: nil) + previewImageView.image = photo + } + func fmPhotoPickerController(_ picker: FMPhotoPickerViewController, didFinishPickingPhotoWith photos: [UIImage]) { - + self.dismiss(animated: true, completion: nil) } @IBOutlet weak var selectMode: UISegmentedControl! @@ -19,6 +24,9 @@ class ViewController: UIViewController, FMPhotoPickerViewControllerDelegate { @IBOutlet weak var allowVideo: UISwitch! @IBOutlet weak var maxImageLB: UILabel! @IBOutlet weak var maxVideoLB: UILabel! + @IBOutlet weak var previewImageView: UIImageView! + @IBOutlet weak var forceCropEnabled: UISwitch! + @IBOutlet weak var eclipsePreviewEnabled: UISwitch! private var maxImage: Int = 5 private var maxVideo: Int = 5 @@ -32,6 +40,9 @@ class ViewController: UIViewController, FMPhotoPickerViewControllerDelegate { // video off by default self.allowVideo.isOn = false + self.forceCropEnabled.isOn = false + self.eclipsePreviewEnabled.isOn = false + self.selectMode.selectedSegmentIndex = 1 // Do any additional setup after loading the view, typically from a nib. } @@ -63,7 +74,7 @@ class ViewController: UIViewController, FMPhotoPickerViewControllerDelegate { self.maxVideoLB.text = "\(self.maxVideo)" } - @IBAction func open(_ sender: Any) { + func config() -> FMPhotoPickerConfig { let selectMode: FMSelectMode = (self.selectMode.selectedSegmentIndex == 0 ? .single : .multiple) var mediaTypes = [FMMediaType]() @@ -76,15 +87,34 @@ class ViewController: UIViewController, FMPhotoPickerViewControllerDelegate { config.mediaTypes = mediaTypes config.maxImage = self.maxImage config.maxVideo = self.maxVideo + config.forceCropEnabled = forceCropEnabled.isOn + config.eclipsePreviewEnabled = eclipsePreviewEnabled.isOn - // all available crops will be used - config.availableCrops = [] + // in force crop mode, only the first crop option is available + config.availableCrops = [ + FMCrop.ratioSquare, + FMCrop.ratioCustom, + FMCrop.ratio4x3, + FMCrop.ratio16x9, + FMCrop.ratioOrigin, + ] // all available filters will be used config.availableFilters = [] - let vc = FMPhotoPickerViewController(config: config) + return config + } + + @IBAction func open(_ sender: Any) { + let vc = FMPhotoPickerViewController(config: config()) + vc.delegate = self + self.present(vc, animated: true) + } + + @IBAction func openEditor(_ sender: Any) { + let vc = FMImageEditorViewController(config: config(), sourceImage: previewImageView.image!) vc.delegate = self + self.present(vc, animated: true) } } diff --git a/FMPhotoPickerExample/FMPhotoPickerExample/file0001608482449.jpg b/FMPhotoPickerExample/FMPhotoPickerExample/file0001608482449.jpg new file mode 100755 index 0000000..68bb3d0 Binary files /dev/null and b/FMPhotoPickerExample/FMPhotoPickerExample/file0001608482449.jpg differ