import Foundation import UIKit import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import MergeLists import ComponentDisplayAdapters public final class AsyncListComponent: Component { public protocol ItemView: UIView { func isReorderable(at point: CGPoint) -> Bool } public final class OverlayContainerView: UIView { public override init(frame: CGRect) { super.init(frame: frame) self.layer.anchorPoint = CGPoint() } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func updatePosition(position: CGPoint, transition: ComponentTransition) { let previousPosition: CGPoint var forceUpdate = false if self.layer.animation(forKey: "positionUpdate") != nil, let presentation = self.layer.presentation() { forceUpdate = true previousPosition = presentation.position if !transition.animation.isImmediate { self.layer.removeAnimation(forKey: "positionUpdate") } } else { previousPosition = self.layer.position } if previousPosition != position || forceUpdate { self.center = position if case let .curve(duration, curve) = transition.animation { self.layer.animate( from: NSValue(cgPoint: CGPoint(x: previousPosition.x - position.x, y: previousPosition.y - position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: duration, delay: 0.0, curve: curve, removeOnCompletion: true, additive: true, completion: nil, key: "positionUpdate" ) } } } } final class ResetScrollingRequest: Equatable { let requestId: Int let id: AnyHashable init(requestId: Int, id: AnyHashable) { self.requestId = requestId self.id = id } static func ==(lhs: ResetScrollingRequest, rhs: ResetScrollingRequest) -> Bool { if lhs === rhs { return true } if lhs.requestId != rhs.requestId { return false } if lhs.id != rhs.id { return false } return true } } public final class ExternalState { public struct Value: Equatable { var resetScrollingRequest: ResetScrollingRequest? public static func ==(lhs: Value, rhs: Value) -> Bool { if lhs.resetScrollingRequest != rhs.resetScrollingRequest { return false } return true } } public private(set) var value: Value = Value() private var nextId: Int = 0 public init() { } public func resetScrolling(id: AnyHashable) { let requestId = self.nextId self.nextId += 1 self.value.resetScrollingRequest = ResetScrollingRequest(requestId: requestId, id: id) } } public enum Direction { case vertical case horizontal } public final class VisibleItem { public let item: AnyComponentWithIdentity public let frame: CGRect init(item: AnyComponentWithIdentity, frame: CGRect) { self.item = item self.frame = frame } } public final class VisibleItems: Sequence, IteratorProtocol { private let view: AsyncListComponent.View private var index: Int = 0 private let indices: [(Int, CGRect)] init(view: AsyncListComponent.View, direction: Direction) { self.view = view var indices: [(Int, CGRect)] = [] view.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ListItemNodeImpl, let index = itemNode.index { var itemFrame = itemNode.frame itemFrame.origin.y -= itemNode.transitionOffset if let animation = itemNode.animationForKey("height") { if let height = animation.to as? CGFloat { itemFrame.size.height = height } } if case .horizontal = direction { itemFrame = CGRect(origin: CGPoint(x: itemFrame.minY, y: itemFrame.minX), size: CGSize(width: itemFrame.height, height: itemFrame.width)) } indices.append((index, itemFrame)) } } indices.sort(by: { $0.0 < $1.0 }) self.indices = indices } public func next() -> VisibleItem? { if self.index >= self.indices.count { return nil } let index = self.index self.index += 1 if let component = self.view.component { let (itemIndex, itemFrame) = self.indices[index] return VisibleItem(item: component.items[itemIndex], frame: itemFrame) } return nil } } public let externalState: ExternalState public let externalStateValue: ExternalState.Value public let items: [AnyComponentWithIdentity] public let itemSetId: AnyHashable // Changing itemSetId supresses update animations public let direction: Direction public let insets: UIEdgeInsets public let reorderItems: ((Int, Int) -> Bool)? public let onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)? public init( externalState: ExternalState, items: [AnyComponentWithIdentity], itemSetId: AnyHashable, direction: Direction, insets: UIEdgeInsets, reorderItems: ((Int, Int) -> Bool)? = nil, onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)? = nil ) { self.externalState = externalState self.externalStateValue = externalState.value self.items = items self.itemSetId = itemSetId self.direction = direction self.insets = insets self.reorderItems = reorderItems self.onVisibleItemsUpdated = onVisibleItemsUpdated } public static func ==(lhs: AsyncListComponent, rhs: AsyncListComponent) -> Bool { if lhs.externalState !== rhs.externalState { return false } if lhs.items != rhs.items { return false } if lhs.itemSetId != rhs.itemSetId { return false } if lhs.direction != rhs.direction { return false } if lhs.insets != rhs.insets { return false } if (lhs.reorderItems == nil) != (rhs.reorderItems == nil) { return false } return true } private struct ItemEntry: Comparable, Identifiable { let contents: AnyComponentWithIdentity let index: Int var id: AnyHashable { return self.contents.id } var stableId: AnyHashable { return self.id } static func ==(lhs: ItemEntry, rhs: ItemEntry) -> Bool { if lhs.contents != rhs.contents { return false } if lhs.index != rhs.index { return false } return true } static func <(lhs: ItemEntry, rhs: ItemEntry) -> Bool { return lhs.index < rhs.index } func item(parentView: AsyncListComponent.View?, direction: Direction) -> ListViewItem { return ListItemImpl(parentView: parentView, contents: self.contents, direction: direction) } } private final class ListItemImpl: ListViewItem { weak var parentView: AsyncListComponent.View? let contents: AnyComponentWithIdentity let direction: Direction let selectable: Bool = false init(parentView: AsyncListComponent.View?, contents: AnyComponentWithIdentity, direction: Direction) { self.parentView = parentView self.contents = contents self.direction = direction } func nodeConfiguredForParams( async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void ) { async { let impl: () -> Void = { let node = ListItemNodeImpl() let (nodeLayout, apply) = node.asyncLayout()(self, params) node.insets = nodeLayout.insets node.contentSize = nodeLayout.contentSize Queue.mainQueue().async { completion(node, { return (nil, { _ in apply(false) }) }) } } if Thread.isMainThread { impl() } else { assert(false) Queue.mainQueue().async { impl() } } } } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { assert(node() is ListItemNodeImpl) if let nodeValue = node() as? ListItemNodeImpl { let layout = nodeValue.asyncLayout() async { let impl: () -> Void = { let (nodeLayout, apply) = layout(self, params) Queue.mainQueue().async { completion(nodeLayout, { _ in apply(animation.isAnimated) }) } } if Thread.isMainThread { impl() } else { assert(false) Queue.mainQueue().async { impl() } } } } } } } private final class ListItemNodeImpl: ListViewItemNode { private let contentsView = ComponentView() private(set) var item: ListItemImpl? init() { super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) } deinit { } override func isReorderable(at point: CGPoint) -> Bool { if let itemView = self.contentsView.view as? ItemView { return itemView.isReorderable(at: self.view.convert(point, to: itemView)) } return false } override func snapshotForReordering() -> UIView? { return self.view.snapshotView(afterScreenUpdates: false) } func asyncLayout() -> (ListItemImpl, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { return { item, params in let containerSize: CGSize switch item.direction { case .vertical: containerSize = CGSize(width: params.width, height: 100000.0) case .horizontal: containerSize = CGSize(width: 100000.0, height: params.width) } let contentsSize = self.contentsView.update( transition: .immediate, component: item.contents.component, environment: {}, containerSize: containerSize ) let mappedContentsSize: CGSize switch item.direction { case .vertical: mappedContentsSize = CGSize(width: params.width, height: contentsSize.height) case .horizontal: mappedContentsSize = CGSize(width: params.width, height: contentsSize.width) } let itemLayout = ListViewItemNodeLayout(contentSize: mappedContentsSize, insets: UIEdgeInsets()) return (itemLayout, { animated in self.item = item switch item.direction { case .vertical: self.layer.sublayerTransform = CATransform3DIdentity case .horizontal: self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } let contentsFrame = CGRect(origin: CGPoint(), size: contentsSize) if let contentsComponentView = self.contentsView.view { if contentsComponentView.superview == nil { self.view.addSubview(contentsComponentView) } contentsComponentView.center = CGPoint(x: mappedContentsSize.width * 0.5, y: mappedContentsSize.height * 0.5) contentsComponentView.bounds = CGRect(origin: CGPoint(), size: contentsFrame.size) } }) } } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } override func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } public final class View: UIView { let listNode: ListView private var externalStateValue: ExternalState.Value? private var isUpdating: Bool = false private(set) var component: AsyncListComponent? private var currentEntries: [ItemEntry] = [] private var ignoreUpdateVisibleItems: Bool = false public override init(frame: CGRect) { self.listNode = ListView() self.listNode.useMainQueueTransactions = true self.listNode.scroller.delaysContentTouches = false self.listNode.reorderedItemHasShadow = false super.init(frame: frame) self.addSubview(self.listNode.view) self.listNode.onContentsUpdated = { [weak self] transition in guard let self else { return } self.updateVisibleItems(transition: ComponentTransition(transition)) } } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } public func stopScrolling() { self.listNode.stopScrolling() } private func updateVisibleItems(transition: ComponentTransition) { if self.ignoreUpdateVisibleItems { return } guard let component = self.component else { return } if let onVisibleItemsUpdated = component.onVisibleItemsUpdated { onVisibleItemsUpdated(VisibleItems(view: self, direction: component.direction), transition) } } func update(component: AsyncListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component let listSize: CGSize let listInsets: UIEdgeInsets switch component.direction { case .vertical: self.listNode.transform = CATransform3DIdentity listSize = CGSize(width: availableSize.width, height: availableSize.height) listInsets = component.insets case .horizontal: self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) listSize = CGSize(width: availableSize.height, height: availableSize.width) listInsets = UIEdgeInsets(top: component.insets.left, left: component.insets.top, bottom: component.insets.right, right: component.insets.bottom) } var updateSizeAndInsets = ListViewUpdateSizeAndInsets( size: listSize, insets: listInsets, duration: 0.0, curve: .Default(duration: nil) ) var animateTransition = false var transactionOptions: ListViewDeleteAndInsertOptions = [] if !transition.animation.isImmediate, let previousComponent { if previousComponent.itemSetId == component.itemSetId { transactionOptions.insert(.AnimateInsertion) } animateTransition = true switch transition.animation { case .none: break case let .curve(duration, curve): updateSizeAndInsets.duration = duration switch curve { case .linear, .easeInOut: updateSizeAndInsets.curve = .Default(duration: duration) case .spring: updateSizeAndInsets.curve = .Spring(duration: duration) case let .custom(a, b, c, d): updateSizeAndInsets.curve = .Custom(duration: duration, a, b, c, d) } } } var entries: [ItemEntry] = [] for item in component.items { entries.append(ItemEntry( contents: item, index: entries.count )) } var scrollToItem: ListViewScrollToItem? if let resetScrollingRequest = component.externalStateValue.resetScrollingRequest, previousComponent?.externalStateValue.resetScrollingRequest != component.externalStateValue.resetScrollingRequest { //TODO:release calculate direction hint if let index = entries.firstIndex(where: { $0.id == resetScrollingRequest.id }) { scrollToItem = ListViewScrollToItem( index: index, position: .visible, animated: animateTransition, curve: updateSizeAndInsets.curve, directionHint: .Down ) } } self.ignoreUpdateVisibleItems = true let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries) self.currentEntries = entries let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: .Down) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: nil) } transactionOptions.insert(.Synchronous) self.listNode.transaction( deleteIndices: deletions, insertIndicesAndItems: insertions, updateIndicesAndItems: updates, options: transactionOptions, scrollToItem: scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in } ) let mappedListFrame: CGRect switch component.direction { case .vertical: mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize) case .horizontal: mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize) } self.listNode.position = mappedListFrame.origin self.listNode.bounds = CGRect(origin: CGPoint(), size: mappedListFrame.size) self.listNode.reorderItem = { [weak self] fromIndex, toIndex, _ in guard let self, let component = self.component else { return .single(false) } guard let reorderItems = component.reorderItems else { return .single(false) } if reorderItems(fromIndex, toIndex) { return .single(true) } else { return .single(false) } } self.ignoreUpdateVisibleItems = false self.updateVisibleItems(transition: transition) return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }