From 74d65534e5dfaa557a737b89a92caf40504cb37f Mon Sep 17 00:00:00 2001 From: High5Apps Date: Thu, 13 Feb 2020 00:45:46 -0800 Subject: [PATCH 1/4] Add selectionType setting with checked and numbered options This change adds a new selectionType setting that defaults to checked. If selectionType is changed to numbered, icons are drawn with a number instead of a checkmark. Numbering is based on the selection order and begins at 1. On deselection, if the cell wasn't the most recently selected, all higher numbered selections are decremented so that the most recent selection always equals the selection count. --- BSImagePicker.xcodeproj/project.pbxproj | 8 ++ Sources/Model/Settings.swift | 10 +- .../Assets/AssetCollectionViewCell.swift | 23 +++-- .../AssetsCollectionViewDataSource.swift | 7 ++ .../Scene/Assets/AssetsViewController.swift | 23 +++++ Sources/Scene/Assets/CheckmarkView.swift | 56 ++--------- Sources/Scene/Assets/NumberView.swift | 50 ++++++++++ Sources/Scene/Assets/SelectionView.swift | 93 +++++++++++++++++++ 8 files changed, 213 insertions(+), 57 deletions(-) mode change 100755 => 100644 Sources/Scene/Assets/CheckmarkView.swift create mode 100644 Sources/Scene/Assets/NumberView.swift create mode 100755 Sources/Scene/Assets/SelectionView.swift diff --git a/BSImagePicker.xcodeproj/project.pbxproj b/BSImagePicker.xcodeproj/project.pbxproj index f7d59421..f90f1c6c 100644 --- a/BSImagePicker.xcodeproj/project.pbxproj +++ b/BSImagePicker.xcodeproj/project.pbxproj @@ -49,6 +49,8 @@ 55CDB45F22347D640050D572 /* ZoomInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CDB45E22347D640050D572 /* ZoomInteractionController.swift */; }; 55F67B77222EEB2500805134 /* VideoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F67B76222EEB2500805134 /* VideoCollectionViewCell.swift */; }; 55F67B7B222F088500805134 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F67B7A222F088500805134 /* GradientView.swift */; }; + C2DC13CA23F75BDB0035FD13 /* NumberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DC13C923F75BDA0035FD13 /* NumberView.swift */; }; + C2DC13CC23F75BE40035FD13 /* SelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DC13CB23F75BE40035FD13 /* SelectionView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -124,6 +126,8 @@ 55DAB07F23145A8A00982A5B /* .travis.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .travis.yml; sourceTree = ""; }; 55F67B76222EEB2500805134 /* VideoCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCollectionViewCell.swift; sourceTree = ""; }; 55F67B7A222F088500805134 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; + C2DC13C923F75BDA0035FD13 /* NumberView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberView.swift; sourceTree = ""; }; + C2DC13CB23F75BE40035FD13 /* SelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -208,7 +212,9 @@ 55BCF8BD21D52C1000386752 /* AssetsCollectionViewDataSource.swift */, 55BCF8C121D52C1000386752 /* AssetCollectionViewCell.swift */, 55F67B76222EEB2500805134 /* VideoCollectionViewCell.swift */, + C2DC13CB23F75BE40035FD13 /* SelectionView.swift */, 55BCF8C321D52C1000386752 /* CheckmarkView.swift */, + C2DC13C923F75BDA0035FD13 /* NumberView.swift */, 55BCF8C521D52C1000386752 /* CameraCollectionViewCell.swift */, 55F67B7A222F088500805134 /* GradientView.swift */, ); @@ -458,10 +464,12 @@ 559DB80F21E655D000CD58B4 /* ImagePickerControllerDelegate.swift in Sources */, 559DB81721E6AFD800CD58B4 /* ImagePickerController+Assets.swift in Sources */, 55CDB45B223435420050D572 /* PlayerView.swift in Sources */, + C2DC13CC23F75BE40035FD13 /* SelectionView.swift in Sources */, 559DB81921E6AFF300CD58B4 /* ImagePickerController+Albums.swift in Sources */, 55BCF8D721D52C1000386752 /* CheckmarkView.swift in Sources */, 555472AE21E538B000B90CA5 /* AssetsViewController.swift in Sources */, 5543942D232A4EB500DB51B7 /* LivePreviewViewController.swift in Sources */, + C2DC13CA23F75BDB0035FD13 /* NumberView.swift in Sources */, 55BCF8DC21D52C1000386752 /* PreviewViewController.swift in Sources */, 55BCF8CF21D52C1000386752 /* Settings.swift in Sources */, 55CDB45D2234523C0050D572 /* PreviewTitleBuilder.swift in Sources */, diff --git a/Sources/Model/Settings.swift b/Sources/Model/Settings.swift index 4e2192dd..65c77a52 100755 --- a/Sources/Model/Settings.swift +++ b/Sources/Model/Settings.swift @@ -35,12 +35,20 @@ public class Settings { /// What color to fill the circle with public lazy var selectionFillColor: UIColor = UIView().tintColor - /// Color for the actual checkmark + /// Color for the actual selection icon public lazy var selectionStrokeColor: UIColor = .white /// Shadow color for the circle public lazy var selectionShadowColor: UIColor = .black + public enum SelectionTypes { + case checked + case numbered + } + + /// The icon to display inside the selection oval + public lazy var selectionType: SelectionTypes = .checked + public lazy var previewTitleAttributes : [NSAttributedString.Key: Any] = [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.black diff --git a/Sources/Scene/Assets/AssetCollectionViewCell.swift b/Sources/Scene/Assets/AssetCollectionViewCell.swift index 279c6098..b96688b2 100755 --- a/Sources/Scene/Assets/AssetCollectionViewCell.swift +++ b/Sources/Scene/Assets/AssetCollectionViewCell.swift @@ -29,7 +29,10 @@ The photo cell. class AssetCollectionViewCell: UICollectionViewCell { let imageView: UIImageView = UIImageView(frame: .zero) var settings: Settings! { - didSet { checkmarkView.settings = settings } + didSet { selectionView.settings = settings } + } + var selectionIndex: Int? { + didSet { selectionView.selectionIndex = selectionIndex } } override var isSelected: Bool { @@ -57,7 +60,7 @@ class AssetCollectionViewCell: UICollectionViewCell { } private let selectionOverlayView: UIView = UIView(frame: .zero) - private let checkmarkView: CheckmarkView = CheckmarkView(frame: .zero) + private let selectionView: SelectionView = SelectionView(frame: .zero) override init(frame: CGRect) { super.init(frame: frame) @@ -68,10 +71,10 @@ class AssetCollectionViewCell: UICollectionViewCell { imageView.clipsToBounds = true selectionOverlayView.backgroundColor = UIColor.lightGray selectionOverlayView.translatesAutoresizingMaskIntoConstraints = false - checkmarkView.translatesAutoresizingMaskIntoConstraints = false + selectionView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(imageView) contentView.addSubview(selectionOverlayView) - contentView.addSubview(checkmarkView) + contentView.addSubview(selectionView) // Add constraints NSLayoutConstraint.activate([ @@ -83,10 +86,10 @@ class AssetCollectionViewCell: UICollectionViewCell { selectionOverlayView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), selectionOverlayView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), selectionOverlayView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - checkmarkView.heightAnchor.constraint(equalToConstant: 25), - checkmarkView.widthAnchor.constraint(equalToConstant: 25), - checkmarkView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), - checkmarkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4) + selectionView.heightAnchor.constraint(equalToConstant: 25), + selectionView.widthAnchor.constraint(equalToConstant: 25), + selectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), + selectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4) ]) updateAlpha(isSelected) @@ -108,10 +111,10 @@ class AssetCollectionViewCell: UICollectionViewCell { private func updateAlpha(_ selected: Bool) { if selected { - self.checkmarkView.alpha = 1.0 + self.selectionView.alpha = 1.0 self.selectionOverlayView.alpha = 0.3 } else { - self.checkmarkView.alpha = 0.0 + self.selectionView.alpha = 0.0 self.selectionOverlayView.alpha = 0.0 } } diff --git a/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift b/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift index 101628d6..c3de78f7 100755 --- a/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift +++ b/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift @@ -23,11 +23,16 @@ import UIKit import Photos +protocol SelectionIndexDelegate: AnyObject { + func selectionIndexForCell(at indexPath: IndexPath) -> Int? +} + class AssetsCollectionViewDataSource : NSObject, UICollectionViewDataSource { private static let assetCellIdentifier = "AssetCell" private static let videoCellIdentifier = "VideoCell" var settings: Settings! + weak var selectionIndexDelegate: SelectionIndexDelegate? private let fetchResult: PHFetchResult private let imageManager = PHCachingImageManager.default() @@ -73,6 +78,8 @@ class AssetsCollectionViewDataSource : NSObject, UICollectionViewDataSource { cell.settings = settings loadImage(for: asset, in: cell) + + cell.selectionIndex = selectionIndexDelegate?.selectionIndexForCell(at: indexPath) cell.isAccessibilityElement = true cell.accessibilityTraits = UIAccessibilityTraits.button diff --git a/Sources/Scene/Assets/AssetsViewController.swift b/Sources/Scene/Assets/AssetsViewController.swift index b6f6d3c7..eb46c951 100644 --- a/Sources/Scene/Assets/AssetsViewController.swift +++ b/Sources/Scene/Assets/AssetsViewController.swift @@ -46,6 +46,7 @@ class AssetsViewController: UIViewController { private var dataSource: AssetsCollectionViewDataSource? { didSet { dataSource?.settings = settings + dataSource?.selectionIndexDelegate = self collectionView.dataSource = dataSource } } @@ -161,14 +162,22 @@ class AssetsViewController: UIViewController { extension AssetsViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectionFeedback.selectionChanged() + let asset = fetchResult.object(at: indexPath.row) delegate?.assetsViewController(self, didSelectAsset: asset) + + updateSelectionIndexForCell(at: indexPath) } func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { selectionFeedback.selectionChanged() + let asset = fetchResult.object(at: indexPath.row) delegate?.assetsViewController(self, didDeselectAsset: asset) + + for indexPath in collectionView.indexPathsForSelectedItems ?? [] { + updateSelectionIndexForCell(at: indexPath) + } } func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { @@ -223,3 +232,17 @@ extension AssetsViewController: PHPhotoLibraryChangeObserver { } } } + +extension AssetsViewController: SelectionIndexDelegate { + func selectionIndexForCell(at indexPath: IndexPath) -> Int? { + let asset = fetchResult.object(at: indexPath.row) + let selections = delegate?.selectedAssets() ?? [] + return selections.firstIndex(of: asset) + } + + private func updateSelectionIndexForCell(at indexPath: IndexPath) { + if let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell { + cell.selectionIndex = selectionIndexForCell(at: indexPath) + } + } +} diff --git a/Sources/Scene/Assets/CheckmarkView.swift b/Sources/Scene/Assets/CheckmarkView.swift old mode 100755 new mode 100644 index 071c739b..8c41e351 --- a/Sources/Scene/Assets/CheckmarkView.swift +++ b/Sources/Scene/Assets/CheckmarkView.swift @@ -22,57 +22,21 @@ import UIKit -/** -Used as an overlay on selected cells -*/ class CheckmarkView: UIView { - var settings: Settings! - - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .clear + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - backgroundColor = .clear + + init() { + super.init(frame: .zero) } override func draw(_ rect: CGRect) { - //// General Declarations - let context = UIGraphicsGetCurrentContext() - - //// Color Declarations - - //// Shadow Declarations - let shadow2Offset = CGSize(width: 0.1, height: -0.1); - let shadow2BlurRadius: CGFloat = 2.5; + let path = UIBezierPath() + path.move(to: CGPoint(x: 7, y: 12.5)) + path.addLine(to: CGPoint(x: 11, y: 16)) + path.addLine(to: CGPoint(x: 17.5, y: 9.5)) - //// Frames - let checkmarkFrame = bounds; - - //// Subframes - let group = CGRect(x: checkmarkFrame.minX + 3, y: checkmarkFrame.minY + 3, width: checkmarkFrame.width - 6, height: checkmarkFrame.height - 6) - - //// CheckedOval Drawing - let checkedOvalPath = UIBezierPath(ovalIn: CGRect(x: group.minX + floor(group.width * 0.0 + 0.5), y: group.minY + floor(group.height * 0.0 + 0.5), width: floor(group.width * 1.0 + 0.5) - floor(group.width * 0.0 + 0.5), height: floor(group.height * 1.0 + 0.5) - floor(group.height * 0.0 + 0.5))) - context?.saveGState() - context?.setShadow(offset: shadow2Offset, blur: shadow2BlurRadius, color: settings.theme.selectionShadowColor.cgColor) - settings.theme.selectionFillColor.setFill() - checkedOvalPath.fill() - context?.restoreGState() - - settings.theme.selectionStrokeColor.setStroke() - checkedOvalPath.lineWidth = 1 - checkedOvalPath.stroke() - - //// Check mark - context?.setStrokeColor(settings.theme.selectionStrokeColor.cgColor) - - let checkPath = UIBezierPath() - checkPath.move(to: CGPoint(x: 7, y: 12.5)) - checkPath.addLine(to: CGPoint(x: 11, y: 16)) - checkPath.addLine(to: CGPoint(x: 17.5, y: 9.5)) - checkPath.stroke() + path.stroke() } } diff --git a/Sources/Scene/Assets/NumberView.swift b/Sources/Scene/Assets/NumberView.swift new file mode 100644 index 00000000..6fafa30a --- /dev/null +++ b/Sources/Scene/Assets/NumberView.swift @@ -0,0 +1,50 @@ +// The MIT License (MIT) +// +// Copyright (c) 2020 Joakim Gyllström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +class NumberView: UILabel { + + override var tintColor: UIColor! { + didSet { + textColor = tintColor + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + init() { + super.init(frame: .zero) + + font = UIFont.preferredFont(forTextStyle: .body) + numberOfLines = 1 + adjustsFontSizeToFitWidth = true + baselineAdjustment = .alignCenters + textAlignment = .center + } + + override func draw(_ rect: CGRect) { + super.drawText(in: rect) + } +} diff --git a/Sources/Scene/Assets/SelectionView.swift b/Sources/Scene/Assets/SelectionView.swift new file mode 100755 index 00000000..e267c5da --- /dev/null +++ b/Sources/Scene/Assets/SelectionView.swift @@ -0,0 +1,93 @@ +// The MIT License (MIT) +// +// Copyright (c) 2020 Joakim Gyllström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/** +Used as an overlay on selected cells +*/ +class SelectionView: UIView { + var settings: Settings! + + var selectionIndex: Int? { + didSet { + guard let numberView = icon as? NumberView, let selectionIndex = selectionIndex else { return } + // Add 1 since selections should be 1-indexed + numberView.text = (selectionIndex + 1).description + setNeedsDisplay() + } + } + + private lazy var icon: UIView = { + switch settings.theme.selectionType { + case .checked: + return CheckmarkView() + case .numbered: + return NumberView() + } + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + backgroundColor = .clear + } + + override func draw(_ rect: CGRect) { + //// General Declarations + let context = UIGraphicsGetCurrentContext() + + //// Shadow Declarations + let shadow2Offset = CGSize(width: 0.1, height: -0.1); + let shadow2BlurRadius: CGFloat = 2.5; + + //// Frames + let selectionFrame = bounds; + + //// Subframes + let group = selectionFrame.insetBy(dx: 3, dy: 3) + + //// SelectedOval Drawing + let selectedOvalPath = UIBezierPath(ovalIn: CGRect(x: group.minX + floor(group.width * 0.0 + 0.5), y: group.minY + floor(group.height * 0.0 + 0.5), width: floor(group.width * 1.0 + 0.5) - floor(group.width * 0.0 + 0.5), height: floor(group.height * 1.0 + 0.5) - floor(group.height * 0.0 + 0.5))) + context?.saveGState() + context?.setShadow(offset: shadow2Offset, blur: shadow2BlurRadius, color: settings.theme.selectionShadowColor.cgColor) + settings.theme.selectionFillColor.setFill() + selectedOvalPath.fill() + context?.restoreGState() + + settings.theme.selectionStrokeColor.setStroke() + selectedOvalPath.lineWidth = 1 + selectedOvalPath.stroke() + + //// Selection icon + let largestSquareInCircleInsetRatio: CGFloat = 0.5 - (0.25 * sqrt(2)) + let dx = group.size.width * largestSquareInCircleInsetRatio + let dy = group.size.height * largestSquareInCircleInsetRatio + icon.frame = group.insetBy(dx: dx, dy: dy) + icon.tintColor = settings.theme.selectionStrokeColor + icon.draw(icon.frame) + } +} From 250ee20092b5d06f6f536c30d8cf5ad0cc66bb04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Gyllstr=C3=B6m?= Date: Sun, 1 Mar 2020 13:39:18 +0100 Subject: [PATCH 2/4] Fix so selection number is kept after photo library did change callback --- .../contents.xcworkspacedata | 3 - Example/ViewController.swift | 2 + .../ImagePickerController+Assets.swift | 8 -- .../Controller/ImagePickerController.swift | 11 ++- Sources/Model/AssetStore.swift | 4 + Sources/Model/Settings.swift | 10 +-- .../Assets/AssetCollectionViewCell.swift | 1 + .../AssetsCollectionViewDataSource.swift | 17 ++-- .../Scene/Assets/AssetsViewController.swift | 79 ++++++++++--------- 9 files changed, 71 insertions(+), 64 deletions(-) diff --git a/BSImagePicker.xcworkspace/contents.xcworkspacedata b/BSImagePicker.xcworkspace/contents.xcworkspacedata index 11628338..a27d092e 100644 --- a/BSImagePicker.xcworkspace/contents.xcworkspacedata +++ b/BSImagePicker.xcworkspace/contents.xcworkspacedata @@ -7,7 +7,4 @@ - - diff --git a/Example/ViewController.swift b/Example/ViewController.swift index b67af3ac..c8bd0973 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -29,7 +29,9 @@ class ViewController: UIViewController { @IBAction func showImagePicker(_ sender: UIButton) { let imagePicker = ImagePickerController() imagePicker.settings.selection.max = 5 + imagePicker.settings.theme.selectionType = .numbered imagePicker.settings.fetch.assets.supportedMediaTypes = [.image, .video] + imagePicker.settings.selection.unselectOnReachingMax = true let start = Date() self.presentImagePicker(imagePicker, select: { (asset) in diff --git a/Sources/Controller/ImagePickerController+Assets.swift b/Sources/Controller/ImagePickerController+Assets.swift index 59f89878..c3c7f3a4 100644 --- a/Sources/Controller/ImagePickerController+Assets.swift +++ b/Sources/Controller/ImagePickerController+Assets.swift @@ -49,12 +49,4 @@ extension ImagePickerController: AssetsViewControllerDelegate { pushViewController(previewViewController, animated: true) } - - func shouldSelect(in assetsViewController: AssetsViewController) -> Bool { - return assetStore.count < settings.selection.max || settings.selection.unselectOnReachingMax - } - - func selectedAssets() -> [PHAsset] { - return assetStore.assets - } } diff --git a/Sources/Controller/ImagePickerController.swift b/Sources/Controller/ImagePickerController.swift index 17171915..00ca9c87 100644 --- a/Sources/Controller/ImagePickerController.swift +++ b/Sources/Controller/ImagePickerController.swift @@ -42,7 +42,7 @@ public class ImagePickerController: UINavigationController { var onCancel: ((_ assets: [PHAsset]) -> Void)? var onFinish: ((_ assets: [PHAsset]) -> Void)? - let assetsViewController = AssetsViewController() + let assetsViewController: AssetsViewController let albumsViewController = AlbumsViewController() let dropdownTransitionDelegate = DropdownTransitionDelegate() let zoomTransitionDelegate = ZoomTransitionDelegate() @@ -64,6 +64,15 @@ public class ImagePickerController: UINavigationController { } }() + public init() { + assetsViewController = AssetsViewController(store: assetStore) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public override func viewDidLoad() { super.viewDidLoad() diff --git a/Sources/Model/AssetStore.swift b/Sources/Model/AssetStore.swift index d2a29aa3..a092127b 100644 --- a/Sources/Model/AssetStore.swift +++ b/Sources/Model/AssetStore.swift @@ -51,4 +51,8 @@ public class AssetStore { func removeFirst() -> PHAsset? { return assets.removeFirst() } + + func index(of asset: PHAsset) -> Int? { + return assets.firstIndex(of: asset) + } } diff --git a/Sources/Model/Settings.swift b/Sources/Model/Settings.swift index 65c77a52..df693955 100755 --- a/Sources/Model/Settings.swift +++ b/Sources/Model/Settings.swift @@ -111,11 +111,11 @@ public class Settings { /// Fetch results for asset collections you want to present to the user public lazy var fetchResults: [PHFetchResult] = [ PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: options), - PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: options), - PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: options), - PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumSelfPortraits, options: options), - PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumPanoramas, options: options), - PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumVideos, options: options), +// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: options), +// PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: options), +// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumSelfPortraits, options: options), +// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumPanoramas, options: options), +// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumVideos, options: options), ] } diff --git a/Sources/Scene/Assets/AssetCollectionViewCell.swift b/Sources/Scene/Assets/AssetCollectionViewCell.swift index b96688b2..3db8c554 100755 --- a/Sources/Scene/Assets/AssetCollectionViewCell.swift +++ b/Sources/Scene/Assets/AssetCollectionViewCell.swift @@ -103,6 +103,7 @@ class AssetCollectionViewCell: UICollectionViewCell { override func prepareForReuse() { super.prepareForReuse() imageView.image = nil + selectionIndex = nil } func updateAccessibilityLabel(_ selected: Bool) { diff --git a/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift b/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift index c3de78f7..30993f13 100755 --- a/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift +++ b/Sources/Scene/Assets/AssetsCollectionViewDataSource.swift @@ -23,26 +23,23 @@ import UIKit import Photos -protocol SelectionIndexDelegate: AnyObject { - func selectionIndexForCell(at indexPath: IndexPath) -> Int? -} - class AssetsCollectionViewDataSource : NSObject, UICollectionViewDataSource { private static let assetCellIdentifier = "AssetCell" private static let videoCellIdentifier = "VideoCell" var settings: Settings! - weak var selectionIndexDelegate: SelectionIndexDelegate? private let fetchResult: PHFetchResult private let imageManager = PHCachingImageManager.default() private let durationFormatter = DateComponentsFormatter() + private let store: AssetStore private let scale: CGFloat private var targetSize: CGSize = .zero - init(fetchResult: PHFetchResult, scale: CGFloat = UIScreen.main.scale) { + init(fetchResult: PHFetchResult, store: AssetStore, scale: CGFloat = UIScreen.main.scale) { self.fetchResult = fetchResult + self.store = store self.scale = scale durationFormatter.unitsStyle = .positional durationFormatter.zeroFormattingBehavior = [.pad] @@ -73,16 +70,14 @@ class AssetsCollectionViewDataSource : NSObject, UICollectionViewDataSource { } UIView.setAnimationsEnabled(animationsWasEnabled) - cell.accessibilityIdentifier = "photo_cell_\(indexPath.item)" + cell.accessibilityIdentifier = "Photo \(indexPath.item + 1)" + cell.accessibilityTraits = UIAccessibilityTraits.button cell.isAccessibilityElement = true cell.settings = settings loadImage(for: asset, in: cell) - cell.selectionIndex = selectionIndexDelegate?.selectionIndexForCell(at: indexPath) - - cell.isAccessibilityElement = true - cell.accessibilityTraits = UIAccessibilityTraits.button + cell.selectionIndex = store.index(of: asset) return cell } diff --git a/Sources/Scene/Assets/AssetsViewController.swift b/Sources/Scene/Assets/AssetsViewController.swift index eb46c951..62e5e4cd 100644 --- a/Sources/Scene/Assets/AssetsViewController.swift +++ b/Sources/Scene/Assets/AssetsViewController.swift @@ -27,8 +27,6 @@ protocol AssetsViewControllerDelegate: class { func assetsViewController(_ assetsViewController: AssetsViewController, didSelectAsset asset: PHAsset) func assetsViewController(_ assetsViewController: AssetsViewController, didDeselectAsset asset: PHAsset) func assetsViewController(_ assetsViewController: AssetsViewController, didLongPressCell cell: AssetCollectionViewCell, displayingAsset asset: PHAsset) - func shouldSelect(in assetsViewController: AssetsViewController) -> Bool - func selectedAssets() -> [PHAsset] } class AssetsViewController: UIViewController { @@ -37,22 +35,31 @@ class AssetsViewController: UIViewController { didSet { dataSource?.settings = settings } } + private let store: AssetStore private let collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) private var fetchResult: PHFetchResult = PHFetchResult() { didSet { - dataSource = AssetsCollectionViewDataSource(fetchResult: fetchResult) + dataSource = AssetsCollectionViewDataSource(fetchResult: fetchResult, store: store) } } private var dataSource: AssetsCollectionViewDataSource? { didSet { dataSource?.settings = settings - dataSource?.selectionIndexDelegate = self collectionView.dataSource = dataSource } } - + private let selectionFeedback = UISelectionFeedbackGenerator() + init(store: AssetStore) { + self.store = store + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + deinit { PHPhotoLibrary.shared().unregisterChangeObserver(self) } @@ -91,18 +98,23 @@ class AssetsViewController: UIViewController { DispatchQueue.global(qos: .userInteractive).async { let fetchResult = PHAsset.fetchAssets(in: album, options: options) DispatchQueue.main.async { [weak self] in - self?.fetchResult = fetchResult - self?.collectionView.reloadData() - let selections = self?.delegate?.selectedAssets() ?? [] - self?.syncSelections(selections) - self?.collectionView.setContentOffset(.zero, animated: false) + guard let self = self else { return } + self.fetchResult = fetchResult + self.collectionView.reloadData() + print("Reload show assets") + let selections = self.store.assets + self.syncSelections(selections) + self.collectionView.setContentOffset(.zero, animated: false) } } } private func syncSelections(_ assets: [PHAsset]) { + NSLog("SYNC SELECTIONS!") collectionView.allowsMultipleSelection = true + print("Selected before: \(collectionView.indexPathsForSelectedItems!)") + // Unselect all for indexPath in collectionView.indexPathsForSelectedItems ?? [] { collectionView.deselectItem(at: indexPath, animated: false) @@ -113,10 +125,14 @@ class AssetsViewController: UIViewController { let index = fetchResult.index(of: asset) guard index != NSNotFound else { continue } let indexPath = IndexPath(item: index, section: 0) - collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .top) + collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) + updateSelectionIndexForCell(at: indexPath) } + + print("Selected after: \(collectionView.indexPathsForSelectedItems!)") } + // TODO: Replace with sync ^^ func unselect(asset:PHAsset) { let index = fetchResult.index(of: asset) guard index != NSNotFound else { return } @@ -157,6 +173,13 @@ class AssetsViewController: UIViewController { collectionViewFlowLayout.minimumInteritemSpacing = itemSpacing collectionViewFlowLayout.itemSize = itemSize } + + private func updateSelectionIndexForCell(at indexPath: IndexPath) { + guard settings.theme.selectionType == .numbered else { return } + guard let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell else { return } + let asset = fetchResult.object(at: indexPath.row) + cell.selectionIndex = store.index(of: asset) + } } extension AssetsViewController: UICollectionViewDelegate { @@ -164,8 +187,9 @@ extension AssetsViewController: UICollectionViewDelegate { selectionFeedback.selectionChanged() let asset = fetchResult.object(at: indexPath.row) + store.append(asset) delegate?.assetsViewController(self, didSelectAsset: asset) - + updateSelectionIndexForCell(at: indexPath) } @@ -173,6 +197,7 @@ extension AssetsViewController: UICollectionViewDelegate { selectionFeedback.selectionChanged() let asset = fetchResult.object(at: indexPath.row) + store.remove(asset) delegate?.assetsViewController(self, didDeselectAsset: asset) for indexPath in collectionView.indexPathsForSelectedItems ?? [] { @@ -181,15 +206,10 @@ extension AssetsViewController: UICollectionViewDelegate { } func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - guard let delegate = delegate else { return true } - - if delegate.shouldSelect(in: self) { - selectionFeedback.prepare() - - return true - } else { - return false - } + guard store.count < settings.selection.max || settings.selection.unselectOnReachingMax else { return false } + selectionFeedback.prepare() + + return true } } @@ -226,23 +246,10 @@ extension AssetsViewController: PHPhotoLibraryChangeObserver { } else { self.fetchResult = changes.fetchResultAfterChanges self.collectionView.reloadData() - let selections = self.delegate?.selectedAssets() ?? [] - self.syncSelections(selections) } - } - } -} -extension AssetsViewController: SelectionIndexDelegate { - func selectionIndexForCell(at indexPath: IndexPath) -> Int? { - let asset = fetchResult.object(at: indexPath.row) - let selections = delegate?.selectedAssets() ?? [] - return selections.firstIndex(of: asset) - } - - private func updateSelectionIndexForCell(at indexPath: IndexPath) { - if let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell { - cell.selectionIndex = selectionIndexForCell(at: indexPath) + // No matter if we have incremental changes or not, sync the selections + self.syncSelections(self.store.assets) } } } From 6568c61f1c4f0dd4f37579d114dd2a9df02f82e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Gyllstr=C3=B6m?= Date: Sun, 1 Mar 2020 14:00:26 +0100 Subject: [PATCH 3/4] Updated numbered selection to work with unselect on reaching max --- Example/ViewController.swift | 2 +- .../ImagePickerController+Assets.swift | 5 ++--- Sources/Model/Settings.swift | 15 ++++++++------- Sources/Scene/Assets/AssetsViewController.swift | 17 +++++++---------- Sources/Scene/Assets/SelectionView.swift | 2 +- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/Example/ViewController.swift b/Example/ViewController.swift index c8bd0973..cdd33a58 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -29,7 +29,7 @@ class ViewController: UIViewController { @IBAction func showImagePicker(_ sender: UIButton) { let imagePicker = ImagePickerController() imagePicker.settings.selection.max = 5 - imagePicker.settings.theme.selectionType = .numbered + imagePicker.settings.theme.selectionStyle = .numbered imagePicker.settings.fetch.assets.supportedMediaTypes = [.image, .video] imagePicker.settings.selection.unselectOnReachingMax = true diff --git a/Sources/Controller/ImagePickerController+Assets.swift b/Sources/Controller/ImagePickerController+Assets.swift index c3c7f3a4..d1e62b18 100644 --- a/Sources/Controller/ImagePickerController+Assets.swift +++ b/Sources/Controller/ImagePickerController+Assets.swift @@ -25,18 +25,17 @@ import Photos extension ImagePickerController: AssetsViewControllerDelegate { func assetsViewController(_ assetsViewController: AssetsViewController, didSelectAsset asset: PHAsset) { - if settings.selection.unselectOnReachingMax && assetStore.count >= settings.selection.max { + if settings.selection.unselectOnReachingMax && assetStore.count > settings.selection.max { if let first = assetStore.removeFirst() { assetsViewController.unselect(asset:first) + imagePickerDelegate?.imagePicker(self, didDeselectAsset: first) } } - assetStore.append(asset) updatedDoneButton() imagePickerDelegate?.imagePicker(self, didSelectAsset: asset) } func assetsViewController(_ assetsViewController: AssetsViewController, didDeselectAsset asset: PHAsset) { - assetStore.remove(asset) updatedDoneButton() imagePickerDelegate?.imagePicker(self, didDeselectAsset: asset) } diff --git a/Sources/Model/Settings.swift b/Sources/Model/Settings.swift index df693955..c669d710 100755 --- a/Sources/Model/Settings.swift +++ b/Sources/Model/Settings.swift @@ -41,13 +41,13 @@ public class Settings { /// Shadow color for the circle public lazy var selectionShadowColor: UIColor = .black - public enum SelectionTypes { + public enum SelectionStyle { case checked case numbered } /// The icon to display inside the selection oval - public lazy var selectionType: SelectionTypes = .checked + public lazy var selectionStyle: SelectionStyle = .checked public lazy var previewTitleAttributes : [NSAttributedString.Key: Any] = [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), @@ -109,13 +109,14 @@ public class Settings { }() /// Fetch results for asset collections you want to present to the user + /// Some other fetch results that you might wanna use: + /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: options), + /// PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: options), + /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumSelfPortraits, options: options), + /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumPanoramas, options: options), + /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumVideos, options: options), public lazy var fetchResults: [PHFetchResult] = [ PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: options), -// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: options), -// PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: options), -// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumSelfPortraits, options: options), -// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumPanoramas, options: options), -// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumVideos, options: options), ] } diff --git a/Sources/Scene/Assets/AssetsViewController.swift b/Sources/Scene/Assets/AssetsViewController.swift index 62e5e4cd..d7ff7dab 100644 --- a/Sources/Scene/Assets/AssetsViewController.swift +++ b/Sources/Scene/Assets/AssetsViewController.swift @@ -101,7 +101,6 @@ class AssetsViewController: UIViewController { guard let self = self else { return } self.fetchResult = fetchResult self.collectionView.reloadData() - print("Reload show assets") let selections = self.store.assets self.syncSelections(selections) self.collectionView.setContentOffset(.zero, animated: false) @@ -110,11 +109,8 @@ class AssetsViewController: UIViewController { } private func syncSelections(_ assets: [PHAsset]) { - NSLog("SYNC SELECTIONS!") collectionView.allowsMultipleSelection = true - print("Selected before: \(collectionView.indexPathsForSelectedItems!)") - // Unselect all for indexPath in collectionView.indexPathsForSelectedItems ?? [] { collectionView.deselectItem(at: indexPath, animated: false) @@ -128,17 +124,18 @@ class AssetsViewController: UIViewController { collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) updateSelectionIndexForCell(at: indexPath) } - - print("Selected after: \(collectionView.indexPathsForSelectedItems!)") } // TODO: Replace with sync ^^ - func unselect(asset:PHAsset) { + func unselect(asset: PHAsset) { let index = fetchResult.index(of: asset) guard index != NSNotFound else { return } let indexPath = IndexPath(item: index, section: 0) - collectionView.deselectItem(at:indexPath, animated: true) - delegate?.assetsViewController(self, didDeselectAsset: asset) + collectionView.deselectItem(at:indexPath, animated: false) + + for indexPath in collectionView.indexPathsForSelectedItems ?? [] { + updateSelectionIndexForCell(at: indexPath) + } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -175,7 +172,7 @@ class AssetsViewController: UIViewController { } private func updateSelectionIndexForCell(at indexPath: IndexPath) { - guard settings.theme.selectionType == .numbered else { return } + guard settings.theme.selectionStyle == .numbered else { return } guard let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell else { return } let asset = fetchResult.object(at: indexPath.row) cell.selectionIndex = store.index(of: asset) diff --git a/Sources/Scene/Assets/SelectionView.swift b/Sources/Scene/Assets/SelectionView.swift index e267c5da..d8100f50 100755 --- a/Sources/Scene/Assets/SelectionView.swift +++ b/Sources/Scene/Assets/SelectionView.swift @@ -38,7 +38,7 @@ class SelectionView: UIView { } private lazy var icon: UIView = { - switch settings.theme.selectionType { + switch settings.theme.selectionStyle { case .checked: return CheckmarkView() case .numbered: From 2868657ef6fe6ac6f04c8fc929f960a3b3e4853c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Gyllstr=C3=B6m?= Date: Sun, 1 Mar 2020 14:04:37 +0100 Subject: [PATCH 4/4] Changed font to be a tiny bit smaller --- Sources/Scene/Assets/NumberView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Scene/Assets/NumberView.swift b/Sources/Scene/Assets/NumberView.swift index 6fafa30a..b830cff4 100644 --- a/Sources/Scene/Assets/NumberView.swift +++ b/Sources/Scene/Assets/NumberView.swift @@ -36,8 +36,8 @@ class NumberView: UILabel { init() { super.init(frame: .zero) - - font = UIFont.preferredFont(forTextStyle: .body) + + font = UIFont.boldSystemFont(ofSize: 12) numberOfLines = 1 adjustsFontSizeToFitWidth = true baselineAdjustment = .alignCenters