diff --git a/FileExplorer/FileExplorer/ActionsViewController.swift b/FileExplorer/FileExplorer/ActionsViewController.swift index aa5e46e..a5cb604 100644 --- a/FileExplorer/FileExplorer/ActionsViewController.swift +++ b/FileExplorer/FileExplorer/ActionsViewController.swift @@ -24,6 +24,7 @@ // SOFTWARE. import UIKit +import GoogleMobileAds protocol ActionsViewControllerDelegate: class { func actionsViewControllerDidRequestRemoval(_ controller: ActionsViewController) @@ -33,7 +34,7 @@ protocol ActionsViewControllerDelegate: class { final class ActionsViewController: UIViewController { weak var delegate: ActionsViewControllerDelegate? - private let toolbar = UIToolbar() + let toolbar = UIToolbar() private let contentViewController: UIViewController init(contentViewController: UIViewController) { @@ -48,7 +49,7 @@ final class ActionsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.white + view.backgroundColor = UIColor.dynamicColor(light: .white, dark: .black)//UIColor.white extendedLayoutIncludesOpaqueBars = false edgesForExtendedLayout = [] @@ -63,10 +64,23 @@ final class ActionsViewController: UIViewController { UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(handleTrashButtonTap)) ] - addContentChildViewController(contentViewController, insets: UIEdgeInsets(top: 0, left: 0, bottom: toolbar.bounds.height, right: 0)) + addContentChildViewController(contentViewController, insets: UIEdgeInsets(top: 0, left: 0, bottom: toolbar.bounds.height+30, right: 0)) navigationItem.title = contentViewController.navigationItem.title + contentViewController.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: 0).isActive = true + interstitial = createAndLoadInterstitial() } + var interstitial: GADInterstitial! + func callAds(){ + if interstitial != nil { + if interstitial.isReady { + interstitial.present(fromRootViewController: self) + } else { + print("Ad wasn't ready") + } + } + } + // MARK: Actions @objc @@ -76,6 +90,25 @@ final class ActionsViewController: UIViewController { @objc private func handleTrashButtonTap() { + callAds() delegate?.actionsViewControllerDidRequestRemoval(self) } } + + +extension ActionsViewController: GADInterstitialDelegate{ + public func interstitialDidReceiveAd(_ ad: GADInterstitial) { + //self.interstitial.present(fromRootViewController: self) + } + func createAndLoadInterstitial() -> GADInterstitial { + let interstitial = GADInterstitial(adUnitID: ) + interstitial.delegate = self + interstitial.load(GADRequest()) + return interstitial + } + + public func interstitialDidDismissScreen(_ ad: GADInterstitial) { + interstitial = createAndLoadInterstitial() + //navigationController?.popViewController(animated: true) + } +} diff --git a/FileExplorer/FileExplorer/Array+Extension.swift b/FileExplorer/FileExplorer/Array+Extension.swift index 96bdff6..01c2437 100644 --- a/FileExplorer/FileExplorer/Array+Extension.swift +++ b/FileExplorer/FileExplorer/Array+Extension.swift @@ -28,7 +28,7 @@ import Foundation extension Array where Element: Equatable { @discardableResult mutating func remove(_ item: Element) -> Bool { - let index = self.index() { $0 == item } + let index = self.firstIndex() { $0 == item } if let index = index { remove(at: index) return true diff --git a/FileExplorer/FileExplorer/DirectoryContentViewController.swift b/FileExplorer/FileExplorer/DirectoryContentViewController.swift index 30fe08c..9d22b93 100644 --- a/FileExplorer/FileExplorer/DirectoryContentViewController.swift +++ b/FileExplorer/FileExplorer/DirectoryContentViewController.swift @@ -24,8 +24,7 @@ // SOFTWARE. import Foundation - -protocol DirectoryContentViewControllerDelegate: class { +protocol DirectoryContentViewControllerDelegate: AnyObject { func directoryContentViewController(_ controller: DirectoryContentViewController, didChangeEditingStatus isEditing: Bool) func directoryContentViewController(_ controller: DirectoryContentViewController, didSelectItem item: Item) func directoryContentViewController(_ controller: DirectoryContentViewController, didSelectItemDetails item: Item) @@ -47,6 +46,13 @@ final class DirectoryContentViewController: UICollectionViewController { } } + //SWIPE + var defaultOptions = SwipeOptions() + var isSwipeRightEnabled = true + var buttonDisplayMode: ButtonDisplayMode = .titleAndImage + var buttonStyle: ButtonStyle = .backgroundColor + var usesTallCells = false + //SWIPE init(viewModel: DirectoryContentViewModel) { self.viewModel = viewModel self.toolbar = UIToolbar.makeToolbar() @@ -57,6 +63,7 @@ final class DirectoryContentViewController: UICollectionViewController { super.init(collectionViewLayout: layout) viewModel.delegate = self + navigationItem.title = "" } required init?(coder aDecoder: NSCoder) { @@ -83,7 +90,7 @@ final class DirectoryContentViewController: UICollectionViewController { extendedLayoutIncludesOpaqueBars = false edgesForExtendedLayout = [] - collectionView.backgroundColor = UIColor.white + collectionView.backgroundColor = UIColor.dynamicColor(light: .white, dark: .black) collectionView.registerCell(ofClass: ItemCell.self) collectionView.registerHeader(ofClass: CollectionViewHeader.self) collectionView.registerFooter(ofClass: CollectionViewFooter.self) @@ -94,9 +101,16 @@ final class DirectoryContentViewController: UICollectionViewController { self.toolbarBottomConstraint = toolbar.pinToBottom(of: view) self.toolbarBottomConstraint?.constant = toolbar.bounds.height + self.toolbar.isHidden = true + syncWithViewModel(false) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = "" + } + func syncWithViewModel(_ animated: Bool) { if let items = toolbar.items { for barButtonItem in items { @@ -127,6 +141,7 @@ final class DirectoryContentViewController: UICollectionViewController { collectionView.setEditing(editing, animated: animated) UIView.animate(withDuration: 0.2) { + self.toolbar.isHidden.toggle() self.toolbarBottomConstraint?.constant = editing ? 0.0 : self.toolbar.bounds.height collectionView.contentInset.bottom = editing ? self.toolbar.bounds.height : 0.0 collectionView.scrollIndicatorInsets = collectionView.contentInset @@ -145,18 +160,18 @@ final class DirectoryContentViewController: UICollectionViewController { selectActionButton, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), deleteActionButton - ].flatMap { $0 } + ].compactMap { $0 } } // MARK: Actions - func handleSelectButtonTap() { + @objc func handleSelectButtonTap() { viewModel.chooseItems { selectedItems in delegate?.directoryContentViewController(self, didChooseItems: selectedItems) } } - func handleDeleteButtonTap() { + @objc func handleDeleteButtonTap() { showLoadingIndicator() viewModel.deleteItems(at: viewModel.indexPathsOfSelectedCells) { [weak self] result in guard let strongSelf = self else { return } @@ -171,7 +186,7 @@ final class DirectoryContentViewController: UICollectionViewController { } } - func handleEditButtonTap() { + @objc func handleEditButtonTap() { viewModel.isEditing = !viewModel.isEditing delegate?.directoryContentViewController(self, didChangeEditingStatus: viewModel.isEditing) } @@ -214,11 +229,12 @@ extension DirectoryContentViewController { cell.subtitle = itemViewModel.subtitle cell.accessoryType = itemViewModel.accessoryType cell.iconImage = itemViewModel.thumbnailImage(with: cell.maximumIconSize) + cell.delegate = self return cell } override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionElementKindSectionHeader { + if kind == UICollectionView.elementKindSectionHeader { let header = collectionView.dequeueReusableHeader(ofClass: CollectionViewHeader.self, for: indexPath) as CollectionViewHeader header.sortModeChangeAction = viewModel.sortModeChangeAction header.sortMode = viewModel.sortMode @@ -226,7 +242,7 @@ extension DirectoryContentViewController { header.layoutIfNeeded() } return header - } else if kind == UICollectionElementKindSectionFooter { + } else if kind == UICollectionView.elementKindSectionFooter { return collectionView.dequeueReusableFooter(ofClass: CollectionViewFooter.self, for: indexPath) as CollectionViewFooter } else { fatalError() @@ -252,3 +268,76 @@ extension DirectoryContentViewController: UISearchResultsUpdating { viewModel.searchQuery = searchController.searchBar.text } } + +extension DirectoryContentViewController: SwipeCollectionViewCellDelegate { + + func collectionView(_ collectionView: UICollectionView, editActionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? { + + let flag = SwipeAction(style: .default, title: nil, handler: nil) + flag.hidesWhenSelected = true + configure(action: flag, with: .flag) + + let delete = SwipeAction(style: .destructive, title: nil) { [self] action, indexPath in + viewModel.deleteItems(at: [indexPath]) { [weak self] result in + guard let strongSelf = self else { return } + delegate?.directoryContentViewController(strongSelf, didChangeEditingStatus: strongSelf.viewModel.isEditing) + } + } + configure(action: delete, with: .trash) + + return [delete] + } + + func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { + var options = SwipeOptions() + options.expansionStyle = orientation == .left ? .selection : .destructive + options.transitionStyle = defaultOptions.transitionStyle + + switch buttonStyle { + case .backgroundColor: + options.buttonSpacing = 11 + case .circular: + options.buttonSpacing = 4 + #if canImport(Combine) + if #available(iOS 13.0, *) { + options.backgroundColor = UIColor.systemGray6 + } else { + options.backgroundColor = #colorLiteral(red: 0.9467939734, green: 0.9468161464, blue: 0.9468042254, alpha: 1) + } + #else + options.backgroundColor = #colorLiteral(red: 0.9467939734, green: 0.9468161464, blue: 0.9468042254, alpha: 1) + #endif + } + + return options + } + + func visibleRect(for collectionView: UICollectionView) -> CGRect? { + if usesTallCells == false { return nil } + + if #available(iOS 11.0, *) { + return collectionView.safeAreaLayoutGuide.layoutFrame + } else { + let topInset = navigationController?.navigationBar.frame.height ?? 0 + let bottomInset = navigationController?.toolbar?.frame.height ?? 0 + let bounds = collectionView.bounds + + return CGRect(x: bounds.origin.x, y: bounds.origin.y + topInset, width: bounds.width, height: bounds.height - bottomInset) + } + } + + func configure(action: SwipeAction, with descriptor: ActionDescriptor) { + action.title = descriptor.title(forDisplayMode: buttonDisplayMode) + action.image = descriptor.image(forStyle: buttonStyle, displayMode: buttonDisplayMode) + + switch buttonStyle { + case .backgroundColor: + action.backgroundColor = descriptor.color(forStyle: buttonStyle) + case .circular: + action.backgroundColor = .clear + action.textColor = descriptor.color(forStyle: buttonStyle) + action.font = .systemFont(ofSize: 13) + action.transitionDelegate = ScaleTransition.default + } + } +} diff --git a/FileExplorer/FileExplorer/DirectoryContentViewModel.swift b/FileExplorer/FileExplorer/DirectoryContentViewModel.swift index 627d913..f188070 100644 --- a/FileExplorer/FileExplorer/DirectoryContentViewModel.swift +++ b/FileExplorer/FileExplorer/DirectoryContentViewModel.swift @@ -191,7 +191,7 @@ final class DirectoryContentViewModel { func deselect(at indexPath: IndexPath) { let item = self.item(for: indexPath) if isEditing { - if let index = selectedItems.index(where: { $0 == item }) { + if let index = selectedItems.firstIndex(where: { $0 == item }) { selectedItems.remove(at: index) } } else { @@ -201,7 +201,7 @@ final class DirectoryContentViewModel { } func deleteItems(at indexPaths: [IndexPath], completionBlock: @escaping (Result) -> Void) { - let items = indexPaths.flatMap { item(for: $0) } + let items = indexPaths.compactMap { item(for: $0) } fileService.delete(items: items) { result, removedItems, itemsNotRemovedDueToFailure in completionBlock(result) self.delegate?.directoryViewModelDidChange(self) diff --git a/FileExplorer/FileExplorer/DirectoryItemPresentationCoordinator.swift b/FileExplorer/FileExplorer/DirectoryItemPresentationCoordinator.swift index a8c5346..b02b6d6 100644 --- a/FileExplorer/FileExplorer/DirectoryItemPresentationCoordinator.swift +++ b/FileExplorer/FileExplorer/DirectoryItemPresentationCoordinator.swift @@ -66,7 +66,6 @@ final class DirectoryItemPresentationCoordinator { return directoryViewController } navigationController?.pushViewController(viewController, animated: animated) - } else { let viewController = ErrorViewController(errorDescription: "URL is incorrect.", finishButtonHidden: finishButtonHidden) viewController.delegate = self diff --git a/FileExplorer/FileExplorer/DirectoryViewController.swift b/FileExplorer/FileExplorer/DirectoryViewController.swift index 84ee28e..1588d38 100644 --- a/FileExplorer/FileExplorer/DirectoryViewController.swift +++ b/FileExplorer/FileExplorer/DirectoryViewController.swift @@ -104,9 +104,14 @@ final class DirectoryViewController: UIViewController { addContentChildViewController(directoryContentViewController, insets: UIEdgeInsets(top: searchController.searchBar.bounds.height, left: 0.0, bottom: 0.0, right: 0.0)) navigationItem.rightBarButtonItem = directoryContentViewController.navigationItem.rightBarButtonItem navigationItem.title = directoryContentViewController.navigationItem.title - view.sendSubview(toBack: directoryContentViewController.view) + view.sendSubviewToBack(directoryContentViewController.view) setUpLeftBarButtonItem() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = "" + } func setUpSearchBarController() { let searchBar = searchController.searchBar @@ -131,10 +136,9 @@ final class DirectoryViewController: UIViewController { searchController.isActive = newValue } } - // MARK: Actions - func handleFinishButtonTap() { + @objc func handleFinishButtonTap() { delegate?.directoryViewControllerDidFinish(self) } } @@ -149,9 +153,11 @@ extension DirectoryViewController: UISearchBarDelegate { extension DirectoryViewController: DirectoryContentViewControllerDelegate { func directoryContentViewController(_ controller: DirectoryContentViewController, didChangeEditingStatus isEditing: Bool) { searchController.searchBar.isEnabled = !isEditing + navigationItem.title = "" } func directoryContentViewController(_ controller: DirectoryContentViewController, didSelectItem item: Item) { + delegate?.directoryViewController(self, didSelectItem: item) } diff --git a/FileExplorer/FileExplorer/ErrorViewController.swift b/FileExplorer/FileExplorer/ErrorViewController.swift index d3d66ee..13b6434 100644 --- a/FileExplorer/FileExplorer/ErrorViewController.swift +++ b/FileExplorer/FileExplorer/ErrorViewController.swift @@ -64,7 +64,7 @@ final class ErrorViewController: UIViewController { // MARK: Actions - func handleFinishButtonTap() { + @objc func handleFinishButtonTap() { delegate?.errorViewControllerDidFinish(self) } } diff --git a/FileExplorer/FileExplorer/FileExplorerViewController.swift b/FileExplorer/FileExplorer/FileExplorerViewController.swift index 13b3691..849e378 100644 --- a/FileExplorer/FileExplorer/FileExplorerViewController.swift +++ b/FileExplorer/FileExplorer/FileExplorerViewController.swift @@ -101,7 +101,7 @@ public final class FileExplorerViewController: UIViewController { override public func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.white + view.backgroundColor = UIColor.dynamicColor(light: .white, dark: .black) let navigationController = UINavigationController() addContentChildViewController(navigationController) @@ -126,6 +126,16 @@ public final class FileExplorerViewController: UIViewController { } else { precondition(false, "Passed URL is incorrect.") } + navigationItem.title = "" + } + + override public var prefersStatusBarHidden: Bool { + return true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationItem.title = "" } override public func viewDidDisappear(_ animated: Bool) { diff --git a/FileExplorer/FileExplorer/FileItemPresentationCoordinator.swift b/FileExplorer/FileExplorer/FileItemPresentationCoordinator.swift index d4721bc..fa224da 100644 --- a/FileExplorer/FileExplorer/FileItemPresentationCoordinator.swift +++ b/FileExplorer/FileExplorer/FileItemPresentationCoordinator.swift @@ -79,8 +79,8 @@ final class FileItemPresentationCoordinator { extension FileItemPresentationCoordinator: ActionsViewControllerDelegate { func actionsViewControllerDidRequestShare(_ controller: ActionsViewController) { - let activityItem = UIActivityItemProvider(placeholderItem: item.url) - let activityViewController = UIActivityViewController(activityItems: [activityItem], applicationActivities: nil) + let activityViewController = UIActivityViewController(activityItems: [item.url], applicationActivities: nil) + activityViewController.popoverPresentationController?.barButtonItem = controller.toolbar.items?[0] navigationController?.present(activityViewController, animated: true, completion: nil) } diff --git a/FileExplorer/FileExplorer/FileService.swift b/FileExplorer/FileExplorer/FileService.swift index 95b9d91..a9560ed 100644 --- a/FileExplorer/FileExplorer/FileService.swift +++ b/FileExplorer/FileExplorer/FileService.swift @@ -35,7 +35,7 @@ protocol FileService: class { enum FileServiceError: Error { case removalFailure(removedItems: [Item], itemsNotRemovedDueToFailure: [Item]) - case loadingFailure() + case loadingFailure } extension Notification.Name { @@ -54,7 +54,7 @@ final class LocalStorageFileService: FileService { func load(item: Item, completionBlock: @escaping (Result>) -> ()) { DispatchQueue.global(qos: .default).async { let result = Result>() { [weak self] in - guard let strongSelf = self else { throw FileServiceError.loadingFailure() } + guard let strongSelf = self else { throw FileServiceError.loadingFailure } let attributes = try strongSelf.fileManager.attributesOfItem(atPath: item.url.path) let result: Any @@ -103,7 +103,7 @@ final class LocalStorageFileService: FileService { deletedItems, itemsNotRemovedDueToFailure) } else { - completionBlock(.success(), deletedItems, itemsNotRemovedDueToFailure) + completionBlock(.success(()), deletedItems, itemsNotRemovedDueToFailure) } } diff --git a/FileExplorer/FileExplorer/FileViewController.swift b/FileExplorer/FileExplorer/FileViewController.swift index 1e6d483..5dd518f 100644 --- a/FileExplorer/FileExplorer/FileViewController.swift +++ b/FileExplorer/FileExplorer/FileViewController.swift @@ -25,6 +25,24 @@ import Foundation +extension UIColor { + + public class func dynamicColor(light: UIColor, dark: UIColor) -> UIColor { + if #available(iOS 13.0, *) { + return UIColor { + switch $0.userInterfaceStyle { + case .dark: + return dark + default: + return light + } + } + } else { + return light + } + } +} + final class FileViewController: UIViewController { private let viewModel: FileViewModel private let scrollView = UIScrollView() @@ -38,27 +56,27 @@ final class FileViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.white + view.backgroundColor = UIColor.dynamicColor(light: .white, dark: .black) extendedLayoutIncludesOpaqueBars = false edgesForExtendedLayout = [] let imageView = ImageView() imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.setContentCompressionResistancePriority(UILayoutPriorityDefaultLow, for: .vertical) - imageView.setContentCompressionResistancePriority(UILayoutPriorityDefaultLow, for: .horizontal) + imageView.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .vertical) + imageView.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .horizontal) let titleView = TitleView() titleView.translatesAutoresizingMaskIntoConstraints = false titleView.title = viewModel.title - titleView.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical) - titleView.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .horizontal) + titleView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .vertical) + titleView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal) let attributesView = AttributesView() attributesView.translatesAutoresizingMaskIntoConstraints = false attributesView.numberOfAttributes = viewModel.numberOfAttributes - attributesView.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical) - attributesView.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .horizontal) + attributesView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .vertical) + attributesView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal) for (index, label) in attributesView.attributeNamesColumn.labels.enumerated() { let attributeViewModel = viewModel.attribute(for: index) label.text = attributeViewModel.attributeName @@ -103,6 +121,11 @@ final class FileViewController: UIViewController { stackView.frame = CGRect(origin: CGPoint.zero, size: view.bounds.size) stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = "" + } } final class ImageView: UIView { @@ -150,11 +173,11 @@ final class TitleView: UIView { titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -1.0).isActive = true titleLabel.widthAnchor.constraint(equalTo: widthAnchor, constant: -20.0).isActive = true - titleLabel.setContentHuggingPriority(UILayoutPriorityRequired, for: .vertical) + titleLabel.setContentHuggingPriority(UILayoutPriority.required, for: .vertical) titleLabel.numberOfLines = 1 titleLabel.lineBreakMode = .byTruncatingTail - let topSeparator = SeparatorView() + /*let topSeparator = SeparatorView() topSeparator.translatesAutoresizingMaskIntoConstraints = false topSeparator.backgroundColor = ColorPallete.gray addSubview(topSeparator) @@ -168,7 +191,7 @@ final class TitleView: UIView { addSubview(bottomSeparator) bottomSeparator.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true bottomSeparator.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - bottomSeparator.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + bottomSeparator.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true*/ } required init?(coder aDecoder: NSCoder) { @@ -185,7 +208,7 @@ final class TitleView: UIView { } override var intrinsicContentSize: CGSize { - return CGSize(width: UIViewNoIntrinsicMetric, height: 42.0) + return CGSize(width: UIView.noIntrinsicMetric, height: 42.0) } } @@ -229,8 +252,8 @@ final class AttributesView: UIView { for label in attributeValuesColumn.labels { label.translatesAutoresizingMaskIntoConstraints = false label.textAlignment = .left - label.font = UIFont.systemFont(ofSize: 15.0) - label.textColor = UIColor.black + label.font = UIFont.systemFont(ofSize: 15.0, weight: .bold) + label.textColor = ColorPallete.gray } } } diff --git a/FileExplorer/FileExplorer/FileViewModel.swift b/FileExplorer/FileExplorer/FileViewModel.swift index 423f63a..878557e 100644 --- a/FileExplorer/FileExplorer/FileViewModel.swift +++ b/FileExplorer/FileExplorer/FileViewModel.swift @@ -47,7 +47,7 @@ final class FileViewModel { FileViewModel.makeFileSizeItem(fromAttributes: self.item.attributes), FileViewModel.makeCreationDateItem(fromAttributes: self.item.attributes), FileViewModel.makeModificationDateItem(fromAttributes: self.item.attributes) - ].flatMap { $0 } + ].compactMap { $0 } } func thumbnailImage(with size: CGSize) -> UIImage { diff --git a/FileExplorer/FileExplorer/ImageViewController.swift b/FileExplorer/FileExplorer/ImageViewController.swift index 247ea64..d2ee26b 100644 --- a/FileExplorer/FileExplorer/ImageViewController.swift +++ b/FileExplorer/FileExplorer/ImageViewController.swift @@ -74,6 +74,10 @@ final class ImageViewController: UIViewController { } scrollView.contentInset = UIEdgeInsets(top: vertical, left: horizontal, bottom: vertical, right: horizontal) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = "" + } } extension ImageViewController: UIScrollViewDelegate { diff --git a/FileExplorer/FileExplorer/Item.swift b/FileExplorer/FileExplorer/Item.swift index ba62473..98f6866 100644 --- a/FileExplorer/FileExplorer/Item.swift +++ b/FileExplorer/FileExplorer/Item.swift @@ -95,8 +95,8 @@ extension Item: Equatable { } extension Item: Hashable { - var hashValue: Int { - return url.hashValue + func hash(into hasher: inout Hasher) { + hasher.combine(url.hashValue) } } diff --git a/FileExplorer/FileExplorer/ItemCell.swift b/FileExplorer/FileExplorer/ItemCell.swift index 42ce185..bfec1bb 100644 --- a/FileExplorer/FileExplorer/ItemCell.swift +++ b/FileExplorer/FileExplorer/ItemCell.swift @@ -25,13 +25,25 @@ import UIKit +extension UIColor { + convenience init(red: Int, green: Int, blue: Int) { + assert(red >= 0 && red <= 255, "Invalid red component") + assert(green >= 0 && green <= 255, "Invalid green component") + assert(blue >= 0 && blue <= 255, "Invalid blue component") + + self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) + } + convenience init(hex:Int) { + self.init(red:(hex >> 16) & 0xff, green:(hex >> 8) & 0xff, blue:hex & 0xff) + } +} protocol Editable { func setEditing(_ editing: Bool, animated: Bool) } enum ColorPallete { - static let gray = UIColor(colorLiteralRed: 200/255.0, green: 199/255.0, blue: 204/255.0, alpha: 1.0) - static let blue = UIColor(colorLiteralRed: 21/255.0, green: 126/255.0, blue: 251/255, alpha: 1.0) + static let gray = UIColor(red: 200/255.0, green: 199/255.0, blue: 204/255.0, alpha: 0.5) + static let blue = UIColor(red: 21/255.0, green: 126/255.0, blue: 251/255, alpha: 1.0) } enum LayoutConstants { @@ -39,19 +51,18 @@ enum LayoutConstants { static let iconWidth: CGFloat = 44.0 } -final class ItemCell: UICollectionViewCell, Editable { +final class ItemCell: SwipeCollectionViewCell, Editable { enum AccessoryType { case detailButton case disclosureIndicator } private var accessoryImageViewTapRecognizer: UITapGestureRecognizer - private let containerView: UIView private let separatorView: SeparatorView private let iconImageView: UIImageView private let titleTextLabel: UILabel private let subtitleTextLabel: UILabel - private let accessoryImageView: UIImageView + private let accessoryImageView: UILabel private var customAccessoryType = AccessoryType.detailButton private let checkmarkButton: CheckmarkButton @@ -61,41 +72,51 @@ final class ItemCell: UICollectionViewCell, Editable { var tapAction: () -> Void = {} override init(frame: CGRect) { - containerView = UIView() - containerView.backgroundColor = UIColor.white separatorView = SeparatorView() separatorView.backgroundColor = ColorPallete.gray - containerView.addSubview(separatorView) + iconImageView = UIImageView() iconImageView.contentMode = .scaleAspectFit - containerView.addSubview(iconImageView) + iconImageView.layer.borderWidth = 0 + //iconImageView.layer.borderColor = UIColor(red:248/255, green:45/255, blue:85/255, alpha:1.00).cgColor titleTextLabel = UILabel() titleTextLabel.numberOfLines = 1 titleTextLabel.lineBreakMode = .byTruncatingMiddle titleTextLabel.font = UIFont.systemFont(ofSize: 17) - containerView.addSubview(titleTextLabel) + subtitleTextLabel = UILabel() subtitleTextLabel.numberOfLines = 1 subtitleTextLabel.lineBreakMode = .byTruncatingMiddle subtitleTextLabel.font = UIFont.systemFont(ofSize: 12) subtitleTextLabel.textColor = UIColor.gray - containerView.addSubview(subtitleTextLabel) - accessoryImageView = UIImageView() - accessoryImageView.contentMode = .center - containerView.addSubview(accessoryImageView) + + accessoryImageView = UILabel() + accessoryImageView.font = UIFont.systemFont(ofSize: 18) + accessoryImageView.textColor = UIColor(red:248/255, green:45/255, blue:85/255, alpha:1.00) + //accessoryImageView.contentMode = .center + checkmarkButton = CheckmarkButton() accessoryImageViewTapRecognizer = UITapGestureRecognizer(target: nil, action: nil) super.init(frame: frame) - - backgroundColor = UIColor.white + + containerView.addSubview(separatorView) + containerView.addSubview(iconImageView) + containerView.addSubview(titleTextLabel) + containerView.addSubview(subtitleTextLabel) + containerView.addSubview(accessoryImageView) + + containerView.backgroundColor = UIColor.dynamicColor(light: .white, dark: .black) + + + backgroundColor = UIColor.dynamicColor(light: .white, dark: .black) accessoryImageViewTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleAccessoryImageTap)) accessoryImageViewTapRecognizer.delegate = self @@ -120,6 +141,8 @@ final class ItemCell: UICollectionViewCell, Editable { setupTitleLabelContstraints() setupSubtitleLabelConstraints() setupCheckmarkButtonConstraints() + + configure() } required init?(coder aDecoder: NSCoder) { @@ -151,26 +174,26 @@ final class ItemCell: UICollectionViewCell, Editable { private func setupAccessoryImageViewConstraints() { accessoryImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15).isActive = true accessoryImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true - accessoryImageView.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .horizontal) - accessoryImageView.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh, for: .vertical) - accessoryImageView.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: .horizontal) + accessoryImageView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal) + accessoryImageView.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: .vertical) + accessoryImageView.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal) } private func setupTitleLabelContstraints() { titleTextLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 12.0).isActive = true titleTextLabel.trailingAnchor.constraint(equalTo: accessoryImageView.leadingAnchor, constant: -10.0).isActive = true titleTextLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12.0).isActive = true - titleTextLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh, for: .horizontal) - titleTextLabel.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: .vertical) + titleTextLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: .horizontal) + titleTextLabel.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical) } private func setupSubtitleLabelConstraints() { subtitleTextLabel.leadingAnchor.constraint(equalTo: titleTextLabel.leadingAnchor).isActive = true subtitleTextLabel.trailingAnchor.constraint(equalTo: titleTextLabel.trailingAnchor).isActive = true subtitleTextLabel.topAnchor.constraint(equalTo: titleTextLabel.bottomAnchor, constant: 3.0).isActive = true - subtitleTextLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh + subtitleTextLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh , for: .horizontal) - subtitleTextLabel.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: .vertical) + subtitleTextLabel.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical) } private func setupCheckmarkButtonConstraints() { @@ -243,10 +266,14 @@ final class ItemCell: UICollectionViewCell, Editable { customAccessoryType = newValue switch customAccessoryType { case .detailButton: - accessoryImageView.image = UIImage.make(for: "DetailButtonImage") + //accessoryImageView.image = UIImage.make(for: "DetailButtonImage") + accessoryImageView.text = "ⓘ" accessoryImageViewTapRecognizer.isEnabled = true + //accessoryImageView.textColor = UIColor(hex: 0x0075ff) case .disclosureIndicator: - accessoryImageView.image = UIImage.make(for: "DisclosureButtonImage") + //accessoryImageView.image = UIImage.make(for: "DisclosureButtonImage") + accessoryImageView.text = "〉" + //accessoryImageView.textColor = UIColor(hex: 0x0075ff) accessoryImageViewTapRecognizer.isEnabled = false } setNeedsLayout() @@ -258,9 +285,9 @@ final class ItemCell: UICollectionViewCell, Editable { return CGSize(width: max, height: max) } - // MARK: Actions + // MARK: Actions - func handleAccessoryImageTap() { + @objc func handleAccessoryImageTap() { tapAction() } } @@ -337,7 +364,7 @@ final class CollectionViewFooter: UICollectionReusableView { } override func layoutSubviews() { - super.layoutSubviews() + super.layoutSubviews() for (i, separator) in separators.enumerated() { let size = CGSize(width: bounds.width - leftInset, height: 1.0) separator.frame.size = separator.sizeThatFits(size) diff --git a/FileExplorer/FileExplorer/ItemPresentationCoordinator.swift b/FileExplorer/FileExplorer/ItemPresentationCoordinator.swift index 2750dc2..53a4c6f 100644 --- a/FileExplorer/FileExplorer/ItemPresentationCoordinator.swift +++ b/FileExplorer/FileExplorer/ItemPresentationCoordinator.swift @@ -46,6 +46,7 @@ final class ItemPresentationCoordinator { func start(item: Item, fileSpecifications: FileSpecifications, configuration: Configuration, animated: Bool) { guard let navigationController = self.navigationController else { return } + navigationController.title = "" self.configuration = configuration self.fileSpecifications = fileSpecifications diff --git a/FileExplorer/FileExplorer/LoadingViewController.swift b/FileExplorer/FileExplorer/LoadingViewController.swift index b3f652c..b7afef6 100644 --- a/FileExplorer/FileExplorer/LoadingViewController.swift +++ b/FileExplorer/FileExplorer/LoadingViewController.swift @@ -61,8 +61,13 @@ final class LoadingViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white + view.backgroundColor = UIColor.dynamicColor(light: .white, dark: .black)//.white } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = "" + } + } extension LoadingViewController { diff --git a/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/Contents.json b/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/Contents.json index 6550889..ec63e74 100644 --- a/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/Contents.json +++ b/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "DetailButtonImage.pdf" + "filename" : "DetailButtonImage.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/DetailButtonImage.pdf b/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/DetailButtonImage.pdf deleted file mode 100644 index 64b7ac2..0000000 Binary files a/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/DetailButtonImage.pdf and /dev/null differ diff --git a/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/DetailButtonImage.png b/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/DetailButtonImage.png new file mode 100644 index 0000000..159edb1 Binary files /dev/null and b/FileExplorer/FileExplorer/Resources/ImageAssets.xcassets/DetailButtonImage.imageset/DetailButtonImage.png differ diff --git a/FileExplorer/FileExplorer/Resources/Info.plist b/FileExplorer/FileExplorer/Resources/Info.plist deleted file mode 100644 index fbe1e6b..0000000 --- a/FileExplorer/FileExplorer/Resources/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/FileExplorer/FileExplorer/SwipeCellKit/Extensions.swift b/FileExplorer/FileExplorer/SwipeCellKit/Extensions.swift new file mode 100644 index 0000000..b672e5f --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/Extensions.swift @@ -0,0 +1,81 @@ +// +// Extensions.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +extension UITableView { + var swipeCells: [SwipeTableViewCell] { + return visibleCells.compactMap({ $0 as? SwipeTableViewCell }) + } + + func hideSwipeCell() { + swipeCells.forEach { $0.hideSwipe(animated: true) } + } +} + +extension UICollectionView { + var swipeCells: [SwipeCollectionViewCell] { + return visibleCells.compactMap({ $0 as? SwipeCollectionViewCell }) + } + + func hideSwipeCell() { + swipeCells.forEach { $0.hideSwipe(animated: true) } + } + + func setGestureEnabled(_ enabled: Bool) { + gestureRecognizers?.forEach { + guard $0 != panGestureRecognizer else { return } + + $0.isEnabled = enabled + } + } +} + +extension UIScrollView { + var swipeables: [Swipeable] { + switch self { + case let tableView as UITableView: + return tableView.swipeCells + case let collectionView as UICollectionView: + return collectionView.swipeCells + default: + return [] + } + } + + func hideSwipeables() { + switch self { + case let tableView as UITableView: + tableView.hideSwipeCell() + case let collectionView as UICollectionView: + collectionView.hideSwipeCell() + default: + return + } + } +} + +extension UIPanGestureRecognizer { + func elasticTranslation(in view: UIView?, withLimit limit: CGSize, fromOriginalCenter center: CGPoint, applyingRatio ratio: CGFloat = 0.20) -> CGPoint { + let translation = self.translation(in: view) + + guard let sourceView = self.view else { + return translation + } + + let updatedCenter = CGPoint(x: center.x + translation.x, y: center.y + translation.y) + let distanceFromCenter = CGSize(width: abs(updatedCenter.x - sourceView.bounds.midX), + height: abs(updatedCenter.y - sourceView.bounds.midY)) + + let inverseRatio = 1.0 - ratio + let scale: (x: CGFloat, y: CGFloat) = (updatedCenter.x < sourceView.bounds.midX ? -1 : 1, updatedCenter.y < sourceView.bounds.midY ? -1 : 1) + let x = updatedCenter.x - (distanceFromCenter.width > limit.width ? inverseRatio * (distanceFromCenter.width - limit.width) * scale.x : 0) + let y = updatedCenter.y - (distanceFromCenter.height > limit.height ? inverseRatio * (distanceFromCenter.height - limit.height) * scale.y : 0) + + return CGPoint(x: x, y: y) + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/Shared.swift b/FileExplorer/FileExplorer/SwipeCellKit/Shared.swift new file mode 100644 index 0000000..4885688 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/Shared.swift @@ -0,0 +1,130 @@ +// +// Shared.swift +// MailExample +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +class IndicatorView: UIView { + var color = UIColor.clear { + didSet { setNeedsDisplay() } + } + + override func draw(_ rect: CGRect) { + color.set() + UIBezierPath(ovalIn: rect).fill() + } +} + +enum ActionDescriptor { + case read, unread, more, flag, trash + + func title(forDisplayMode displayMode: ButtonDisplayMode) -> String? { + guard displayMode != .imageOnly else { return nil } + + switch self { + case .read: return "Read" + case .unread: return "Unread" + case .more: return "More" + case .flag: return "Flag" + case .trash: return "Trash" + } + } + + func image(forStyle style: ButtonStyle, displayMode: ButtonDisplayMode) -> UIImage? { + guard displayMode != .titleOnly else { return nil } + + let name: String + switch self { + case .read: name = "Read" + case .unread: name = "Unread" + case .more: name = "More" + case .flag: name = "Flag" + case .trash: name = "Trash" + } + + #if canImport(Combine) + if #available(iOS 13.0, *) { + let name: String + switch self { + case .read: name = "envelope.open.fill" + case .unread: name = "envelope.badge.fill" + case .more: name = "ellipsis.circle.fill" + case .flag: name = "flag.fill" + case .trash: name = "trash.fill" + } + + if style == .backgroundColor { + let config = UIImage.SymbolConfiguration(pointSize: 23.0, weight: .regular) + return UIImage(systemName: name, withConfiguration: config) + } else { + let config = UIImage.SymbolConfiguration(pointSize: 22.0, weight: .regular) + let image = UIImage(systemName: name, withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysTemplate) + return circularIcon(with: color(forStyle: style), size: CGSize(width: 50, height: 50), icon: image) + } + } else { + return UIImage(named: style == .backgroundColor ? name : name + "-circle") + } + #else + return UIImage(named: style == .backgroundColor ? name : name + "-circle") + #endif + } + + func color(forStyle style: ButtonStyle) -> UIColor { + #if canImport(Combine) + switch self { + case .read, .unread: return UIColor.systemBlue + case .more: + if #available(iOS 13.0, *) { + if UITraitCollection.current.userInterfaceStyle == .dark { + return UIColor.systemGray + } + return style == .backgroundColor ? UIColor.systemGray3 : UIColor.systemGray2 + } else { + return #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1) + } + case .flag: return UIColor.systemOrange + case .trash: return UIColor.systemRed + } + #else + switch self { + case .read, .unread: return #colorLiteral(red: 0, green: 0.4577052593, blue: 1, alpha: 1) + case .more: return #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1) + case .flag: return #colorLiteral(red: 1, green: 0.5803921569, blue: 0, alpha: 1) + case .trash: return #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1) + } + #endif + } + + func circularIcon(with color: UIColor, size: CGSize, icon: UIImage? = nil) -> UIImage? { + let rect = CGRect(origin: .zero, size: size) + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + + UIBezierPath(ovalIn: rect).addClip() + + color.setFill() + UIRectFill(rect) + + if let icon = icon { + let iconRect = CGRect(x: (rect.size.width - icon.size.width) / 2, + y: (rect.size.height - icon.size.height) / 2, + width: icon.size.width, + height: icon.size.height) + icon.draw(in: iconRect, blendMode: .normal, alpha: 1.0) + } + + defer { UIGraphicsEndImageContext() } + + return UIGraphicsGetImageFromCurrentImageContext() + } +} +enum ButtonDisplayMode { + case titleAndImage, titleOnly, imageOnly +} + +enum ButtonStyle { + case backgroundColor, circular +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeAccessibilityCustomAction.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeAccessibilityCustomAction.swift new file mode 100644 index 0000000..acc2deb --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeAccessibilityCustomAction.swift @@ -0,0 +1,28 @@ +// +// SwipeAccessibilityCustomAction.swift +// SwipeCellKit +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +class SwipeAccessibilityCustomAction: UIAccessibilityCustomAction { + let action: SwipeAction + let indexPath: IndexPath + + init?(action: SwipeAction, indexPath: IndexPath, target: Any, selector: Selector) { + + self.action = action + self.indexPath = indexPath + + let name = action.accessibilityLabel ?? action.title ?? action.image?.accessibilityIdentifier ?? nil + + if let name = name { + super.init(name: name, target: target, selector: selector) + } else { + return nil + } + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeAction.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeAction.swift new file mode 100644 index 0000000..783ad14 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeAction.swift @@ -0,0 +1,131 @@ +// +// SwipeAction.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/// Constants that help define the appearance of action buttons. +public enum SwipeActionStyle: Int { + /// Apply a style that reflects standard non-destructive actions. + case `default` + + /// Apply a style that reflects destructive actions. + case destructive +} + +/** + The `SwipeAction` object defines a single action to present when the user swipes horizontally in a table/collection item. + + This class lets you define one or more custom actions to display for a given item in your table/collection. Each instance of this class represents a single action to perform and includes the text, formatting information, and behavior for the corresponding button. + */ +public class SwipeAction: NSObject { + /// An optional unique action identifier. + public var identifier: String? + + /// The title of the action button. + /// + /// - note: You must specify a title or an image. + public var title: String? + + /// The style applied to the action button. + public var style: SwipeActionStyle + + /// The object that is notified as transitioning occurs. + public var transitionDelegate: SwipeActionTransitioning? + + /// The font to use for the title of the action button. + /// + /// - note: If you do not specify a font, a 15pt system font is used. + public var font: UIFont? + + /// The text color of the action button. + /// + /// - note: If you do not specify a color, white is used. + public var textColor: UIColor? + + /// The highlighted text color of the action button. + /// + /// - note: If you do not specify a color, `textColor` is used. + public var highlightedTextColor: UIColor? + + /// The image used for the action button. + /// + /// - note: You must specify a title or an image. + public var image: UIImage? + + /// The highlighted image used for the action button. + /// + /// - note: If you do not specify a highlight image, the default `image` is used for the highlighted state. + public var highlightedImage: UIImage? + + /// The closure to execute when the user taps the button associated with this action. + public var handler: ((SwipeAction, IndexPath) -> Void)? + + /// The background color of the action button. + /// + /// - note: Use this property to specify the background color for your button. If you do not specify a value for this property, the framework assigns a default color based on the value in the style property. + public var backgroundColor: UIColor? + + /// The highlighted background color of the action button. + /// + /// - note: Use this property to specify the highlighted background color for your button. + public var highlightedBackgroundColor: UIColor? + + /// The visual effect to apply to the action button. + /// + /// - note: Assigning a visual effect object to this property adds that effect to the background of the action button. + public var backgroundEffect: UIVisualEffect? + + /// A Boolean value that determines whether the actions menu is automatically hidden upon selection. + /// + /// - note: When set to `true`, the actions menu is automatically hidden when the action is selected. The default value is `false`. + public var hidesWhenSelected = false + + /** + Constructs a new `SwipeAction` instance. + + - parameter style: The style of the action button. + - parameter title: The title of the action button. + - parameter handler: The closure to execute when the user taps the button associated with this action. + */ + public init(style: SwipeActionStyle, title: String?, handler: ((SwipeAction, IndexPath) -> Void)?) { + self.title = title + self.style = style + self.handler = handler + } + + /** + Calling this method performs the configured expansion completion animation including deletion, if necessary. Calling this method more than once has no effect. + + You should only call this method from the implementation of your action `handler` method. + + - parameter style: The desired style for completing the expansion action. + */ + public func fulfill(with style: ExpansionFulfillmentStyle) { + completionHandler?(style) + } + + // MARK: - Internal + + internal var completionHandler: ((ExpansionFulfillmentStyle) -> Void)? +} + +/// Describes how expansion should be resolved once the action has been fulfilled. +public enum ExpansionFulfillmentStyle { + /// Implies the item will be deleted upon action fulfillment. + case delete + + /// Implies the item will be reset and the actions view hidden upon action fulfillment. + case reset +} + +// MARK: - Internal + +internal extension SwipeAction { + var hasBackgroundColor: Bool { + return backgroundColor != .clear && backgroundEffect == nil + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionButton.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionButton.swift new file mode 100644 index 0000000..8e83b1f --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionButton.swift @@ -0,0 +1,108 @@ +// +// SwipeActionButton.swift +// +// Created by Jeremy Koch. +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +class SwipeActionButton: UIButton { + var spacing: CGFloat = 8 + var shouldHighlight = true + var highlightedBackgroundColor: UIColor? + + var maximumImageHeight: CGFloat = 0 + var verticalAlignment: SwipeVerticalAlignment = .centerFirstBaseline + + + var currentSpacing: CGFloat { + return (currentTitle?.isEmpty == false && imageHeight > 0) ? spacing : 0 + } + + var alignmentRect: CGRect { + let contentRect = self.contentRect(forBounds: bounds) + let titleHeight = titleBoundingRect(with: verticalAlignment == .centerFirstBaseline ? CGRect.infinite.size : contentRect.size).integral.height + let totalHeight = imageHeight + titleHeight + currentSpacing + + return contentRect.center(size: CGSize(width: contentRect.width, height: totalHeight)) + } + + private var imageHeight: CGFloat { + get { + return currentImage == nil ? 0 : maximumImageHeight + } + } + + override var intrinsicContentSize: CGSize { + return CGSize(width: UIView.noIntrinsicMetric, height: contentEdgeInsets.top + alignmentRect.height + contentEdgeInsets.bottom) + } + + convenience init(action: SwipeAction) { + self.init(frame: .zero) + + contentHorizontalAlignment = .center + + tintColor = action.textColor ?? .white + let highlightedTextColor = action.highlightedTextColor ?? tintColor + highlightedBackgroundColor = action.highlightedBackgroundColor ?? UIColor.black.withAlphaComponent(0.1) + + titleLabel?.font = action.font ?? UIFont.systemFont(ofSize: 15, weight: UIFont.Weight.medium) + titleLabel?.textAlignment = .center + titleLabel?.lineBreakMode = .byWordWrapping + titleLabel?.numberOfLines = 0 + + accessibilityLabel = action.accessibilityLabel + + setTitle(action.title, for: .normal) + setTitleColor(tintColor, for: .normal) + setTitleColor(highlightedTextColor, for: .highlighted) + setImage(action.image, for: .normal) + setImage(action.highlightedImage ?? action.image, for: .highlighted) + } + + override var isHighlighted: Bool { + didSet { + guard shouldHighlight else { return } + + backgroundColor = isHighlighted ? highlightedBackgroundColor : .clear + } + } + + func preferredWidth(maximum: CGFloat) -> CGFloat { + let width = maximum > 0 ? maximum : CGFloat.greatestFiniteMagnitude + let textWidth = titleBoundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)).width + let imageWidth = currentImage?.size.width ?? 0 + + return min(width, max(textWidth, imageWidth) + contentEdgeInsets.left + contentEdgeInsets.right) + } + + func titleBoundingRect(with size: CGSize) -> CGRect { + guard let title = currentTitle, let font = titleLabel?.font else { return .zero } + + return title.boundingRect(with: size, + options: [.usesLineFragmentOrigin], + attributes: [NSAttributedString.Key.font: font], + context: nil).integral + } + + override func titleRect(forContentRect contentRect: CGRect) -> CGRect { + var rect = contentRect.center(size: titleBoundingRect(with: contentRect.size).size) + rect.origin.y = alignmentRect.minY + imageHeight + currentSpacing + return rect.integral + } + + override func imageRect(forContentRect contentRect: CGRect) -> CGRect { + var rect = contentRect.center(size: currentImage?.size ?? .zero) + rect.origin.y = alignmentRect.minY + (imageHeight - rect.height) / 2 + return rect + } +} + +extension CGRect { + func center(size: CGSize) -> CGRect { + let dx = width - size.width + let dy = height - size.height + return CGRect(x: origin.x + dx * 0.5, y: origin.y + dy * 0.5, width: size.width, height: size.height) + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionTransitioning.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionTransitioning.swift new file mode 100644 index 0000000..8a316f1 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionTransitioning.swift @@ -0,0 +1,106 @@ +// +// SwipeActionTransitioning.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/** + Adopt the `SwipeActionTransitioning` protocol in objects that implement custom appearance of actions during transition. + */ +public protocol SwipeActionTransitioning { + /** + Tells the delegate that transition change has occured. + */ + func didTransition(with context: SwipeActionTransitioningContext) -> Void +} + +/** + The `SwipeActionTransitioningContext` type provides information relevant to a specific action as transitioning occurs. + */ +public struct SwipeActionTransitioningContext { + /// The unique action identifier. + public let actionIdentifier: String? + + /// The button that is changing. + public let button: UIButton + + /// The old visibility percentage between 0.0 and 1.0. + public let newPercentVisible: CGFloat + + /// The new visibility percentage between 0.0 and 1.0. + public let oldPercentVisible: CGFloat + + internal let wrapperView: UIView + + internal init(actionIdentifier: String?, button: UIButton, newPercentVisible: CGFloat, oldPercentVisible: CGFloat, wrapperView: UIView) { + self.actionIdentifier = actionIdentifier + self.button = button + self.newPercentVisible = newPercentVisible + self.oldPercentVisible = oldPercentVisible + self.wrapperView = wrapperView + } + + /// Sets the background color behind the action button. + /// + /// - parameter color: The background color. + public func setBackgroundColor(_ color: UIColor?) { + wrapperView.backgroundColor = color + } +} + +/** + A scale transition object drives the custom appearance of actions during transition. + + As button's percentage visibility crosses the `threshold`, the `ScaleTransition` object will animate from `initialScale` to `identity`. The default settings provide a "pop-like" effect as the buttons are exposed more than 50%. + */ +public struct ScaleTransition: SwipeActionTransitioning { + + /// Returns a `ScaleTransition` instance with default transition options. + public static var `default`: ScaleTransition { return ScaleTransition() } + + /// The duration of the animation. + public let duration: Double + + /// The initial scale factor used before the action button percent visible is greater than the threshold. + public let initialScale: CGFloat + + /// The percent visible threshold that triggers the scaling animation. + public let threshold: CGFloat + + /** + Contructs a new `ScaleTransition` instance. + + - parameter duration: The duration of the animation. + + - parameter initialScale: The initial scale factor used before the action button percent visible is greater than the threshold. + + - parameter threshold: The percent visible threshold that triggers the scaling animation. + + - returns: The new `ScaleTransition` instance. + */ + public init(duration: Double = 0.15, initialScale: CGFloat = 0.8, threshold: CGFloat = 0.5) { + self.duration = duration + self.initialScale = initialScale + self.threshold = threshold + } + + /// :nodoc: + public func didTransition(with context: SwipeActionTransitioningContext) -> Void { + if context.oldPercentVisible == 0 { + context.button.transform = .init(scaleX: initialScale, y: initialScale) + } + + if context.oldPercentVisible < threshold && context.newPercentVisible >= threshold { + UIView.animate(withDuration: duration) { + context.button.transform = .identity + } + } else if context.oldPercentVisible >= threshold && context.newPercentVisible < threshold { + UIView.animate(withDuration: duration) { + context.button.transform = .init(scaleX: self.initialScale, y: self.initialScale) + } + } + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionsView.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionsView.swift new file mode 100644 index 0000000..fb9023b --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeActionsView.swift @@ -0,0 +1,348 @@ +// +// SwipeActionsView.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +protocol SwipeActionsViewDelegate: class { + func swipeActionsView(_ swipeActionsView: SwipeActionsView, didSelect action: SwipeAction) +} + +class SwipeActionsView: UIView { + weak var delegate: SwipeActionsViewDelegate? + + let transitionLayout: SwipeTransitionLayout + var layoutContext: ActionsViewLayoutContext + + var feedbackGenerator: SwipeFeedback + + var expansionAnimator: SwipeAnimator? + + var expansionDelegate: SwipeExpanding? { + return options.expansionDelegate ?? (expandableAction?.hasBackgroundColor == false ? ScaleAndAlphaExpansion.default : nil) + } + + weak var safeAreaInsetView: UIView? + let orientation: SwipeActionsOrientation + let actions: [SwipeAction] + let options: SwipeOptions + + var buttons: [SwipeActionButton] = [] + + var minimumButtonWidth: CGFloat = 0 + var maximumImageHeight: CGFloat { + return actions.reduce(0, { initial, next in max(initial, next.image?.size.height ?? 0) }) + } + + var safeAreaMargin: CGFloat { + guard #available(iOS 11, *) else { return 0 } + guard let scrollView = self.safeAreaInsetView else { return 0 } + return orientation == .left ? scrollView.safeAreaInsets.left : scrollView.safeAreaInsets.right + } + + var visibleWidth: CGFloat = 0 { + didSet { + // If necessary, adjust for safe areas + visibleWidth = max(0, visibleWidth - safeAreaMargin) + + let preLayoutVisibleWidths = transitionLayout.visibleWidthsForViews(with: layoutContext) + + layoutContext = ActionsViewLayoutContext.newContext(for: self) + + transitionLayout.container(view: self, didChangeVisibleWidthWithContext: layoutContext) + + setNeedsLayout() + layoutIfNeeded() + + notifyVisibleWidthChanged(oldWidths: preLayoutVisibleWidths, + newWidths: transitionLayout.visibleWidthsForViews(with: layoutContext)) + } + } + + var preferredWidth: CGFloat { + return minimumButtonWidth * CGFloat(actions.count) + safeAreaMargin + } + + var contentSize: CGSize { + if options.expansionStyle?.elasticOverscroll != true || visibleWidth < preferredWidth { + return CGSize(width: visibleWidth, height: bounds.height) + } else { + let scrollRatio = max(0, visibleWidth - preferredWidth) + return CGSize(width: preferredWidth + (scrollRatio * 0.25), height: bounds.height) + } + } + + private(set) var expanded: Bool = false + + var expandableAction: SwipeAction? { + return options.expansionStyle != nil ? actions.last : nil + } + + init(contentEdgeInsets: UIEdgeInsets, + maxSize: CGSize, + safeAreaInsetView: UIView, + options: SwipeOptions, + orientation: SwipeActionsOrientation, + actions: [SwipeAction]) { + + self.safeAreaInsetView = safeAreaInsetView + self.options = options + self.orientation = orientation + self.actions = actions.reversed() + + switch options.transitionStyle { + case .border: + transitionLayout = BorderTransitionLayout() + case .reveal: + transitionLayout = RevealTransitionLayout() + default: + transitionLayout = DragTransitionLayout() + } + + self.layoutContext = ActionsViewLayoutContext(numberOfActions: actions.count, orientation: orientation) + + feedbackGenerator = SwipeFeedback(style: .light) + feedbackGenerator.prepare() + + super.init(frame: .zero) + + clipsToBounds = true + translatesAutoresizingMaskIntoConstraints = false + + + #if canImport(Combine) + if let backgroundColor = options.backgroundColor { + self.backgroundColor = backgroundColor + } + else if #available(iOS 13.0, *) { + backgroundColor = UIColor.systemGray5 + } else { + backgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1) + } + #else + if let backgroundColor = options.backgroundColor { + self.backgroundColor = backgroundColor + } + else { + backgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1) + } + #endif + + buttons = addButtons(for: self.actions, withMaximum: maxSize, contentEdgeInsets: contentEdgeInsets) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func addButtons(for actions: [SwipeAction], withMaximum size: CGSize, contentEdgeInsets: UIEdgeInsets) -> [SwipeActionButton] { + let buttons: [SwipeActionButton] = actions.map({ action in + let actionButton = SwipeActionButton(action: action) + actionButton.addTarget(self, action: #selector(actionTapped(button:)), for: .touchUpInside) + actionButton.autoresizingMask = [.flexibleHeight, orientation == .right ? .flexibleRightMargin : .flexibleLeftMargin] + actionButton.spacing = options.buttonSpacing ?? 8 + actionButton.contentEdgeInsets = buttonEdgeInsets(fromOptions: options) + return actionButton + }) + + let maximum = options.maximumButtonWidth ?? (size.width - 30) / CGFloat(actions.count) + let minimum = options.minimumButtonWidth ?? min(maximum, 74) + minimumButtonWidth = buttons.reduce(minimum, { initial, next in max(initial, next.preferredWidth(maximum: maximum)) }) + + + buttons.enumerated().forEach { (index, button) in + let action = actions[index] + let frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: bounds.height)) + let wrapperView = SwipeActionButtonWrapperView(frame: frame, action: action, orientation: orientation, contentWidth: minimumButtonWidth) + wrapperView.translatesAutoresizingMaskIntoConstraints = false + wrapperView.addSubview(button) + + if let effect = action.backgroundEffect { + let effectView = UIVisualEffectView(effect: effect) + effectView.frame = wrapperView.frame + effectView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + effectView.contentView.addSubview(wrapperView) + addSubview(effectView) + } else { + addSubview(wrapperView) + } + + button.frame = wrapperView.contentRect + button.maximumImageHeight = maximumImageHeight + button.verticalAlignment = options.buttonVerticalAlignment + button.shouldHighlight = action.hasBackgroundColor + + wrapperView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true + wrapperView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true + + let topConstraint = wrapperView.topAnchor.constraint(equalTo: topAnchor, constant: contentEdgeInsets.top) + topConstraint.priority = contentEdgeInsets.top == 0 ? .required : .defaultHigh + topConstraint.isActive = true + + let bottomConstraint = wrapperView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -1 * contentEdgeInsets.bottom) + bottomConstraint.priority = contentEdgeInsets.bottom == 0 ? .required : .defaultHigh + bottomConstraint.isActive = true + + if contentEdgeInsets != .zero { + let heightConstraint = wrapperView.heightAnchor.constraint(greaterThanOrEqualToConstant: button.intrinsicContentSize.height) + heightConstraint.priority = .required + heightConstraint.isActive = true + } + } + return buttons + } + + @objc func actionTapped(button: SwipeActionButton) { + guard let index = buttons.firstIndex(of: button) else { return } + + delegate?.swipeActionsView(self, didSelect: actions[index]) + } + + func buttonEdgeInsets(fromOptions options: SwipeOptions) -> UIEdgeInsets { + let padding = options.buttonPadding ?? 8 + return UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) + } + + func setExpanded(expanded: Bool, feedback: Bool = false) { + guard self.expanded != expanded else { return } + + self.expanded = expanded + + if feedback { + feedbackGenerator.impactOccurred() + feedbackGenerator.prepare() + } + + let timingParameters = expansionDelegate?.animationTimingParameters(buttons: buttons.reversed(), expanding: expanded) + + if expansionAnimator?.isRunning == true { + expansionAnimator?.stopAnimation(true) + } + + if #available(iOS 10, *) { + expansionAnimator = UIViewPropertyAnimator(duration: timingParameters?.duration ?? 0.6, dampingRatio: 1.0) + } else { + expansionAnimator = UIViewSpringAnimator(duration: timingParameters?.duration ?? 0.6, + damping: 1.0, + initialVelocity: 1.0) + } + + expansionAnimator?.addAnimations { + self.setNeedsLayout() + self.layoutIfNeeded() + } + + expansionAnimator?.startAnimation(afterDelay: timingParameters?.delay ?? 0) + + notifyExpansion(expanded: expanded) + } + + func notifyVisibleWidthChanged(oldWidths: [CGFloat], newWidths: [CGFloat]) { + DispatchQueue.main.async { + oldWidths.enumerated().forEach { index, oldWidth in + let newWidth = newWidths[index] + if oldWidth != newWidth { + let context = SwipeActionTransitioningContext(actionIdentifier: self.actions[index].identifier, + button: self.buttons[index], + newPercentVisible: newWidth / self.minimumButtonWidth, + oldPercentVisible: oldWidth / self.minimumButtonWidth, + wrapperView: self.subviews[index]) + + self.actions[index].transitionDelegate?.didTransition(with: context) + } + } + } + } + + func notifyExpansion(expanded: Bool) { + guard let expandedButton = buttons.last else { return } + + expansionDelegate?.actionButton(expandedButton, didChange: expanded, otherActionButtons: buttons.dropLast().reversed()) + } + + func createDeletionMask() -> UIView { + let mask = UIView(frame: CGRect(x: min(0, frame.minX), y: 0, width: bounds.width * 2, height: bounds.height)) + mask.backgroundColor = UIColor.white + return mask + } + + override func layoutSubviews() { + super.layoutSubviews() + + for subview in subviews.enumerated() { + transitionLayout.layout(view: subview.element, atIndex: subview.offset, with: layoutContext) + } + + if expanded { + subviews.last?.frame.origin.x = 0 + bounds.origin.x + } + } +} + +class SwipeActionButtonWrapperView: UIView { + let contentRect: CGRect + var actionBackgroundColor: UIColor? + + init(frame: CGRect, action: SwipeAction, orientation: SwipeActionsOrientation, contentWidth: CGFloat) { + switch orientation { + case .left: + contentRect = CGRect(x: frame.width - contentWidth, y: 0, width: contentWidth, height: frame.height) + case .right: + contentRect = CGRect(x: 0, y: 0, width: contentWidth, height: frame.height) + } + + super.init(frame: frame) + + configureBackgroundColor(with: action) + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + if let actionBackgroundColor = self.actionBackgroundColor, let context = UIGraphicsGetCurrentContext() { + actionBackgroundColor.setFill() + context.fill(rect); + } + } + + func configureBackgroundColor(with action: SwipeAction) { + guard action.hasBackgroundColor else { + isOpaque = false + return + } + + if let backgroundColor = action.backgroundColor { + actionBackgroundColor = backgroundColor + } else { + switch action.style { + case .destructive: + #if canImport(Combine) + if #available(iOS 13.0, *) { + actionBackgroundColor = UIColor.systemRed + } else { + actionBackgroundColor = #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1) + } + #else + actionBackgroundColor = #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1) + #endif + default: + #if canImport(Combine) + if #available(iOS 13.0, *) { + actionBackgroundColor = UIColor.systemGray3 + } else { + actionBackgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1) + } + #else + actionBackgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1) + #endif + } + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeAnimator.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeAnimator.swift new file mode 100644 index 0000000..732dbe1 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeAnimator.swift @@ -0,0 +1,112 @@ +// +// SwipeAnimator.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +protocol SwipeAnimator { + /// A Boolean value indicating whether the animation is currently running. + var isRunning: Bool { get } + + /** + The animation to be run by the SwipeAnimator + + - parameter animation: The closure to be executed by the animator + */ + func addAnimations(_ animation: @escaping () -> Void) + + /** + Completion handler for the animation that is going to be started + + - parameter completion: The closure to be execute on completion of the animator + */ + func addCompletion(completion: @escaping (Bool) -> Void) + + /** + Starts the defined animation + */ + func startAnimation() + + /** + Starts the defined animation after the given delay + + - parameter delay: Delay of the animation + */ + func startAnimation(afterDelay delay: TimeInterval) + + /** + Stops the animations at their current positions. + + - parameter withoutFinishing: A Boolean indicating whether any final actions should be performed. + */ + func stopAnimation(_ withoutFinishing: Bool) +} + +@available(iOS 10.0, *) +extension UIViewPropertyAnimator: SwipeAnimator { + func addCompletion(completion: @escaping (Bool) -> Void) { + addCompletion { position in + completion(position == .end) + } + } +} + +class UIViewSpringAnimator: SwipeAnimator { + var isRunning: Bool = false + + let duration:TimeInterval + let damping:CGFloat + let velocity:CGFloat + + var animations:(() -> Void)? + var completion:((Bool) -> Void)? + + required init(duration: TimeInterval, + damping: CGFloat, + initialVelocity velocity: CGFloat = 0) { + self.duration = duration + self.damping = damping + self.velocity = velocity + } + + func addAnimations(_ animations: @escaping () -> Void) { + self.animations = animations + } + + func addCompletion(completion: @escaping (Bool) -> Void) { + self.completion = { [weak self] finished in + guard self?.isRunning == true else { return } + + self?.isRunning = false + self?.animations = nil + self?.completion = nil + + completion(finished) + } + } + + func startAnimation() { + self.startAnimation(afterDelay: 0) + } + + func startAnimation(afterDelay delay:TimeInterval) { + guard let animations = animations else { return } + + isRunning = true + + UIView.animate(withDuration: duration, + delay: delay, + usingSpringWithDamping: damping, + initialSpringVelocity: velocity, + options: [.curveEaseInOut, .allowUserInteraction], + animations: animations, + completion: completion) + } + + func stopAnimation(_ withoutFinishing: Bool) { + isRunning = false + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell+Accessibility.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell+Accessibility.swift new file mode 100644 index 0000000..b26284f --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell+Accessibility.swift @@ -0,0 +1,80 @@ +// +// SwipeCollectionViewCell+Accessibility.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +extension SwipeCollectionViewCell { + /// :nodoc: + open override func accessibilityElementCount() -> Int { + guard state != .center else { + return super.accessibilityElementCount() + } + + return 1 + } + + /// :nodoc: + open override func accessibilityElement(at index: Int) -> Any? { + guard state != .center else { + return super.accessibilityElement(at: index) + } + + return actionsView + } + + /// :nodoc: + open override func index(ofAccessibilityElement element: Any) -> Int { + guard state != .center else { + return super.index(ofAccessibilityElement: element) + } + + return element is SwipeActionsView ? 0 : NSNotFound + } +} + +extension SwipeCollectionViewCell { + /// :nodoc: + open override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { + get { + guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { + return super.accessibilityCustomActions + } + + let leftActions = delegate?.collectionView(collectionView, editActionsForItemAt: indexPath, for: .left) ?? [] + let rightActions = delegate?.collectionView(collectionView, editActionsForItemAt: indexPath, for: .right) ?? [] + + let actions = [rightActions.first, leftActions.first].compactMap({ $0 }) + rightActions.dropFirst() + leftActions.dropFirst() + + if actions.count > 0 { + return actions.compactMap({ SwipeAccessibilityCustomAction(action: $0, + indexPath: indexPath, + target: self, + selector: #selector(performAccessibilityCustomAction(accessibilityCustomAction:))) }) + } else { + return super.accessibilityCustomActions + } + } + + set { + super.accessibilityCustomActions = newValue + } + } + + @objc func performAccessibilityCustomAction(accessibilityCustomAction: SwipeAccessibilityCustomAction) -> Bool { + guard let collectionView = collectionView else { return false } + + let swipeAction = accessibilityCustomAction.action + + swipeAction.handler?(swipeAction, accessibilityCustomAction.indexPath) + + if swipeAction.style == .destructive { + collectionView.deleteItems(at: [accessibilityCustomAction.indexPath]) + } + + return true + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell+Display.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell+Display.swift new file mode 100644 index 0000000..684fd24 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell+Display.swift @@ -0,0 +1,55 @@ +// +// SwipeCollectionViewCell+Display.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +extension SwipeCollectionViewCell { + /// The point at which the origin of the cell is offset from the non-swiped origin. + public var swipeOffset: CGFloat { + set { setSwipeOffset(newValue, animated: false) } + get { return contentView.frame.midX - bounds.midX } + } + + /** + Hides the swipe actions and returns the cell to center. + + - parameter animated: Specify `true` to animate the hiding of the swipe actions or `false` to hide it immediately. + + - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. + */ + public func hideSwipe(animated: Bool, completion: ((Bool) -> Void)? = nil) { + swipeController.hideSwipe(animated: animated, completion: completion) + } + + /** + Shows the swipe actions for the specified orientation. + + - parameter orientation: The side of the cell on which to show the swipe actions. + + - parameter animated: Specify `true` to animate the showing of the swipe actions or `false` to show them immediately. + + - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. + */ + public func showSwipe(orientation: SwipeActionsOrientation, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { + setSwipeOffset(.greatestFiniteMagnitude * orientation.scale * -1, + animated: animated, + completion: completion) + } + + /** + The point at which the origin of the cell is offset from the non-swiped origin. + + - parameter offset: A point (expressed in points) that is offset from the non-swiped origin. + + - parameter animated: Specify `true` to animate the transition to the new offset, `false` to make the transition immediate. + + - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. + */ + public func setSwipeOffset(_ offset: CGFloat, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { + swipeController.setSwipeOffset(offset, animated: animated, completion: completion) + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell.swift new file mode 100644 index 0000000..3f448d2 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCell.swift @@ -0,0 +1,257 @@ +// +// SwipeCollectionViewCell.swift +// SwipeCellKit +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/** + The `SwipeCollectionViewCell` class extends `UICollectionViewCell` and provides more flexible options for cell swiping behavior. + + + The default behavior closely matches the stock Mail.app. If you want to customize the transition style (ie. how the action buttons are exposed), or the expansion style (the behavior when the row is swiped passes a defined threshold), you can return the appropriately configured `SwipeOptions` via the `SwipeCollectionViewCellDelegate` delegate. + */ +open class SwipeCollectionViewCell: UICollectionViewCell { + /// The object that acts as the delegate of the `SwipeCollectionViewCell`. + public weak var delegate: SwipeCollectionViewCellDelegate? + public var containerView: UIView = UIView() + + var state = SwipeState.center + var actionsView: SwipeActionsView? + var scrollView: UIScrollView? { + return collectionView + } + var indexPath: IndexPath? { + return collectionView?.indexPath(for: self) + } + var panGestureRecognizer: UIGestureRecognizer + { + return swipeController.panGestureRecognizer; + } + + var swipeController: SwipeController! + var isPreviouslySelected = false + + weak var collectionView: UICollectionView? + + /// :nodoc: + open override var frame: CGRect { + set { super.frame = state.isActive ? CGRect(origin: CGPoint(x: frame.minX, y: newValue.minY), size: newValue.size) : newValue } + get { return super.frame } + } + + /// :nodoc: + open override var isHighlighted: Bool { + set { + guard state == .center else { return } + super.isHighlighted = newValue + } + get { return super.isHighlighted } + } + + /// :nodoc: + open override var layoutMargins: UIEdgeInsets { + get { + return frame.origin.x != 0 ? swipeController.originalLayoutMargins : super.layoutMargins + } + set { + super.layoutMargins = newValue + } + } + + /// :nodoc: + override public init(frame: CGRect) { + super.init(frame: frame) + + + } + + /// :nodoc: + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + + } + + deinit { + collectionView?.panGestureRecognizer.removeTarget(self, action: nil) + } + + public func configure() { + containerView.clipsToBounds = false + + if containerView.translatesAutoresizingMaskIntoConstraints == true { + containerView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + containerView.topAnchor.constraint(equalTo: self.topAnchor), + containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ]) + } + + swipeController = SwipeController(swipeable: self, actionsContainerView: containerView) + swipeController.delegate = self + } + + /// :nodoc: + override open func prepareForReuse() { + super.prepareForReuse() + + reset() + resetSelectedState() + } + + /// :nodoc: + override open func didMoveToSuperview() { + super.didMoveToSuperview() + + var view: UIView = self + while let superview = view.superview { + view = superview + + if let collectionView = view as? UICollectionView { + self.collectionView = collectionView + + swipeController.scrollView = scrollView + + collectionView.panGestureRecognizer.removeTarget(self, action: nil) + collectionView.panGestureRecognizer.addTarget(self, action: #selector(handleCollectionPan(gesture:))) + return + } + } + } + + /// :nodoc: + open override func willMove(toWindow newWindow: UIWindow?) { + super.willMove(toWindow: newWindow) + + if newWindow == nil { + reset() + } + } + + // Override so we can accept touches anywhere within the cell's original frame. + // This is required to detect touches on the `SwipeActionsView` sitting alongside the + // `SwipeCollectionViewCell`. + /// :nodoc: + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + guard let superview = superview else { return false } + + let point = convert(point, to: superview) + + if !UIAccessibility.isVoiceOverRunning { + for cell in collectionView?.swipeCells ?? [] { + if (cell.state == .left || cell.state == .right) && !cell.contains(point: point) { + collectionView?.hideSwipeCell() + return false + } + } + } + + return contains(point: point) + } + + func contains(point: CGPoint) -> Bool { + return frame.contains(point) + } + + // Override hitTest(_:with:) here so that we can make sure our `actionsView` gets the touch event + // if it's supposed to, since otherwise, our `containerView` will swallow it and pass it up to + // the collection view. + /// :nodoc: + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard + let actionsView = actionsView, + isHidden == false + else { return super.hitTest(point, with: event) } + + let modifiedPoint = actionsView.convert(point, from: self) + return actionsView.hitTest(modifiedPoint, with: event) ?? super.hitTest(point, with: event) + } + + /// :nodoc: + override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return swipeController.gestureRecognizerShouldBegin(gestureRecognizer) + } + + /// :nodoc: + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + swipeController.traitCollectionDidChange(from: previousTraitCollection, to: self.traitCollection) + } + + @objc func handleCollectionPan(gesture: UIPanGestureRecognizer) { + if gesture.state == .began { + hideSwipe(animated: true) + } + } + + func reset() { + containerView.clipsToBounds = false + swipeController.reset() + collectionView?.setGestureEnabled(true) + } + + func resetSelectedState() { + if isPreviouslySelected { + if let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) { + collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) + } + } + isPreviouslySelected = false + } +} + +extension SwipeCollectionViewCell: SwipeControllerDelegate { + func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool { + return true + } + + func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]? { + guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return nil } + + return delegate?.collectionView(collectionView, editActionsForItemAt: indexPath, for: orientation) + } + + func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions { + guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return SwipeOptions() } + + return delegate?.collectionView(collectionView, editActionsOptionsForItemAt: indexPath, for: orientation) ?? SwipeOptions() + } + + func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect? { + guard let collectionView = collectionView else { return nil } + + return delegate?.visibleRect(for: collectionView) + } + + func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation) { + guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return } + + // Remove highlight and deselect any selected cells + super.isHighlighted = false + isPreviouslySelected = isSelected + collectionView.deselectItem(at: indexPath, animated: false) + + delegate?.collectionView(collectionView, willBeginEditingItemAt: indexPath, for: orientation) + } + + func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation) { + guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self), let actionsView = self.actionsView else { return } + + resetSelectedState() + + delegate?.collectionView(collectionView, didEndEditingItemAt: indexPath, for: actionsView.orientation) + } + + func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath) { + collectionView?.deleteItems(at: [indexPath]) + } +} + diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCellDelegate.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCellDelegate.swift new file mode 100644 index 0000000..3383d39 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeCollectionViewCellDelegate.swift @@ -0,0 +1,91 @@ +// +// SwipeCollectionViewCellDelegate.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/** + The `SwipeCollectionViewCellDelegate` protocol is adopted by an object that manages the display of action buttons when the item is swiped. + */ +public protocol SwipeCollectionViewCellDelegate: class { + /** + Asks the delegate for the actions to display in response to a swipe in the specified item. + + - parameter collectionView: The collection view object which owns the item requesting this information. + + - parameter indexPath: The index path of the item. + + - parameter orientation: The side of the item requesting this information. + + - returns: An array of `SwipeAction` objects representing the actions for the item. Each action you provide is used to create a button that the user can tap. Returning `nil` will prevent swiping for the supplied orientation. + */ + func collectionView(_ collectionView: UICollectionView, editActionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? + + /** + Asks the delegate for the display options to be used while presenting the action buttons. + + - parameter collectionView: The collection view object which owns the item requesting this information. + + - parameter indexPath: The index path of the item. + + - parameter orientation: The side of the item requesting this information. + + - returns: A `SwipeOptions` instance which configures the behavior of the action buttons. + + - note: If not implemented, a default `SwipeOptions` instance is used. + */ + func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions + + /** + Tells the delegate that the collection view is about to go into editing mode. + + - parameter collectionView: The collection view object providing this information. + + - parameter indexPath: The index path of the item. + + - parameter orientation: The side of the item. + */ + func collectionView(_ collectionView: UICollectionView, willBeginEditingItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) + + /** + Tells the delegate that the collection view has left editing mode. + + - parameter collectionView: The collection view object providing this information. + + - parameter indexPath: The index path of the item. + + - parameter orientation: The side of the item. + */ + func collectionView(_ collectionView: UICollectionView, didEndEditingItemAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) + + /** + Asks the delegate for visibile rectangle of the collection view, which is used to ensure swipe actions are vertically centered within the visible portion of the item. + + - parameter collectionView: The collection view object providing this information. + + - returns: The visible rectangle of the collection view. + + - note: The returned rectange should be in the collection view's own coordinate system. Returning `nil` will result in no vertical offset to be be calculated. + */ + func visibleRect(for collectionView: UICollectionView) -> CGRect? +} + +/** + Default implementation of `SwipeCollectionViewCellDelegate` methods + */ +public extension SwipeCollectionViewCellDelegate { + func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { + return SwipeOptions() + } + + func collectionView(_ collectionView: UICollectionView, willBeginEditingItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) {} + + func collectionView(_ collectionView: UICollectionView, didEndEditingItemAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) {} + + func visibleRect(for collectionView: UICollectionView) -> CGRect? { + return nil + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeController.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeController.swift new file mode 100644 index 0000000..4083543 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeController.swift @@ -0,0 +1,531 @@ +// +// SwipeController.swift +// SwipeCellKit +// +// Created by Mohammad Kurabi on 5/19/18. +// + +import Foundation +import UIKit + +protocol SwipeControllerDelegate: class { + + func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool + + func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]? + + func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions + + func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation) + + func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation) + + func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath) + + func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect? + +} + +class SwipeController: NSObject { + + weak var swipeable: (UIView & Swipeable)? + weak var actionsContainerView: UIView? + + weak var delegate: SwipeControllerDelegate? + weak var scrollView: UIScrollView? + + var animator: SwipeAnimator? + + let elasticScrollRatio: CGFloat = 0.4 + + var originalCenter: CGFloat = 0 + var scrollRatio: CGFloat = 1.0 + var originalLayoutMargins: UIEdgeInsets = .zero + + lazy var panGestureRecognizer: UIPanGestureRecognizer = { + let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:))) + gesture.delegate = self + return gesture + }() + + lazy var tapGestureRecognizer: UITapGestureRecognizer = { + let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(gesture:))) + gesture.delegate = self + return gesture + }() + + init(swipeable: UIView & Swipeable, actionsContainerView: UIView) { + self.swipeable = swipeable + self.actionsContainerView = actionsContainerView + + super.init() + + configure() + } + + @objc func handlePan(gesture: UIPanGestureRecognizer) { + guard let target = actionsContainerView, var swipeable = self.swipeable else { return } + + let velocity = gesture.velocity(in: target) + + if delegate?.swipeController(self, canBeginEditingSwipeableFor: velocity.x > 0 ? .left : .right) == false { + return + } + + switch gesture.state { + case .began: + if let swipeable = scrollView?.swipeables.first(where: { $0.state == .dragging }) as? UIView, self.swipeable != nil, swipeable != self.swipeable! { + return + } + + stopAnimatorIfNeeded() + + originalCenter = target.center.x + + if swipeable.state == .center || swipeable.state == .animatingToCenter { + let orientation: SwipeActionsOrientation = velocity.x > 0 ? .left : .right + + showActionsView(for: orientation) + } + case .changed: + guard let actionsView = swipeable.actionsView, let actionsContainerView = self.actionsContainerView else { return } + guard swipeable.state.isActive else { return } + + if swipeable.state == .animatingToCenter { + let swipedCell = scrollView?.swipeables.first(where: { $0.state == .dragging || $0.state == .left || $0.state == .right }) as? UIView + if let swipedCell = swipedCell, self.swipeable != nil, swipedCell != self.swipeable! { + return + } + } + + let translation = gesture.translation(in: target).x + scrollRatio = 1.0 + + // Check if dragging past the center of the opposite direction of action view, if so + // then we need to apply elasticity + if (translation + originalCenter - swipeable.bounds.midX) * actionsView.orientation.scale > 0 { + target.center.x = gesture.elasticTranslation(in: target, + withLimit: .zero, + fromOriginalCenter: CGPoint(x: originalCenter, y: 0)).x + swipeable.actionsView?.visibleWidth = abs((swipeable as Swipeable).frame.minX) + scrollRatio = elasticScrollRatio + return + } + + if let expansionStyle = actionsView.options.expansionStyle, let scrollView = scrollView { + + let referenceFrame = actionsContainerView != swipeable ? actionsContainerView.frame : nil; + let expanded = expansionStyle.shouldExpand(view: swipeable, gesture: gesture, in: scrollView, within: referenceFrame) + let targetOffset = expansionStyle.targetOffset(for: swipeable) + let currentOffset = abs(translation + originalCenter - swipeable.bounds.midX) + + if expanded && !actionsView.expanded && targetOffset > currentOffset { + let centerForTranslationToEdge = swipeable.bounds.midX - targetOffset * actionsView.orientation.scale + let delta = centerForTranslationToEdge - originalCenter + + animate(toOffset: centerForTranslationToEdge) + gesture.setTranslation(CGPoint(x: delta, y: 0), in: swipeable.superview!) + } else { + target.center.x = gesture.elasticTranslation(in: target, + withLimit: CGSize(width: targetOffset, height: 0), + fromOriginalCenter: CGPoint(x: originalCenter, y: 0), + applyingRatio: expansionStyle.targetOverscrollElasticity).x + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + } + + actionsView.setExpanded(expanded: expanded, feedback: true) + } else { + target.center.x = gesture.elasticTranslation(in: target, + withLimit: CGSize(width: actionsView.preferredWidth, height: 0), + fromOriginalCenter: CGPoint(x: originalCenter, y: 0), + applyingRatio: elasticScrollRatio).x + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + + if (target.center.x - originalCenter) / translation != 1.0 { + scrollRatio = elasticScrollRatio + } + } + case .ended, .cancelled, .failed: + guard let actionsView = swipeable.actionsView, let actionsContainerView = self.actionsContainerView else { return } + if swipeable.state.isActive == false && swipeable.bounds.midX == target.center.x { + return + } + + swipeable.state = targetState(forVelocity: velocity) + + if actionsView.expanded == true, let expandedAction = actionsView.expandableAction { + perform(action: expandedAction) + } else { + let targetOffset = targetCenter(active: swipeable.state.isActive) + let distance = targetOffset - actionsContainerView.center.x + let normalizedVelocity = velocity.x * scrollRatio / distance + + animate(toOffset: targetOffset, withInitialVelocity: normalizedVelocity) { _ in + if self.swipeable?.state == .center { + self.reset() + } + } + + if !swipeable.state.isActive { + delegate?.swipeController(self, didEndEditingSwipeableFor: actionsView.orientation) + } + } + default: break + } + } + + @discardableResult + func showActionsView(for orientation: SwipeActionsOrientation) -> Bool { + guard let actions = delegate?.swipeController(self, editActionsForSwipeableFor: orientation), actions.count > 0 else { return false } + guard let swipeable = self.swipeable else { return false } + + originalLayoutMargins = swipeable.layoutMargins + + configureActionsView(with: actions, for: orientation) + + delegate?.swipeController(self, willBeginEditingSwipeableFor: orientation) + + return true + } + + func configureActionsView(with actions: [SwipeAction], for orientation: SwipeActionsOrientation) { + guard var swipeable = self.swipeable, + let actionsContainerView = self.actionsContainerView, + let scrollView = self.scrollView else { + return + } + + let options = delegate?.swipeController(self, editActionsOptionsForSwipeableFor: orientation) ?? SwipeOptions() + + swipeable.actionsView?.removeFromSuperview() + swipeable.actionsView = nil + + var contentEdgeInsets = UIEdgeInsets.zero + if let visibleTableViewRect = delegate?.swipeController(self, visibleRectFor: scrollView) { + + let frame = (swipeable as Swipeable).frame + let visibleSwipeableRect = frame.intersection(visibleTableViewRect) + if visibleSwipeableRect.isNull == false { + let top = visibleSwipeableRect.minY > frame.minY ? max(0, visibleSwipeableRect.minY - frame.minY) : 0 + let bottom = max(0, frame.size.height - visibleSwipeableRect.size.height - top) + contentEdgeInsets = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0) + } + } + + let actionsView = SwipeActionsView(contentEdgeInsets: contentEdgeInsets, + maxSize: swipeable.bounds.size, + safeAreaInsetView: scrollView, + options: options, + orientation: orientation, + actions: actions) + actionsView.delegate = self + + actionsContainerView.addSubview(actionsView) + + actionsView.heightAnchor.constraint(equalTo: swipeable.heightAnchor).isActive = true + actionsView.widthAnchor.constraint(equalTo: swipeable.widthAnchor, multiplier: 2).isActive = true + actionsView.topAnchor.constraint(equalTo: swipeable.topAnchor).isActive = true + + if orientation == .left { + actionsView.rightAnchor.constraint(equalTo: actionsContainerView.leftAnchor).isActive = true + } else { + actionsView.leftAnchor.constraint(equalTo: actionsContainerView.rightAnchor).isActive = true + } + + actionsView.setNeedsUpdateConstraints() + + swipeable.actionsView = actionsView + + swipeable.state = .dragging + } + + func animate(duration: Double = 0.7, toOffset offset: CGFloat, withInitialVelocity velocity: CGFloat = 0, completion: ((Bool) -> Void)? = nil) { + stopAnimatorIfNeeded() + + swipeable?.layoutIfNeeded() + + let animator: SwipeAnimator = { + if velocity != 0 { + if #available(iOS 10, *) { + let velocity = CGVector(dx: velocity, dy: velocity) + let parameters = UISpringTimingParameters(mass: 1.0, stiffness: 100, damping: 18, initialVelocity: velocity) + return UIViewPropertyAnimator(duration: 0.0, timingParameters: parameters) + } else { + return UIViewSpringAnimator(duration: duration, damping: 1.0, initialVelocity: velocity) + } + } else { + if #available(iOS 10, *) { + return UIViewPropertyAnimator(duration: duration, dampingRatio: 1.0) + } else { + return UIViewSpringAnimator(duration: duration, damping: 1.0) + } + } + }() + + animator.addAnimations({ + guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return } + + actionsContainerView.center = CGPoint(x: offset, y: actionsContainerView.center.y) + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + swipeable.layoutIfNeeded() + }) + + if let completion = completion { + animator.addCompletion(completion: completion) + } + + self.animator = animator + + animator.startAnimation() + } + + func traitCollectionDidChange(from previousTraitCollrection: UITraitCollection?, to traitCollection: UITraitCollection) { + guard let swipeable = self.swipeable, + let actionsContainerView = self.actionsContainerView, + previousTraitCollrection != nil else { + return + } + + if swipeable.state == .left || swipeable.state == .right { + let targetOffset = targetCenter(active: swipeable.state.isActive) + actionsContainerView.center = CGPoint(x: targetOffset, y: actionsContainerView.center.y) + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + swipeable.layoutIfNeeded() + } + } + + func stopAnimatorIfNeeded() { + if animator?.isRunning == true { + animator?.stopAnimation(true) + } + } + + @objc func handleTap(gesture: UITapGestureRecognizer) { + hideSwipe(animated: true) + } + + @objc func handleTablePan(gesture: UIPanGestureRecognizer) { + if gesture.state == .began { + hideSwipe(animated: true) + } + } + + func targetState(forVelocity velocity: CGPoint) -> SwipeState { + guard let actionsView = swipeable?.actionsView else { return .center } + + switch actionsView.orientation { + case .left: + return (velocity.x < 0 && !actionsView.expanded) ? .center : .left + case .right: + return (velocity.x > 0 && !actionsView.expanded) ? .center : .right + } + } + + func targetCenter(active: Bool) -> CGFloat { + guard let swipeable = self.swipeable else { return 0 } + guard let actionsView = swipeable.actionsView, active == true else { return swipeable.bounds.midX } + + return swipeable.bounds.midX - actionsView.preferredWidth * actionsView.orientation.scale + } + + func configure() { + swipeable?.addGestureRecognizer(tapGestureRecognizer) + swipeable?.addGestureRecognizer(panGestureRecognizer) + } + + func reset() { + swipeable?.state = .center + + swipeable?.actionsView?.removeFromSuperview() + swipeable?.actionsView = nil + } + +} + +extension SwipeController: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer == tapGestureRecognizer { + if UIAccessibility.isVoiceOverRunning { + scrollView?.hideSwipeables() + } + + let swipedCell = scrollView?.swipeables.first(where: { + $0.state.isActive || + $0.panGestureRecognizer.state == .began || + $0.panGestureRecognizer.state == .changed || + $0.panGestureRecognizer.state == .ended + }) + return swipedCell == nil ? false : true + } + + if gestureRecognizer == panGestureRecognizer, + let view = gestureRecognizer.view, + let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer { + let translation = gestureRecognizer.translation(in: view) + return abs(translation.y) <= abs(translation.x) + } + + return true + } +} + +extension SwipeController: SwipeActionsViewDelegate { + func swipeActionsView(_ swipeActionsView: SwipeActionsView, didSelect action: SwipeAction) { + perform(action: action) + } + + func perform(action: SwipeAction) { + guard let actionsView = swipeable?.actionsView else { return } + + if action == actionsView.expandableAction, let expansionStyle = actionsView.options.expansionStyle { + // Trigger the expansion (may already be expanded from drag) + actionsView.setExpanded(expanded: true) + + switch expansionStyle.completionAnimation { + case .bounce: + perform(action: action, hide: true) + case .fill(let fillOption): + performFillAction(action: action, fillOption: fillOption) + } + } else { + perform(action: action, hide: action.hidesWhenSelected) + } + } + + func perform(action: SwipeAction, hide: Bool) { + guard let indexPath = swipeable?.indexPath else { return } + + if hide { + hideSwipe(animated: true) + } + + action.handler?(action, indexPath) + } + + func performFillAction(action: SwipeAction, fillOption: SwipeExpansionStyle.FillOptions) { + guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return } + guard let actionsView = swipeable.actionsView, let indexPath = swipeable.indexPath else { return } + + let newCenter = swipeable.bounds.midX - (swipeable.bounds.width + actionsView.minimumButtonWidth) * actionsView.orientation.scale + + action.completionHandler = { [weak self] style in + guard let `self` = self else { return } + action.completionHandler = nil + + self.delegate?.swipeController(self, didEndEditingSwipeableFor: actionsView.orientation) + + switch style { + case .delete: + actionsContainerView.mask = actionsView.createDeletionMask() + + self.delegate?.swipeController(self, didDeleteSwipeableAt: indexPath) + + UIView.animate(withDuration: 0.3, animations: { + guard let actionsContainerView = self.actionsContainerView else { return } + + actionsContainerView.center.x = newCenter + actionsContainerView.mask?.frame.size.height = 0 + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + + if fillOption.timing == .after { + actionsView.alpha = 0 + } + }) { [weak self] _ in + self?.actionsContainerView?.mask = nil + self?.resetSwipe() + self?.reset() + } + case .reset: + self.hideSwipe(animated: true) + } + } + + let invokeAction = { + action.handler?(action, indexPath) + + if let style = fillOption.autoFulFillmentStyle { + action.fulfill(with: style) + } + } + + animate(duration: 0.3, toOffset: newCenter) { _ in + if fillOption.timing == .after { + invokeAction() + } + } + + if fillOption.timing == .with { + invokeAction() + } + } + + func hideSwipe(animated: Bool, completion: ((Bool) -> Void)? = nil) { + guard var swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return } + guard swipeable.state == .left || swipeable.state == .right else { return } + guard let actionView = swipeable.actionsView else { return } + + swipeable.state = .animatingToCenter + + let targetCenter = self.targetCenter(active: false) + + if animated { + animate(toOffset: targetCenter) { complete in + self.reset() + completion?(complete) + } + } else { + actionsContainerView.center = CGPoint(x: targetCenter, y: actionsContainerView.center.y) + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + reset() + } + + delegate?.swipeController(self, didEndEditingSwipeableFor: actionView.orientation) + } + + func resetSwipe() { + guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return } + + let targetCenter = self.targetCenter(active: false) + + actionsContainerView.center = CGPoint(x: targetCenter, y: actionsContainerView.center.y) + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + } + + func showSwipe(orientation: SwipeActionsOrientation, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { + setSwipeOffset(.greatestFiniteMagnitude * orientation.scale * -1, + animated: animated, + completion: completion) + } + + func setSwipeOffset(_ offset: CGFloat, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { + guard var swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return } + + guard offset != 0 else { + hideSwipe(animated: animated, completion: completion) + return + } + + let orientation: SwipeActionsOrientation = offset > 0 ? .left : .right + let targetState = SwipeState(orientation: orientation) + + if swipeable.state != targetState { + guard showActionsView(for: orientation) else { return } + + scrollView?.hideSwipeables() + + swipeable.state = targetState + } + + let maxOffset = min(swipeable.bounds.width, abs(offset)) * orientation.scale * -1 + let targetCenter = abs(offset) == CGFloat.greatestFiniteMagnitude ? self.targetCenter(active: true) : swipeable.bounds.midX + maxOffset + + if animated { + animate(toOffset: targetCenter) { complete in + completion?(complete) + } + } else { + actionsContainerView.center.x = targetCenter + swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX) + } + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeExpanding.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeExpanding.swift new file mode 100644 index 0000000..44bf558 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeExpanding.swift @@ -0,0 +1,120 @@ +// +// SwipeExpanding.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/** + Adopt the `SwipeExpanding` protocol in objects that implement custom appearance of actions during expansion. + */ +public protocol SwipeExpanding { + + /** + Asks your object for the animation timing parameters. + + - parameter buttons: The expansion action button, which includes expanding action plus the remaining actions in the view. + + - parameter expanding: The new expansion state. + + - parameter otherActionButtons: The other action buttons in the view, not including the action button being expanded. + */ + + func animationTimingParameters(buttons: [UIButton], expanding: Bool) -> SwipeExpansionAnimationTimingParameters + + /** + Tells your object when the expansion state is changing. + + - parameter button: The expansion action button. + + - parameter expanding: The new expansion state. + + - parameter otherActionButtons: The other action buttons in the view, not including the action button being expanded. + */ + func actionButton(_ button: UIButton, didChange expanding: Bool, otherActionButtons: [UIButton]) +} + +/** + Specifies timing information for the overall expansion animation. + */ +public struct SwipeExpansionAnimationTimingParameters { + + /// Returns a `SwipeExpansionAnimationTimingParameters` instance with default animation parameters. + public static var `default`: SwipeExpansionAnimationTimingParameters { return SwipeExpansionAnimationTimingParameters() } + + /// The duration of the expansion animation. + public var duration: Double + + /// The delay before starting the expansion animation. + public var delay: Double + + /** + Contructs a new `SwipeExpansionAnimationTimingParameters` instance. + + - parameter duration: The duration of the animation. + + - parameter delay: The delay before starting the expansion animation. + + - returns: The new `SwipeExpansionAnimationTimingParameters` instance. + */ + public init(duration: Double = 0.6, delay: Double = 0) { + self.duration = duration + self.delay = delay + } +} + +/** + A scale and alpha expansion object drives the custom appearance of the effected actions during expansion. + */ +public struct ScaleAndAlphaExpansion: SwipeExpanding { + + /// Returns a `ScaleAndAlphaExpansion` instance with default expansion options. + public static var `default`: ScaleAndAlphaExpansion { return ScaleAndAlphaExpansion() } + + /// The duration of the animation. + public let duration: Double + + /// The scale factor used during animation. + public let scale: CGFloat + + /// The inter-button delay between animations. + public let interButtonDelay: Double + + /** + Contructs a new `ScaleAndAlphaExpansion` instance. + + - parameter duration: The duration of the animation. + + - parameter scale: The scale factor used during animation. + + - parameter interButtonDelay: The inter-button delay between animations. + + - returns: The new `ScaleAndAlphaExpansion` instance. + */ + public init(duration: Double = 0.15, scale: CGFloat = 0.8, interButtonDelay: Double = 0.1) { + self.duration = duration + self.scale = scale + self.interButtonDelay = interButtonDelay + } + + /// :nodoc: + public func animationTimingParameters(buttons: [UIButton], expanding: Bool) -> SwipeExpansionAnimationTimingParameters { + var timingParameters = SwipeExpansionAnimationTimingParameters.default + timingParameters.delay = expanding ? interButtonDelay : 0 + return timingParameters + } + + /// :nodoc: + public func actionButton(_ button: UIButton, didChange expanding: Bool, otherActionButtons: [UIButton]) { + let buttons = expanding ? otherActionButtons : otherActionButtons.reversed() + + buttons.enumerated().forEach { index, button in + UIView.animate(withDuration: duration, delay: interButtonDelay * Double(expanding ? index : index + 1), options: [], animations: { + button.transform = expanding ? .init(scaleX: self.scale, y: self.scale) : .identity + button.alpha = expanding ? 0.0 : 1.0 + }, completion: nil) + } + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeExpansionStyle.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeExpansionStyle.swift new file mode 100644 index 0000000..40a2012 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeExpansionStyle.swift @@ -0,0 +1,239 @@ +// +// SwipeExpansionStyle.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/// Describes the expansion style. Expansion is the behavior when the cell is swiped past a defined threshold. +public struct SwipeExpansionStyle { + /// The default action performs a selection-type behavior. The cell bounces back to its unopened state upon selection and the row remains in the table/collection view. + public static var selection: SwipeExpansionStyle { return SwipeExpansionStyle(target: .percentage(0.5), + elasticOverscroll: true, + completionAnimation: .bounce) } + + /// The default action performs a destructive behavior. The cell is removed from the table/collection view in an animated fashion. + public static var destructive: SwipeExpansionStyle { return .destructive(automaticallyDelete: true, timing: .with) } + + /// The default action performs a destructive behavior after the fill animation completes. The cell is removed from the table/collection view in an animated fashion. + public static var destructiveAfterFill: SwipeExpansionStyle { return .destructive(automaticallyDelete: true, timing: .after) } + + /// The default action performs a fill behavior. + /// + /// - note: The action handle must call `SwipeAction.fulfill(style:)` to resolve the fill expansion. + public static var fill: SwipeExpansionStyle { return SwipeExpansionStyle(target: .edgeInset(30), + additionalTriggers: [.overscroll(30)], + completionAnimation: .fill(.manual(timing: .after))) } + + /** + Returns a `SwipeExpansionStyle` instance for the default action which peforms destructive behavior with the specified options. + + - parameter automaticallyDelete: Specifies if row/item deletion should be peformed automatically. If `false`, you must call `SwipeAction.fulfill(with style:)` at some point while/after your action handler is invoked to trigger deletion. + + - parameter timing: The timing which specifies when the action handler will be invoked with respect to the fill animation. + + - returns: The new `SwipeExpansionStyle` instance. + */ + public static func destructive(automaticallyDelete: Bool, timing: FillOptions.HandlerInvocationTiming = .with) -> SwipeExpansionStyle { + return SwipeExpansionStyle(target: .edgeInset(30), + additionalTriggers: [.touchThreshold(0.8)], + completionAnimation: .fill(automaticallyDelete ? .automatic(.delete, timing: timing) : .manual(timing: timing))) + } + + /// The relative target expansion threshold. Expansion will occur at the specified value. + public let target: Target + + /// Additional triggers to useful for determining if expansion should occur. + public let additionalTriggers: [Trigger] + + /// Specifies if buttons should expand to fully fill overscroll, or expand at a percentage relative to the overscroll. + public let elasticOverscroll: Bool + + /// Specifies the expansion animation completion style. + public let completionAnimation: CompletionAnimation + + /// Specifies the minimum amount of overscroll required if the configured target is less than the fully exposed action view. + public var minimumTargetOverscroll: CGFloat = 20 + + /// The amount of elasticity applied when dragging past the expansion target. + /// + /// - note: Default value is 0.2. Valid range is from 0.0 for no movement past the expansion target, to 1.0 for unrestricted movement with dragging. + public var targetOverscrollElasticity: CGFloat = 0.2 + + var minimumExpansionTranslation: CGFloat = 8.0 + + /** + Contructs a new `SwipeExpansionStyle` instance. + + - parameter target: The relative target expansion threshold. Expansion will occur at the specified value. + + - parameter additionalTriggers: Additional triggers to useful for determining if expansion should occur. + + - parameter elasticOverscroll: Specifies if buttons should expand to fully fill overscroll, or expand at a percentage relative to the overscroll. + + - parameter completionAnimation: Specifies the expansion animation completion style. + + - returns: The new `SwipeExpansionStyle` instance. + */ + public init(target: Target, additionalTriggers: [Trigger] = [], elasticOverscroll: Bool = false, completionAnimation: CompletionAnimation = .bounce) { + self.target = target + self.additionalTriggers = additionalTriggers + self.elasticOverscroll = elasticOverscroll + self.completionAnimation = completionAnimation + } + + func shouldExpand(view: Swipeable, gesture: UIPanGestureRecognizer, in superview: UIView, within frame: CGRect? = nil) -> Bool { + guard let actionsView = view.actionsView, let gestureView = gesture.view else { return false } + guard abs(gesture.translation(in: gestureView).x) > minimumExpansionTranslation else { return false } + + let xDelta = floor(abs(frame?.minX ?? view.frame.minX)) + if xDelta <= actionsView.preferredWidth { + return false + } else if xDelta > targetOffset(for: view) { + return true + } + + // Use the frame instead of superview as Swipeable may not be full width of superview + let referenceFrame: CGRect = frame != nil ? view.frame : superview.bounds + for trigger in additionalTriggers { + if trigger.isTriggered(view: view, gesture: gesture, in: superview, referenceFrame: referenceFrame) { + return true + } + } + + return false + } + + func targetOffset(for view: Swipeable) -> CGFloat { + return target.offset(for: view, minimumOverscroll: minimumTargetOverscroll) + } +} + +extension SwipeExpansionStyle { + /// Describes the relative target expansion threshold. Expansion will occur at the specified value. + public enum Target { + /// The target is specified by a percentage. + case percentage(CGFloat) + + /// The target is specified by a edge inset. + case edgeInset(CGFloat) + + func offset(for view: Swipeable, minimumOverscroll: CGFloat) -> CGFloat { + guard let actionsView = view.actionsView else { return .greatestFiniteMagnitude } + + let offset: CGFloat = { + switch self { + case .percentage(let value): + return view.frame.width * value + case .edgeInset(let value): + return view.frame.width - value + } + }() + + return max(actionsView.preferredWidth + minimumOverscroll, offset) + } + } + + /// Describes additional triggers to useful for determining if expansion should occur. + public enum Trigger { + /// The trigger is specified by a touch occuring past the supplied percentage in the superview. + case touchThreshold(CGFloat) + + /// The trigger is specified by the distance in points past the fully exposed action view. + case overscroll(CGFloat) + + func isTriggered(view: Swipeable, gesture: UIPanGestureRecognizer, in superview: UIView, referenceFrame: CGRect) -> Bool { + guard let actionsView = view.actionsView else { return false } + + switch self { + case .touchThreshold(let value): + let location = gesture.location(in: superview).x - referenceFrame.origin.x + let locationRatio = (actionsView.orientation == .left ? location : referenceFrame.width - location) / referenceFrame.width + return locationRatio > value + case .overscroll(let value): + return abs(view.frame.minX) > actionsView.preferredWidth + value + } + } + } + + /// Describes the expansion animation completion style. + public enum CompletionAnimation { + /// The expansion will completely fill the item. + case fill(FillOptions) + + /// The expansion will bounce back from the trigger point and hide the action view, resetting the item. + case bounce + } + + /// Specifies the options for the fill completion animation. + public struct FillOptions { + /// Describes when the action handler will be invoked with respect to the fill animation. + public enum HandlerInvocationTiming { + /// The action handler is invoked with the fill animation. + case with + + /// The action handler is invoked after the fill animation completes. + case after + } + + /** + Returns a `FillOptions` instance with automatic fulfillemnt. + + - parameter style: The fulfillment style describing how expansion should be resolved once the action has been fulfilled. + + - parameter timing: The timing which specifies when the action handler will be invoked with respect to the fill animation. + + - returns: The new `FillOptions` instance. + */ + public static func automatic(_ style: ExpansionFulfillmentStyle, timing: HandlerInvocationTiming) -> FillOptions { + return FillOptions(autoFulFillmentStyle: style, timing: timing) + } + + /** + Returns a `FillOptions` instance with manual fulfillemnt. + + - parameter timing: The timing which specifies when the action handler will be invoked with respect to the fill animation. + + - returns: The new `FillOptions` instance. + */ + public static func manual(timing: HandlerInvocationTiming) -> FillOptions { + return FillOptions(autoFulFillmentStyle: nil, timing: timing) + } + + /// The fulfillment style describing how expansion should be resolved once the action has been fulfilled. + public let autoFulFillmentStyle: ExpansionFulfillmentStyle? + + /// The timing which specifies when the action handler will be invoked with respect to the fill animation. + public let timing: HandlerInvocationTiming + } +} + +extension SwipeExpansionStyle.Target: Equatable { + /// :nodoc: + public static func ==(lhs: SwipeExpansionStyle.Target, rhs: SwipeExpansionStyle.Target) -> Bool { + switch (lhs, rhs) { + case (.percentage(let lhs), .percentage(let rhs)): + return lhs == rhs + case (.edgeInset(let lhs), .edgeInset(let rhs)): + return lhs == rhs + default: + return false + } + } +} + +extension SwipeExpansionStyle.CompletionAnimation: Equatable { + /// :nodoc: + public static func ==(lhs: SwipeExpansionStyle.CompletionAnimation, rhs: SwipeExpansionStyle.CompletionAnimation) -> Bool { + switch (lhs, rhs) { + case (.fill, .fill): + return true + case (.bounce, .bounce): + return true + default: + return false + } + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeFeedback.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeFeedback.swift new file mode 100644 index 0000000..6c2be8e --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeFeedback.swift @@ -0,0 +1,56 @@ +// +// SwipeFeedback.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +final class SwipeFeedback { + enum Style { + case light + case medium + case heavy + } + + @available(iOS 10.0.1, *) + private var feedbackGenerator: UIImpactFeedbackGenerator? { + get { + return _feedbackGenerator as? UIImpactFeedbackGenerator + } + set { + _feedbackGenerator = newValue + } + } + + private var _feedbackGenerator: Any? + + init(style: Style) { + if #available(iOS 10.0.1, *) { + switch style { + case .light: + feedbackGenerator = UIImpactFeedbackGenerator(style: .light) + case .medium: + feedbackGenerator = UIImpactFeedbackGenerator(style: .medium) + case .heavy: + feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) + } + } else { + _feedbackGenerator = nil + } + } + + func prepare() { + if #available(iOS 10.0.1, *) { + feedbackGenerator?.prepare() + } + } + + func impactOccurred() { + if #available(iOS 10.0.1, *) { + feedbackGenerator?.impactOccurred() + } + } +} + diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeOptions.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeOptions.swift new file mode 100644 index 0000000..31227b3 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeOptions.swift @@ -0,0 +1,88 @@ +// +// Options.swift +// +// Created by Jeremy Koch. +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/// :nodoc: +public typealias SwipeTableOptions = SwipeOptions + +/// The `SwipeOptions` class provides options for transistion and expansion behavior for swiped cell. +public struct SwipeOptions { + /// The transition style. Transition is the style of how the action buttons are exposed during the swipe. + public var transitionStyle: SwipeTransitionStyle = .border + + /// The expansion style. Expansion is the behavior when the cell is swiped past a defined threshold. + public var expansionStyle: SwipeExpansionStyle? + + /// The object that is notified when expansion changes. + /// + /// - note: If an `expansionDelegate` is not provided, and the expanding action is configured with a clear background, the system automatically uses the default `ScaleAndAlphaExpansion` to show/hide underlying actions. + public var expansionDelegate: SwipeExpanding? + + /// The background color behind the action buttons. + public var backgroundColor: UIColor? + + /// The largest allowable button width. + /// + /// - note: By default, the value is set to the table/collection view divided by the number of action buttons minus some additional padding. If the value is set to 0, then word wrapping will not occur and the buttons will grow as large as needed to fit the entire title/image. + public var maximumButtonWidth: CGFloat? + + /// The smallest allowable button width. + /// + /// - note: By default, the system chooses an appropriate size. + public var minimumButtonWidth: CGFloat? + + /// The vertical alignment mode used for when a button image and title are present. + public var buttonVerticalAlignment: SwipeVerticalAlignment = .centerFirstBaseline + + /// The amount of space, in points, between the border and the button image or title. + public var buttonPadding: CGFloat? + + /// The amount of space, in points, between the button image and the button title. + public var buttonSpacing: CGFloat? + + /// Constructs a new `SwipeOptions` instance with default options. + public init() {} +} + +/// Describes the transition style. Transition is the style of how the action buttons are exposed during the swipe. +public enum SwipeTransitionStyle { + /// The visible action area is equally divide between all action buttons. + case border + + /// The visible action area is dragged, pinned to the cell, with each action button fully sized as it is exposed. + case drag + + /// The visible action area sits behind the cell, pinned to the edge of the table/collection view, and is revealed as the cell is dragged aside. + case reveal +} + +/// Describes which side of the cell that the action buttons will be displayed. +public enum SwipeActionsOrientation: CGFloat { + /// The left side of the cell. + case left = -1 + + /// The right side of the cell. + case right = 1 + + var scale: CGFloat { + return rawValue + } +} + +/// Describes the alignment mode used when action button images and titles are provided. +public enum SwipeVerticalAlignment { + /// All actions will be inspected and the tallest image and first baseline offset of title text will be used to create the alignment rectangle. + /// + /// - note: This mode will ensure the image and first line of each button title and consistently aligned across the swipe view. + case centerFirstBaseline + + /// The action button image height and full title height are used to create the aligment rectange. + /// + /// - note: Buttons with varying number of lines will not be consistently aligned across the swipe view. + case center +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell+Accessibility.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell+Accessibility.swift new file mode 100644 index 0000000..1274c3c --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell+Accessibility.swift @@ -0,0 +1,80 @@ +// +// SwipeTableViewCell+Accessibility.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +extension SwipeTableViewCell { + /// :nodoc: + open override func accessibilityElementCount() -> Int { + guard state != .center else { + return super.accessibilityElementCount() + } + + return 1 + } + + /// :nodoc: + open override func accessibilityElement(at index: Int) -> Any? { + guard state != .center else { + return super.accessibilityElement(at: index) + } + + return actionsView + } + + /// :nodoc: + open override func index(ofAccessibilityElement element: Any) -> Int { + guard state != .center else { + return super.index(ofAccessibilityElement: element) + } + + return element is SwipeActionsView ? 0 : NSNotFound + } +} + +extension SwipeTableViewCell { + /// :nodoc: + open override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { + get { + guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { + return super.accessibilityCustomActions + } + + let leftActions = delegate?.tableView(tableView, editActionsForRowAt: indexPath, for: .left) ?? [] + let rightActions = delegate?.tableView(tableView, editActionsForRowAt: indexPath, for: .right) ?? [] + + let actions = [rightActions.first, leftActions.first].compactMap({ $0 }) + rightActions.dropFirst() + leftActions.dropFirst() + + if actions.count > 0 { + return actions.compactMap({ SwipeAccessibilityCustomAction(action: $0, + indexPath: indexPath, + target: self, + selector: #selector(performAccessibilityCustomAction(accessibilityCustomAction:))) }) + } else { + return super.accessibilityCustomActions + } + } + + set { + super.accessibilityCustomActions = newValue + } + } + + @objc func performAccessibilityCustomAction(accessibilityCustomAction: SwipeAccessibilityCustomAction) -> Bool { + guard let tableView = tableView else { return false } + + let swipeAction = accessibilityCustomAction.action + + swipeAction.handler?(swipeAction, accessibilityCustomAction.indexPath) + + if swipeAction.style == .destructive { + tableView.deleteRows(at: [accessibilityCustomAction.indexPath], with: .fade) + } + + return true + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell+Display.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell+Display.swift new file mode 100644 index 0000000..85133b5 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell+Display.swift @@ -0,0 +1,55 @@ +// +// SwipeTableViewCell+Display.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +extension SwipeTableViewCell { + /// The point at which the origin of the cell is offset from the non-swiped origin. + public var swipeOffset: CGFloat { + set { setSwipeOffset(newValue, animated: false) } + get { return frame.midX - bounds.midX } + } + + /** + Hides the swipe actions and returns the cell to center. + + - parameter animated: Specify `true` to animate the hiding of the swipe actions or `false` to hide it immediately. + + - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. + */ + public func hideSwipe(animated: Bool, completion: ((Bool) -> Void)? = nil) { + swipeController.hideSwipe(animated: animated, completion: completion) + } + + /** + Shows the swipe actions for the specified orientation. + + - parameter orientation: The side of the cell on which to show the swipe actions. + + - parameter animated: Specify `true` to animate the showing of the swipe actions or `false` to show them immediately. + + - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. + */ + public func showSwipe(orientation: SwipeActionsOrientation, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { + setSwipeOffset(.greatestFiniteMagnitude * orientation.scale * -1, + animated: animated, + completion: completion) + } + + /** + The point at which the origin of the cell is offset from the non-swiped origin. + + - parameter offset: A point (expressed in points) that is offset from the non-swiped origin. + + - parameter animated: Specify `true` to animate the transition to the new offset, `false` to make the transition immediate. + + - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. + */ + public func setSwipeOffset(_ offset: CGFloat, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { + swipeController.setSwipeOffset(offset, animated: animated, completion: completion) + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell.swift new file mode 100644 index 0000000..e5d5f09 --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCell.swift @@ -0,0 +1,227 @@ +// +// SwipeTableViewCell.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/** + The `SwipeTableViewCell` class extends `UITableViewCell` and provides more flexible options for cell swiping behavior. + + + The default behavior closely matches the stock Mail.app. If you want to customize the transition style (ie. how the action buttons are exposed), or the expansion style (the behavior when the row is swiped passes a defined threshold), you can return the appropriately configured `SwipeOptions` via the `SwipeTableViewCellDelegate` delegate. + */ +open class SwipeTableViewCell: UITableViewCell { + + /// The object that acts as the delegate of the `SwipeTableViewCell`. + public weak var delegate: SwipeTableViewCellDelegate? + + var state = SwipeState.center + var actionsView: SwipeActionsView? + var scrollView: UIScrollView? { + return tableView + } + var indexPath: IndexPath? { + return tableView?.indexPath(for: self) + } + var panGestureRecognizer: UIGestureRecognizer + { + return swipeController.panGestureRecognizer; + } + + var swipeController: SwipeController! + var isPreviouslySelected = false + + weak var tableView: UITableView? + + /// :nodoc: + open override var frame: CGRect { + set { super.frame = state.isActive ? CGRect(origin: CGPoint(x: frame.minX, y: newValue.minY), size: newValue.size) : newValue } + get { return super.frame } + } + + /// :nodoc: + open override var layoutMargins: UIEdgeInsets { + get { + return frame.origin.x != 0 ? swipeController.originalLayoutMargins : super.layoutMargins + } + set { + super.layoutMargins = newValue + } + } + + /// :nodoc: + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + configure() + } + + /// :nodoc: + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + configure() + } + + deinit { + tableView?.panGestureRecognizer.removeTarget(self, action: nil) + } + + func configure() { + clipsToBounds = false + + swipeController = SwipeController(swipeable: self, actionsContainerView: self) + swipeController.delegate = self + } + + /// :nodoc: + override open func prepareForReuse() { + super.prepareForReuse() + + reset() + resetSelectedState() + } + + /// :nodoc: + override open func didMoveToSuperview() { + super.didMoveToSuperview() + + var view: UIView = self + while let superview = view.superview { + view = superview + + if let tableView = view as? UITableView { + self.tableView = tableView + + swipeController.scrollView = tableView; + + tableView.panGestureRecognizer.removeTarget(self, action: nil) + tableView.panGestureRecognizer.addTarget(self, action: #selector(handleTablePan(gesture:))) + return + } + } + } + + /// :nodoc: + override open func setEditing(_ editing: Bool, animated: Bool) { + super.setEditing(editing, animated: animated) + + if editing { + hideSwipe(animated: false) + } + } + + // Override so we can accept touches anywhere within the cell's minY/maxY. + // This is required to detect touches on the `SwipeActionsView` sitting alongside the + // `SwipeTableCell`. + /// :nodoc: + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + guard let superview = superview else { return false } + + let point = convert(point, to: superview) + + if !UIAccessibility.isVoiceOverRunning { + for cell in tableView?.swipeCells ?? [] { + if (cell.state == .left || cell.state == .right) && !cell.contains(point: point) { + tableView?.hideSwipeCell() + return false + } + } + } + + return contains(point: point) + } + + func contains(point: CGPoint) -> Bool { + return point.y > frame.minY && point.y < frame.maxY + } + + /// :nodoc: + override open func setHighlighted(_ highlighted: Bool, animated: Bool) { + if state == .center { + super.setHighlighted(highlighted, animated: animated) + } + } + + /// :nodoc: + override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return swipeController.gestureRecognizerShouldBegin(gestureRecognizer) + } + + /// :nodoc: + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + swipeController.traitCollectionDidChange(from: previousTraitCollection, to: self.traitCollection) + } + + @objc func handleTablePan(gesture: UIPanGestureRecognizer) { + if gesture.state == .began { + hideSwipe(animated: true) + } + } + + func reset() { + swipeController.reset() + clipsToBounds = false + } + + func resetSelectedState() { + if isPreviouslySelected { + if let tableView = tableView, let indexPath = tableView.indexPath(for: self) { + tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) + } + } + isPreviouslySelected = false + } +} + +extension SwipeTableViewCell: SwipeControllerDelegate { + func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool { + return self.isEditing == false + } + + func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]? { + guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { return nil } + + return delegate?.tableView(tableView, editActionsForRowAt: indexPath, for: orientation) + } + + func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions { + guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { return SwipeOptions() } + + return delegate?.tableView(tableView, editActionsOptionsForRowAt: indexPath, for: orientation) ?? SwipeOptions() + } + + func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect? { + guard let tableView = tableView else { return nil } + + return delegate?.visibleRect(for: tableView) + } + + func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation) { + guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { return } + + // Remove highlight and deselect any selected cells + super.setHighlighted(false, animated: false) + isPreviouslySelected = isSelected + tableView.deselectRow(at: indexPath, animated: false) + + delegate?.tableView(tableView, willBeginEditingRowAt: indexPath, for: orientation) + } + + func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation) { + guard let tableView = tableView, let indexPath = tableView.indexPath(for: self), let actionsView = self.actionsView else { return } + + resetSelectedState() + + delegate?.tableView(tableView, didEndEditingRowAt: indexPath, for: actionsView.orientation) + } + + func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath) { + tableView?.deleteRows(at: [indexPath], with: .none) + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCellDelegate.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCellDelegate.swift new file mode 100644 index 0000000..2cf23ba --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTableViewCellDelegate.swift @@ -0,0 +1,92 @@ +// +// SwipeTableViewCellDelegate.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +/** + The `SwipeTableViewCellDelegate` protocol is adopted by an object that manages the display of action buttons when the cell is swiped. + */ +public protocol SwipeTableViewCellDelegate: class { + + /** + Asks the delegate for the actions to display in response to a swipe in the specified row. + + - parameter tableView: The table view object which owns the cell requesting this information. + + - parameter indexPath: The index path of the row. + + - parameter orientation: The side of the cell requesting this information. + + - returns: An array of `SwipeAction` objects representing the actions for the row. Each action you provide is used to create a button that the user can tap. Returning `nil` will prevent swiping for the supplied orientation. + */ + func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? + + /** + Asks the delegate for the display options to be used while presenting the action buttons. + + - parameter tableView: The table view object which owns the cell requesting this information. + + - parameter indexPath: The index path of the row. + + - parameter orientation: The side of the cell requesting this information. + + - returns: A `SwipeOptions` instance which configures the behavior of the action buttons. + + - note: If not implemented, a default `SwipeOptions` instance is used. + */ + func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions + + /** + Tells the delegate that the table view is about to go into editing mode. + + - parameter tableView: The table view object providing this information. + + - parameter indexPath: The index path of the row. + + - parameter orientation: The side of the cell. + */ + func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) + + /** + Tells the delegate that the table view has left editing mode. + + - parameter tableView: The table view object providing this information. + + - parameter indexPath: The index path of the row. + + - parameter orientation: The side of the cell. + */ + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) + + /** + Asks the delegate for visibile rectangle of the table view, which is used to ensure swipe actions are vertically centered within the visible portion of the cell. + + - parameter tableView: The table view object providing this information. + + - returns: The visible rectangle of the table view. + + - note: The returned rectange should be in the table view's own coordinate system. Returning `nil` will result in no vertical offset to be be calculated. + */ + func visibleRect(for tableView: UITableView) -> CGRect? +} + +/** + Default implementation of `SwipeTableViewCellDelegate` methods + */ +public extension SwipeTableViewCellDelegate { + func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { + return SwipeOptions() + } + + func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) {} + + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) {} + + func visibleRect(for tableView: UITableView) -> CGRect? { + return nil + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/SwipeTransitionLayout.swift b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTransitionLayout.swift new file mode 100644 index 0000000..15baf8e --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/SwipeTransitionLayout.swift @@ -0,0 +1,88 @@ +// +// SwipeTransitionLayout.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +// MARK: - Layout Protocol + +protocol SwipeTransitionLayout { + func container(view: UIView, didChangeVisibleWidthWithContext context: ActionsViewLayoutContext) + func layout(view: UIView, atIndex index: Int, with context: ActionsViewLayoutContext) + func visibleWidthsForViews(with context: ActionsViewLayoutContext) -> [CGFloat] +} + +// MARK: - Layout Context + +struct ActionsViewLayoutContext { + let numberOfActions: Int + let orientation: SwipeActionsOrientation + let contentSize: CGSize + let visibleWidth: CGFloat + let minimumButtonWidth: CGFloat + + init(numberOfActions: Int, orientation: SwipeActionsOrientation, contentSize: CGSize = .zero, visibleWidth: CGFloat = 0, minimumButtonWidth: CGFloat = 0) { + self.numberOfActions = numberOfActions + self.orientation = orientation + self.contentSize = contentSize + self.visibleWidth = visibleWidth + self.minimumButtonWidth = minimumButtonWidth + } + + static func newContext(for actionsView: SwipeActionsView) -> ActionsViewLayoutContext { + return ActionsViewLayoutContext(numberOfActions: actionsView.actions.count, + orientation: actionsView.orientation, + contentSize: actionsView.contentSize, + visibleWidth: actionsView.visibleWidth, + minimumButtonWidth: actionsView.minimumButtonWidth) + } +} + +// MARK: - Supported Layout Implementations + +class BorderTransitionLayout: SwipeTransitionLayout { + func container(view: UIView, didChangeVisibleWidthWithContext context: ActionsViewLayoutContext) { + } + + func layout(view: UIView, atIndex index: Int, with context: ActionsViewLayoutContext) { + let diff = context.visibleWidth - context.contentSize.width + view.frame.origin.x = (CGFloat(index) * context.contentSize.width / CGFloat(context.numberOfActions) + diff) * context.orientation.scale + } + + func visibleWidthsForViews(with context: ActionsViewLayoutContext) -> [CGFloat] { + let diff = context.visibleWidth - context.contentSize.width + let visibleWidth = context.contentSize.width / CGFloat(context.numberOfActions) + diff + + // visible widths are all the same regardless of the action view position + return (0.. [CGFloat] { + return (0.. [CGFloat] { + return super.visibleWidthsForViews(with: context).reversed() + } +} diff --git a/FileExplorer/FileExplorer/SwipeCellKit/Swipeable.swift b/FileExplorer/FileExplorer/SwipeCellKit/Swipeable.swift new file mode 100644 index 0000000..ba0474a --- /dev/null +++ b/FileExplorer/FileExplorer/SwipeCellKit/Swipeable.swift @@ -0,0 +1,41 @@ +// +// Swipeable.swift +// +// Created by Jeremy Koch +// Copyright © 2017 Jeremy Koch. All rights reserved. +// + +import UIKit + +// MARK: - Internal + +protocol Swipeable { + var state: SwipeState { get set } + + var actionsView: SwipeActionsView? { get set } + + var frame: CGRect { get } + + var scrollView: UIScrollView? { get } + + var indexPath: IndexPath? { get } + + var panGestureRecognizer: UIGestureRecognizer { get } +} + +extension SwipeTableViewCell: Swipeable {} +extension SwipeCollectionViewCell: Swipeable {} + +enum SwipeState: Int { + case center = 0 + case left + case right + case dragging + case animatingToCenter + + init(orientation: SwipeActionsOrientation) { + self = orientation == .left ? .left : .right + } + + var isActive: Bool { return self != .center } +} diff --git a/FileExplorer/FileExplorer/UICollectionView+Extension.swift b/FileExplorer/FileExplorer/UICollectionView+Extension.swift index fe1b876..e0cbfc9 100644 --- a/FileExplorer/FileExplorer/UICollectionView+Extension.swift +++ b/FileExplorer/FileExplorer/UICollectionView+Extension.swift @@ -42,23 +42,23 @@ extension UICollectionView { } func registerFooter(ofClass viewClass: AnyClass) { - register(viewClass, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: String(describing: viewClass)) + register(viewClass, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: String(describing: viewClass)) } func dequeueReusableFooter(ofClass cellClass: AnyClass, for indexPath: IndexPath) -> T { - return dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: String(describing: cellClass), for: indexPath) as! T + return dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: String(describing: cellClass), for: indexPath) as! T } func registerHeader(ofClass viewClass: AnyClass) { - register(viewClass, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: String(describing: viewClass)) + register(viewClass, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: viewClass)) } func dequeueReusableHeader(ofClass cellClass: AnyClass, for indexPath: IndexPath) -> T { - return dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: String(describing: cellClass), for: indexPath) as! T + return dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: cellClass), for: indexPath) as! T } func header(for indexPath: IndexPath) -> T? { - return supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: indexPath) as? T + return supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: indexPath) as? T } } diff --git a/FileExplorer/FileExplorer/UITableView+Extension.swift b/FileExplorer/FileExplorer/UITableView+Extension.swift index dbd3e16..d6655ba 100644 --- a/FileExplorer/FileExplorer/UITableView+Extension.swift +++ b/FileExplorer/FileExplorer/UITableView+Extension.swift @@ -38,7 +38,7 @@ extension UITableView { return cell } - func makeCell(with style: UITableViewCellStyle) -> UITableViewCell { + func makeCell(with style: UITableViewCell.CellStyle) -> UITableViewCell { return UITableViewCell(style: style, reuseIdentifier: String(describing: UITableViewCell.self)) } } diff --git a/FileExplorer/FileExplorer/UIView+Extension.swift b/FileExplorer/FileExplorer/UIView+Extension.swift index a54f088..d112a05 100644 --- a/FileExplorer/FileExplorer/UIView+Extension.swift +++ b/FileExplorer/FileExplorer/UIView+Extension.swift @@ -29,18 +29,34 @@ import Foundation extension UIView { @discardableResult func pinToBottom(of view: UIView) -> NSLayoutConstraint { - let constraint = bottomAnchor.constraint(equalTo: view.bottomAnchor) - constraint.isActive = true - leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - heightAnchor.constraint(equalToConstant: self.bounds.height).isActive = true - return constraint + if #available(iOS 11.0, *) { + let guide = view.safeAreaLayoutGuide + let constraint = bottomAnchor.constraint(equalTo: guide.bottomAnchor) + constraint.isActive = true + leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + heightAnchor.constraint(equalToConstant: self.bounds.height).isActive = true + return constraint + } else { + // Fallback on earlier versions + let constraint = bottomAnchor.constraint(equalTo: view.bottomAnchor) + constraint.isActive = true + leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + heightAnchor.constraint(equalToConstant: self.bounds.height).isActive = true + return constraint + } + } func edges(equalTo view: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) { leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left).isActive = true trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: insets.right).isActive = true topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top).isActive = true - bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: insets.bottom).isActive = true + if #available(iOS 11.0, *) { + bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: insets.bottom).isActive = true} + else{ + bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: insets.bottom).isActive = true + } } } diff --git a/FileExplorer/FileExplorer/UIViewController+Extension.swift b/FileExplorer/FileExplorer/UIViewController+Extension.swift index 3eb4f1f..de33f29 100644 --- a/FileExplorer/FileExplorer/UIViewController+Extension.swift +++ b/FileExplorer/FileExplorer/UIViewController+Extension.swift @@ -39,7 +39,7 @@ extension UIViewController { func showLoadingIndicator() { guard self.activityIndicatorView == nil else { return } - let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge) + let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) activityIndicatorView.color = .gray activityIndicatorView.hidesWhenStopped = true activityIndicatorView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY) @@ -77,8 +77,8 @@ extension UIViewController { return activeNavigationItem?.title } set(newValue) { - navigationItem.title = newValue - activeNavigationItem?.title = newValue + //navigationItem.title = newValue + //activeNavigationItem?.title = newValue } } @@ -96,9 +96,9 @@ extension UIViewController { extension UIViewController { func addContentChildViewController(_ content: UIViewController, insets: UIEdgeInsets = UIEdgeInsets.zero) { view.addSubview(content.view) - addChildViewController(content) - content.view.frame = UIEdgeInsetsInsetRect(view.bounds, insets) + addChild(content) + content.view.frame = view.bounds.inset(by: insets) content.view.autoresizingMask = [.flexibleHeight, .flexibleWidth] - content.didMove(toParentViewController: self) + content.didMove(toParent: self) } } diff --git a/FileExplorer/FileExplorer/URL+Extension.swift b/FileExplorer/FileExplorer/URL+Extension.swift index cf66ab4..aed6d9d 100644 --- a/FileExplorer/FileExplorer/URL+Extension.swift +++ b/FileExplorer/FileExplorer/URL+Extension.swift @@ -43,6 +43,6 @@ extension URL { } func makeStandarizedFirstCharacterOfLastPathComponent() -> Character? { - return makeStandarizedLastPathComponent().localizedUppercase.characters.first + return makeStandarizedLastPathComponent().localizedUppercase.first } } diff --git a/FileExplorer/FileExplorer/UknownFileTypeViewController.swift b/FileExplorer/FileExplorer/UknownFileTypeViewController.swift index e7fbd4f..ce43bb7 100644 --- a/FileExplorer/FileExplorer/UknownFileTypeViewController.swift +++ b/FileExplorer/FileExplorer/UknownFileTypeViewController.swift @@ -41,7 +41,7 @@ final class UknownFileTypeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.white + view.backgroundColor = UIColor.dynamicColor(light: .white, dark: .black)//UIColor.white setUpImageView() setUpTextLabel() } diff --git a/FileExplorer/FileExplorer/WebViewController.swift b/FileExplorer/FileExplorer/WebViewController.swift index 7ac6ae6..ceaee13 100644 --- a/FileExplorer/FileExplorer/WebViewController.swift +++ b/FileExplorer/FileExplorer/WebViewController.swift @@ -25,10 +25,11 @@ import Foundation import WebKit +import GoogleMobileAds -final class WebViewController: UIViewController { +class WebViewController: UIViewController, WKNavigationDelegate { let url: URL - + var interstitial: GADInterstitial! init(url: URL) { self.url = url super.init(nibName: nil, bundle: nil) @@ -46,5 +47,46 @@ final class WebViewController: UIViewController { view.addSubview(webView) webView.edges(equalTo: view) webView.loadFileURL(url, allowingReadAccessTo: url) + webView.navigationDelegate = self + //interstitial = createAndLoadInterstitial() + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + //callAds() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = "" + } + + override func viewWillDisappear(_ animated: Bool) { + + } + + + func callAds(){ + if interstitial.isReady { + interstitial.present(fromRootViewController: self) + } else { + print("Ad wasn't ready") + } + } +} + +extension WebViewController: GADInterstitialDelegate{ + public func interstitialDidReceiveAd(_ ad: GADInterstitial) { + //self.interstitial.present(fromRootViewController: self) + } + func createAndLoadInterstitial() -> GADInterstitial { + let interstitial = GADInterstitial(adUnitID: ) + interstitial.delegate = self + interstitial.load(GADRequest()) + return interstitial + } + + public func interstitialDidDismissScreen(_ ad: GADInterstitial) { + interstitial = createAndLoadInterstitial() + //navigationController?.popViewController(animated: true) } }