diff --git a/Source/Pages/Gallery/YPAssetViewContainer.swift b/Source/Pages/Gallery/YPAssetViewContainer.swift index 9c57902f0..4f12cc393 100644 --- a/Source/Pages/Gallery/YPAssetViewContainer.swift +++ b/Source/Pages/Gallery/YPAssetViewContainer.swift @@ -12,13 +12,17 @@ import Stevia import AVFoundation /// The container for asset (video or image). It containts the YPGridView and YPAssetZoomableView. -class YPAssetViewContainer: UIView { - public var zoomableView: YPAssetZoomableView? +final class YPAssetViewContainer: UIView { + public var zoomableView: YPAssetZoomableView public var itemOverlay: UIView? public let curtain = UIView() public let spinnerView = UIView() public let squareCropButton = UIButton() - public let multipleSelectionButton = UIButton() + public let multipleSelectionButton: UIButton = { + let v = UIButton() + v.setImage(YPConfig.icons.multipleSelectionOffIcon, for: .normal) + return v + }() public var onlySquare = YPConfig.library.onlySquare public var isShown = true public var spinnerIsShown = false @@ -28,97 +32,96 @@ class YPAssetViewContainer: UIView { private var isMultipleSelection = false public var itemOverlayType = YPConfig.library.itemOverlayType - - override func awakeFromNib() { - super.awakeFromNib() - + + init(frame: CGRect, zoomableView: YPAssetZoomableView) { + self.zoomableView = zoomableView + super.init(frame: frame) + + self.zoomableView.zoomableViewDelegate = self + switch itemOverlayType { case .grid: itemOverlay = YPGridView() default: break } - + if let itemOverlay = itemOverlay { addSubview(itemOverlay) itemOverlay.frame = frame clipsToBounds = true - + itemOverlay.alpha = 0 } - - for sv in subviews { - if let cv = sv as? YPAssetZoomableView { - zoomableView = cv - zoomableView?.myDelegate = self - } - } - + let touchDownGR = UILongPressGestureRecognizer(target: self, action: #selector(handleTouchDown)) touchDownGR.minimumPressDuration = 0 touchDownGR.delegate = self addGestureRecognizer(touchDownGR) - + // TODO: Add tap gesture to play/pause. Add double tap gesture to square/unsquare - + subviews( spinnerView.subviews( spinner ), curtain ) - + spinner.centerInContainer() spinnerView.fillContainer() curtain.fillContainer() - + spinner.startAnimating() spinnerView.backgroundColor = UIColor.ypLabel.withAlphaComponent(0.3) curtain.backgroundColor = UIColor.ypLabel.withAlphaComponent(0.7) curtain.alpha = 0 - + if !onlySquare { // Crop Button squareCropButton.setImage(YPConfig.icons.cropIcon, for: .normal) subviews(squareCropButton) squareCropButton.size(42) |-15-squareCropButton - squareCropButton.Bottom == zoomableView!.Bottom - 15 + squareCropButton.Bottom == self.Bottom - 15 } - + // Multiple selection button subviews(multipleSelectionButton) - multipleSelectionButton.size(42) - multipleSelectionButton-15-| - multipleSelectionButton.setImage(YPConfig.icons.multipleSelectionOffIcon, for: .normal) - multipleSelectionButton.Bottom == zoomableView!.Bottom - 15 - + multipleSelectionButton.size(42).trailing(15) + multipleSelectionButton.Bottom == self.Bottom - 15 } - + + required init?(coder: NSCoder) { + zoomableView = YPAssetZoomableView() + super.init(coder: coder) + fatalError("Only code layout.") + } + // MARK: - Square button @objc public func squareCropButtonTapped() { - if let zoomableView = zoomableView { +// if let zoomableView = zoomableView { let z = zoomableView.zoomScale shouldCropToSquare = (z >= 1 && z < zoomableView.squaredZoomScale) - } - zoomableView?.fitImage(shouldCropToSquare, animated: true) +// } + zoomableView.fitImage(shouldCropToSquare, animated: true) } public func refreshSquareCropButton() { if onlySquare { squareCropButton.isHidden = true } else { - if let image = zoomableView?.assetImageView.image { + if let image = zoomableView.assetImageView.image { let isImageASquare = image.size.width == image.size.height squareCropButton.isHidden = isImageASquare } } let shouldFit = YPConfig.library.onlySquare ? true : shouldCropToSquare - zoomableView?.fitImage(shouldFit) - zoomableView?.layoutSubviews() + zoomableView.fitImage(shouldFit) + zoomableView.layoutSubviews() } // MARK: - Multiple selection diff --git a/Source/Pages/Gallery/YPAssetZoomableView.swift b/Source/Pages/Gallery/YPAssetZoomableView.swift index 1e492d4c1..bc0717c4b 100644 --- a/Source/Pages/Gallery/YPAssetZoomableView.swift +++ b/Source/Pages/Gallery/YPAssetZoomableView.swift @@ -17,7 +17,7 @@ protocol YPAssetZoomableViewDelegate: AnyObject { } final class YPAssetZoomableView: UIScrollView { - public weak var myDelegate: YPAssetZoomableViewDelegate? + public weak var zoomableViewDelegate: YPAssetZoomableViewDelegate? public var cropAreaDidChange = {} public var isVideoMode = false public var photoImageView = UIImageView() @@ -92,7 +92,7 @@ final class YPAssetZoomableView: UIScrollView { strongSelf.videoView.loadVideo(playerItem) strongSelf.videoView.play() - strongSelf.myDelegate?.ypAssetZoomableViewDidLayoutSubviews(strongSelf) + strongSelf.zoomableViewDelegate?.ypAssetZoomableViewDidLayoutSubviews(strongSelf) } } @@ -143,10 +143,10 @@ final class YPAssetZoomableView: UIScrollView { photoImageView.removeFromSuperview() } - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder)! + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = YPConfig.colors.assetViewBackgroundColor - frame.size = CGSize.zero clipsToBounds = true photoImageView.frame = CGRect(origin: CGPoint.zero, size: CGSize.zero) videoView.frame = CGRect(origin: CGPoint.zero, size: CGSize.zero) @@ -160,9 +160,14 @@ final class YPAssetZoomableView: UIScrollView { isScrollEnabled = true } + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder)! + fatalError("Only code layout.") + } + override func layoutSubviews() { super.layoutSubviews() - myDelegate?.ypAssetZoomableViewDidLayoutSubviews(self) + zoomableViewDelegate?.ypAssetZoomableViewDidLayoutSubviews(self) } } @@ -253,7 +258,7 @@ extension YPAssetZoomableView: UIScrollViewDelegate { } func scrollViewDidZoom(_ scrollView: UIScrollView) { - myDelegate?.ypAssetZoomableViewScrollViewDidZoom() + zoomableViewDelegate?.ypAssetZoomableViewScrollViewDidZoom() centerAssetView() } @@ -266,7 +271,7 @@ extension YPAssetZoomableView: UIScrollViewDelegate { self.fitImage(true, animated: true) } - myDelegate?.ypAssetZoomableViewScrollViewDidEndZooming() + zoomableViewDelegate?.ypAssetZoomableViewScrollViewDidEndZooming() cropAreaDidChange() } diff --git a/Source/Pages/Gallery/YPLibrary+LibraryChange.swift b/Source/Pages/Gallery/YPLibrary+LibraryChange.swift index 34672f460..8b0991ca4 100644 --- a/Source/Pages/Gallery/YPLibrary+LibraryChange.swift +++ b/Source/Pages/Gallery/YPLibrary+LibraryChange.swift @@ -16,13 +16,13 @@ extension YPLibraryVC: PHPhotoLibraryChangeObserver { public func photoLibraryDidChange(_ changeInstance: PHChange) { guard let fetchResult = self.mediaManager.fetchResult, - let collectionChanges = changeInstance.changeDetails(for: fetchResult), - let collectionView = self.v.collectionView else { + let collectionChanges = changeInstance.changeDetails(for: fetchResult) else { ypLog("Some problems there.") return } DispatchQueue.main.async { + let collectionView = self.v.collectionView self.mediaManager.fetchResult = collectionChanges.fetchResultAfterChanges if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves { collectionView.reloadData() diff --git a/Source/Pages/Gallery/YPLibraryVC+CollectionView.swift b/Source/Pages/Gallery/YPLibraryVC+CollectionView.swift index 60d43ae30..1b17cd385 100644 --- a/Source/Pages/Gallery/YPLibraryVC+CollectionView.swift +++ b/Source/Pages/Gallery/YPLibraryVC+CollectionView.swift @@ -12,7 +12,6 @@ extension YPLibraryVC { var isLimitExceeded: Bool { return selectedItems.count >= YPConfig.library.maxNumberOfItems } func setupCollectionView() { - v.collectionView.backgroundColor = YPConfig.colors.libraryScreenBackgroundColor v.collectionView.dataSource = self v.collectionView.delegate = self v.collectionView.register(YPLibraryViewCell.self, forCellWithReuseIdentifier: "YPLibraryViewCell") diff --git a/Source/Pages/Gallery/YPLibraryVC+PanGesture.swift b/Source/Pages/Gallery/YPLibraryVC+PanGesture.swift index 0e478f64a..c04890871 100644 --- a/Source/Pages/Gallery/YPLibraryVC+PanGesture.swift +++ b/Source/Pages/Gallery/YPLibraryVC+PanGesture.swift @@ -21,10 +21,12 @@ public class PanGestureHelper: NSObject, UIGestureRecognizerDelegate { // The height constraint of the view with main selected image var topHeight: CGFloat { - get { return v.assetViewContainerConstraintTop.constant } + get { + return v.assetViewContainerConstraintTop?.constant ?? 0 + } set { if newValue >= v.assetZoomableViewMinimalVisibleHeight - v.assetViewContainer.frame.height { - v.assetViewContainerConstraintTop.constant = newValue + v.assetViewContainerConstraintTop?.constant = newValue } } } diff --git a/Source/Pages/Gallery/YPLibraryVC.swift b/Source/Pages/Gallery/YPLibraryVC.swift index ff617c54c..9e36ba03f 100644 --- a/Source/Pages/Gallery/YPLibraryVC.swift +++ b/Source/Pages/Gallery/YPLibraryVC.swift @@ -10,9 +10,9 @@ import UIKit import Photos import PhotosUI -internal class YPLibraryVC: UIViewController, YPPermissionCheckable { +internal final class YPLibraryVC: UIViewController, YPPermissionCheckable { internal weak var delegate: YPLibraryViewDelegate? - internal var v: YPLibraryView! + internal var v = YPLibraryView(frame: .zero) internal var isProcessing = false // true if video or image is in processing state internal var selectedItems = [YPLibrarySelection]() internal let mediaManager = LibraryMediaManager() @@ -22,30 +22,20 @@ internal class YPLibraryVC: UIViewController, YPPermissionCheckable { internal var isInitialized = false // MARK: - Init - - internal required init(items: [YPMediaItem]?) { + + internal override func loadView() { + view = v + } + + required init() { super.init(nibName: nil, bundle: nil) title = YPConfig.wordings.libraryTitle } - - internal convenience init() { - self.init(items: nil) - } - + internal required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func setAlbum(_ album: YPAlbum) { - title = album.title - mediaManager.collection = album.collection - currentlySelectedIndex = 0 - if !multipleSelectionEnabled { - selectedItems.removeAll() - } - refreshMediaRequest() - } - func initialize() { guard isInitialized == false else { return @@ -54,7 +44,7 @@ internal class YPLibraryVC: UIViewController, YPPermissionCheckable { defer { isInitialized = true } - + mediaManager.initialize() mediaManager.v = v @@ -97,13 +87,18 @@ internal class YPLibraryVC: UIViewController, YPPermissionCheckable { showMultipleSelection() } } - - // MARK: - View Lifecycle - - public override func loadView() { - v = YPLibraryView.xibView() - view = v + + func setAlbum(_ album: YPAlbum) { + title = album.title + mediaManager.collection = album.collection + currentlySelectedIndex = 0 + if !multipleSelectionEnabled { + selectedItems.removeAll() + } + refreshMediaRequest() } + + // MARK: - View Lifecycle public override func viewDidLoad() { super.viewDidLoad() @@ -199,8 +194,8 @@ internal class YPLibraryVC: UIViewController, YPPermissionCheckable { selectedItems = [ YPLibrarySelection(index: currentlySelectedIndex, cropRect: v.currentCropRect(), - scrollViewContentOffset: v.assetZoomableView!.contentOffset, - scrollViewZoomScale: v.assetZoomableView!.zoomScale, + scrollViewContentOffset: v.assetZoomableView.contentOffset, + scrollViewZoomScale: v.assetZoomableView.zoomScale, assetIdentifier: asset.localIdentifier) ] } diff --git a/Source/Pages/Gallery/YPLibraryView.swift b/Source/Pages/Gallery/YPLibraryView.swift index a97b4d3be..d42db8866 100644 --- a/Source/Pages/Gallery/YPLibraryView.swift +++ b/Source/Pages/Gallery/YPLibraryView.swift @@ -10,20 +10,68 @@ import UIKit import Stevia import Photos -final class YPLibraryView: UIView { - - let assetZoomableViewMinimalVisibleHeight: CGFloat = 50 - - @IBOutlet weak var collectionView: UICollectionView! - @IBOutlet weak var assetZoomableView: YPAssetZoomableView! - @IBOutlet weak var assetViewContainer: YPAssetViewContainer! - @IBOutlet weak var assetViewContainerConstraintTop: NSLayoutConstraint! - - let maxNumberWarningView = UIView() - let maxNumberWarningLabel = UILabel() - let progressView = UIProgressView() - let line = UIView() - var shouldShowLoader = false { +internal final class YPLibraryView: UIView { + + // MARK: - Public vars + + internal let assetZoomableViewMinimalVisibleHeight: CGFloat = 50 + internal var assetViewContainerConstraintTop: NSLayoutConstraint? + internal let collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + let v = UICollectionView(frame: .zero, collectionViewLayout: layout) + v.backgroundColor = YPConfig.colors.libraryScreenBackgroundColor + v.collectionViewLayout = layout + v.showsHorizontalScrollIndicator = false + v.alwaysBounceVertical = true + return v + }() + internal lazy var assetViewContainer: YPAssetViewContainer = { + let v = YPAssetViewContainer(frame: .zero, zoomableView: assetZoomableView) + v.accessibilityIdentifier = "assetViewContainer" + return v + }() + internal let assetZoomableView: YPAssetZoomableView = { + let v = YPAssetZoomableView(frame: .zero) + v.accessibilityIdentifier = "assetZoomableView" + return v + }() + /// At the bottom there is a view that is visible when selected a limit of items with multiple selection + internal let maxNumberWarningView: UIView = { + let v = UIView() + v.backgroundColor = .ypSecondarySystemBackground + v.isHidden = true + return v + }() + internal let maxNumberWarningLabel: UILabel = { + let v = UILabel() + v.font = YPConfig.fonts.libaryWarningFont + return v + }() + + // MARK: - Private vars + + private let line: UIView = { + let v = UIView() + v.backgroundColor = .ypSystemBackground + return v + }() + /// When video is processing this bar appears + private let progressView: UIProgressView = { + let v = UIProgressView() + v.progressViewStyle = .bar + v.trackTintColor = YPConfig.colors.progressBarTrackColor + v.progressTintColor = YPConfig.colors.progressBarCompletedColor ?? YPConfig.colors.tintColor + v.isHidden = true + v.isUserInteractionEnabled = false + return v + }() + private let collectionContainerView: UIView = { + let v = UIView() + v.accessibilityIdentifier = "collectionContainerView" + return v + }() + private var shouldShowLoader = false { didSet { DispatchQueue.main.async { self.assetViewContainer.squareCropButton.isEnabled = !self.shouldShowLoader @@ -33,85 +81,31 @@ final class YPLibraryView: UIView { } } } - - override func awakeFromNib() { - super.awakeFromNib() - - subviews( - line - ) - - layout( - assetViewContainer!, - |line| ~ 1 - ) - - line.backgroundColor = .ypSystemBackground - - setupMaxNumberOfItemsView() - setupProgressBarView() - } - - /// At the bottom there is a view that is visible when selected a limit of items with multiple selection - func setupMaxNumberOfItemsView() { - // View Hierarchy - subviews( - maxNumberWarningView.subviews( - maxNumberWarningLabel - ) - ) - - // Layout - |maxNumberWarningView|.bottom(0) - if #available(iOS 11.0, *) { - maxNumberWarningView.Top == safeAreaLayoutGuide.Bottom - 40 - maxNumberWarningLabel.centerHorizontally().top(11) - } else { - maxNumberWarningView.height(40) - maxNumberWarningLabel.centerInContainer() - } - - // Style - maxNumberWarningView.backgroundColor = .ypSecondarySystemBackground - maxNumberWarningLabel.font = YPConfig.fonts.libaryWarningFont - maxNumberWarningView.isHidden = true + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + clipsToBounds = true } - - /// When video is processing this bar appears - func setupProgressBarView() { - subviews( - progressView - ) - - progressView.height(5) - progressView.Top == line.Top - progressView.Width == line.Width - progressView.progressViewStyle = .bar - progressView.trackTintColor = YPConfig.colors.progressBarTrackColor - progressView.progressTintColor = YPConfig.colors.progressBarCompletedColor ?? YPConfig.colors.tintColor - progressView.isHidden = true - progressView.isUserInteractionEnabled = false + + required init?(coder: NSCoder) { + super.init(coder: coder) + fatalError("Only code layout.") } -} -// MARK: - UI Helpers + // MARK: - Public Methods + + // MARK: Overlay view -extension YPLibraryView { - - class func xibView() -> YPLibraryView? { - let nib = UINib(nibName: "YPLibraryView", bundle: Bundle.local) - let xibView = nib.instantiate(withOwner: self, options: nil)[0] as? YPLibraryView - return xibView - } - - // MARK: - Overlay view - func hideOverlayView() { assetViewContainer.itemOverlay?.alpha = 0 } - - // MARK: - Loader and progress - + + // MARK: Loader and progress + func fadeInLoader() { shouldShowLoader = true // Only show loader if full res image takes more than 0.5s to load. @@ -130,39 +124,37 @@ extension YPLibraryView { } } } - + func hideLoader() { shouldShowLoader = false assetViewContainer.spinnerView.alpha = 0 } - + func updateProgress(_ progress: Float) { progressView.isHidden = progress > 0.99 || progress == 0 progressView.progress = progress UIView.animate(withDuration: 0.1, animations: progressView.layoutIfNeeded) } - - // MARK: - Crop Rect - + + // MARK: Crop Rect + func currentCropRect() -> CGRect { - guard let cropView = assetZoomableView else { - return CGRect.zero - } + let cropView = assetZoomableView let normalizedX = min(1, cropView.contentOffset.x &/ cropView.contentSize.width) let normalizedY = min(1, cropView.contentOffset.y &/ cropView.contentSize.height) let normalizedWidth = min(1, cropView.frame.width / cropView.contentSize.width) let normalizedHeight = min(1, cropView.frame.height / cropView.contentSize.height) return CGRect(x: normalizedX, y: normalizedY, width: normalizedWidth, height: normalizedHeight) } - - // MARK: - Curtain - + + // MARK: Curtain + func refreshImageCurtainAlpha() { - let imageCurtainAlpha = abs(assetViewContainerConstraintTop.constant) - / (assetViewContainer.frame.height - assetZoomableViewMinimalVisibleHeight) + let imageCurtainAlpha = abs(assetViewContainerConstraintTop?.constant ?? 0) + / (assetViewContainer.frame.height - assetZoomableViewMinimalVisibleHeight) assetViewContainer.curtain.alpha = imageCurtainAlpha } - + func cellSize() -> CGSize { var screenWidth: CGFloat = UIScreen.main.bounds.width if UIDevice.current.userInterfaceIdiom == .pad && YPImagePickerConfiguration.widthOniPad > 0 { @@ -171,4 +163,42 @@ extension YPLibraryView { let size = screenWidth / 4 * UIScreen.main.scale return CGSize(width: size, height: size) } + + // MARK: - Private Methods + + private func setupLayout() { + subviews( + collectionContainerView.subviews( + collectionView + ), + line, + assetViewContainer.subviews( + assetZoomableView + ), + progressView, + maxNumberWarningView.subviews( + maxNumberWarningLabel + ) + ) + + collectionContainerView.fillContainer() + collectionView.fillHorizontally().bottom(0) + + assetViewContainer.Bottom == line.Top + line.height(1) + line.fillHorizontally() + + assetViewContainer.top(0).fillHorizontally().heightEqualsWidth() + self.assetViewContainerConstraintTop = assetViewContainer.topConstraint + assetZoomableView.fillContainer().heightEqualsWidth() + assetZoomableView.Bottom == collectionView.Top + assetViewContainer.sendSubviewToBack(assetZoomableView) + + progressView.height(5).fillHorizontally() + progressView.Bottom == line.Top + + |maxNumberWarningView|.bottom(0) + maxNumberWarningView.Top == safeAreaLayoutGuide.Bottom - 40 + maxNumberWarningLabel.centerHorizontally().top(11) + } } diff --git a/Source/Pages/Gallery/YPLibraryView.xib b/Source/Pages/Gallery/YPLibraryView.xib index 6048a783b..58887657d 100644 --- a/Source/Pages/Gallery/YPLibraryView.xib +++ b/Source/Pages/Gallery/YPLibraryView.xib @@ -4,6 +4,7 @@ + @@ -39,12 +40,13 @@ + - + @@ -76,4 +78,9 @@ + + + + +