mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
563 lines
25 KiB
Swift
563 lines
25 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import Display
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import MergeLists
|
|
import AccountContext
|
|
import ChatPresentationInterfaceState
|
|
import ChatControllerInteraction
|
|
import ItemListUI
|
|
import ChatContextQuery
|
|
import ChatInputContextPanelNode
|
|
import ChatListUI
|
|
import ComponentFlow
|
|
import ComponentDisplayAdapters
|
|
|
|
private enum CommandChatInputContextPanelEntryStableId: Hashable {
|
|
case editShortcuts
|
|
case command(PeerCommand)
|
|
case shortcut(Int32)
|
|
}
|
|
|
|
private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
|
|
struct Command: Equatable {
|
|
let command: ChatInputTextCommand
|
|
let accountPeer: EnginePeer?
|
|
let searchQuery: String?
|
|
|
|
static func ==(lhs: Command, rhs: Command) -> Bool {
|
|
return lhs.command == rhs.command && lhs.accountPeer == rhs.accountPeer && lhs.searchQuery == rhs.searchQuery
|
|
}
|
|
}
|
|
|
|
enum Content: Equatable {
|
|
case editShortcuts
|
|
case command(Command)
|
|
}
|
|
|
|
let content: Content
|
|
let index: Int
|
|
let theme: PresentationTheme
|
|
|
|
init(index: Int, content: Content, theme: PresentationTheme) {
|
|
self.content = content
|
|
self.index = index
|
|
self.theme = theme
|
|
}
|
|
|
|
static func ==(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool {
|
|
return lhs.index == rhs.index && lhs.content == rhs.content && lhs.theme === rhs.theme
|
|
}
|
|
|
|
static func <(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool {
|
|
return lhs.index < rhs.index
|
|
}
|
|
|
|
var stableId: CommandChatInputContextPanelEntryStableId {
|
|
switch self.content {
|
|
case .editShortcuts:
|
|
return .editShortcuts
|
|
case let .command(command):
|
|
switch command.command {
|
|
case let .command(command):
|
|
return .command(command)
|
|
case let .shortcut(shortcut):
|
|
if let shortcutId = shortcut.id {
|
|
return .shortcut(shortcutId)
|
|
} else {
|
|
return .shortcut(0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func withUpdatedTheme(_ theme: PresentationTheme) -> CommandChatInputContextPanelEntry {
|
|
return CommandChatInputContextPanelEntry(index: self.index, content: self.content, theme: theme)
|
|
}
|
|
|
|
func item(context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (ChatInputTextCommand, Bool) -> Void, openEditShortcuts: @escaping () -> Void) -> ListViewItem {
|
|
switch self.content {
|
|
case .editShortcuts:
|
|
return VerticalListContextResultsChatInputPanelButtonItem(theme: presentationData.theme, style: .round, title: presentationData.strings.Chat_CommandList_EditQuickReplies, pressed: {
|
|
openEditShortcuts()
|
|
})
|
|
case let .command(command):
|
|
switch command.command {
|
|
case let .command(command):
|
|
return CommandChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), command: command, commandSelected: { value, sendImmediately in
|
|
commandSelected(.command(value), sendImmediately)
|
|
})
|
|
case let .shortcut(shortcut):
|
|
let chatListNodeInteraction = ChatListNodeInteraction(
|
|
context: context,
|
|
animationCache: context.animationCache,
|
|
animationRenderer: context.animationRenderer,
|
|
activateSearch: {
|
|
},
|
|
peerSelected: { _, _, _, _ in
|
|
commandSelected(.shortcut(shortcut), true)
|
|
},
|
|
disabledPeerSelected: { _, _, _ in
|
|
},
|
|
togglePeerSelected: { _, _ in
|
|
},
|
|
togglePeersSelection: { _, _ in
|
|
},
|
|
additionalCategorySelected: { _ in
|
|
},
|
|
messageSelected: { _, _, _, _ in
|
|
commandSelected(.shortcut(shortcut), true)
|
|
},
|
|
groupSelected: { _ in
|
|
},
|
|
addContact: { _ in
|
|
},
|
|
setPeerIdWithRevealedOptions: { _, _ in
|
|
},
|
|
setItemPinned: { _, _ in
|
|
},
|
|
setPeerMuted: { _, _ in
|
|
},
|
|
setPeerThreadMuted: { _, _, _ in
|
|
},
|
|
deletePeer: { _, _ in
|
|
},
|
|
deletePeerThread: { _, _ in
|
|
},
|
|
setPeerThreadStopped: { _, _, _ in
|
|
},
|
|
setPeerThreadPinned: { _, _, _ in
|
|
},
|
|
setPeerThreadHidden: { _, _, _ in
|
|
},
|
|
updatePeerGrouping: { _, _ in
|
|
},
|
|
togglePeerMarkedUnread: { _, _ in
|
|
},
|
|
toggleArchivedFolderHiddenByDefault: {
|
|
},
|
|
toggleThreadsSelection: { _, _ in
|
|
},
|
|
hidePsa: { _ in
|
|
},
|
|
activateChatPreview: { _, _, _, _, _ in
|
|
},
|
|
present: { _ in
|
|
},
|
|
openForumThread: { _, _ in
|
|
},
|
|
openStorageManagement: {
|
|
},
|
|
openPasswordSetup: {
|
|
},
|
|
openPremiumIntro: {
|
|
},
|
|
openPremiumGift: { _ in
|
|
},
|
|
openActiveSessions: {
|
|
},
|
|
openBirthdaySetup: {
|
|
},
|
|
performActiveSessionAction: { _, _ in
|
|
},
|
|
openChatFolderUpdates: {
|
|
},
|
|
hideChatFolderUpdates: {
|
|
},
|
|
openStories: { _, _ in
|
|
},
|
|
dismissNotice: { _ in
|
|
},
|
|
editPeer: { _ in
|
|
}
|
|
)
|
|
|
|
let chatListPresentationData = ChatListPresentationData(
|
|
theme: presentationData.theme,
|
|
fontSize: presentationData.listsFontSize,
|
|
strings: presentationData.strings,
|
|
dateTimeFormat: presentationData.dateTimeFormat,
|
|
nameSortOrder: presentationData.nameSortOrder,
|
|
nameDisplayOrder: presentationData.nameDisplayOrder,
|
|
disableAnimations: false
|
|
)
|
|
|
|
let renderedPeer: EngineRenderedPeer
|
|
if let accountPeer = command.accountPeer {
|
|
renderedPeer = EngineRenderedPeer(peer: accountPeer)
|
|
} else {
|
|
renderedPeer = EngineRenderedPeer(peerId: context.account.peerId, peers: [:], associatedMedia: [:])
|
|
}
|
|
|
|
return ChatListItem(
|
|
presentationData: chatListPresentationData,
|
|
context: context,
|
|
chatListLocation: .chatList(groupId: .root),
|
|
filterData: nil,
|
|
index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: context.account.peerId, namespace: 0, id: 0), timestamp: 0))),
|
|
content: .peer(ChatListItemContent.PeerData(
|
|
messages: [shortcut.topMessage],
|
|
peer: renderedPeer,
|
|
threadInfo: nil,
|
|
combinedReadState: nil,
|
|
isRemovedFromTotalUnreadCount: false,
|
|
presence: nil,
|
|
hasUnseenMentions: false,
|
|
hasUnseenReactions: false,
|
|
draftState: nil,
|
|
mediaDraftContentType: nil,
|
|
inputActivities: nil,
|
|
promoInfo: nil,
|
|
ignoreUnreadBadge: false,
|
|
displayAsMessage: false,
|
|
hasFailedMessages: false,
|
|
forumTopicData: nil,
|
|
topForumTopicItems: [],
|
|
autoremoveTimeout: nil,
|
|
storyState: nil,
|
|
requiresPremiumForMessaging: false,
|
|
displayAsTopicList: false,
|
|
tags: [],
|
|
customMessageListData: ChatListItemContent.CustomMessageListData(
|
|
commandPrefix: "/\(shortcut.shortcut)",
|
|
searchQuery: command.searchQuery.flatMap { "/\($0)"},
|
|
messageCount: shortcut.totalCount,
|
|
hideSeparator: false,
|
|
hideDate: true,
|
|
hidePeerStatus: true
|
|
)
|
|
)),
|
|
editing: false,
|
|
hasActiveRevealControls: false,
|
|
selected: false,
|
|
header: nil,
|
|
enableContextActions: false,
|
|
hiddenOffset: false,
|
|
interaction: chatListNodeInteraction
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct CommandChatInputContextPanelTransition {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
let hasShortcuts: Bool
|
|
let itemCountChanged: Bool
|
|
}
|
|
|
|
private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (ChatInputTextCommand, Bool) -> Void, openEditShortcuts: @escaping () -> Void) -> CommandChatInputContextPanelTransition {
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
|
|
|
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
|
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected, openEditShortcuts: openEditShortcuts), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected, openEditShortcuts: openEditShortcuts), directionHint: nil) }
|
|
|
|
let itemCountChanged = fromEntries.count != toEntries.count
|
|
|
|
return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates, hasShortcuts: toEntries.contains(where: { entry in
|
|
if case .editShortcuts = entry.content {
|
|
return true
|
|
}
|
|
return false
|
|
}), itemCountChanged: itemCountChanged)
|
|
}
|
|
|
|
final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
|
|
private let listView: ListView
|
|
private let listBackgroundView: UIView
|
|
private var currentEntries: [CommandChatInputContextPanelEntry]?
|
|
private var contentOffsetChangeTransition: Transition?
|
|
private var isAnimatingOut: Bool = false
|
|
|
|
private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = []
|
|
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
|
|
|
|
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
|
|
self.listView = ListView()
|
|
self.listView.isOpaque = false
|
|
self.listView.stackFromBottom = true
|
|
self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor
|
|
self.listView.limitHitTestToNodes = true
|
|
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
self.listView.accessibilityPageScrolledString = { row, count in
|
|
return strings.VoiceOver_ScrollStatus(row, count).string
|
|
}
|
|
|
|
self.listBackgroundView = UIView()
|
|
self.listBackgroundView.backgroundColor = theme.list.plainBackgroundColor
|
|
self.listBackgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
self.listBackgroundView.layer.cornerRadius = 10.0
|
|
|
|
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext)
|
|
|
|
self.isOpaque = false
|
|
self.clipsToBounds = true
|
|
|
|
self.view.addSubview(self.listBackgroundView)
|
|
self.addSubnode(self.listView)
|
|
|
|
self.listView.visibleContentOffsetChanged = { [weak self] offset in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if self.isAnimatingOut {
|
|
return
|
|
}
|
|
|
|
var topItemOffset: CGFloat = self.listView.bounds.height
|
|
var isFirst = true
|
|
self.listView.forEachItemNode { itemNode in
|
|
if isFirst {
|
|
isFirst = false
|
|
topItemOffset = itemNode.frame.minY
|
|
}
|
|
}
|
|
|
|
let transition: Transition = self.contentOffsetChangeTransition ?? .immediate
|
|
transition.setFrame(view: self.listBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: topItemOffset), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0)))
|
|
}
|
|
}
|
|
|
|
func updateResults(_ results: [ChatInputTextCommand], accountPeer: EnginePeer?, hasShortcuts: Bool, query: String?) {
|
|
var entries: [CommandChatInputContextPanelEntry] = []
|
|
var index = 0
|
|
var stableIds = Set<CommandChatInputContextPanelEntryStableId>()
|
|
if hasShortcuts {
|
|
let entry = CommandChatInputContextPanelEntry(index: index, content: .editShortcuts, theme: self.theme)
|
|
stableIds.insert(entry.stableId)
|
|
entries.append(entry)
|
|
index += 1
|
|
}
|
|
for command in results {
|
|
let entry = CommandChatInputContextPanelEntry(index: index, content: .command(CommandChatInputContextPanelEntry.Command(command: command, accountPeer: accountPeer, searchQuery: query)), theme: self.theme)
|
|
if stableIds.contains(entry.stableId) {
|
|
continue
|
|
}
|
|
stableIds.insert(entry.stableId)
|
|
entries.append(entry)
|
|
index += 1
|
|
}
|
|
self.prepareTransition(from: self.currentEntries ?? [], to: entries)
|
|
}
|
|
|
|
private func prepareTransition(from: [CommandChatInputContextPanelEntry]? , to: [CommandChatInputContextPanelEntry]) {
|
|
let firstTime = self.currentEntries == nil
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let transition = preparedTransition(from: from ?? [], to: to, context: self.context, presentationData: presentationData, commandSelected: { [weak self] command, sendImmediately in
|
|
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else {
|
|
return
|
|
}
|
|
switch command {
|
|
case let .command(command):
|
|
if sendImmediately {
|
|
interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text)
|
|
} else {
|
|
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
|
|
var commandQueryRange: NSRange?
|
|
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) {
|
|
if type == [.command] {
|
|
commandQueryRange = range
|
|
break inner
|
|
}
|
|
}
|
|
|
|
if let range = commandQueryRange {
|
|
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
|
|
|
let replacementText = command.command.text + " "
|
|
inputText.replaceCharacters(in: range, with: replacementText)
|
|
|
|
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
|
|
|
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
|
|
}
|
|
return (textInputState, inputMode)
|
|
}
|
|
}
|
|
case let .shortcut(shortcut):
|
|
if let shortcutId = shortcut.id {
|
|
interfaceInteraction.sendShortcut(shortcutId)
|
|
}
|
|
}
|
|
}, openEditShortcuts: { [weak self] in
|
|
guard let self, let interfaceInteraction = self.interfaceInteraction else {
|
|
return
|
|
}
|
|
interfaceInteraction.openEditShortcuts()
|
|
})
|
|
self.currentEntries = to
|
|
self.enqueueTransition(transition, firstTime: firstTime)
|
|
}
|
|
|
|
private func enqueueTransition(_ transition: CommandChatInputContextPanelTransition, firstTime: Bool) {
|
|
enqueuedTransitions.append((transition, firstTime))
|
|
|
|
if self.validLayout != nil {
|
|
while !self.enqueuedTransitions.isEmpty {
|
|
self.dequeueTransition()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dequeueTransition() {
|
|
if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first {
|
|
self.enqueuedTransitions.remove(at: 0)
|
|
|
|
var options = ListViewDeleteAndInsertOptions()
|
|
options.insert(.Synchronous)
|
|
options.insert(.LowLatency)
|
|
options.insert(.PreferSynchronousResourceLoading)
|
|
if firstTime {
|
|
self.contentOffsetChangeTransition = .immediate
|
|
|
|
self.listBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.listView.bounds.height), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0))
|
|
} else {
|
|
if transition.itemCountChanged {
|
|
options.insert(.AnimateTopItemPosition)
|
|
options.insert(.AnimateCrossfade)
|
|
}
|
|
|
|
self.contentOffsetChangeTransition = .spring(duration: 0.4)
|
|
}
|
|
|
|
var insets = UIEdgeInsets()
|
|
insets.top = topInsetForLayout(size: validLayout.0)
|
|
insets.left = validLayout.1
|
|
insets.right = validLayout.2
|
|
|
|
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default(duration: nil))
|
|
|
|
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in
|
|
if let strongSelf = self, firstTime {
|
|
var topItemOffset: CGFloat?
|
|
strongSelf.listView.forEachItemNode { itemNode in
|
|
if topItemOffset == nil {
|
|
topItemOffset = itemNode.frame.minY
|
|
}
|
|
}
|
|
|
|
if let topItemOffset = topItemOffset {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
|
|
|
transition.animatePositionAdditive(layer: strongSelf.listView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
|
|
transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
|
|
}
|
|
}
|
|
})
|
|
|
|
self.contentOffsetChangeTransition = nil
|
|
}
|
|
}
|
|
|
|
private func topInsetForLayout(size: CGSize) -> CGFloat {
|
|
var minimumItemHeights: CGFloat = 0.0
|
|
if let currentEntries = self.currentEntries, !currentEntries.isEmpty {
|
|
let indexLimit = min(4, currentEntries.count - 1)
|
|
for i in 0 ... indexLimit {
|
|
var itemHeight: CGFloat
|
|
switch currentEntries[i].content {
|
|
case .editShortcuts:
|
|
itemHeight = VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
|
|
case let .command(command):
|
|
switch command.command {
|
|
case .command:
|
|
itemHeight = MentionChatInputPanelItemNode.itemHeight
|
|
case .shortcut:
|
|
itemHeight = 58.0
|
|
}
|
|
}
|
|
if indexLimit >= 4 && i == indexLimit {
|
|
minimumItemHeights += floor(itemHeight * 0.5)
|
|
} else {
|
|
minimumItemHeights += itemHeight
|
|
}
|
|
}
|
|
} else {
|
|
minimumItemHeights = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
|
|
}
|
|
|
|
return max(size.height - minimumItemHeights, 0.0)
|
|
}
|
|
|
|
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
|
|
let hadValidLayout = self.validLayout != nil
|
|
self.validLayout = (size, leftInset, rightInset, bottomInset)
|
|
|
|
var insets = UIEdgeInsets()
|
|
insets.top = self.topInsetForLayout(size: size)
|
|
insets.left = leftInset
|
|
insets.right = rightInset
|
|
|
|
transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
|
|
|
|
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
|
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve)
|
|
|
|
self.contentOffsetChangeTransition = Transition(transition)
|
|
|
|
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
|
|
self.contentOffsetChangeTransition = nil
|
|
|
|
if !hadValidLayout {
|
|
while !self.enqueuedTransitions.isEmpty {
|
|
self.dequeueTransition()
|
|
}
|
|
}
|
|
|
|
if self.theme !== interfaceState.theme {
|
|
self.theme = interfaceState.theme
|
|
self.listView.keepBottomItemOverscrollBackground = self.theme.list.plainBackgroundColor
|
|
|
|
let new = self.currentEntries?.map({$0.withUpdatedTheme(interfaceState.theme)}) ?? []
|
|
prepareTransition(from: self.currentEntries, to: new)
|
|
}
|
|
}
|
|
|
|
override func animateOut(completion: @escaping () -> Void) {
|
|
self.isAnimatingOut = true
|
|
|
|
var topItemOffset: CGFloat?
|
|
self.listView.forEachItemNode { itemNode in
|
|
if topItemOffset == nil {
|
|
topItemOffset = itemNode.frame.minY
|
|
}
|
|
}
|
|
|
|
if let topItemOffset = topItemOffset {
|
|
let position = self.listView.layer.position
|
|
let offset = (self.listView.bounds.size.height - topItemOffset)
|
|
self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
|
completion()
|
|
})
|
|
self.listBackgroundView.layer.animatePosition(from: self.listBackgroundView.layer.position, to: CGPoint(x: self.listBackgroundView.layer.position.x, y: self.listBackgroundView.layer.position.y + offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
|
})
|
|
} else {
|
|
completion()
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let listViewFrame = self.listView.frame
|
|
return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event)
|
|
}
|
|
|
|
override var topItemFrame: CGRect? {
|
|
var topItemFrame: CGRect?
|
|
self.listView.forEachItemNode { itemNode in
|
|
if topItemFrame == nil {
|
|
topItemFrame = itemNode.frame
|
|
}
|
|
}
|
|
return topItemFrame
|
|
}
|
|
}
|