import UIKit import AsyncDisplayKit final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate { private let theme: ActionSheetControllerTheme private let centerDimView: UIImageView private let topDimView: UIView private let bottomDimView: UIView let trailingDimView: UIView private let clippingNode: ASDisplayNode private let backgroundEffectView: UIVisualEffectView private let scrollNode: ASScrollNode private var itemNodes: [ActionSheetItemNode] = [] private var leadingVisibleNodeCount: CGFloat = 100.0 init(theme: ActionSheetControllerTheme) { self.theme = theme self.centerDimView = UIImageView() self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: self.theme.dimColor) self.topDimView = UIView() self.topDimView.backgroundColor = self.theme.dimColor self.topDimView.isUserInteractionEnabled = false self.bottomDimView = UIView() self.bottomDimView.backgroundColor = self.theme.dimColor self.bottomDimView.isUserInteractionEnabled = false self.trailingDimView = UIView() self.trailingDimView.backgroundColor = self.theme.dimColor self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.clippingNode.cornerRadius = 16.0 self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark)) self.scrollNode = ASScrollNode() self.scrollNode.canCancelAllTouchesInViews = true if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false super.init() self.view.addSubview(self.centerDimView) self.view.addSubview(self.topDimView) self.view.addSubview(self.bottomDimView) self.view.addSubview(self.trailingDimView) self.scrollNode.view.delegate = self self.clippingNode.view.addSubview(self.backgroundEffectView) self.clippingNode.addSubnode(self.scrollNode) self.addSubnode(self.clippingNode) } func updateItemNodes(_ nodes: [ActionSheetItemNode], leadingVisibleNodeCount: CGFloat = 1000.0) { for node in self.itemNodes { if !nodes.contains(where: { $0 === node }) { node.removeFromSupernode() } } for node in nodes { if !self.itemNodes.contains(where: { $0 === node }) { self.scrollNode.addSubnode(node) } } self.itemNodes = nodes self.leadingVisibleNodeCount = leadingVisibleNodeCount self.invalidateCalculatedLayout() } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { var itemNodesHeight: CGFloat = 0.0 var leadingVisibleNodeSize: CGFloat = 0.0 var i = 0 for node in self.itemNodes { if CGFloat(0.0).isLess(than: itemNodesHeight), node.hasSeparator { itemNodesHeight += UIScreenPixel } let size = node.measure(constrainedSize) itemNodesHeight += size.height if ceil(CGFloat(i)).isLessThanOrEqualTo(leadingVisibleNodeCount) { if CGFloat(0.0).isLess(than: leadingVisibleNodeSize), node.hasSeparator { leadingVisibleNodeSize += UIScreenPixel } let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i)) leadingVisibleNodeSize += size.height * factor } i += 1 } return CGSize(width: constrainedSize.width, height: min(floorToScreenPixels(itemNodesHeight), constrainedSize.height)) } func updateLayout(transition: ContainedViewLayoutTransition) { let scrollViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize) var updateOffset = false if !self.scrollNode.frame.equalTo(scrollViewFrame) { self.scrollNode.frame = scrollViewFrame updateOffset = true } let backgroundEffectViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize) if !self.backgroundEffectView.frame.equalTo(backgroundEffectViewFrame) { transition.updateFrame(view: self.backgroundEffectView, frame: backgroundEffectViewFrame) } var itemNodesHeight: CGFloat = 0.0 var leadingVisibleNodeSize: CGFloat = 0.0 var i = 0 var previousHadSeparator = false for node in self.itemNodes { if CGFloat(0.0).isLess(than: itemNodesHeight), previousHadSeparator { itemNodesHeight += UIScreenPixel } previousHadSeparator = node.hasSeparator node.frame = CGRect(origin: CGPoint(x: 0.0, y: itemNodesHeight), size: node.calculatedSize) itemNodesHeight += node.calculatedSize.height if CGFloat(i).isLessThanOrEqualTo(leadingVisibleNodeCount) { if CGFloat(0.0).isLess(than: leadingVisibleNodeSize), node.hasSeparator { leadingVisibleNodeSize += UIScreenPixel } let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i)) leadingVisibleNodeSize += node.calculatedSize.height * factor } i += 1 } let scrollViewContentSize = CGSize(width: self.calculatedSize.width, height: itemNodesHeight) if !self.scrollNode.view.contentSize.equalTo(scrollViewContentSize) { self.scrollNode.view.contentSize = scrollViewContentSize } let scrollViewContentInsets = UIEdgeInsets(top: max(0.0, self.calculatedSize.height - leadingVisibleNodeSize), left: 0.0, bottom: 0.0, right: 0.0) if self.scrollNode.view.contentInset != scrollViewContentInsets { self.scrollNode.view.contentInset = scrollViewContentInsets } if updateOffset { self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -scrollViewContentInsets.top) } self.updateOverscroll(transition: transition) } private func currentVerticalOverscroll() -> CGFloat { var verticalOverscroll: CGFloat = 0.0 if scrollNode.view.contentOffset.y < 0.0 { verticalOverscroll = scrollNode.view.contentOffset.y } else if scrollNode.view.contentOffset.y > scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height { verticalOverscroll = scrollNode.view.contentOffset.y - (scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height) } return verticalOverscroll } private func currentRealVerticalOverscroll() -> CGFloat { var verticalOverscroll: CGFloat = 0.0 if scrollNode.view.contentOffset.y < 0.0 { verticalOverscroll = scrollNode.view.contentOffset.y } else if scrollNode.view.contentOffset.y > scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height { verticalOverscroll = scrollNode.view.contentOffset.y - (scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height) } return verticalOverscroll } private func updateOverscroll(transition: ContainedViewLayoutTransition) { let verticalOverscroll = self.currentVerticalOverscroll() self.clippingNode.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, min(0.0, verticalOverscroll), 0.0) let clippingNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -verticalOverscroll)), size: CGSize(width: self.calculatedSize.width, height: self.calculatedSize.height - abs(verticalOverscroll))) if !self.clippingNode.frame.equalTo(clippingNodeFrame) { transition.updateFrame(node: self.clippingNode, frame: clippingNodeFrame) transition.updateFrame(view: self.centerDimView, frame: clippingNodeFrame) transition.updateFrame(view: self.topDimView, frame: CGRect(x: 0.0, y: 0.0, width: clippingNodeFrame.size.width, height: max(0.0, clippingNodeFrame.minY))) transition.updateFrame(view: self.bottomDimView, frame: CGRect(x: 0.0, y: clippingNodeFrame.maxY, width: clippingNodeFrame.size.width, height: max(0.0, self.bounds.size.height - clippingNodeFrame.maxY))) } } func scrollViewDidScroll(_ scrollView: UIScrollView) { self.updateOverscroll(transition: .immediate) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.clippingNode.frame.contains(point) { return super.hitTest(point, with: event) } else { return nil } } func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) { for node in [self.centerDimView, self.topDimView, self.bottomDimView] { node.layer.animateAlpha(from: from, to: to, duration: duration) } } func itemNode(at index: Int) -> ActionSheetItemNode { return self.itemNodes[index] } }