mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1105 lines
51 KiB
Swift
1105 lines
51 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import ComponentFlow
|
|
import AccountContext
|
|
import UIKitRuntimeUtils
|
|
|
|
private let minimizedNavigationHeight: CGFloat = 44.0
|
|
private let minimizedTopMargin: CGFloat = 3.0
|
|
|
|
final class ScrollViewImpl: UIScrollView {
|
|
var passthrough = false
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if result === self && self.passthrough {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
|
final class Item {
|
|
let id: AnyHashable
|
|
let controller: MinimizableController
|
|
let beforeMaximize: (NavigationController, @escaping () -> Void) -> Void
|
|
|
|
init(id: AnyHashable, controller: MinimizableController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void) {
|
|
self.id = id
|
|
self.controller = controller
|
|
self.beforeMaximize = beforeMaximize
|
|
}
|
|
}
|
|
|
|
final class ItemNode: ASDisplayNode {
|
|
var theme: PresentationTheme {
|
|
didSet {
|
|
if self.theme !== oldValue {
|
|
self.headerNode.theme = NavigationControllerTheme(presentationTheme: self.theme)
|
|
}
|
|
}
|
|
}
|
|
|
|
var isReady = false
|
|
|
|
let item: Item
|
|
private let containerNode: ASDisplayNode
|
|
private let headerNode: MinimizedHeaderNode
|
|
private let dimCoverNode: ASDisplayNode
|
|
private let shadowNode: ASImageNode
|
|
|
|
private var controllerView: UIView?
|
|
fileprivate let snapshotContainerView = UIView()
|
|
fileprivate var snapshotView: UIView?
|
|
fileprivate var blurredSnapshotView: UIView?
|
|
|
|
var tapped: (() -> Void)?
|
|
var highlighted: ((Bool) -> Void)?
|
|
var closeTapped: (() -> Void)?
|
|
|
|
var isCovered: Bool = false {
|
|
didSet {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
|
transition.updateAlpha(node: self.dimCoverNode, alpha: self.isCovered ? 0.25 : 0.0)
|
|
}
|
|
}
|
|
|
|
private var validLayout: (CGSize, UIEdgeInsets, Bool)?
|
|
|
|
init(theme: PresentationTheme, strings: PresentationStrings, item: Item) {
|
|
self.theme = theme
|
|
self.item = item
|
|
|
|
self.shadowNode = ASImageNode()
|
|
self.shadowNode.clipsToBounds = true
|
|
self.shadowNode.cornerRadius = 10.0
|
|
self.shadowNode.displaysAsynchronously = false
|
|
self.shadowNode.displayWithoutProcessing = true
|
|
self.shadowNode.contentMode = .scaleToFill
|
|
self.shadowNode.isUserInteractionEnabled = false
|
|
|
|
self.containerNode = ASDisplayNode()
|
|
self.containerNode.isUserInteractionEnabled = false
|
|
self.containerNode.cornerRadius = 10.0
|
|
|
|
self.headerNode = MinimizedHeaderNode(theme: NavigationControllerTheme(presentationTheme: theme), strings: strings)
|
|
self.headerNode.layer.allowsGroupOpacity = true
|
|
|
|
self.dimCoverNode = ASDisplayNode()
|
|
self.dimCoverNode.alpha = 0.0
|
|
self.dimCoverNode.backgroundColor = UIColor.black
|
|
self.dimCoverNode.isUserInteractionEnabled = false
|
|
|
|
self.snapshotContainerView.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.clipsToBounds = true
|
|
self.cornerRadius = 10.0
|
|
applySmoothRoundedCorners(self.layer)
|
|
applySmoothRoundedCorners(self.containerNode.layer)
|
|
|
|
self.shadowNode.image = shadowImage
|
|
|
|
self.addSubnode(self.containerNode)
|
|
self.controllerView = self.item.controller.displayNode.view
|
|
self.containerNode.view.addSubview(self.item.controller.displayNode.view)
|
|
|
|
Queue.mainQueue().after(0.45) {
|
|
self.isReady = true
|
|
if !self.isDismissed, let snapshotView = self.item.controller.makeContentSnapshotView() {
|
|
self.containerNode.view.addSubview(self.snapshotContainerView)
|
|
self.snapshotView = snapshotView
|
|
self.controllerView?.removeFromSuperview()
|
|
self.controllerView = nil
|
|
self.snapshotContainerView.addSubview(snapshotView)
|
|
self.requestLayout(transition: .immediate)
|
|
}
|
|
}
|
|
|
|
self.addSubnode(self.headerNode)
|
|
self.addSubnode(self.dimCoverNode)
|
|
self.addSubnode(self.shadowNode)
|
|
|
|
self.headerNode.requestClose = { [weak self] in
|
|
if let self {
|
|
self.closeTapped?()
|
|
}
|
|
}
|
|
|
|
self.headerNode.requestMaximize = { [weak self] in
|
|
if let self {
|
|
self.tapped?()
|
|
}
|
|
}
|
|
|
|
self.headerNode.controllers = [item.controller]
|
|
}
|
|
|
|
func setTitleControllers(_ controllers: [MinimizableController]?) {
|
|
self.headerNode.controllers = controllers ?? [self.item.controller]
|
|
}
|
|
|
|
func animateIn() {
|
|
self.headerNode.alpha = 0.0
|
|
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
|
alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0)
|
|
}
|
|
|
|
private var isDismissed = false
|
|
func animateOut() {
|
|
self.isDismissed = true
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
|
transition.updateAlpha(node: self.headerNode, alpha: 0.0)
|
|
transition.updateAlpha(node: self.shadowNode, alpha: 0.0)
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
recognizer.tapActionAtPoint = { point in
|
|
return .waitForSingleTap
|
|
}
|
|
recognizer.highlight = { [weak self] point in
|
|
if let point = point, point.x > 280.0 {
|
|
self?.highlighted?(true)
|
|
} else {
|
|
self?.highlighted?(false)
|
|
}
|
|
}
|
|
self.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
guard let (_, insets, _) = self.validLayout, self.isReady else {
|
|
return
|
|
}
|
|
switch recognizer.state {
|
|
case .ended:
|
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
switch gesture {
|
|
case .tap:
|
|
if location.x < insets.left + minimizedNavigationHeight && location.y < minimizedNavigationHeight {
|
|
self.closeTapped?()
|
|
} else {
|
|
self.tapped?()
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func requestLayout(transition: ContainedViewLayoutTransition) {
|
|
guard let (size, insets, isExpanded) = self.validLayout else {
|
|
return
|
|
}
|
|
self.updateLayout(size: size, insets: insets, isExpanded: isExpanded, transition: transition)
|
|
}
|
|
|
|
func updateLayout(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = (size, insets, isExpanded)
|
|
|
|
var topInset = insets.top
|
|
if size.width < size.height {
|
|
topInset += 10.0
|
|
}
|
|
self.containerNode.frame = CGRect(origin: .zero, size: size)
|
|
if let _ = self.item.controller.minimizedTopEdgeOffset {
|
|
self.containerNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -topInset, 0.0)
|
|
}
|
|
|
|
self.snapshotContainerView.frame = CGRect(origin: .zero, size: size)
|
|
|
|
self.shadowNode.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height - topInset))
|
|
|
|
var navigationHeight: CGFloat = minimizedNavigationHeight
|
|
if !isExpanded {
|
|
navigationHeight += insets.bottom
|
|
}
|
|
|
|
let headerFrame = CGRect(origin: .zero, size: CGSize(width: size.width, height: navigationHeight))
|
|
self.headerNode.update(size: size, insets: insets, isExpanded: isExpanded, transition: transition)
|
|
transition.updateFrame(node: self.headerNode, frame: headerFrame)
|
|
transition.updateFrame(node: self.dimCoverNode, frame: CGRect(origin: .zero, size: size))
|
|
|
|
if let snapshotView = self.snapshotView {
|
|
var snapshotFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - snapshotView.bounds.size.width) / 2.0), y: 0.0), size: snapshotView.bounds.size)
|
|
|
|
var requiresBlur = false
|
|
var blurFrame = snapshotFrame
|
|
if snapshotView.frame.width * 1.1 < size.width {
|
|
if let _ = self.item.controller.minimizedTopEdgeOffset {
|
|
snapshotFrame = snapshotFrame.offsetBy(dx: 0.0, dy: -66.0)
|
|
}
|
|
blurFrame = CGRect(origin: CGPoint(x: 0.0, y: snapshotFrame.minY), size: CGSize(width: size.width, height: snapshotFrame.height))
|
|
requiresBlur = true
|
|
} else if snapshotView.frame.width > size.width * 1.5 {
|
|
if let _ = self.item.controller.minimizedTopEdgeOffset {
|
|
snapshotFrame = snapshotFrame.offsetBy(dx: 0.0, dy: 66.0)
|
|
}
|
|
blurFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - snapshotView.frame.width) / 2.0), y: snapshotFrame.minY), size: CGSize(width: snapshotFrame.width, height: size.height))
|
|
requiresBlur = true
|
|
}
|
|
|
|
if requiresBlur {
|
|
let blurredSnapshotView: UIView?
|
|
if let current = self.blurredSnapshotView {
|
|
blurredSnapshotView = current
|
|
} else {
|
|
blurredSnapshotView = snapshotView.snapshotView(afterScreenUpdates: false)
|
|
if let blurredSnapshotView {
|
|
if let blurFilter = makeBlurFilter() {
|
|
blurFilter.setValue(20.0 as NSNumber, forKey: "inputRadius")
|
|
blurFilter.setValue(true as NSNumber, forKey: "inputNormalizeEdges")
|
|
blurredSnapshotView.layer.filters = [blurFilter]
|
|
}
|
|
self.snapshotContainerView.insertSubview(blurredSnapshotView, at: 0)
|
|
self.blurredSnapshotView = blurredSnapshotView
|
|
}
|
|
}
|
|
blurredSnapshotView?.frame = blurFrame
|
|
} else if let blurredSnapshotView = self.blurredSnapshotView {
|
|
self.blurredSnapshotView = nil
|
|
blurredSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
blurredSnapshotView.removeFromSuperview()
|
|
})
|
|
}
|
|
transition.updateFrame(view: snapshotView, frame: snapshotFrame)
|
|
}
|
|
|
|
if !self.isDismissed {
|
|
transition.updateAlpha(node: self.shadowNode, alpha: isExpanded ? 1.0 : 0.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let sharedContext: SharedAccountContext
|
|
public weak var navigationController: NavigationController?
|
|
private var items: [Item] = []
|
|
|
|
private var presentationData: PresentationData
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
public private(set) var isExpanded: Bool = false
|
|
public var willMaximize: (() -> Void)?
|
|
|
|
public private(set) var statusBarStyle: StatusBarStyle = .White
|
|
public var statusBarStyleUpdated: (() -> Void)?
|
|
|
|
private let bottomEdgeView: UIImageView
|
|
private let blurView: BlurView
|
|
private let dimView: UIView
|
|
private let scrollView: ScrollViewImpl
|
|
private var itemNodes: [AnyHashable: ItemNode] = [:]
|
|
|
|
private var highlightedItemId: AnyHashable?
|
|
|
|
private var dismissingItemId: AnyHashable?
|
|
private var dismissingItemOffset: CGFloat?
|
|
|
|
private var expandedTapGestureRecoginzer: UITapGestureRecognizer?
|
|
|
|
private var currentTransition: Transition?
|
|
private var isApplyingTransition = false
|
|
private var validLayout: ContainerViewLayout?
|
|
|
|
public var controllers: [MinimizableController] {
|
|
return self.items.map { $0.controller }
|
|
}
|
|
|
|
public init(sharedContext: SharedAccountContext) {
|
|
self.sharedContext = sharedContext
|
|
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
|
|
|
self.bottomEdgeView = UIImageView()
|
|
self.bottomEdgeView.contentMode = .scaleToFill
|
|
self.bottomEdgeView.image = generateImage(CGSize(width: 22.0, height: 24.0), rotatedContext: { size, context in
|
|
context.setFillColor(UIColor.black.cgColor)
|
|
context.fill(CGRect(origin: .zero, size: size))
|
|
|
|
context.setBlendMode(.clear)
|
|
context.setFillColor(UIColor.clear.cgColor)
|
|
|
|
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: -10, width: 22, height: 20), cornerRadius: 10)
|
|
context.addPath(path.cgPath)
|
|
context.fillPath()
|
|
})?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 12)
|
|
|
|
self.blurView = BlurView(effect: nil)
|
|
self.dimView = UIView()
|
|
self.dimView.alpha = 0.0
|
|
self.dimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6)
|
|
self.dimView.isUserInteractionEnabled = false
|
|
|
|
self.scrollView = ScrollViewImpl()
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
self.scrollView.alwaysBounceVertical = true
|
|
|
|
super.init()
|
|
|
|
self.view.addSubview(self.bottomEdgeView)
|
|
self.view.addSubview(self.blurView)
|
|
self.view.addSubview(self.dimView)
|
|
self.view.addSubview(self.scrollView)
|
|
|
|
self.presentationDataDisposable = (self.sharedContext.presentationData
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.presentationData = presentationData
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.presentationDataDisposable?.dispose()
|
|
}
|
|
|
|
public override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.scrollView.delegate = self.wrappedScrollViewDelegate
|
|
self.scrollView.alwaysBounceVertical = true
|
|
self.scrollView.showsVerticalScrollIndicator = false
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
|
|
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
|
panGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
|
panGestureRecognizer.delaysTouchesBegan = true
|
|
self.scrollView.addGestureRecognizer(panGestureRecognizer)
|
|
|
|
let expandedTapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
expandedTapGestureRecoginzer.isEnabled = false
|
|
self.expandedTapGestureRecoginzer = expandedTapGestureRecoginzer
|
|
self.scrollView.addGestureRecognizer(expandedTapGestureRecoginzer)
|
|
}
|
|
|
|
func item(at y: CGFloat) -> Int? {
|
|
guard let layout = self.validLayout else {
|
|
return nil
|
|
}
|
|
|
|
let insets = layout.insets(options: [.statusBar])
|
|
let itemCount = self.items.count
|
|
let spacing = interitemSpacing(itemCount: itemCount, boundingSize: self.scrollView.bounds.size, insets: insets)
|
|
return max(0, min(Int(floor((y - additionalInsetTop - insets.top) / spacing)), itemCount - 1))
|
|
}
|
|
|
|
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
|
|
return false
|
|
}
|
|
|
|
let location = panGesture.location(in: gestureRecognizer.view)
|
|
let velocity = panGesture.velocity(in: gestureRecognizer.view)
|
|
|
|
if let _ = self.item(at: location.y) {
|
|
if self.isExpanded {
|
|
return abs(velocity.x) > abs(velocity.y)
|
|
} else {
|
|
return abs(velocity.y) > abs(velocity.x)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
@objc func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
guard self.isExpanded else {
|
|
return
|
|
}
|
|
if let result = self.scrollView.hitTest(gestureRecognizer.location(in: self.scrollView), with: nil), result === self.scrollView {
|
|
self.collapse()
|
|
}
|
|
}
|
|
|
|
@objc func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
|
if self.isExpanded {
|
|
self.dismissPanGesture(gestureRecognizer)
|
|
} else {
|
|
self.expandPanGesture(gestureRecognizer)
|
|
}
|
|
}
|
|
|
|
@objc func expandPanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
|
guard let lastItem = self.items.last, let itemNode = self.itemNodes[lastItem.id], itemNode.isReady else {
|
|
return
|
|
}
|
|
let translation = gestureRecognizer.translation(in: self.view)
|
|
if translation.y < -10.0 {
|
|
gestureRecognizer.isEnabled = false
|
|
gestureRecognizer.isEnabled = true
|
|
|
|
self.expand()
|
|
}
|
|
}
|
|
|
|
@objc func dismissPanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
|
let scrollView = self.scrollView
|
|
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
let location = gestureRecognizer.location(in: scrollView)
|
|
guard let item = self.item(at: location.y) else { return }
|
|
|
|
self.dismissingItemId = self.items[item].id
|
|
case .changed:
|
|
guard let _ = self.dismissingItemId else { return }
|
|
|
|
var translation = gestureRecognizer.translation(in: scrollView)
|
|
translation.y = 0
|
|
|
|
if let offset = self.dismissingItemOffset {
|
|
self.dismissingItemOffset = offset + translation.x
|
|
} else {
|
|
self.dismissingItemOffset = translation.x
|
|
}
|
|
|
|
gestureRecognizer.setTranslation(.zero, in: scrollView)
|
|
|
|
self.requestUpdate(transition: .immediate)
|
|
case .ended:
|
|
var needsLayout = true
|
|
if let itemId = self.dismissingItemId {
|
|
if let offset = self.dismissingItemOffset {
|
|
let velocity = gestureRecognizer.velocity(in: self.view)
|
|
if offset < -self.frame.width / 3.0 || velocity.x < -300.0 {
|
|
let proceed = {
|
|
self.currentTransition = .dismiss(itemId: itemId)
|
|
|
|
self.items.removeAll(where: { $0.id == itemId })
|
|
if self.items.count == 1 {
|
|
self.isExpanded = false
|
|
self.willMaximize?()
|
|
needsLayout = false
|
|
}
|
|
}
|
|
if let item = self.items.first(where: { $0.id == itemId }), !item.controller.shouldDismissImmediately() {
|
|
self.displayDismissConfirmation(completion: { commit in
|
|
self.dismissingItemOffset = nil
|
|
self.dismissingItemId = nil
|
|
if commit {
|
|
proceed()
|
|
} else {
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
|
}
|
|
})
|
|
} else {
|
|
proceed()
|
|
self.dismissingItemOffset = nil
|
|
self.dismissingItemId = nil
|
|
}
|
|
} else {
|
|
self.dismissingItemOffset = nil
|
|
self.dismissingItemId = nil
|
|
}
|
|
}
|
|
}
|
|
if needsLayout {
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
|
}
|
|
case .cancelled, .failed:
|
|
self.dismissingItemId = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if result === self.view {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func addController(_ viewController: MinimizableController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) {
|
|
let item = Item(
|
|
id: AnyHashable(Int64.random(in: Int64.min ... Int64.max)),
|
|
controller: viewController,
|
|
beforeMaximize: beforeMaximize
|
|
)
|
|
self.items.append(item)
|
|
|
|
self.currentTransition = .minimize(itemId: item.id)
|
|
self.requestUpdate(transition: transition)
|
|
}
|
|
|
|
private enum Transition: Equatable {
|
|
case minimize(itemId: AnyHashable)
|
|
case maximize(itemId: AnyHashable)
|
|
case dismiss(itemId: AnyHashable)
|
|
case dismissAll
|
|
case collapse
|
|
|
|
func matches(item: Item) -> Bool {
|
|
switch self {
|
|
case .minimize:
|
|
return false
|
|
case let .maximize(itemId), let .dismiss(itemId):
|
|
return item.id == itemId
|
|
case .dismissAll:
|
|
return true
|
|
case .collapse:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
public func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void) {
|
|
guard let item = self.items.first(where: { $0.controller === viewController }) else {
|
|
completion(self.items.count == 0)
|
|
return
|
|
}
|
|
if !animated {
|
|
self.items.removeAll(where: { $0.id == item.id })
|
|
self.itemNodes[item.id]?.removeFromSupernode()
|
|
self.itemNodes[item.id] = nil
|
|
completion(self.items.count == 0)
|
|
self.scrollView.contentOffset = .zero
|
|
return
|
|
}
|
|
self.isExpanded = false
|
|
self.currentTransition = .maximize(itemId: item.id)
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
completion(self.items.count == 0)
|
|
self.scrollView.contentOffset = .zero
|
|
})
|
|
self.items.removeAll(where: { $0.id == item.id })
|
|
}
|
|
|
|
public func dismissAll(completion: @escaping () -> Void) {
|
|
self.currentTransition = .dismissAll
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { _ in
|
|
completion()
|
|
})
|
|
}
|
|
|
|
public func expand() {
|
|
guard !self.items.isEmpty && !self.isExpanded && self.currentTransition == nil else {
|
|
return
|
|
}
|
|
if self.items.count == 1, let item = self.items.first {
|
|
if let navigationController = self.navigationController {
|
|
item.beforeMaximize(navigationController, { [weak self] in
|
|
self?.navigationController?.maximizeViewController(item.controller, animated: true)
|
|
})
|
|
}
|
|
} else {
|
|
self.isExpanded = true
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
|
}
|
|
}
|
|
|
|
public func collapse() {
|
|
self.isExpanded = false
|
|
self.currentTransition = .collapse
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
|
}
|
|
|
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
guard self.isExpanded, let layout = self.validLayout else {
|
|
return
|
|
}
|
|
self.requestUpdate(transition: .immediate)
|
|
|
|
let contentOffset = scrollView.contentOffset
|
|
if scrollView.contentOffset.y < -64.0, let lastItemId = self.items.last?.id, let itemNode = self.itemNodes[lastItemId] {
|
|
let velocity = scrollView.panGestureRecognizer.velocity(in: self.view).y
|
|
let distance = layout.size.height - self.collapsedHeight(layout: layout) - itemNode.frame.minY
|
|
let initialVelocity = distance != 0.0 ? abs(velocity / distance) : 0.0
|
|
|
|
self.isExpanded = false
|
|
scrollView.isScrollEnabled = false
|
|
scrollView.panGestureRecognizer.isEnabled = false
|
|
scrollView.panGestureRecognizer.isEnabled = true
|
|
scrollView.contentOffset = contentOffset
|
|
self.currentTransition = .collapse
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .customSpring(damping: 180.0, initialVelocity: initialVelocity)))
|
|
}
|
|
}
|
|
|
|
private func displayDismissConfirmation(completion: @escaping (Bool) -> Void) {
|
|
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
|
actionSheet.setItemGroups([
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: self.presentationData.strings.WebApp_CloseConfirmation),
|
|
ActionSheetButtonItem(title: self.presentationData.strings.WebApp_CloseAnyway, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
completion(true)
|
|
})
|
|
]),
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
completion(false)
|
|
})
|
|
])
|
|
])
|
|
self.navigationController?.presentOverlay(controller: actionSheet, inGlobal: false, blockInteraction: false)
|
|
}
|
|
|
|
private func requestUpdate(transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) {
|
|
guard let layout = self.validLayout else {
|
|
return
|
|
}
|
|
self.updateLayout(layout, transition: transition, completion: completion)
|
|
}
|
|
|
|
public func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
self.updateLayout(layout, transition: transition, completion: { _ in })
|
|
}
|
|
|
|
private func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) {
|
|
let isFirstTime = self.validLayout == nil
|
|
var containerTransition = transition
|
|
if isFirstTime {
|
|
containerTransition = .immediate
|
|
}
|
|
|
|
self.validLayout = layout
|
|
|
|
let bounds = CGRect(origin: .zero, size: layout.size)
|
|
|
|
containerTransition.updateFrame(view: self.blurView, frame: bounds)
|
|
containerTransition.updateFrame(view: self.dimView, frame: bounds)
|
|
if self.isExpanded {
|
|
if self.blurView.effect == nil {
|
|
UIView.animate(withDuration: 0.25, animations: {
|
|
self.blurView.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light)
|
|
self.dimView.alpha = 1.0
|
|
})
|
|
}
|
|
} else {
|
|
if self.blurView.effect != nil {
|
|
UIView.animate(withDuration: 0.25, animations: {
|
|
self.blurView.effect = nil
|
|
self.dimView.alpha = 0.0
|
|
})
|
|
}
|
|
}
|
|
self.blurView.isUserInteractionEnabled = self.isExpanded
|
|
|
|
let bottomEdgeHeight = 24.0 + 33.0 + layout.intrinsicInsets.bottom
|
|
let bottomEdgeOrigin = layout.size.height - bottomEdgeHeight
|
|
containerTransition.updateFrame(view: self.bottomEdgeView, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomEdgeHeight), size: CGSize(width: layout.size.width, height: bottomEdgeHeight)))
|
|
|
|
if isFirstTime {
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
|
transition.animatePosition(layer: self.bottomEdgeView.layer, from: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: minimizedNavigationHeight + minimizedTopMargin), to: self.bottomEdgeView.layer.position)
|
|
}
|
|
|
|
if self.isApplyingTransition {
|
|
return
|
|
}
|
|
|
|
let insets = layout.insets(options: [.statusBar])
|
|
let itemInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: insets.bottom, right: layout.safeInsets.right)
|
|
var topInset = insets.top
|
|
if layout.size.width < layout.size.height {
|
|
topInset += 10.0
|
|
}
|
|
|
|
var index = 0
|
|
let contentHeight = frameForIndex(index: self.items.count - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size).midY - 70.0
|
|
for item in self.items {
|
|
if let currentTransition = self.currentTransition {
|
|
if currentTransition.matches(item: item) {
|
|
continue
|
|
} else if case .dismiss = currentTransition, self.items.count == 1 {
|
|
continue
|
|
}
|
|
}
|
|
|
|
var itemTransition = containerTransition
|
|
|
|
let itemNode: ItemNode
|
|
if let current = self.itemNodes[item.id] {
|
|
itemNode = current
|
|
itemNode.theme = self.presentationData.theme
|
|
} else {
|
|
itemTransition = .immediate
|
|
itemNode = ItemNode(theme: self.presentationData.theme, strings: self.presentationData.strings, item: item)
|
|
self.scrollView.addSubnode(itemNode)
|
|
self.itemNodes[item.id] = itemNode
|
|
}
|
|
itemNode.closeTapped = { [weak self, weak itemNode] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.isExpanded {
|
|
let proceed = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
var needsLayout = true
|
|
self.currentTransition = .dismiss(itemId: item.id)
|
|
|
|
self.items.removeAll(where: { $0.id == item.id })
|
|
if self.items.count == 1 {
|
|
self.isExpanded = false
|
|
self.willMaximize?()
|
|
needsLayout = false
|
|
}
|
|
if needsLayout {
|
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
|
}
|
|
}
|
|
if let item = itemNode?.item, !item.controller.shouldDismissImmediately() {
|
|
self.displayDismissConfirmation(completion: { commit in
|
|
if commit {
|
|
proceed()
|
|
}
|
|
})
|
|
} else {
|
|
proceed()
|
|
}
|
|
} else {
|
|
if self.items.count > 1 {
|
|
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
|
actionSheet.setItemGroups([
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: self.presentationData.strings.WebApp_Minimized_CloseAllTitle),
|
|
ActionSheetButtonItem(title: self.presentationData.strings.WebApp_Minimized_CloseAll(Int32(self.items.count)), color: .destructive, action: { [weak self, weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
|
|
self?.navigationController?.dismissMinimizedControllers(animated: true)
|
|
})
|
|
]),
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])
|
|
])
|
|
self.navigationController?.presentOverlay(controller: actionSheet, inGlobal: false, blockInteraction: false)
|
|
} else if let item = self.items.first {
|
|
if !item.controller.shouldDismissImmediately() {
|
|
self.displayDismissConfirmation(completion: { [weak self] commit in
|
|
if commit {
|
|
self?.navigationController?.dismissMinimizedControllers(animated: true)
|
|
}
|
|
})
|
|
} else {
|
|
self.navigationController?.dismissMinimizedControllers(animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
itemNode.tapped = { [weak self, weak itemNode] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.isExpanded, let itemNode {
|
|
if let navigationController = self.navigationController {
|
|
itemNode.item.beforeMaximize(navigationController, { [weak self, weak itemNode] in
|
|
if let item = itemNode?.item {
|
|
self?.navigationController?.maximizeViewController(item.controller, animated: true)
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
self.expand()
|
|
}
|
|
}
|
|
|
|
let itemFrame: CGRect
|
|
let itemTransform: CATransform3D
|
|
|
|
if index == self.items.count - 1 {
|
|
itemNode.layer.zPosition = 10000.0
|
|
} else {
|
|
itemNode.layer.zPosition = 0.0
|
|
}
|
|
|
|
if self.isExpanded {
|
|
let currentItemFrame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size)
|
|
let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
|
|
|
var effectiveItemFrame = currentItemFrame
|
|
var effectiveItemTransform = currentItemTransform
|
|
|
|
if let dismissingItemId = self.dismissingItemId, let deletingIndex = self.items.firstIndex(where: { $0.id == dismissingItemId }), let offset = self.dismissingItemOffset {
|
|
var targetItemFrame: CGRect?
|
|
var targetItemTransform: CATransform3D?
|
|
if deletingIndex == index {
|
|
let effectiveOffset: CGFloat
|
|
if offset <= 0.0 {
|
|
effectiveOffset = offset
|
|
} else {
|
|
effectiveOffset = scrollingRubberBandingOffset(offset: offset, bandingStart: 0.0, range: 20.0)
|
|
}
|
|
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: effectiveOffset, dy: 0.0)
|
|
} else if index < deletingIndex {
|
|
let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size)
|
|
let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets)
|
|
|
|
targetItemFrame = frame
|
|
targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
|
} else {
|
|
let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size)
|
|
let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets)
|
|
|
|
targetItemFrame = frame
|
|
targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
|
}
|
|
|
|
if let targetItemFrame, let targetItemTransform {
|
|
let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5)))
|
|
effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction)
|
|
effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction)
|
|
}
|
|
}
|
|
itemFrame = effectiveItemFrame
|
|
itemTransform = effectiveItemTransform
|
|
|
|
itemNode.isCovered = false
|
|
} else {
|
|
var itemOffset: CGFloat = bottomEdgeOrigin + 13.0
|
|
var hideTransform = false
|
|
if let currentTransition = self.currentTransition {
|
|
if case let .maximize(itemId) = currentTransition {
|
|
itemOffset += layout.size.height * 0.25
|
|
if let lastItemNode = self.scrollView.subviews.last?.asyncdisplaykit_node as? ItemNode, lastItemNode.item.id == itemId {
|
|
hideTransform = true
|
|
}
|
|
} else if case .dismiss = currentTransition, self.items.count == 1 {
|
|
itemOffset += layout.size.height * 0.25
|
|
}
|
|
}
|
|
|
|
var effectiveItemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemOffset), size: layout.size)
|
|
var effectiveItemTransform = itemNode.transform
|
|
if hideTransform {
|
|
effectiveItemTransform = CATransform3DMakeScale(0.7, 0.7, 1.0)
|
|
} else if index == self.items.count - 1 {
|
|
if self.items.count > 1 {
|
|
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: 4.0)
|
|
}
|
|
effectiveItemTransform = CATransform3DIdentity
|
|
} else {
|
|
let sideInset: CGFloat = 10.0
|
|
let scaledWidth = layout.size.width - sideInset * 2.0
|
|
let scale = scaledWidth / layout.size.width
|
|
let scaledHeight = layout.size.height * scale
|
|
let verticalOffset = layout.size.height - scaledHeight
|
|
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: -verticalOffset / 2.0)
|
|
effectiveItemTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
|
}
|
|
itemFrame = effectiveItemFrame
|
|
itemTransform = effectiveItemTransform
|
|
|
|
itemNode.isCovered = index <= self.items.count - 2
|
|
}
|
|
|
|
itemNode.bounds = CGRect(origin: .zero, size: itemFrame.size)
|
|
itemNode.updateLayout(size: itemFrame.size, insets: itemInsets, isExpanded: self.isExpanded, transition: itemTransition)
|
|
|
|
if index == self.items.count - 1 && !self.isExpanded {
|
|
itemNode.setTitleControllers(self.items.map { $0.controller })
|
|
} else {
|
|
itemNode.setTitleControllers(nil)
|
|
}
|
|
|
|
itemTransition.updateTransform(node: itemNode, transform: itemTransform)
|
|
itemTransition.updatePosition(node: itemNode, position: itemFrame.center)
|
|
|
|
index += 1
|
|
}
|
|
|
|
let contentSize = CGSize(width: layout.size.width, height: contentHeight)
|
|
if self.scrollView.contentSize != contentSize {
|
|
self.scrollView.contentSize = contentSize
|
|
}
|
|
if self.scrollView.frame != bounds {
|
|
self.scrollView.frame = bounds
|
|
}
|
|
self.scrollView.passthrough = !self.isExpanded
|
|
self.scrollView.isScrollEnabled = self.isExpanded
|
|
self.expandedTapGestureRecoginzer?.isEnabled = self.isExpanded
|
|
|
|
var resolvedStatusBarStyle: StatusBarStyle = .Ignore
|
|
if self.isExpanded {
|
|
if self.scrollView.contentOffset.y > additionalInsetTop + insets.top / 2.0 {
|
|
resolvedStatusBarStyle = .Hide
|
|
} else {
|
|
resolvedStatusBarStyle = .White
|
|
}
|
|
}
|
|
if self.statusBarStyle != resolvedStatusBarStyle {
|
|
self.statusBarStyle = resolvedStatusBarStyle
|
|
Queue.mainQueue().justDispatch {
|
|
self.statusBarStyleUpdated?()
|
|
}
|
|
}
|
|
|
|
if let currentTransition = self.currentTransition {
|
|
self.isApplyingTransition = true
|
|
switch self.currentTransition {
|
|
case let .minimize(itemId):
|
|
guard let itemNode = self.itemNodes[itemId] else {
|
|
return
|
|
}
|
|
|
|
let dimView = UIView()
|
|
dimView.alpha = 1.0
|
|
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
|
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
|
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
|
dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
|
dimView.removeFromSuperview()
|
|
})
|
|
|
|
itemNode.animateIn()
|
|
|
|
var initialOffset = insets.top
|
|
if let minimizedTopEdgeOffset = itemNode.item.controller.minimizedTopEdgeOffset {
|
|
initialOffset += minimizedTopEdgeOffset
|
|
}
|
|
if layout.size.width < layout.size.height {
|
|
initialOffset += 10.0
|
|
}
|
|
if let minimizedBounds = itemNode.item.controller.minimizedBounds {
|
|
initialOffset += -minimizedBounds.minY
|
|
}
|
|
|
|
transition.animatePosition(node: itemNode, from: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + initialOffset), completion: { _ in
|
|
self.isApplyingTransition = false
|
|
if self.currentTransition == currentTransition {
|
|
self.currentTransition = nil
|
|
}
|
|
completion(currentTransition)
|
|
})
|
|
case let .maximize(itemId):
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
|
guard let itemNode = self.itemNodes[itemId] else {
|
|
return
|
|
}
|
|
|
|
let dimView = UIView()
|
|
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
|
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
|
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
|
dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
|
|
itemNode.animateOut()
|
|
if itemInsets.left > 0.0 {
|
|
itemNode.updateLayout(size: layout.size, insets: itemInsets, isExpanded: true, transition: transition)
|
|
transition.updateBounds(node: itemNode, bounds: CGRect(origin: .zero, size: layout.size))
|
|
}
|
|
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
|
|
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
|
|
self.isApplyingTransition = false
|
|
if self.currentTransition == currentTransition {
|
|
self.currentTransition = nil
|
|
}
|
|
|
|
completion(currentTransition)
|
|
|
|
if let _ = itemNode.snapshotView {
|
|
let snapshotContainerView = itemNode.snapshotContainerView
|
|
snapshotContainerView.layer.allowsGroupOpacity = true
|
|
snapshotContainerView.center = CGPoint(x: itemNode.item.controller.displayNode.view.bounds.width / 2.0, y: snapshotContainerView.bounds.height / 2.0)
|
|
itemNode.item.controller.displayNode.view.addSubview(snapshotContainerView)
|
|
Queue.mainQueue().after(0.15, {
|
|
snapshotContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
|
snapshotContainerView.removeFromSuperview()
|
|
})
|
|
})
|
|
}
|
|
|
|
self.itemNodes[itemId] = nil
|
|
itemNode.removeFromSupernode()
|
|
dimView.removeFromSuperview()
|
|
|
|
self.requestUpdate(transition: .immediate)
|
|
})
|
|
case let .dismiss(itemId):
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
|
guard let dismissedItemNode = self.itemNodes[itemId] else {
|
|
return
|
|
}
|
|
if self.items.count == 1 {
|
|
if let itemNode = self.itemNodes.first(where: { $0.0 != itemId })?.value, let navigationController = self.navigationController {
|
|
itemNode.item.beforeMaximize(navigationController, { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let dimView = UIView()
|
|
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
|
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
|
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
|
dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
|
|
itemNode.animateOut()
|
|
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
|
|
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
|
|
self.isApplyingTransition = false
|
|
if self.currentTransition == currentTransition {
|
|
self.currentTransition = nil
|
|
}
|
|
completion(currentTransition)
|
|
self.itemNodes[itemId] = nil
|
|
itemNode.removeFromSupernode()
|
|
dimView.removeFromSuperview()
|
|
|
|
self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false)
|
|
|
|
self.requestUpdate(transition: .immediate)
|
|
})
|
|
})
|
|
}
|
|
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y))
|
|
} else {
|
|
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in
|
|
self.isApplyingTransition = false
|
|
if self.currentTransition == currentTransition {
|
|
self.currentTransition = nil
|
|
}
|
|
completion(currentTransition)
|
|
|
|
self.itemNodes[itemId] = nil
|
|
dismissedItemNode.removeFromSupernode()
|
|
})
|
|
}
|
|
case .dismissAll:
|
|
let dismissOffset = collapsedHeight(layout: layout)
|
|
transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset), completion: { _ in
|
|
self.isApplyingTransition = false
|
|
if self.currentTransition == currentTransition {
|
|
self.currentTransition = nil
|
|
}
|
|
completion(currentTransition)
|
|
})
|
|
transition.updatePosition(layer: self.scrollView.layer, position: self.scrollView.center.offsetBy(dx: 0.0, dy: dismissOffset))
|
|
case .collapse:
|
|
transition.updateBounds(layer: self.scrollView.layer, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size), completion: { _ in
|
|
self.isApplyingTransition = false
|
|
if self.currentTransition == currentTransition {
|
|
self.currentTransition = nil
|
|
}
|
|
completion(currentTransition)
|
|
})
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
public func collapsedHeight(layout: ContainerViewLayout) -> CGFloat {
|
|
return minimizedNavigationHeight + minimizedTopMargin + layout.intrinsicInsets.bottom
|
|
}
|
|
}
|