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
@@ -0,0 +1,122 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
protocol FormBlockItemNodeProto {
}
enum FormBlockItemInset {
case regular
case custom(CGFloat)
}
class FormBlockItemNode<Item: FormControllerItem>: ASDisplayNode, FormControllerItemNode, FormBlockItemNodeProto {
private let topSeparatorInset: FormBlockItemInset
private let highlightedBackgroundNode: ASDisplayNode
let backgroundNode: ASDisplayNode
private let topSeparatorNode: ASDisplayNode
private let bottomSeparatorNode: ASDisplayNode
private let selectionButtonNode: HighlightTrackingButtonNode
init(selectable: Bool, topSeparatorInset: FormBlockItemInset) {
self.topSeparatorInset = topSeparatorInset
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.highlightedBackgroundNode.alpha = 0.0
self.selectionButtonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.highlightedBackgroundNode)
if selectable {
self.selectionButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
}
self.addSubnode(self.selectionButtonNode)
self.selectionButtonNode.addTarget(self, action: #selector(self.selectionButtonPressed), forControlEvents: .touchUpInside)
}
}
final func updateInternal(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
let (preLayout, apply) = self.update(item: item, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
return (preLayout, { params in
self.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
let height = apply(params)
let topSeparatorInset: CGFloat
switch previousNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
switch self.topSeparatorInset {
case .regular:
topSeparatorInset = 16.0
case let .custom(value):
topSeparatorInset = value
}
default:
topSeparatorInset = 0.0
}
switch nextNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
self.bottomSeparatorNode.isHidden = true
default:
self.bottomSeparatorNode.isHidden = false
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.selectionButtonNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: topSeparatorInset, y: 0.0), size: CGSize(width: width - topSeparatorInset, height: UIScreenPixel)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
return height
})
}
func update(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
preconditionFailure()
}
@objc private func selectionButtonPressed() {
self.selected()
}
func selected() {
}
var preventsTouchesToOtherItems: Bool {
return false
}
func touchesToOtherItemsPrevented() {
}
}
@@ -0,0 +1,49 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
public class FormController<InnerState, InitParams, Node: FormControllerNode<InitParams, InnerState>>: ViewController {
public var controllerNode: Node {
return self.displayNode as! Node
}
private let initParams: InitParams
private var presentationData: PresentationData
init(initParams: InitParams, presentationData: PresentationData) {
self.initParams = initParams
self.presentationData = presentationData
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.view.endEditing(true)
super.dismiss(completion: completion)
}
override public func loadDisplayNode() {
self.displayNode = Node(initParams: self.initParams, presentationData: self.presentationData)
self.controllerNode.present = { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
@@ -0,0 +1,81 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let textFont = Font.regular(17.0)
enum FormControllerActionType {
case accent
case destructive
}
final class FormControllerActionItem: FormControllerItem {
let type: FormControllerActionType
let title: String
let fullTopInset: Bool
let activated: () -> Void
init(type: FormControllerActionType, title: String, fullTopInset: Bool = false, activated: @escaping () -> Void) {
self.type = type
self.title = title
self.fullTopInset = fullTopInset
self.activated = activated
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerActionItemNode(fullTopInset: self.fullTopInset)
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? FormControllerActionItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class FormControllerActionItemNode: FormBlockItemNode<FormControllerActionItem> {
private let titleNode: ImmediateTextNode
private var item: FormControllerActionItem?
init(fullTopInset: Bool = false) {
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
super.init(selectable: true, topSeparatorInset: fullTopInset ? .custom(0.0) : .regular)
self.addSubnode(self.titleNode)
}
override func update(item: FormControllerActionItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
self.item = item
let leftInset: CGFloat = 16.0
let titleColor: UIColor
switch item.type {
case .accent:
titleColor = theme.list.itemAccentColor
case .destructive:
titleColor = theme.list.itemDestructiveColor
}
self.titleNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: titleColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
return (FormControllerItemPreLayout(aligningInset: 0.0), { params in
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleSize))
return 44.0
})
}
override func selected() {
self.item?.activated()
}
}
@@ -0,0 +1,115 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let textFont = Font.regular(17.0)
private let errorFont = Font.regular(13.0)
final class FormControllerDetailActionItem: FormControllerItem {
let title: String
let text: String
let placeholder: String
let error: String?
let activated: () -> Void
init(title: String, text: String, placeholder: String, error: String?, activated: @escaping () -> Void) {
self.title = title
self.text = text
self.placeholder = placeholder
self.error = error
self.activated = activated
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerDetailActionItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? FormControllerDetailActionItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class FormControllerDetailActionItemNode: FormBlockItemNode<FormControllerDetailActionItem> {
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let errorNode: ImmediateTextNode
private var item: FormControllerDetailActionItem?
init() {
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 1
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.errorNode = ImmediateTextNode()
self.errorNode.displaysAsynchronously = false
self.errorNode.maximumNumberOfLines = 0
super.init(selectable: true, topSeparatorInset: .regular)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.errorNode)
}
override func update(item: FormControllerDetailActionItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
self.item = item
let leftInset: CGFloat = 16.0
self.titleNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: theme.list.itemPrimaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude))
let aligningInset: CGFloat
if titleSize.width.isZero {
aligningInset = 0.0
} else {
aligningInset = leftInset + titleSize.width + 17.0
}
return (FormControllerItemPreLayout(aligningInset: aligningInset), { params in
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleSize))
if !item.text.isEmpty {
self.textNode.attributedText = NSAttributedString(string: item.text, font: textFont, textColor: theme.list.itemPrimaryTextColor)
} else {
self.textNode.attributedText = NSAttributedString(string: item.placeholder, font: textFont, textColor: theme.list.itemPlaceholderTextColor)
}
let textSize = self.textNode.updateLayout(CGSize(width: width - params.maxAligningInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 11.0), size: textSize))
self.errorNode.attributedText = NSAttributedString(string: item.error ?? "", font: errorFont, textColor: theme.list.freeTextErrorColor)
let errorSize = self.errorNode.updateLayout(CGSize(width: width - params.maxAligningInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.errorNode, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 44.0 - 4.0), size: errorSize))
var height: CGFloat = 44.0
if !errorSize.width.isZero {
height += -4.0 + errorSize.height + 8.0
}
return height
})
}
func activate() {
self.item?.activated()
}
override func selected() {
activate()
}
}
@@ -0,0 +1,70 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let titleFont = Font.regular(14.0)
final class FormControllerHeaderItem: FormControllerItem {
let text: String
init(text: String) {
self.text = text
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerHeaderItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
guard let node = node as? FormControllerHeaderItemNode else {
assertionFailure()
return 0.0
}
return node.update(item: self, previousNeighbor: previousNeighbor, width: width, theme: theme, transition: transition)
})
}
}
final class FormControllerHeaderItemNode: ASDisplayNode, FormControllerItemNode {
private let textNode: ImmediateTextNode
override init() {
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 1
super.init()
self.addSubnode(self.textNode)
}
func update(item: FormControllerHeaderItem, previousNeighbor: FormControllerItemNeighbor, width: CGFloat, theme: PresentationTheme, transition: ContainedViewLayoutTransition) -> CGFloat {
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
let leftInset: CGFloat = 16.0
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - 10.0, height: CGFloat.greatestFiniteMagnitude))
let height: CGFloat
switch previousNeighbor {
case .none:
height = 20.0 + 30.0
case .spacer:
height = 14.0
case .item:
height = 14.0
}
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: height - 23.0), size: textSize))
return height
}
var preventsTouchesToOtherItems: Bool {
return false
}
func touchesToOtherItemsPrevented() {
}
}
@@ -0,0 +1,37 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import MergeLists
public protocol FormControllerEntry: Identifiable {
associatedtype ItemParams
func isEqual(to: Self) -> Bool
func item(params: ItemParams, strings: PresentationStrings) -> FormControllerItem
}
public enum FormControllerItemNeighbor {
case none
case spacer
case item(FormControllerItemNode)
}
public struct FormControllerItemPreLayout {
let aligningInset: CGFloat
}
public struct FormControllerItemLayoutParams {
let maxAligningInset: CGFloat
}
public protocol FormControllerItem {
func node() -> ASDisplayNode & FormControllerItemNode
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat)
}
public protocol FormControllerItemNode {
var preventsTouchesToOtherItems: Bool { get }
func touchesToOtherItemsPrevented()
}
@@ -0,0 +1,412 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
private func hasFirstResponder(_ view: UIView) -> Bool {
if view.isFirstResponder {
return true
}
for subview in view.subviews {
if hasFirstResponder(subview) {
return true
}
}
return false
}
struct FormControllerLayoutState {
var layout: ContainerViewLayout
var navigationHeight: CGFloat
func isEqual(to: FormControllerLayoutState) -> Bool {
if self.layout != to.layout {
return false
}
if self.navigationHeight != to.navigationHeight {
return false
}
return true
}
}
struct FormControllerPresentationState {
var theme: PresentationTheme
var strings: PresentationStrings
var dateTimeFormat: PresentationDateTimeFormat
func isEqual(to: FormControllerPresentationState) -> Bool {
if self.theme !== to.theme {
return false
}
if self.strings !== to.strings {
return false
}
if self.dateTimeFormat != to.dateTimeFormat {
return false
}
return true
}
}
struct FormControllerInternalState<InnerState: FormControllerInnerState> {
var layoutState: FormControllerLayoutState?
var presentationState: FormControllerPresentationState
var innerState: InnerState?
func isEqual(to: FormControllerInternalState) -> Bool {
if let lhsLayoutState = self.layoutState, let rhsLayoutState = to.layoutState {
if !lhsLayoutState.isEqual(to: rhsLayoutState) {
return false
}
} else if (self.layoutState != nil) != (to.layoutState != nil) {
return false
}
if !self.presentationState.isEqual(to: to.presentationState) {
return false
}
if let lhsInnerState = self.innerState, let rhsInnerState = to.innerState {
if !lhsInnerState.isEqual(to: rhsInnerState) {
return false
}
} else if (self.innerState != nil) != (to.innerState != nil) {
return false
}
return true
}
}
public struct FormControllerState<InnerState: FormControllerInnerState> {
let layoutState: FormControllerLayoutState
let presentationState: FormControllerPresentationState
let innerState: InnerState
}
public enum FormControllerItemEntry<Entry: FormControllerEntry> {
case entry(Entry)
case spacer
}
public protocol FormControllerInnerState {
associatedtype Entry: FormControllerEntry
func isEqual(to: Self) -> Bool
func entries() -> [FormControllerItemEntry<Entry>]
}
private enum FilteredItemNeighbor {
case spacer
case item(FormControllerItem)
}
public class FormControllerNode<InitParams, InnerState: FormControllerInnerState>: ViewControllerTracingNode, ASScrollViewDelegate {
private typealias InternalState = FormControllerInternalState<InnerState>
typealias State = FormControllerState<InnerState>
typealias Entry = InnerState.Entry
private var internalState: InternalState
var innerState: InnerState? {
return self.internalState.innerState
}
var layoutState: FormControllerLayoutState? {
return self.internalState.layoutState
}
let scrollNode: FormControllerScrollerNode
private var appliedLayout: FormControllerLayoutState?
private var appliedEntries: [Entry] = []
private(set) var itemNodes: [ASDisplayNode & FormControllerItemNode] = []
var present: (ViewController, Any?) -> Void = { _, _ in }
var itemParams: Entry.ItemParams {
preconditionFailure()
}
required public init(initParams: InitParams, presentationData: PresentationData) {
self.internalState = FormControllerInternalState(layoutState: nil, presentationState: FormControllerPresentationState(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat), innerState: nil)
self.scrollNode = FormControllerScrollerNode()
super.init()
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.scrollNode.backgroundColor = nil
self.scrollNode.isOpaque = false
self.scrollNode.delegate = self.wrappedScrollViewDelegate
self.addSubnode(self.scrollNode)
self.scrollNode.view.delaysContentTouches = true
self.scrollNode.touchesPrevented = { [weak self] position in
guard let strongSelf = self else {
return false
}
for i in 0 ..< strongSelf.itemNodes.count {
if strongSelf.itemNodes[i].preventsTouchesToOtherItems {
if let node = strongSelf.itemNodeAtPoint(position), node === strongSelf.itemNodes[i] {
return false
}
strongSelf.itemNodes[i].touchesToOtherItemsPrevented()
return true
}
}
return false
}
}
func itemNodeAtPoint(_ point: CGPoint) -> (ASDisplayNode & FormControllerItemNode)? {
for node in self.itemNodes {
if node.frame.contains(point) {
return node
}
}
return nil
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.updateInternalState(transition: transition, { state in
var state = state
state.layoutState = FormControllerLayoutState(layout: layout, navigationHeight: navigationHeight)
return state
})
}
func updateInnerState(transition: ContainedViewLayoutTransition, with innerState: InnerState) {
self.updateInternalState(transition: transition, { state in
var state = state
state.innerState = innerState
return state
})
}
private func updateInternalState(transition: ContainedViewLayoutTransition, _ f: (InternalState) -> InternalState) {
let updated = f(self.internalState)
if !updated.isEqual(to: self.internalState) {
self.internalState = updated
if let layoutState = updated.layoutState, let innerState = updated.innerState {
self.stateUpdated(state: FormControllerState(layoutState: layoutState, presentationState: updated.presentationState, innerState: innerState), transition: transition)
}
}
}
private weak var preivousItemNodeWithFocus: (ASDisplayNode & FormControllerItemNode)?
func stateUpdated(state: State, transition: ContainedViewLayoutTransition) {
let previousLayout = self.appliedLayout
self.appliedLayout = state.layoutState
let layout = state.layoutState.layout
var insets = layout.insets(options: [.input])
insets.top += max(state.layoutState.navigationHeight, layout.insets(options: [.statusBar]).top)
let entries = state.innerState.entries()
var filteredEntries: [Entry] = []
var filteredItemNeighbors: [FilteredItemNeighbor] = []
var itemNodes: [ASDisplayNode & FormControllerItemNode] = []
var insertedItemNodeIndices = Set<Int>()
for i in 0 ..< entries.count {
if case let .entry(entry) = entries[i] {
let item = entry.item(params: self.itemParams, strings: state.presentationState.strings)
filteredEntries.append(entry)
filteredItemNeighbors.append(.item(item))
var found = false
inner: for j in 0 ..< self.appliedEntries.count {
if entry.stableId == self.appliedEntries[j].stableId {
itemNodes.append(self.itemNodes[j])
found = true
break inner
}
}
if !found {
let itemNode = item.node()
insertedItemNodeIndices.insert(itemNodes.count)
itemNodes.append(itemNode)
self.scrollNode.addSubnode(itemNode)
}
} else {
filteredItemNeighbors.append(.spacer)
}
}
for itemNode in self.itemNodes {
var found = false
inner: for updated in itemNodes {
if updated === itemNode {
found = true
break inner
}
}
if !found {
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
}
}
self.appliedEntries = filteredEntries
self.itemNodes = itemNodes
var applyLayouts: [(ContainedViewLayoutTransition, FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat)] = []
var itemNodeIndex = 0
for i in 0 ..< filteredItemNeighbors.count {
if case let .item(item) = filteredItemNeighbors[i] {
let previousNeighbor: FormControllerItemNeighbor
let nextNeighbor: FormControllerItemNeighbor
if i != 0 {
switch filteredItemNeighbors[i - 1] {
case .spacer:
previousNeighbor = .spacer
case .item:
previousNeighbor = .item(itemNodes[itemNodeIndex - 1])
}
} else {
previousNeighbor = .none
}
if i != filteredItemNeighbors.count - 1 {
switch filteredItemNeighbors[i + 1] {
case .spacer:
nextNeighbor = .spacer
case .item:
nextNeighbor = .item(itemNodes[itemNodeIndex + 1])
}
} else {
nextNeighbor = .none
}
let itemTransition: ContainedViewLayoutTransition
if insertedItemNodeIndices.contains(i) {
itemTransition = .immediate
} else {
itemTransition = transition
}
let (preLayout, apply) = item.update(node: itemNodes[itemNodeIndex], theme: state.presentationState.theme, strings: state.presentationState.strings, dateTimeFormat: state.presentationState.dateTimeFormat, width: layout.size.width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: itemTransition)
applyLayouts.append((itemTransition, preLayout, apply))
itemNodeIndex += 1
}
}
var commonAligningInset: CGFloat = 0.0
for i in 0 ..< itemNodes.count {
commonAligningInset = max(commonAligningInset, applyLayouts[i].1.aligningInset)
}
var contentHeight: CGFloat = 0.0
itemNodeIndex = 0
for i in 0 ..< filteredItemNeighbors.count {
if case .item = filteredItemNeighbors[i] {
let itemNode = itemNodes[itemNodeIndex]
let (itemTransition, _, apply) = applyLayouts[itemNodeIndex]
let itemHeight = apply(FormControllerItemLayoutParams(maxAligningInset: commonAligningInset))
itemTransition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: itemHeight)))
contentHeight += itemHeight
itemNodeIndex += 1
} else {
contentHeight += 35.0
}
}
contentHeight += 36.0
let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight)
let previousBoundsOrigin = self.scrollNode.bounds.origin
self.scrollNode.view.ignoreUpdateBounds = true
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.scrollNode.view.contentSize = scrollContentSize
self.scrollNode.view.contentInset = insets
self.scrollNode.view.scrollIndicatorInsets = insets
self.scrollNode.view.ignoreUpdateBounds = false
var updatedItemNodeWithFocus: (ASDisplayNode & FormControllerItemNode)?
for itemNode in self.itemNodes {
if hasFirstResponder(itemNode.view) {
if self.preivousItemNodeWithFocus !== itemNode {
self.preivousItemNodeWithFocus = itemNode
updatedItemNodeWithFocus = itemNode
}
break
}
}
if let previousLayout = previousLayout {
var previousInsets = previousLayout.layout.insets(options: [.input])
previousInsets.top += max(previousLayout.navigationHeight, previousLayout.layout.insets(options: [.statusBar]).top)
let insetsScrollOffset = insets.top - previousInsets.top
let negativeOverscroll = min(previousBoundsOrigin.y + insets.top, 0.0)
let cleanOrigin = max(previousBoundsOrigin.y, -insets.top)
var contentOffset = CGPoint(x: 0.0, y: cleanOrigin + insetsScrollOffset)
contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height)
contentOffset.y = max(contentOffset.y, -insets.top)
contentOffset.y += negativeOverscroll
if let updatedItemNodeWithFocus = updatedItemNodeWithFocus {
let itemRect = updatedItemNodeWithFocus.view.convert(updatedItemNodeWithFocus.view.bounds, to: self.scrollNode.view)
if contentOffset.y + layout.size.height - insets.bottom < itemRect.maxY + 4.0 {
contentOffset.y = itemRect.maxY + 4.0 - (layout.size.height - insets.bottom)
}
}
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size))
} else {
let contentOffset = CGPoint(x: 0.0, y: -insets.top)
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size))
}
if previousLayout == nil {
self.didAppear()
}
}
func didAppear() {
}
func animateIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
func enumerateItemsAndEntries(_ f: (InnerState.Entry, ASDisplayNode & FormControllerItemNode) -> Bool) {
for i in 0 ..< self.appliedEntries.count {
if !f(self.appliedEntries[i], self.itemNodes[i]) {
break
}
}
}
func forceUpdateState(transition: ContainedViewLayoutTransition) {
if let layoutState = self.layoutState, let innerState = self.innerState {
self.stateUpdated(state: FormControllerState(layoutState: layoutState, presentationState: self.internalState.presentationState, innerState: innerState), transition: transition)
}
}
func scrollToItemNode(_ itemNode: ASDisplayNode & FormControllerItemNode) {
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: max(0.0, min(itemNode.frame.minY, self.scrollNode.view.contentSize.height - self.scrollNode.view.frame.height)))
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
final class FormControllerScrollerNodeView: UIScrollView {
weak var target: FormControllerScrollerNode?
var ignoreUpdateBounds = false
override init(frame: CGRect) {
super.init(frame: frame)
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
self.alwaysBounceVertical = true
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if !self.ignoreUpdateBounds {
super.bounds = value
}
}
}
override func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
}
override func touchesShouldBegin(_ touches: Set<UITouch>, with event: UIEvent?, in view: UIView) -> Bool {
return self.target?.touchesShouldBegin(touches, with: event) ?? super.touchesShouldBegin(touches, with: event, in: view)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if self.target?.touchesShouldBegin(touches, with: event) ?? true {
super.touchesBegan(touches, with: event)
}
}
}
final class FormControllerScrollerNode: ASDisplayNode, ASScrollViewDelegate {
override var view: FormControllerScrollerNodeView {
return super.view as! FormControllerScrollerNodeView
}
weak var delegate: UIScrollViewDelegate?
var touchesPrevented: ((CGPoint) -> Bool)?
override init() {
super.init()
self.setViewBlock({
return FormControllerScrollerNodeView(frame: CGRect())
})
self.view.target = self
}
override func didLoad() {
super.didLoad()
self.view.delegate = self.wrappedScrollViewDelegate
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.delegate?.scrollViewDidScroll?(scrollView)
}
func touchesShouldBegin(_ touches: Set<UITouch>, with event: UIEvent?) -> Bool {
let touchesPosition = touches.first!.location(in: self.view)
if let touchesPrevented = self.touchesPrevented {
if touchesPrevented(touchesPosition) {
return false
}
}
return true
}
}
@@ -0,0 +1,192 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let textFont = Font.regular(17.0)
private let errorFont = Font.regular(13.0)
enum FormControllerTextInputItemColor {
case primary
case error
}
enum FormControllerTextInputItemType: Equatable {
case regular(capitalization: UITextAutocapitalizationType, autocorrection: Bool)
case latin(capitalization: UITextAutocapitalizationType)
case email
case number
}
final class FormControllerTextInputItem: FormControllerItem {
let title: String
let text: String
let placeholder: String
let color: FormControllerTextInputItemColor
let type: FormControllerTextInputItemType
let returnKeyType: UIReturnKeyType
let error: String?
let textUpdated: (String) -> Void
let returnPressed: () -> Void
init(title: String, text: String, placeholder: String, color: FormControllerTextInputItemColor = .primary, type: FormControllerTextInputItemType, returnKeyType: UIReturnKeyType = .next, error: String? = nil, textUpdated: @escaping (String) -> Void, returnPressed: @escaping () -> Void) {
self.title = title
self.text = text
self.placeholder = placeholder
self.color = color
self.type = type
self.returnKeyType = returnKeyType
self.error = error
self.textUpdated = textUpdated
self.returnPressed = returnPressed
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerTextInputItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? FormControllerTextInputItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class FormControllerTextInputItemNode: FormBlockItemNode<FormControllerTextInputItem>, UITextFieldDelegate {
private let titleNode: ImmediateTextNode
private let errorNode: ImmediateTextNode
private let textField: TextFieldNode
private var item: FormControllerTextInputItem?
init() {
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.errorNode = ImmediateTextNode()
self.errorNode.displaysAsynchronously = false
self.errorNode.maximumNumberOfLines = 0
self.textField = TextFieldNode()
self.textField.textField.font = textFont
super.init(selectable: false, topSeparatorInset: .regular)
self.addSubnode(self.titleNode)
self.addSubnode(self.errorNode)
self.addSubnode(self.textField)
self.textField.textField.delegate = self
self.textField.textField.addTarget(self, action: #selector(self.editingChanged), for: [.editingChanged])
}
override func update(item: FormControllerTextInputItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
self.item = item
let leftInset: CGFloat = 16.0
self.titleNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: theme.list.itemPrimaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude))
let aligningInset: CGFloat
if titleSize.width.isZero {
aligningInset = 0.0
} else {
aligningInset = leftInset + titleSize.width + 17.0
}
return (FormControllerItemPreLayout(aligningInset: aligningInset), { params in
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleSize))
let capitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let keyboardType: UIKeyboardType
switch item.type {
case let .regular(capitalization, autocorrection):
capitalizationType = capitalization
autocorrectionType = autocorrection ? .default : .no
keyboardType = .default
case let .latin(capitalization):
capitalizationType = capitalization
autocorrectionType = .no
keyboardType = .asciiCapable
case .email:
capitalizationType = .none
autocorrectionType = .no
keyboardType = .emailAddress
case .number:
capitalizationType = .none
autocorrectionType = .no
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
keyboardType = .asciiCapableNumberPad
} else {
keyboardType = .numberPad
}
}
if self.textField.textField.keyboardType != keyboardType {
self.textField.textField.keyboardType = keyboardType
}
if self.textField.textField.autocapitalizationType != capitalizationType {
self.textField.textField.autocapitalizationType = capitalizationType
}
if self.textField.textField.autocorrectionType != autocorrectionType {
self.textField.textField.autocorrectionType = autocorrectionType
}
if self.textField.textField.returnKeyType != item.returnKeyType {
self.textField.textField.returnKeyType = item.returnKeyType
}
self.textField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textField.textField.tintColor = theme.list.itemAccentColor
let attributedPlaceholder = NSAttributedString(string: item.placeholder, font: textFont, textColor: theme.list.itemPlaceholderTextColor)
if !(self.textField.textField.attributedPlaceholder?.isEqual(to: attributedPlaceholder) ?? false) {
self.textField.textField.attributedPlaceholder = attributedPlaceholder
}
switch item.color {
case .primary:
self.textField.textField.textColor = theme.list.itemPrimaryTextColor
case .error:
self.textField.textField.textColor = theme.list.itemDestructiveColor
}
if self.textField.textField.text != item.text {
self.textField.textField.text = item.text
}
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 3.0), size: CGSize(width: max(1.0, width - params.maxAligningInset - 8.0), height: 40.0)))
self.errorNode.attributedText = NSAttributedString(string: item.error ?? "", font: errorFont, textColor: theme.list.freeTextErrorColor)
let errorSize = self.errorNode.updateLayout(CGSize(width: width - params.maxAligningInset - 8.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.errorNode, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 44.0 - 4.0), size: errorSize))
var height: CGFloat = 44.0
if !errorSize.width.isZero {
height += -4.0 + errorSize.height + 8.0
}
return height
})
}
@objc private func editingChanged() {
self.item?.textUpdated(self.textField.textField.text ?? "")
}
@objc func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.item?.returnPressed()
return false
}
func activate() {
self.textField.textField.becomeFirstResponder()
}
}
@@ -0,0 +1,74 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let titleFont = Font.regular(14.0)
enum FormControllerTextItemColor {
case standard
case error
}
final class FormControllerTextItem: FormControllerItem {
let text: String
let color: FormControllerTextItemColor
init(text: String, color: FormControllerTextItemColor = .standard) {
self.text = text
self.color = color
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerTextItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
guard let node = node as? FormControllerTextItemNode else {
assertionFailure()
return 0.0
}
return node.update(item: self, width: width, theme: theme, transition: transition)
})
}
}
final class FormControllerTextItemNode: ASDisplayNode, FormControllerItemNode {
private let textNode: ImmediateTextNode
override init() {
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 0
super.init()
self.addSubnode(self.textNode)
}
func update(item: FormControllerTextItem, width: CGFloat, theme: PresentationTheme, transition: ContainedViewLayoutTransition) -> CGFloat {
let color: UIColor
switch item.color {
case .standard:
color = theme.list.freeTextColor
case .error:
color = theme.list.freeTextErrorColor
}
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: color)
let leftInset: CGFloat = 16.0
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - 10.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: textSize))
return textSize.height + 14.0
}
var preventsTouchesToOtherItems: Bool {
return false
}
func touchesToOtherItemsPrevented() {
}
}
@@ -0,0 +1,476 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
class FormEditableBlockItemNode<Item: FormControllerItem>: ASDisplayNode, FormControllerItemNode, FormBlockItemNodeProto, ASGestureRecognizerDelegate {
private let topSeparatorInset: FormBlockItemInset
private let highlightedBackgroundNode: ASDisplayNode
let backgroundNode: ASDisplayNode
private let topSeparatorNode: ASDisplayNode
private let bottomSeparatorNode: ASDisplayNode
private let selectionButtonNode: HighlightTrackingButtonNode
private var leftRevealNode: ItemListRevealOptionsNode?
private var rightRevealNode: ItemListRevealOptionsNode?
private var revealOptions: (left: [ItemListRevealOption], right: [ItemListRevealOption]) = ([], [])
private var initialRevealOffset: CGFloat = 0.0
public private(set) var revealOffset: CGFloat = 0.0
private var recognizer: ItemListRevealOptionsGestureRecognizer?
private var hapticFeedback: HapticFeedback?
private var allowAnyDirection = false
private var validLayout: (CGSize, CGFloat, CGFloat)?
var isDisplayingRevealedOptions: Bool {
return !self.revealOffset.isZero
}
var canBeSelected: Bool {
return !self.isDisplayingRevealedOptions
}
init(selectable: Bool, topSeparatorInset: FormBlockItemInset) {
self.topSeparatorInset = topSeparatorInset
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.highlightedBackgroundNode.alpha = 0.0
self.selectionButtonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.highlightedBackgroundNode)
if selectable {
self.selectionButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self, strongSelf.canBeSelected {
if highlighted {
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
}
self.addSubnode(self.selectionButtonNode)
self.selectionButtonNode.addTarget(self, action: #selector(self.selectionButtonPressed), forControlEvents: .touchUpInside)
}
let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
self.recognizer = recognizer
recognizer.allowAnyDirection = self.allowAnyDirection
self.view.addGestureRecognizer(recognizer)
}
func setRevealOptions(_ options: (left: [ItemListRevealOption], right: [ItemListRevealOption])) {
if self.revealOptions == options {
return
}
let previousOptions = self.revealOptions
let wasEmpty = self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty
self.revealOptions = options
let isEmpty = options.left.isEmpty && options.right.isEmpty
if options.left.isEmpty {
if let _ = self.leftRevealNode {
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} else if previousOptions.left != options.left {
/*if let _ = self.leftRevealNode {
self.revealOptionsInteractivelyClosed()
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}*/
}
if options.right.isEmpty {
if let _ = self.rightRevealNode {
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} else if previousOptions.right != options.right {
if let _ = self.rightRevealNode {
/*self.revealOptionsInteractivelyClosed()
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))*/
}
}
if wasEmpty != isEmpty {
self.recognizer?.isEnabled = !isEmpty
}
let allowAnyDirection = !options.left.isEmpty || !self.revealOffset.isZero
if allowAnyDirection != self.allowAnyDirection {
self.allowAnyDirection = allowAnyDirection
self.recognizer?.allowAnyDirection = allowAnyDirection
if self.isNodeLoaded {
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
}
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let recognizer = self.recognizer, otherGestureRecognizer == recognizer {
return true
} else {
return false
}
}
@objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) {
guard let (size, _, _) = self.validLayout else {
return
}
switch recognizer.state {
case .began:
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.bounds.size
let location = recognizer.location(in: self.view)
if location.x < revealSize.width {
recognizer.becomeCancelled()
} else {
self.initialRevealOffset = self.revealOffset
}
} else if let rightRevealNode = self.rightRevealNode {
let revealSize = rightRevealNode.bounds.size
let location = recognizer.location(in: self.view)
if location.x > size.width - revealSize.width {
recognizer.becomeCancelled()
} else {
self.initialRevealOffset = self.revealOffset
}
} else {
if self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty {
recognizer.becomeCancelled()
}
self.initialRevealOffset = self.revealOffset
}
case .changed:
var translation = recognizer.translation(in: self.view)
translation.x += self.initialRevealOffset
if self.revealOptions.left.isEmpty {
translation.x = min(0.0, translation.x)
}
if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) {
self.setupAndAddLeftRevealNode()
self.revealOptionsInteractivelyOpened()
} else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) {
self.setupAndAddRightRevealNode()
self.revealOptionsInteractivelyOpened()
}
self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate)
if self.leftRevealNode == nil && self.rightRevealNode == nil {
self.revealOptionsInteractivelyClosed()
}
case .ended, .cancelled:
guard let recognizer = self.recognizer else {
break
}
if let leftRevealNode = self.leftRevealNode {
let velocity = recognizer.velocity(in: self.view)
let revealSize = leftRevealNode.bounds.size
var reveal = false
if abs(velocity.x) < 100.0 {
if self.initialRevealOffset.isZero && self.revealOffset > 0.0 {
reveal = true
} else if self.revealOffset > revealSize.width {
reveal = true
} else {
reveal = false
}
} else {
if velocity.x > 0.0 {
reveal = true
} else {
reveal = false
}
}
var selectedOption: ItemListRevealOption?
if reveal && leftRevealNode.isDisplayingExtendedAction() {
reveal = false
selectedOption = self.revealOptions.left.first
} else {
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
if let selectedOption = selectedOption {
self.revealOptionSelected(selectedOption, animated: true)
} else {
if !reveal {
self.revealOptionsInteractivelyClosed()
}
}
} else if let rightRevealNode = self.rightRevealNode {
let velocity = recognizer.velocity(in: self.view)
let revealSize = rightRevealNode.bounds.size
var reveal = false
if abs(velocity.x) < 100.0 {
if self.initialRevealOffset.isZero && self.revealOffset < 0.0 {
reveal = true
} else if self.revealOffset < -revealSize.width {
reveal = true
} else {
reveal = false
}
} else {
if velocity.x < 0.0 {
reveal = true
} else {
reveal = false
}
}
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
if !reveal {
self.revealOptionsInteractivelyClosed()
}
}
default:
break
}
}
private func setupAndAddLeftRevealNode() {
if !self.revealOptions.left.isEmpty {
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
self?.revealOptionSelected(option, animated: false)
}, tapticAction: { [weak self] in
self?.hapticImpact()
})
revealNode.setOptions(self.revealOptions.left, isLeft: true, enableAnimations: true)
self.leftRevealNode = revealNode
if let (size, leftInset, _) = self.validLayout {
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += leftInset
revealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
revealNode.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate)
}
self.addSubnode(revealNode)
}
}
private func setupAndAddRightRevealNode() {
if !self.revealOptions.right.isEmpty {
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
self?.revealOptionSelected(option, animated: false)
}, tapticAction: { [weak self] in
self?.hapticImpact()
})
revealNode.setOptions(self.revealOptions.right, isLeft: false, enableAnimations: true)
self.rightRevealNode = revealNode
if let (size, _, rightInset) = self.validLayout {
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += rightInset
revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate)
}
self.addSubnode(revealNode)
}
}
final func updateInternal(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
let (preLayout, apply) = self.update(item: item, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
return (preLayout, { params in
self.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
let height = apply(params)
let topSeparatorInset: CGFloat
switch previousNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
switch self.topSeparatorInset {
case .regular:
topSeparatorInset = 16.0
case let .custom(value):
topSeparatorInset = value
}
default:
topSeparatorInset = 0.0
}
switch nextNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
self.bottomSeparatorNode.isHidden = true
default:
self.bottomSeparatorNode.isHidden = false
}
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height))
//revealSize.width += leftInset
leftRevealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
}
if let rightRevealNode = self.rightRevealNode {
let revealSize = rightRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height))
//revealSize.width += rightInset
rightRevealNode.frame = CGRect(origin: CGPoint(x: width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
}
self.validLayout = (CGSize(width: width, height: height), 0.0, 0.0)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.selectionButtonNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: topSeparatorInset, y: 0.0), size: CGSize(width: width - topSeparatorInset, height: UIScreenPixel)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
return height
})
}
func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.revealOffset = offset
guard let (size, leftInset, rightInset) = self.validLayout else {
return
}
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.bounds.size
let revealFrame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
//let revealNodeOffset = max(-self.revealOffset, revealSize.width)
let revealNodeOffset = -self.revealOffset
leftRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: leftInset, transition: transition)
if CGFloat(offset).isLessThanOrEqualTo(0.0) {
self.leftRevealNode = nil
transition.updateFrame(node: leftRevealNode, frame: revealFrame, completion: { [weak leftRevealNode] _ in
leftRevealNode?.removeFromSupernode()
})
} else {
transition.updateFrame(node: leftRevealNode, frame: revealFrame)
}
}
if let rightRevealNode = self.rightRevealNode {
let revealSize = rightRevealNode.bounds.size
let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
let revealNodeOffset = -max(self.revealOffset, -revealSize.width)
rightRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition)
if CGFloat(0.0).isLessThanOrEqualTo(offset) {
self.rightRevealNode = nil
transition.updateFrame(node: rightRevealNode, frame: revealFrame, completion: { [weak rightRevealNode] _ in
rightRevealNode?.removeFromSupernode()
})
} else {
transition.updateFrame(node: rightRevealNode, frame: revealFrame)
}
}
let allowAnyDirection = !self.revealOptions.left.isEmpty || !offset.isZero
if allowAnyDirection != self.allowAnyDirection {
self.allowAnyDirection = allowAnyDirection
self.recognizer?.allowAnyDirection = allowAnyDirection
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
}
self.updateRevealOffset(offset: offset, transition: transition)
}
func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
}
func revealOptionsInteractivelyOpened() {
}
func revealOptionsInteractivelyClosed() {
}
func setRevealOptionsOpened(_ value: Bool, animated: Bool) {
if value != !self.revealOffset.isZero {
if !self.revealOffset.isZero {
self.recognizer?.becomeCancelled()
}
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .spring)
} else {
transition = .immediate
}
if value {
if self.rightRevealNode == nil {
self.setupAndAddRightRevealNode()
if let rightRevealNode = self.rightRevealNode, rightRevealNode.isNodeLoaded, let _ = self.validLayout {
rightRevealNode.layout()
let revealSize = rightRevealNode.bounds.size
self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition)
}
}
} else if !self.revealOffset.isZero {
self.updateRevealOffsetInternal(offset: 0.0, transition: transition)
}
}
}
func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
}
private func hapticImpact() {
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
self.hapticFeedback?.impact(.medium)
}
func update(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
preconditionFailure()
}
@objc private func selectionButtonPressed() {
if self.canBeSelected {
self.selected()
} else {
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
self.revealOptionsInteractivelyClosed()
}
}
func selected() {
}
var preventsTouchesToOtherItems: Bool {
return self.isDisplayingRevealedOptions
}
func touchesToOtherItemsPrevented() {
if self.isDisplayingRevealedOptions {
self.setRevealOptionsOpened(false, animated: true)
}
}
}