Untitled
unknown
swift
a year ago
20 kB
8
Indexable
final class ContextMenuInteraction: NSObject, UIInteraction { private var controller: _ContextMenuController? weak var delegate: (any ContextMenuInteractionDelegate)? private(set) weak var view: UIView? func willMove(to view: UIView?) { guard view == nil else { return } controller = nil delegate = nil self.view = nil } func didMove(to view: UIView?) { self.view = view guard let view, let delegate else { return } let configuration = delegate.contextMenuInteraction(self, configurationForMenuAtLocation: .zero) let menu = configuration.actionProvider() self.controller = _ContextMenuController(menu, for: view) } init(delegate: any ContextMenuInteractionDelegate) { self.delegate = delegate } } protocol ContextMenuInteractionDelegate: AnyObject { func contextMenuInteraction(_ interaction: ContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> ContextMenuConfiguration } struct ContextMenuConfiguration { typealias ActionProvider = () -> UIMenu fileprivate let actionProvider: ActionProvider init(actionProvider: @escaping ActionProvider) { self.actionProvider = actionProvider } } fileprivate final class _ContextMenuController { private unowned let targetView: UIView private let menu: UIMenu private var containerView: _ContextMenuContainerView = { $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(_ContextMenuContainerView(frame: .zero)) init(_ menu: UIMenu, for targetView: UIView) { self.menu = menu self.targetView = targetView guard let window = targetView.window else { return } targetView.alpha = 0.0 containerView.transitionView.targetSnapshotView = targetView.snapshotView(afterScreenUpdates: false) containerView.transitionView.menuView.menu = menu window.addSubview(containerView) NSLayoutConstraint.activate([ containerView.topAnchor.constraint(equalTo: window.topAnchor), containerView.leadingAnchor.constraint(equalTo: window.leadingAnchor), window.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), window.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), ]) } } fileprivate final class _ContextMenuContainerView: UIView { // [blurFilter setValue:@10 forKey:@"inputRadius"]; private let backdropEffectView: UIVisualEffectView = { $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(UIVisualEffectView(effect: nil)) let transitionView: _ContextMenuTransitionView = { $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(_ContextMenuTransitionView(frame: .zero)) override func didMoveToWindow() { super.didMoveToWindow() transitionView.animateIn() } override init(frame: CGRect) { super.init(frame: frame) addSubview(backdropEffectView) addSubview(transitionView) NSLayoutConstraint.activate([ backdropEffectView.topAnchor.constraint(equalTo: topAnchor), backdropEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), trailingAnchor.constraint(equalTo: backdropEffectView.trailingAnchor), bottomAnchor.constraint(equalTo: backdropEffectView.bottomAnchor), transitionView.topAnchor.constraint(equalTo: topAnchor), transitionView.leadingAnchor.constraint(equalTo: leadingAnchor), trailingAnchor.constraint(equalTo: transitionView.trailingAnchor), bottomAnchor.constraint(equalTo: transitionView.bottomAnchor), ]) } required init?(coder: NSCoder) { fatalError() } } fileprivate final class _ContextMenuTransitionView: UIView { var targetSnapshotView: UIView? let menuView: _ContextMenuView = { $0.translatesAutoresizingMaskIntoConstraints = false $0.alpha = 0.0 return $0 }(_ContextMenuView(frame: .zero)) func animateIn() { guard let targetSnapshotView else { return } //targetSnapshotView.translatesAutoresizingMaskIntoConstraints = false addSubview(targetSnapshotView) addSubview(menuView) NSLayoutConstraint.activate([ menuView.centerXAnchor.constraint(equalTo: centerXAnchor), menuView.centerYAnchor.constraint(equalTo: centerYAnchor), menuView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6), ]) menuView.transform = CGAffineTransform(scaleX: 0.2, y: 0.2) let animator = UIViewPropertyAnimator(duration: 0.0, timingParameters: UISpringTimingParameters(dampingRatio: 0.7, frequencyResponse: 0.4)) animator.addAnimations { [self] in menuView.alpha = 1.0 menuView.transform = .identity } animator.startAnimation() } func animateOut() { } } fileprivate final class _ContextMenuView: UIView { var menu: UIMenu? { didSet { listView.displayedMenu = menu } } private let listView: _ContextMenuListView = { $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(_ContextMenuListView(frame: .zero)) override init(frame: CGRect) { super.init(frame: frame) addSubview(listView) NSLayoutConstraint.activate([ listView.topAnchor.constraint(equalTo: topAnchor), listView.leadingAnchor.constraint(equalTo: leadingAnchor), trailingAnchor.constraint(equalTo: listView.trailingAnchor), bottomAnchor.constraint(equalTo: listView.bottomAnchor), ]) } required init?(coder: NSCoder) { fatalError() } } fileprivate final class _ContextMenuListView: UIView { var displayedMenu: UIMenu? { didSet { var snapshot = NSDiffableDataSourceSnapshot<Int, ObjectIdentifier>() if let displayedMenu { snapshot.appendSections([0]) snapshot.appendItems(displayedMenu.children.lazy.map { ObjectIdentifier($0) }) } collectionDataSource.applySnapshotUsingReloadData(snapshot) } } private let cutoutShadowView: _ContextMenuCutoutShadowView = { $0.translatesAutoresizingMaskIntoConstraints = false let shadow = NSShadow() shadow.shadowOffset = .zero shadow.shadowBlurRadius = 16.0 shadow.shadowColor = UIColor.black.withAlphaComponent(0.3) $0.shadow = shadow $0.cornerRadius = 13.0 return $0 }(_ContextMenuCutoutShadowView(frame: .zero)) private let contentView: UIView = { $0.translatesAutoresizingMaskIntoConstraints = false $0.clipsToBounds = true $0.layer.cornerCurve = .continuous $0.layer.cornerRadius = 13.0 $0.layer.allowsEdgeAntialiasing = true $0.layer.allowsGroupOpacity = true return $0 }(UIView(frame: .zero)) private let backdropEffectView: UIVisualEffectView = { $0.translatesAutoresizingMaskIntoConstraints = false $0.isUserInteractionEnabled = false return $0 }(UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))) private let collectionView: _ContextMenuListCollectionView = { $0.translatesAutoresizingMaskIntoConstraints = false return $0 }(_ContextMenuListCollectionView(frame: .zero)) private lazy var collectionDataSource = makeCollectionDatSource() override init(frame: CGRect) { super.init(frame: frame) addSubview(cutoutShadowView) addSubview(contentView) contentView.addSubview(backdropEffectView) contentView.addSubview(collectionView) NSLayoutConstraint.activate([ cutoutShadowView.widthAnchor.constraint(equalTo: widthAnchor).priority(.defaultHigh), cutoutShadowView.heightAnchor.constraint(equalTo: heightAnchor).priority(.defaultHigh), cutoutShadowView.centerXAnchor.constraint(equalTo: centerXAnchor), cutoutShadowView.centerYAnchor.constraint(equalTo: centerYAnchor), contentView.topAnchor.constraint(equalTo: topAnchor), contentView.leadingAnchor.constraint(equalTo: leadingAnchor), trailingAnchor.constraint(equalTo: contentView.trailingAnchor), bottomAnchor.constraint(equalTo: contentView.bottomAnchor), backdropEffectView.topAnchor.constraint(equalTo: contentView.topAnchor), backdropEffectView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: backdropEffectView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: backdropEffectView.bottomAnchor), collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor), ]) } required init?(coder: NSCoder) { fatalError() } private func makeCollectionDatSource() -> UICollectionViewDiffableDataSource<Int, ObjectIdentifier> { let cellRegistration = makeCollectionCellRegistration() let supplementaryRegistration = makeCollectionSeparatorSupplementaryRegistration() return .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } supplementaryViewProvider: { collectionView, elementKind, indexPath in collectionView.dequeueConfiguredReusableSupplementary(using: supplementaryRegistration, for: indexPath) } } private func makeCollectionCellRegistration() -> UICollectionView.CellRegistration<_ContextMenuCollectionViewCell, ObjectIdentifier> { return .init { [unowned self] cell, indexPath, itemIdentifier in guard let menuElement = displayedMenu?.children.first(where: { ObjectIdentifier($0) == itemIdentifier }) else { preconditionFailure() } cell.configureWith(title: menuElement.title, image: menuElement.image) } } private func makeCollectionSeparatorSupplementaryRegistration() -> UICollectionView.SupplementaryRegistration<_ContextMenuSeparatorCollectionReusableView> { return .init(elementKind: _ContextMenuListCollectionView.elementKindItemSeparator) { [unowned self] supplementaryView, elementKind, indexPath in supplementaryView.isHidden = indexPath.item == displayedMenu?.children.index(displayedMenu!.children.endIndex, offsetBy: -1, limitedBy: 0) ? true : false } } } fileprivate final class _ContextMenuCutoutShadowView: UIImageView { var shadow = NSShadow() { didSet { deferredUpdateShadowImage() } } var cornerRadius: CGFloat = 0.0 { didSet { deferredUpdateShadowImage() } } override var intrinsicContentSize: CGSize { CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) } private var oldBounds: CGRect = .zero override func layoutSubviews() { super.layoutSubviews() if oldBounds != bounds { oldBounds = bounds updateShadowImage() } } override init(frame: CGRect) { super.init(frame: frame) isUserInteractionEnabled = false contentMode = .center } required init?(coder: NSCoder) { fatalError() } private func deferredUpdateShadowImage() { RunLoop.main.cancelPerform(#selector(updateShadowImage), target: self, argument: nil) RunLoop.main.perform(#selector(updateShadowImage), target: self, argument: nil, order: 0, modes: [.common]) } @objc private func updateShadowImage() { image = UIGraphicsImageRenderer( size: CGSize(width: bounds.width + shadow.shadowBlurRadius * 2.0, height: bounds.height + shadow.shadowBlurRadius * 2.0), format: .preferred() ).image { context in let cgContext = context.cgContext let roundedRect = CGRect(x: shadow.shadowBlurRadius, y: shadow.shadowBlurRadius, width: bounds.width, height: bounds.height) let cgShadowPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius).cgPath cgContext.addRect(cgContext.boundingBoxOfClipPath) cgContext.addPath(cgShadowPath) cgContext.clip(using: .evenOdd) let color = (shadow.shadowColor as! UIColor).cgColor cgContext.setStrokeColor(color) cgContext.addPath(cgShadowPath) cgContext.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: color) cgContext.fillPath() } } } fileprivate final class _ContextMenuListCollectionView: UICollectionView { let settedCollectionViewLayout = _ContextMenuListCollectionView.makeCollectionViewLayout() override var contentSize: CGSize { didSet { invalidateIntrinsicContentSize() } } override var intrinsicContentSize: CGSize { CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height == 0.0 ? 44.0 : contentSize.height) } init(frame: CGRect) { super.init(frame: frame, collectionViewLayout: settedCollectionViewLayout) backgroundColor = .clear delaysContentTouches = true isScrollEnabled = false showsHorizontalScrollIndicator = false showsVerticalScrollIndicator = false } required init?(coder: NSCoder) { fatalError() } private static func makeCollectionViewLayout() -> UICollectionViewCompositionalLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let supplementaryItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(1.0 / UIScreen.main.scale)) let supplementaryItem = NSCollectionLayoutSupplementaryItem(layoutSize: supplementaryItemSize, elementKind: elementKindItemSeparator, containerAnchor: NSCollectionLayoutAnchor(edges: .bottom, absoluteOffset: CGPoint(x: 0.0, y: 1.0 / UIScreen.main.scale))) let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [supplementaryItem]) item.contentInsets = NSDirectionalEdgeInsets(top: 0.0, leading: 0.0, bottom: 1.0 / UIScreen.main.scale, trailing: 0.0) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44.0)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, repeatingSubitem: item, count: 1) let section = NSCollectionLayoutSection(group: group) return UICollectionViewCompositionalLayout(section: section) } static let elementKindItemSeparator = "CollectionElementKindItemSeparator" } fileprivate final class _ContextMenuCollectionViewCell: UICollectionViewCell { private let highlightVisualEffectView = UIVisualEffectView(effect: nil) private let titleLabel: UILabel = { $0.translatesAutoresizingMaskIntoConstraints = false $0.textColor = .label $0.font = .preferredFont(forTextStyle: .body) $0.numberOfLines = 0 return $0 }(UILabel(frame: .zero)) private let imageView: UIImageView = { $0.isUserInteractionEnabled = false $0.translatesAutoresizingMaskIntoConstraints = false $0.tintColor = .label return $0 }(UIImageView(image: nil)) override var isHighlighted: Bool { didSet { highlightVisualEffectView.contentView.backgroundColor = isHighlighted ? .black : nil highlightVisualEffectView.effect = isHighlighted ? UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemMaterial), style: .tertiaryFill) : nil } } func configureWith(title: String, image: UIImage?) { titleLabel.text = title imageView.image = image } override init(frame: CGRect) { super.init(frame: frame) backgroundView = highlightVisualEffectView contentView.addSubview(titleLabel) contentView.addSubview(imageView) NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12.0), titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0), contentView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12.0), imageView.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 16.0), imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), imageView.heightAnchor.constraint(equalToConstant: 24.0), contentView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 16.0), imageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } required init?(coder: NSCoder) { fatalError() } } fileprivate final class _ContextMenuSeparatorCollectionReusableView: UICollectionReusableView { private let contentVisualEffectView: UIVisualEffectView = { $0.translatesAutoresizingMaskIntoConstraints = false $0.isUserInteractionEnabled = false $0.contentView.backgroundColor = .black return $0 }(UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemMaterial), style: .separator))) override init(frame: CGRect) { super.init(frame: frame) addSubview(contentVisualEffectView) NSLayoutConstraint.activate([ contentVisualEffectView.topAnchor.constraint(equalTo: topAnchor), contentVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), trailingAnchor.constraint(equalTo: contentVisualEffectView.trailingAnchor), bottomAnchor.constraint(equalTo: contentVisualEffectView.bottomAnchor), ]) } required init?(coder: NSCoder) { fatalError() } } fileprivate extension UICollectionViewDiffableDataSource { convenience init(collectionView: UICollectionView, cellProvider: @escaping CellProvider, supplementaryViewProvider: @escaping SupplementaryViewProvider) { self.init(collectionView: collectionView, cellProvider: cellProvider) self.supplementaryViewProvider = supplementaryViewProvider } } fileprivate extension NSLayoutConstraint { func priority(_ newPriority: UILayoutPriority) -> NSLayoutConstraint { priority = newPriority return self } } fileprivate extension UISpringTimingParameters { convenience init(dampingRatio: CGFloat, frequencyResponse: CGFloat) { precondition(dampingRatio >= 0) precondition(frequencyResponse > 0) let mass = 1 as CGFloat let stiffness = pow(2 * .pi / frequencyResponse, 2.0) * mass let damping = 4.0 * .pi * dampingRatio * mass / frequencyResponse self.init(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: .zero) } } fileprivate class LayerView<LayerType: CALayer>: UIView { override final class var layerClass: AnyClass { LayerType.self } final var settedLayer: LayerType { layer as! LayerType } }
Editor is loading...
Leave a Comment