Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SearchUI",
module_name = "SearchUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/ChatListSearchItemNode:ChatListSearchItemNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,68 @@
import Foundation
import AsyncDisplayKit
import Display
import ChatListSearchItemNode
public func fixSearchableListNodeScrolling(_ listNode: ListView) {
var searchItemNode: ListViewItemNode?
var nextItemNode: ListViewItemNode?
listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatListSearchItemNode {
searchItemNode = itemNode
} else if searchItemNode != nil && nextItemNode == nil {
nextItemNode = itemNode as? ListViewItemNode
}
}
if let searchItemNode = searchItemNode {
let itemFrame = searchItemNode.apparentFrame
if itemFrame.contains(CGPoint(x: 0.0, y: listNode.insets.top)) {
if itemFrame.minY + itemFrame.height * 0.6 < listNode.insets.top {
if let nextItemNode = nextItemNode {
listNode.ensureItemNodeVisibleAtTopInset(nextItemNode)
}
} else {
listNode.ensureItemNodeVisibleAtTopInset(searchItemNode)
}
}
}
}
public func fixNavigationSearchableListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
let scrollToItem: ListViewScrollToItem
let targetProgress: CGFloat
if searchNode.expansionProgress < 0.6 {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 0.0
} else {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 1.0
}
searchNode.updateExpansionProgress(targetProgress, animated: true)
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
return true
}
return false
}
func fixNavigationSearchableGridNodeScrolling(_ gridNode: GridNode, searchNode: NavigationBarSearchContentNode) -> Bool {
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
let scrollToItem: GridNodeScrollToItem
let targetProgress: CGFloat
if searchNode.expansionProgress < 0.6 {
scrollToItem = GridNodeScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), transition: .animated(duration: 0.3, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true)
targetProgress = 0.0
} else {
scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.3, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true)
targetProgress = 1.0
}
searchNode.updateExpansionProgress(targetProgress, animated: true)
gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { _ in })
return true
}
return false
}
@@ -0,0 +1,167 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import SearchBarNode
private let searchBarFont = Font.regular(17.0)
public let navigationBarSearchContentHeight: CGFloat = 54.0
public class NavigationBarSearchContentNode: NavigationBarContentNode {
public var theme: PresentationTheme?
public var placeholder: String
public var compactPlaceholder: String
private let inline: Bool
public let placeholderNode: SearchBarPlaceholderNode
public var placeholderHeight: CGFloat?
private var disabledOverlay: ASDisplayNode?
public var expansionProgress: CGFloat = 1.0
public var additionalHeight: CGFloat = 0.0
private var validLayout: (CGSize, CGFloat, CGFloat)?
public init(theme: PresentationTheme, placeholder: String, compactPlaceholder: String? = nil, inline: Bool = false, activate: @escaping () -> Void) {
self.theme = theme
self.placeholder = placeholder
self.compactPlaceholder = compactPlaceholder ?? placeholder
self.inline = inline
self.placeholderNode = SearchBarPlaceholderNode(fieldStyle: .modern)
self.placeholderNode.labelNode.displaysAsynchronously = false
super.init()
self.placeholderNode.isAccessibilityElement = true
self.placeholderNode.accessibilityLabel = placeholder
self.placeholderNode.accessibilityTraits = .searchField
self.addSubnode(self.placeholderNode)
self.placeholderNode.activate = activate
}
public func updateThemeAndPlaceholder(theme: PresentationTheme, placeholder: String, compactPlaceholder: String? = nil) {
self.theme = theme
self.placeholder = placeholder
self.compactPlaceholder = compactPlaceholder ?? placeholder
self.placeholderNode.accessibilityLabel = placeholder
if let disabledOverlay = self.disabledOverlay {
disabledOverlay.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor.withAlphaComponent(0.5)
}
if let validLayout = self.validLayout {
self.updatePlaceholder(self.expansionProgress, size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: .immediate)
}
}
public func updateListVisibleContentOffset(_ offset: ListViewVisibleContentOffset, transition: ContainedViewLayoutTransition = .immediate) {
var progress: CGFloat = 0.0
switch offset {
case let .known(offset):
progress = max(0.0, (self.nominalHeight - offset)) / self.nominalHeight
case .none:
progress = 1.0
default:
break
}
self.updateExpansionProgress(progress, animated: transition.isAnimated)
}
public func updateGridVisibleContentOffset(_ offset: GridNodeVisibleContentOffset) {
var progress: CGFloat = 0.0
switch offset {
case let .known(offset):
progress = max(0.0, (self.nominalHeight - offset)) / self.nominalHeight
case .none:
progress = 1.0
default:
break
}
self.updateExpansionProgress(progress)
}
public func updateExpansionProgress(_ progress: CGFloat, animated: Bool = false) {
let newProgress = max(0.0, min(10.0, progress))
if abs(newProgress - self.expansionProgress) > 0.0001 {
self.expansionProgress = newProgress
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: ContainedViewLayoutTransitionCurve.slide) : .immediate
if let validLayout = self.validLayout, animated {
self.updatePlaceholder(self.expansionProgress, size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: transition)
}
self.requestContainerLayout(transition)
}
}
public func setIsEnabled(_ enabled: Bool, animated: Bool = false) {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.25, curve: .easeInOut) : .immediate
transition.updateAlpha(node: self.placeholderNode, alpha: enabled ? 1.0 : 0.6)
self.placeholderNode.isUserInteractionEnabled = enabled
}
private func updatePlaceholder(_ progress: CGFloat, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let padding: CGFloat = 10.0
let baseWidth = size.width - padding * 2.0 - leftInset - rightInset
let fieldHeight: CGFloat = 36.0
let fraction = fieldHeight / self.nominalHeight
let fullFraction = navigationBarSearchContentHeight / self.nominalHeight
let fromLow: CGFloat = fullFraction - fraction
let toLow: CGFloat = 0.0
let fromHigh: CGFloat = fullFraction
let toHigh: CGFloat = 1.0
var visibleProgress: CGFloat = toLow + (self.expansionProgress - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
visibleProgress = max(0.0, min(1.0, visibleProgress))
let searchBarNodeLayout = self.placeholderNode.asyncLayout()
let textColor = self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93)
var fillColor = self.theme?.rootController.navigationSearchBar.inputFillColor ?? .clear
if self.inline, let theme = self.theme, fillColor.distance(to: theme.list.blocksBackgroundColor) < 100 {
fillColor = fillColor.withMultipliedBrightnessBy(0.8)
}
let backgroundColor = self.theme?.rootController.navigationBar.opaqueBackgroundColor ?? .clear
let placeholderString = NSAttributedString(string: self.placeholder, font: searchBarFont, textColor: textColor)
let compactPlaceholderString = NSAttributedString(string: self.compactPlaceholder, font: searchBarFont, textColor: textColor)
let (searchBarHeight, searchBarApply) = searchBarNodeLayout(placeholderString, compactPlaceholderString, CGSize(width: baseWidth, height: fieldHeight), visibleProgress, textColor, fillColor, backgroundColor, transition)
searchBarApply()
let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: size.height + (1.0 - visibleProgress) * fieldHeight - 8.0 - fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight))
transition.updateFrame(node: self.placeholderNode, frame: searchBarFrame)
self.placeholderHeight = searchBarHeight
if let disabledOverlay = self.disabledOverlay {
var disabledOverlayFrame = self.placeholderNode.frame
disabledOverlayFrame.size.height = searchBarHeight
transition.updateFrame(node: disabledOverlay, frame: disabledOverlayFrame)
}
}
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, leftInset, rightInset)
self.updatePlaceholder(self.expansionProgress, size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
override public var height: CGFloat {
return self.nominalHeight * self.expansionProgress
}
override public var clippedHeight: CGFloat {
return self.nominalHeight * min(1.0, self.expansionProgress)
}
override public var nominalHeight: CGFloat {
return navigationBarSearchContentHeight + self.additionalHeight
}
override public var mode: NavigationBarContentMode {
return .expansion
}
}
@@ -0,0 +1,295 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import TelegramPresentationData
import SearchBarNode
public enum SearchDisplayControllerMode {
case list
case navigation
}
public final class SearchDisplayController {
private final class BackgroundNode: ASDisplayNode {
var isTransparent: Bool = false
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = self.view.hitTest(point, with: event)
if self.isTransparent, result === self.view {
return nil
} else {
return result
}
}
}
private let searchBar: SearchBarNode
private let mode: SearchDisplayControllerMode
private let backgroundNode: BackgroundNode
public let contentNode: SearchDisplayControllerContentNode
private var hasSeparator: Bool
private let inline: Bool
private var containerLayout: (ContainerViewLayout, CGFloat)?
public var isDeactivating = false
private var isSearchingDisposable: Disposable?
public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, hasBackground: Bool = false, hasSeparator: Bool = false, contentNode: SearchDisplayControllerContentNode, inline: Bool = false, cancel: @escaping () -> Void) {
self.inline = inline
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator, inline: inline), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator, displayBackground: hasBackground)
self.backgroundNode = BackgroundNode()
self.backgroundNode.allowsGroupOpacity = true
self.mode = mode
self.contentNode = contentNode
self.hasSeparator = hasSeparator
self.searchBar.textUpdated = { [weak contentNode] text, _ in
contentNode?.searchTextUpdated(text: text)
}
self.searchBar.tokensUpdated = { [weak contentNode] tokens in
contentNode?.searchTokensUpdated(tokens: tokens)
}
self.searchBar.cancel = { [weak self] in
self?.isDeactivating = true
cancel()
}
self.searchBar.clearPrefix = { [weak contentNode] in
contentNode?.searchTextClearPrefix()
}
self.searchBar.clearTokens = { [weak contentNode] in
contentNode?.searchTextClearTokens()
}
self.contentNode.cancel = { [weak self] in
self?.isDeactivating = true
cancel()
}
self.contentNode.dismissInput = { [weak self] in
self?.searchBar.deactivate(clear: false)
}
var isFirstTime = true
self.contentNode.setQuery = { [weak self] prefix, tokens, query in
if let strongSelf = self {
strongSelf.searchBar.prefixString = prefix
let previousTokens = strongSelf.searchBar.tokens
strongSelf.searchBar.tokens = tokens
strongSelf.searchBar.text = query
if previousTokens.count < tokens.count && !isFirstTime {
if let lastToken = tokens.last, !lastToken.permanent {
strongSelf.searchBar.selectLastToken()
}
}
isFirstTime = false
}
}
if let placeholder = placeholder {
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
}
self.contentNode.setPlaceholder = { [weak self] string in
guard string != self?.searchBar.placeholderString?.string else {
return
}
if let mutableAttributedString = self?.searchBar.placeholderString?.mutableCopy() as? NSMutableAttributedString {
mutableAttributedString.mutableString.setString(string)
self?.searchBar.placeholderString = mutableAttributedString
}
}
self.isSearchingDisposable = (contentNode.isSearching
|> deliverOnMainQueue).start(next: { [weak self] value in
self?.searchBar.activity = value
})
if self.contentNode.hasDim {
self.backgroundNode.backgroundColor = .clear
self.backgroundNode.isTransparent = true
} else {
self.backgroundNode.backgroundColor = presentationData.theme.chatList.backgroundColor
self.backgroundNode.isTransparent = false
}
}
public func updatePresentationData(_ presentationData: PresentationData) {
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator, inline: self.inline), strings: presentationData.strings)
self.contentNode.updatePresentationData(presentationData)
if self.contentNode.hasDim {
self.backgroundNode.backgroundColor = .clear
self.backgroundNode.isTransparent = true
} else {
self.backgroundNode.backgroundColor = presentationData.theme.chatList.backgroundColor
self.backgroundNode.isTransparent = false
}
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
let searchBarHeight: CGFloat = max(20.0, statusBarHeight) + 44.0
let navigationBarOffset: CGFloat
if statusBarHeight.isZero {
navigationBarOffset = -20.0
} else {
navigationBarOffset = 0.0
}
var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight))
if layout.statusBarHeight == nil {
navigationBarFrame.size.height = 64.0
}
navigationBarFrame.size.height += 10.0
let searchBarFrame: CGRect
if case .navigation = self.mode {
searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0)
} else {
searchBarFrame = navigationBarFrame
}
transition.updateFrame(node: self.searchBar, frame: searchBarFrame)
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
self.containerLayout = (layout, navigationBarFrame.maxY)
let bounds = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.backgroundNode, frame: bounds.insetBy(dx: -20.0, dy: -20.0))
var size = layout.size
size.width += 20.0 * 2.0
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 20.0), size: size))
var safeInsets = layout.safeInsets
safeInsets.left += 20.0
safeInsets.right += 20.0
self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: safeInsets, additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition)
}
public func activate(insertSubnode: @escaping (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode?, focus: Bool = true) {
guard let (layout, navigationBarHeight) = self.containerLayout else {
return
}
insertSubnode(self.backgroundNode, false)
self.backgroundNode.addSubnode(self.contentNode)
if self.contentNode.hasDim {
self.backgroundNode.backgroundColor = .clear
self.backgroundNode.isTransparent = true
} else {
self.backgroundNode.alpha = 0.0
self.backgroundNode.isTransparent = false
}
var size = layout.size
size.width += 20.0 * 2.0
self.contentNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 20.0), size: size)
var safeInsets = layout.safeInsets
safeInsets.left += 20.0
safeInsets.right += 20.0
self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: safeInsets, additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate)
var contentNavigationBarHeight = navigationBarHeight
if layout.statusBarHeight == nil {
contentNavigationBarHeight += 28.0
}
if !self.contentNode.hasDim {
self.backgroundNode.alpha = 1.0
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue)
self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
if !self.contentNode.hasDim {
if let placeholder = placeholder {
self.searchBar.placeholderString = placeholder.placeholderString
}
} else {
if let placeholder = placeholder {
let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil)
let contentNodePosition = self.backgroundNode.layer.position
if contentNode.animateBackgroundAppearance {
self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
self.searchBar.placeholderString = placeholder.placeholderString
}
}
let navigationBarFrame: CGRect
switch self.mode {
case .list:
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
let searchBarHeight: CGFloat = max(20.0, statusBarHeight) + 44.0
let navigationBarOffset: CGFloat
if statusBarHeight.isZero {
navigationBarOffset = -20.0
} else {
navigationBarOffset = 0.0
}
var frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight))
if layout.statusBarHeight == nil {
frame.size.height = 64.0
}
navigationBarFrame = frame
case .navigation:
navigationBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0)
}
self.searchBar.frame = navigationBarFrame
insertSubnode(self.searchBar, true)
self.searchBar.layout()
if focus {
self.searchBar.activate()
}
if let placeholder = placeholder {
self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
if self.contentNode.hasDim {
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
} else {
self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
}
public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) {
self.searchBar.deactivate(clear: false)
let searchBar = self.searchBar
if let placeholder = placeholder {
searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: {
[weak searchBar] in
searchBar?.removeFromSupernode()
})
} else {
searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] finished in
if finished {
searchBar?.removeFromSupernode()
}
})
}
let backgroundNode = self.backgroundNode
let contentNode = self.contentNode
if animated {
backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak backgroundNode] finished in
if finished {
backgroundNode?.removeFromSupernode()
}
})
} else {
backgroundNode.removeFromSupernode()
contentNode.removeFromSupernode()
}
}
public func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? {
return self.contentNode.previewViewAndActionAtLocation(location)
}
}
@@ -0,0 +1,60 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import SearchBarNode
open class SearchDisplayControllerContentNode: ASDisplayNode {
public final var dismissInput: (() -> Void)?
public final var cancel: (() -> Void)?
public final var setQuery: ((NSAttributedString?, [SearchBarToken], String) -> Void)?
public final var setPlaceholder: ((String) -> Void)?
open var animateBackgroundAppearance: Bool {
return true
}
open var hasDim: Bool {
return false
}
open var isSearching: Signal<Bool, NoError> {
return .single(false)
}
override public init() {
super.init()
}
open func updatePresentationData(_ presentationData: PresentationData) {
}
open func searchTextUpdated(text: String) {
}
open func searchTokensUpdated(tokens: [SearchBarToken]) {
}
open func searchTextClearPrefix() {
}
open func searchTextClearTokens() {
}
open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}
open func ready() -> Signal<Void, NoError> {
return .single(Void())
}
open func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? {
return nil
}
open func scrollToTop() {
}
}