Swiftgram/submodules/WalletUI/Sources/WalletSendScreen.swift
2020-04-23 23:29:22 +04:00

655 lines
32 KiB
Swift

import Foundation
import UIKit
import AppBundle
import AsyncDisplayKit
import Display
import SwiftSignalKit
import AlertUI
import OverlayStatusController
import WalletUrl
import WalletCore
import Markdown
private let balanceIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed()
private final class WalletSendScreenArguments {
let context: WalletContext
let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void
let updateText: (WalletSendScreenEntryTag, String) -> Void
let selectInputItem: (WalletSendScreenEntryTag) -> Void
let scrollToBottom: () -> Void
let dismissInput: () -> Void
let openQrScanner: () -> Void
let proceed: () -> Void
init(context: WalletContext, updateState: @escaping ((WalletSendScreenState) -> WalletSendScreenState) -> Void, updateText: @escaping (WalletSendScreenEntryTag, String) -> Void, selectInputItem: @escaping (WalletSendScreenEntryTag) -> Void, scrollToBottom: @escaping () -> Void, dismissInput: @escaping () -> Void, openQrScanner: @escaping () -> Void, proceed: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.updateText = updateText
self.selectInputItem = selectInputItem
self.scrollToBottom = scrollToBottom
self.dismissInput = dismissInput
self.openQrScanner = openQrScanner
self.proceed = proceed
}
}
private enum WalletSendScreenSection: Int32 {
case address
case amount
case comment
}
private enum WalletSendScreenEntryTag: ItemListItemTag {
case address
case amount
case comment
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? WalletSendScreenEntryTag {
return self == other
} else {
return false
}
}
}
private enum WalletSendScreenEntry: ItemListNodeEntry {
case amount(WalletTheme, String)
case balance(WalletTheme, String, String, Bool)
case addressHeader(WalletTheme, String)
case address(WalletTheme, String, String)
case addressInfo(WalletTheme, String)
case commentHeader(WalletTheme, String)
case comment(WalletTheme, String, String, Bool)
var section: ItemListSectionId {
switch self {
case .amount, .balance:
return WalletSendScreenSection.amount.rawValue
case .addressHeader, .address, .addressInfo:
return WalletSendScreenSection.address.rawValue
case .commentHeader, .comment:
return WalletSendScreenSection.comment.rawValue
}
}
var stableId: Int32 {
switch self {
case .amount:
return 0
case .balance:
return 1
case .addressHeader:
return 2
case .address:
return 3
case .addressInfo:
return 4
case .commentHeader:
return 5
case .comment:
return 6
}
}
static func ==(lhs: WalletSendScreenEntry, rhs: WalletSendScreenEntry) -> Bool {
switch lhs {
case let .amount(lhsTheme, lhsAmount):
if case let .amount(rhsTheme, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsAmount == rhsAmount {
return true
} else {
return false
}
case let .balance(lhsTheme, lhsTitle, lhsBalance, lhsInsufficient):
if case let .balance(rhsTheme, rhsTitle, rhsBalance, rhsInsufficient) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsBalance == rhsBalance, lhsInsufficient == rhsInsufficient {
return true
} else {
return false
}
case let .addressHeader(lhsTheme, lhsText):
if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .address(lhsTheme, lhsPlaceholder, lhsAddress):
if case let .address(rhsTheme, rhsPlaceholder, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsAddress == rhsAddress {
return true
} else {
return false
}
case let .addressInfo(lhsTheme, lhsText):
if case let .addressInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .commentHeader(lhsTheme, lhsText):
if case let .commentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .comment(lhsTheme, lhsPlaceholder, lhsText, lhsSendEnabled):
if case let .comment(rhsTheme, rhsPlaceholder, rhsText, rhsSendEnabled) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsText == rhsText, lhsSendEnabled == rhsSendEnabled {
return true
} else {
return false
}
}
}
static func <(lhs: WalletSendScreenEntry, rhs: WalletSendScreenEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! WalletSendScreenArguments
switch self {
case let .amount(theme, text):
return WalletAmountItem(theme: theme, amount: text, sectionId: self.section, textUpdated: { text in
let text = formatAmountText(text, decimalSeparator: arguments.context.presentationData.dateTimeFormat.decimalSeparator)
arguments.updateText(WalletSendScreenEntryTag.amount, text)
}, shouldUpdateText: { text in
return isValidAmount(text)
}, processPaste: { pastedText in
if isValidAmount(pastedText) {
let presentationData = arguments.context.presentationData
return normalizedStringForGramsString(pastedText, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
return text
}
}, updatedFocus: { focus in
if !focus {
let presentationData = arguments.context.presentationData
arguments.updateState { state in
var state = state
if !state.amount.isEmpty {
state.amount = normalizedStringForGramsString(state.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
return state
}
}
}, tag: WalletSendScreenEntryTag.amount)
case let .balance(theme, title, balance, insufficient):
return WalletBalanceItem(theme: theme, title: title, value: balance, insufficient: insufficient, sectionId: self.section)
case let .addressHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .address(theme, placeholder, address):
return ItemListMultilineInputItem(theme: theme, text: address, placeholder: placeholder, maxLength: .init(value: walletAddressLength, display: false), sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .next, minimalHeight: 68.0, textUpdated: { text in
arguments.updateText(WalletSendScreenEntryTag.address, text.replacingOccurrences(of: "\n", with: ""))
}, shouldUpdateText: { text in
return isValidAddress(text)
}, processPaste: { text in
if let url = URL(string: text), let parsedUrl = parseWalletUrl(url) {
var focusItemTag: WalletSendScreenEntryTag?
arguments.updateState { state in
var state = state
state.address = parsedUrl.address
if let amount = parsedUrl.amount {
state.amount = formatBalanceText(amount, decimalSeparator: arguments.context.presentationData.dateTimeFormat.decimalSeparator)
} else if state.amount.isEmpty {
focusItemTag = WalletSendScreenEntryTag.amount
}
if let comment = parsedUrl.comment {
state.comment = comment
} else if state.comment.isEmpty && focusItemTag == nil {
focusItemTag = WalletSendScreenEntryTag.comment
}
return state
}
if let focusItemTag = focusItemTag {
arguments.selectInputItem(focusItemTag)
} else {
arguments.dismissInput()
}
} else if isValidAddress(text) {
arguments.updateText(WalletSendScreenEntryTag.address, text)
if isValidAddress(text, exactLength: true) {
var focusItemTag: WalletSendScreenEntryTag? = .comment
arguments.updateState { state in
if state.amount.isEmpty {
focusItemTag = .amount
} else if state.comment.isEmpty {
focusItemTag = .comment
}
return state
}
if let focusItemTag = focusItemTag {
arguments.selectInputItem(focusItemTag)
} else {
arguments.dismissInput()
}
}
}
}, tag: WalletSendScreenEntryTag.address, action: {
var focusItemTag: WalletSendScreenEntryTag?
arguments.updateState { state in
if state.amount.isEmpty {
focusItemTag = .amount
} else if state.comment.isEmpty {
focusItemTag = .comment
}
return state
}
if let focusItemTag = focusItemTag {
arguments.selectInputItem(focusItemTag)
} else {
arguments.dismissInput()
}
}, inlineAction: ItemListMultilineInputInlineAction(icon: UIImage(bundleImageName: "Wallet/QrIcon")!, action: {
arguments.openQrScanner()
}))
case let .addressInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .comment(theme, placeholder, value, sendEnabled):
return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: walletTextLimit, display: true, mode: .bytes), sectionId: self.section, style: .blocks, returnKeyType: .send, textUpdated: { text in
arguments.updateText(WalletSendScreenEntryTag.comment, text)
}, updatedFocus: { focus in
if focus {
arguments.scrollToBottom()
}
}, tag: WalletSendScreenEntryTag.comment, action: {
if sendEnabled {
arguments.proceed()
}
})
}
}
}
private struct WalletSendScreenState: Equatable {
var address: String
var amount: String
var comment: String
}
private func walletSendScreenEntries(presentationData: WalletPresentationData, balance: Int64?, state: WalletSendScreenState, sendEnabled: Bool) -> [WalletSendScreenEntry] {
if balance == nil {
return []
}
var entries: [WalletSendScreenEntry] = []
let amount = amountValue(state.amount)
let balance = max(0, balance ?? 0)
entries.append(.amount(presentationData.theme, state.amount))
entries.append(.balance(presentationData.theme, presentationData.strings.Wallet_Send_Balance("").0, formatBalanceText(balance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), balance == 0 || (amount > 0 && balance < amount)))
entries.append(.addressHeader(presentationData.theme, presentationData.strings.Wallet_Send_AddressHeader))
entries.append(.address(presentationData.theme, presentationData.strings.Wallet_Send_AddressText, state.address))
entries.append(.addressInfo(presentationData.theme, presentationData.strings.Wallet_Send_AddressInfo))
entries.append(.commentHeader(presentationData.theme, presentationData.strings.Wallet_Receive_CommentHeader))
entries.append(.comment(presentationData.theme, presentationData.strings.Wallet_Receive_CommentInfo, state.comment, sendEnabled))
return entries
}
protocol WalletSendScreen {
}
private final class WalletSendScreenImpl: ItemListController, WalletSendScreen {
}
public func walletSendScreen(context: WalletContext, randomId: Int64, walletInfo: WalletInfo, address: String? = nil, amount: Int64? = nil, comment: String? = nil) -> ViewController {
let presentationData = context.presentationData
let initialState = WalletSendScreenState(address: address ?? "", amount: amount.flatMap { formatBalanceText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } ?? "", comment: comment ?? "")
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let serverSaltValue = Promise<Data?>()
serverSaltValue.set(context.getServerSalt()
|> map(Optional.init)
|> `catch` { _ -> Signal<Data?, NoError> in
return .single(nil)
})
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController, Any?) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var popImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var selectInputItemImpl: ((WalletSendScreenEntryTag) -> Void)?
var ensureItemVisibleImpl: ((WalletSendScreenEntryTag, Bool) -> Void)?
let arguments = WalletSendScreenArguments(context: context, updateState: { f in
updateState(f)
}, updateText: { tag, value in
updateState { state in
var state = state
switch tag {
case .address:
state.address = value
case .amount:
state.amount = value
case .comment:
state.comment = value
}
return state
}
ensureItemVisibleImpl?(tag, false)
}, selectInputItem: { tag in
selectInputItemImpl?(tag)
}, scrollToBottom: {
ensureItemVisibleImpl?(WalletSendScreenEntryTag.comment, true)
}, dismissInput: {
dismissInputImpl?()
}, openQrScanner: {
dismissInputImpl?()
context.authorizeAccessToCamera(completion: {
pushImpl?(WalletQrScanScreen(context: context, completion: { parsedUrl in
var updatedState: WalletSendScreenState?
updateState { state in
var state = state
state.address = parsedUrl.address
if let amount = parsedUrl.amount {
state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
if let comment = parsedUrl.comment {
state.comment = comment
}
updatedState = state
return state
}
popImpl?()
if let updatedState = updatedState {
if updatedState.amount.isEmpty {
selectInputItemImpl?(WalletSendScreenEntryTag.amount)
} else if updatedState.comment.isEmpty {
selectInputItemImpl?(WalletSendScreenEntryTag.comment)
}
}
}))
})
}, proceed: {
let proceed: () -> Void = {
let presentationData = context.presentationData
let state = stateValue.with { $0 }
let amount = amountValue(state.amount)
guard amount > 0 else {
return
}
let commentData = state.comment.data(using: .utf8)
let formattedAddress = String(state.address[state.address.startIndex..<state.address.index(state.address.startIndex, offsetBy: walletAddressLength / 2)] + " \n " + state.address[state.address.index(state.address.startIndex, offsetBy: walletAddressLength / 2)..<state.address.endIndex])
let destinationAddress = state.address
updateState { state in
var state = state
state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
return state
}
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(controller, nil)
let _ = (verifySendGramsRequestAndEstimateFees(tonInstance: context.tonInstance, walletInfo: walletInfo, toAddress: destinationAddress, amount: amount, comment: commentData ?? Data(), encryptComment: true, timeout: 0)
|> deliverOnMainQueue).start(next: { [weak controller] verificationResult in
controller?.dismiss()
let presentationData = context.presentationData
let title = NSAttributedString(string: presentationData.strings.Wallet_Send_Confirmation, font: Font.semibold(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
let feeAmount = verificationResult.fees.inFwdFee + verificationResult.fees.storageFee + verificationResult.fees.gasFee + verificationResult.fees.fwdFee
let (text, ranges) = presentationData.strings.Wallet_Send_ConfirmationText(formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), formattedAddress, "\(formatBalanceText(feeAmount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))")
let bodyAttributes = MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
let boldAttributes = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: bodyAttributes, bold: boldAttributes, link: bodyAttributes, linkAttribute: { _ in return nil }), textAlignment: .center))
for (index, range) in ranges {
if index == 1 {
attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range)
}
}
if verificationResult.canNotEncryptComment {
//TODO:localize
attributedText.append(NSAttributedString(string: "\n\nThe destination wallet is not initialized. The comment will be sent unencrypted.", font: Font.regular(13.0), textColor: presentationData.theme.list.itemDestructiveColor))
}
var dismissAlertImpl: ((Bool) -> Void)?
let theme = context.presentationData.theme
let controller = richTextAlertController(alertContext: AlertControllerContext(theme: theme.alert, themeSignal: .single(theme.alert)), title: title, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Wallet_Navigation_Cancel, action: {
dismissAlertImpl?(true)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Wallet_Send_ConfirmationConfirm, action: {
dismissAlertImpl?(false)
dismissInputImpl?()
let presentationData = context.presentationData
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
var serverSaltSignal = serverSaltValue.get()
|> take(1)
serverSaltSignal = serverSaltSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (serverSaltSignal
|> deliverOnMainQueue).start(next: { serverSalt in
if let serverSalt = serverSalt {
if let commentData = state.comment.data(using: .utf8) {
pushImpl?(WalletSplashScreen(context: context, mode: .sending(WalletSplashModeSending(walletInfo: walletInfo, address: state.address, amount: amount, comment: commentData, encryptComment: !verificationResult.canNotEncryptComment, randomId: randomId, serverSalt: serverSalt)), walletCreatedPreloadState: nil))
}
}
})
})], allowInputInset: false, dismissAutomatically: false)
presentInGlobalOverlayImpl?(controller, nil)
dismissAlertImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
}, error: { [weak controller] error in
controller?.dismiss()
let presentationData = context.presentationData
var title: String?
let text: String
switch error {
case .generic:
text = presentationData.strings.Wallet_UnknownError
case .network:
title = presentationData.strings.Wallet_Send_NetworkErrorTitle
text = presentationData.strings.Wallet_Send_NetworkErrorText
case .notEnoughFunds:
title = presentationData.strings.Wallet_Send_ErrorNotEnoughFundsTitle
text = presentationData.strings.Wallet_Send_ErrorNotEnoughFundsText
case .messageTooLong:
text = presentationData.strings.Wallet_UnknownError
case .invalidAddress:
text = presentationData.strings.Wallet_Send_ErrorInvalidAddress
case .secretDecryptionFailed:
text = presentationData.strings.Wallet_Send_ErrorDecryptionFailed
case .destinationIsNotInitialized:
text = presentationData.strings.Wallet_UnknownError
}
let theme = presentationData.theme
let controller = textAlertController(alertContext: AlertControllerContext(theme: theme.alert, themeSignal: .single(theme.alert)), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Wallet_Alert_OK, action: {
})])
presentControllerImpl?(controller, nil)
})
}
let _ = (walletAddress(walletInfo: walletInfo, tonInstance: context.tonInstance)
|> deliverOnMainQueue).start(next: { walletAddress in
let presentationData = context.presentationData
let state = stateValue.with { $0 }
let destinationAddress = state.address
if destinationAddress == walletAddress {
presentControllerImpl?(standardTextAlertController(theme: presentationData.theme.alert, title: presentationData.strings.Wallet_Send_OwnAddressAlertTitle, text: presentationData.strings.Wallet_Send_OwnAddressAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Wallet_Alert_Cancel, action: {
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Wallet_Send_OwnAddressAlertProceed, action: {
proceed()
})
]), nil)
} else {
proceed()
}
})
})
let walletState: Signal<WalletState?, NoError> = getCombinedWalletState(storage: context.storage, subject: .wallet(walletInfo), tonInstance: context.tonInstance, onlyCached: true)
|> map { combinedState -> WalletState? in
var state: WalletState?
switch combinedState {
case let .cached(combinedState):
state = combinedState?.walletState
case let .updated(combinedState):
state = combinedState.walletState
}
return state
}
|> `catch` { _ -> Signal<WalletState?, NoError> in
return .single(nil)
|> then(
getCombinedWalletState(storage: context.storage, subject: .wallet(walletInfo), tonInstance: context.tonInstance, onlyCached: false)
|> map { combinedState -> WalletState? in
var state: WalletState?
switch combinedState {
case let .cached(combinedState):
state = combinedState?.walletState
case let .updated(combinedState):
state = combinedState.walletState
}
return state
}
|> `catch` { _ -> Signal<WalletState?, NoError> in
return .single(nil)
}
)
}
var focusItemTag: ItemListItemTag?
if amount == nil {
focusItemTag = WalletSendScreenEntryTag.amount
} else if address == nil {
focusItemTag = WalletSendScreenEntryTag.address
}
let signal = combineLatest(queue: .mainQueue(), .single(context.presentationData), walletState, statePromise.get())
|> map { presentationData, walletState, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Navigation_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let rightNavigationButton: ItemListNavigationButton?
let amount = amountValue(state.amount)
var sendEnabled = false
var emptyItem: ItemListControllerEmptyStateItem?
if let walletState = walletState {
let textLength: Int = state.comment.data(using: .utf8, allowLossyConversion: true)?.count ?? 0
sendEnabled = isValidAddress(state.address, exactLength: true) && amount > 0 && amount <= walletState.effectiveAvailableBalance && textLength <= walletTextLimit
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Send_Send), style: .bold, enabled: sendEnabled, action: {
arguments.proceed()
})
} else {
rightNavigationButton = nil
emptyItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_Send_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Wallet_Navigation_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletSendScreenEntries(presentationData: presentationData, balance: walletState?.effectiveAvailableBalance, state: state, sendEnabled: sendEnabled), style: .blocks, focusItemTag: focusItemTag, emptyStateItem: emptyItem, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = WalletSendScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil, hasNavigationBarSeparator: false)
controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
presentInGlobalOverlayImpl = { [weak controller] c, a in
controller?.presentInGlobalOverlay(c, with: a)
}
pushImpl = { [weak controller] c in
controller?.push(c)
}
popImpl = { [weak controller] in
let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
let _ = controller?.dismiss()
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
selectInputItemImpl = { [weak controller] nextTag in
guard let controller = controller else {
return
}
var resultItemNode: ItemListItemFocusableNode?
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, let focusableItemNode = itemNode as? ItemListItemFocusableNode {
if nextTag.isEqual(to: tag) {
resultItemNode = focusableItemNode
return true
}
}
return false
})
if let resultItemNode = resultItemNode {
resultItemNode.focus()
}
}
ensureItemVisibleImpl = { [weak controller] targetTag, animated in
controller?.afterLayout({
guard let controller = controller else {
return
}
var resultItemNode: ListViewItemNode?
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
resultItemNode = itemNode as? ListViewItemNode
return true
}
}
return false
})
if let resultItemNode = resultItemNode {
controller.ensureItemNodeVisible(resultItemNode, animated: animated)
}
})
}
return controller
}