import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import ComponentFlow import TelegramPresentationData import AccountContext import ComponentDisplayAdapters import MultilineTextComponent import EmojiStatusComponent import TelegramStringFormatting import SolidRoundedButtonComponent import PresentationDataUtils protocol ContextMenuItemWithAction: AnyObject { func performAction() -> ContextMenuPerformActionResult } enum ContextMenuPerformActionResult { case none case clearHighlight } private final class ContextMenuActionItem: Component, ContextMenuItemWithAction { typealias EnvironmentType = ContextMenuActionItemEnvironment let title: String let action: () -> ContextMenuPerformActionResult init(title: String, action: @escaping () -> ContextMenuPerformActionResult) { self.title = title self.action = action } static func ==(lhs: ContextMenuActionItem, rhs: ContextMenuActionItem) -> Bool { if lhs.title != rhs.title { return false } return true } func performAction() -> ContextMenuPerformActionResult { return self.action() } final class View: UIView { private let titleView: ComponentView override init(frame: CGRect) { self.titleView = ComponentView() super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: ContextMenuActionItem, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let contextEnvironment = environment[EnvironmentType.self].value let sideInset: CGFloat = 16.0 let height: CGFloat = 44.0 let titleSize = self.titleView.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: contextEnvironment.theme.contextMenu.primaryColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) if let view = self.titleView.view { if view.superview == nil { self.addSubview(view) } transition.setFrame(view: view, frame: titleFrame) } return CGSize(width: sideInset * 2.0 + titleSize.width, height: height) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class ContextMenuActionItemEnvironment: Equatable { let theme: PresentationTheme init( theme: PresentationTheme ) { self.theme = theme } static func ==(lhs: ContextMenuActionItemEnvironment, rhs: ContextMenuActionItemEnvironment) -> Bool { if lhs.theme !== rhs.theme { return false } return true } } private final class ContextMenuActionsComponent: Component { let theme: PresentationTheme let items: [AnyComponentWithIdentity] init( theme: PresentationTheme, items: [AnyComponentWithIdentity] ) { self.theme = theme self.items = items } static func ==(lhs: ContextMenuActionsComponent, rhs: ContextMenuActionsComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.items != rhs.items { return false } return true } final class View: UIButton { private final class ItemView { let view = ComponentView() let separatorView = UIView() } private let backgroundView: BlurredBackgroundView private var itemViews: [AnyHashable: ItemView] = [:] private var highligntedBackgroundView: UIView? private var component: ContextMenuActionsComponent? override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) super.init(frame: frame) self.clipsToBounds = true self.layer.cornerRadius = 14.0 self.addSubview(self.backgroundView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { self.setHighlightedItem(id: self.itemAtPoint(point: touch.location(in: self))) return super.beginTracking(touch, with: event) } override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { self.setHighlightedItem(id: self.itemAtPoint(point: touch.location(in: self))) return super.continueTracking(touch, with: event) } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { if let component = self.component, let touch = touch, let id = self.itemAtPoint(point: touch.location(in: self)) { self.setHighlightedItem(id: id) for item in component.items { if item.id == id { if let itemComponent = item.component.wrapped as? ContextMenuItemWithAction { switch itemComponent.performAction() { case .none: break case .clearHighlight: self.setHighlightedItem(id: nil) } } break } } } else { self.setHighlightedItem(id: nil) } super.endTracking(touch, with: event) } override func cancelTracking(with event: UIEvent?) { self.setHighlightedItem(id: nil) super.cancelTracking(with: event) } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { self.setHighlightedItem(id: nil) super.touchesCancelled(touches, with: event) } private func itemAtPoint(point: CGPoint) -> AnyHashable? { for (id, itemView) in self.itemViews { guard let itemComponentView = itemView.view.view else { continue } let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemComponentView.frame.minY), size: CGSize(width: self.bounds.width, height: itemComponentView.bounds.height)) if itemFrame.contains(point) { return id } } return nil } private func setHighlightedItem(id: AnyHashable?) { if let component = self.component, let id = id, let itemView = self.itemViews[id], let itemComponentView = itemView.view.view { let highligntedBackgroundView: UIView if let current = self.highligntedBackgroundView { highligntedBackgroundView = current } else { highligntedBackgroundView = UIView() self.highligntedBackgroundView = highligntedBackgroundView var found = false outer: for subview in self.subviews { for (_, listItemView) in self.itemViews { if subview === listItemView.view.view { self.insertSubview(highligntedBackgroundView, belowSubview: subview) found = true break outer } } } if !found { self.insertSubview(highligntedBackgroundView, aboveSubview: self.backgroundView) } highligntedBackgroundView.backgroundColor = component.theme.contextMenu.itemHighlightedBackgroundColor } var highlightFrame = CGRect(origin: CGPoint(x: 0.0, y: itemComponentView.frame.minY), size: CGSize(width: self.bounds.width, height: itemComponentView.bounds.height)) if id != component.items.last?.id { highlightFrame.size.height += UIScreenPixel } highligntedBackgroundView.frame = highlightFrame } else { if let highligntedBackgroundView = self.highligntedBackgroundView { self.highligntedBackgroundView = nil highligntedBackgroundView.removeFromSuperview() } } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } return self } func update(component: ContextMenuActionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component let availableItemSize = availableSize var itemsSize = CGSize() var validIds = Set() var currentItems: [(id: AnyHashable, itemFrame: CGRect, itemTransition: Transition)] = [] for i in 0 ..< component.items.count { let item = component.items[i] validIds.insert(item.id) let itemView: ItemView var itemTransition = transition if let current = self.itemViews[item.id] { itemView = current } else { itemTransition = .immediate itemView = ItemView() self.itemViews[item.id] = itemView self.insertSubview(itemView.separatorView, aboveSubview: self.backgroundView) } let itemSize = itemView.view.update( transition: itemTransition, component: item.component, environment: { ContextMenuActionItemEnvironment(theme: component.theme) }, containerSize: availableItemSize ) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsSize.height), size: itemSize) if let view = itemView.view.view { if view.superview == nil { self.addSubview(view) } itemTransition.setFrame(view: view, frame: itemFrame) } currentItems.append((item.id, itemFrame, itemTransition)) itemsSize.width = max(itemsSize.width, itemSize.width) itemsSize.height += itemSize.height } itemsSize.width = max(itemsSize.width, 180.0) for i in 0 ..< currentItems.count { let item = currentItems[i] guard let itemView = self.itemViews[item.id] else { continue } itemView.separatorView.backgroundColor = component.theme.contextMenu.itemSeparatorColor itemView.separatorView.isHidden = i == currentItems.count - 1 item.itemTransition.setFrame(view: itemView.separatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: item.itemFrame.maxY), size: CGSize(width: itemsSize.width, height: UIScreenPixel))) } var removeIds: [AnyHashable] = [] for (id, itemView) in self.itemViews { if !validIds.contains(id) { removeIds.append(id) itemView.view.view?.removeFromSuperview() itemView.separatorView.removeFromSuperview() } } self.backgroundView.updateColor(color: component.theme.contextMenu.backgroundColor, transition: .immediate) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: itemsSize)) self.backgroundView.update(size: itemsSize, transition: transition.containedViewLayoutTransition) return itemsSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class TimeSelectionControlComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let bottomInset: CGFloat let apply: (Int32) -> Void let cancel: () -> Void init( theme: PresentationTheme, strings: PresentationStrings, bottomInset: CGFloat, apply: @escaping (Int32) -> Void, cancel: @escaping () -> Void ) { self.theme = theme self.strings = strings self.bottomInset = bottomInset self.apply = apply self.cancel = cancel } static func ==(lhs: TimeSelectionControlComponent, rhs: TimeSelectionControlComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.bottomInset != rhs.bottomInset { return false } return true } final class View: UIView { private let backgroundView: BlurredBackgroundView private let pickerView: UIDatePicker private let titleView: ComponentView private let leftButtonView: ComponentView private let actionButtonView: ComponentView private var component: TimeSelectionControlComponent? override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.pickerView = UIDatePicker() self.titleView = ComponentView() self.leftButtonView = ComponentView() self.actionButtonView = ComponentView() super.init(frame: frame) self.addSubview(self.backgroundView) self.pickerView.timeZone = TimeZone(secondsFromGMT: 0) self.pickerView.datePickerMode = .countDownTimer self.pickerView.datePickerMode = .dateAndTime if #available(iOS 13.4, *) { self.pickerView.preferredDatePickerStyle = .wheels } self.pickerView.minimumDate = Date(timeIntervalSince1970: Date().timeIntervalSince1970 + Double(TimeZone.current.secondsFromGMT())) self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) self.addSubview(self.pickerView) self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func datePickerUpdated() { } func update(component: TimeSelectionControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { if self.component?.theme !== component.theme { UILabel.setDateLabel(component.theme.list.itemPrimaryTextColor) self.pickerView.setValue(component.theme.list.itemPrimaryTextColor, forKey: "textColor") self.backgroundView.updateColor(color: component.theme.contextMenu.backgroundColor, transition: .immediate) } self.component = component let topPanelHeight: CGFloat = 54.0 let pickerSpacing: CGFloat = 10.0 let pickerSize = CGSize(width: availableSize.width, height: 216.0) let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + pickerSpacing), size: pickerSize) let titleSize = self.titleView.update( transition: transition, component: AnyComponent(Text(text: component.strings.EmojiStatusSetup_SetUntil, font: Font.semibold(17.0), color: component.theme.list.itemPrimaryTextColor)), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) ) if let titleComponentView = self.titleView.view { if titleComponentView.superview == nil { self.addSubview(titleComponentView) } transition.setFrame(view: titleComponentView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((topPanelHeight - titleSize.height) / 2.0)), size: titleSize)) } let leftButtonSize = self.leftButtonView.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(Text( text: component.strings.Common_Cancel, font: Font.regular(17.0), color: component.theme.list.itemAccentColor )), action: { [weak self] in self?.component?.cancel() } ).minSize(CGSize(width: 16.0, height: topPanelHeight))), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) ) if let leftButtonComponentView = self.leftButtonView.view { if leftButtonComponentView.superview == nil { self.addSubview(leftButtonComponentView) } transition.setFrame(view: leftButtonComponentView, frame: CGRect(origin: CGPoint(x: 16.0, y: floor((topPanelHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize)) } let actionButtonSize = self.actionButtonView.update( transition: transition, component: AnyComponent(SolidRoundedButtonComponent( title: component.strings.EmojiStatusSetup_SetUntil, icon: nil, theme: SolidRoundedButtonComponent.Theme(theme: component.theme), font: .bold, fontSize: 17.0, height: 50.0, cornerRadius: 10.0, gloss: false, action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return } let timestamp = Int32(strongSelf.pickerView.date.timeIntervalSince1970 - Double(TimeZone.current.secondsFromGMT())) component.apply(timestamp) } )), environment: {}, containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 50.0) ) let actionButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - actionButtonSize.width) / 2.0), y: pickerFrame.maxY + pickerSpacing), size: actionButtonSize) if let actionButtonComponentView = self.actionButtonView.view { if actionButtonComponentView.superview == nil { self.addSubview(actionButtonComponentView) } transition.setFrame(view: actionButtonComponentView, frame: actionButtonFrame) } self.pickerView.frame = pickerFrame var size = CGSize(width: availableSize.width, height: actionButtonFrame.maxY) if component.bottomInset.isZero { size.height += 10.0 } else { size.height += max(10.0, component.bottomInset) } self.backgroundView.update(size: size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) return size } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } final class EmojiStatusPreviewScreenComponent: Component { struct StatusResult { let timestamp: Int32 let sourceView: UIView } final class TransitionAnimation { enum TransitionType { case animateIn(sourceLayer: CALayer) } let transitionType: TransitionType init(transitionType: TransitionType) { self.transitionType = transitionType } } private enum CurrentState { case menu case timeSelection } typealias EnvironmentType = Empty let theme: PresentationTheme let strings: PresentationStrings let bottomInset: CGFloat let item: EmojiStatusComponent let dismiss: (StatusResult?) -> Void init( theme: PresentationTheme, strings: PresentationStrings, bottomInset: CGFloat, item: EmojiStatusComponent, dismiss: @escaping (StatusResult?) -> Void ) { self.theme = theme self.strings = strings self.bottomInset = bottomInset self.item = item self.dismiss = dismiss } static func ==(lhs: EmojiStatusPreviewScreenComponent, rhs: EmojiStatusPreviewScreenComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.bottomInset != rhs.bottomInset { return false } if lhs.item != rhs.item { return false } return true } final class View: UIView { private let backgroundView: BlurredBackgroundView private let itemView: ComponentView private let actionsView: ComponentView private let timeSelectionView: ComponentView private var currentState: CurrentState = .menu private var component: EmojiStatusPreviewScreenComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.itemView = ComponentView() self.actionsView = ComponentView() self.timeSelectionView = ComponentView() super.init(frame: frame) self.addSubview(self.backgroundView) self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { switch self.currentState { case .menu: self.component?.dismiss(nil) case .timeSelection: self.toggleState() } } } private func toggleState() { switch self.currentState { case .menu: self.currentState = .timeSelection self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring))) case .timeSelection: self.currentState = .menu self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) } } func update(component: EmojiStatusPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state let itemSpacing: CGFloat = 12.0 let itemSize = self.itemView.update( transition: transition, component: AnyComponent(component.item), environment: {}, containerSize: CGSize(width: 128.0, height: 128.0) ) var menuItems: [AnyComponentWithIdentity] = [] let delayDurations: [Int] = [ 1 * 60 * 60, 2 * 60 * 60, 8 * 60 * 60, 2 * 24 * 60 * 60 ] for duration in delayDurations { menuItems.append(AnyComponentWithIdentity(id: duration, component: AnyComponent(ContextMenuActionItem( title: setTimeoutForIntervalString(strings: component.strings, value: Int32(duration)), action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return .none } guard let itemComponentView = strongSelf.itemView.view else { return .none } component.dismiss(StatusResult(timestamp: Int32(Date().timeIntervalSince1970) + Int32(duration), sourceView: itemComponentView)) return .none } )))) } //TODO:localize menuItems.append(AnyComponentWithIdentity(id: "Other", component: AnyComponent(ContextMenuActionItem( title: component.strings.EmojiStatusSetup_TimerOther, action: { [weak self] in self?.toggleState() return .clearHighlight } )))) let actionsSize = self.actionsView.update( transition: transition, component: AnyComponent(ContextMenuActionsComponent( theme: component.theme, items: menuItems )), environment: {}, containerSize: availableSize ) let timeSelectionSize = self.timeSelectionView.update( transition: transition, component: AnyComponent(TimeSelectionControlComponent( theme: component.theme, strings: component.strings, bottomInset: component.bottomInset, apply: { [weak self] timestamp in guard let strongSelf = self, let component = strongSelf.component else { return } guard let itemComponentView = strongSelf.itemView.view else { return } component.dismiss(StatusResult(timestamp: timestamp, sourceView: itemComponentView)) }, cancel: { [weak self] in self?.toggleState() } )), environment: {}, containerSize: availableSize ) let totalContentHeight = itemSize.height + itemSpacing + max(actionsSize.height, timeSelectionSize.height) let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - totalContentHeight) / 2.0)), size: CGSize(width: availableSize.width, height: totalContentHeight)) let itemFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - itemSize.width) / 2.0), y: contentFrame.minY), size: itemSize) let actionsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - actionsSize.width) / 2.0), y: itemFrame.maxY + itemSpacing), size: actionsSize) var timeSelectionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - timeSelectionSize.width) / 2.0), y: availableSize.height - timeSelectionSize.height), size: timeSelectionSize) if case .menu = self.currentState { timeSelectionFrame.origin.y = availableSize.height } if let itemComponentView = self.itemView.view { if itemComponentView.superview == nil { self.addSubview(itemComponentView) } transition.setFrame(view: itemComponentView, frame: itemFrame) } if let actionsComponentView = self.actionsView.view { if actionsComponentView.superview == nil { self.addSubview(actionsComponentView) } transition.setPosition(view: actionsComponentView, position: actionsFrame.center) transition.setBounds(view: actionsComponentView, bounds: CGRect(origin: CGPoint(), size: actionsFrame.size)) if case .menu = self.currentState { transition.setTransform(view: actionsComponentView, transform: CATransform3DIdentity) transition.setAlpha(view: actionsComponentView, alpha: 1.0) actionsComponentView.isUserInteractionEnabled = true } else { transition.setTransform(view: actionsComponentView, transform: CATransform3DMakeScale(0.001, 0.001, 1.0)) transition.setAlpha(view: actionsComponentView, alpha: 0.0) actionsComponentView.isUserInteractionEnabled = false } } if let timeSelectionComponentView = self.timeSelectionView.view { if timeSelectionComponentView.superview == nil { self.addSubview(timeSelectionComponentView) } transition.setFrame(view: timeSelectionComponentView, frame: timeSelectionFrame) } self.backgroundView.updateColor(color: component.theme.contextMenu.dimColor, transition: .immediate) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize)) self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition) if let transitionAnimation = transition.userData(TransitionAnimation.self) { switch transitionAnimation.transitionType { case let .animateIn(sourceLayer): var additionalPositionDifference = CGPoint() if let copyLayer = sourceLayer.snapshotContentTree(), let itemComponentView = self.itemView.view { sourceLayer.isHidden = true copyLayer.frame = sourceLayer.convert(sourceLayer.bounds, to: self.layer) self.layer.addSublayer(copyLayer) copyLayer.animatePosition(from: copyLayer.frame.center, to: itemComponentView.frame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) copyLayer.animateScale(from: 1.0, to: itemComponentView.bounds.width / copyLayer.bounds.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) copyLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyLayer] _ in copyLayer?.removeFromSuperlayer() }) additionalPositionDifference = CGPoint(x: itemComponentView.frame.center.x - copyLayer.frame.center.x, y: itemComponentView.frame.center.y - copyLayer.frame.center.y) itemComponentView.layer.animatePosition(from: copyLayer.frame.center, to: itemComponentView.frame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16) itemComponentView.layer.animateScale(from: copyLayer.bounds.width / itemComponentView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if let actionsComponentView = self.actionsView.view { actionsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) actionsComponentView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6) actionsComponentView.layer.animateSpring(from: (-actionsComponentView.bounds.height / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.translation.y", duration: 0.6) let _ = additionalPositionDifference } } } return availableSize } func animateOut(targetLayer: CALayer?, completion: @escaping () -> Void) { if let targetLayer = targetLayer, let itemComponentView = self.itemView.view { targetLayer.isHidden = false let targetLayerPosition = targetLayer.position let targetLayerSuperlayer = targetLayer.superlayer var targetLayerIndexPosition: UInt32? if let targetLayerSuperlayer = targetLayerSuperlayer { if let index = targetLayerSuperlayer.sublayers?.firstIndex(of: targetLayer) { targetLayerIndexPosition = UInt32(index) } } let localTargetPosition = targetLayer.convert(targetLayer.bounds.center, to: self.layer) self.layer.addSublayer(targetLayer) targetLayer.position = localTargetPosition targetLayer.animatePosition(from: itemComponentView.frame.center, to: localTargetPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) targetLayer.animateScale(from: itemComponentView.bounds.width / targetLayer.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak targetLayer, weak targetLayerSuperlayer] _ in if let targetLayer = targetLayer, let targetLayerSuperlayer = targetLayerSuperlayer { if let targetLayerIndexPosition = targetLayerIndexPosition { targetLayerSuperlayer.insertSublayer(targetLayer, at: targetLayerIndexPosition) targetLayer.position = targetLayerPosition } } completion() }) targetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16) itemComponentView.layer.animatePosition(from: itemComponentView.frame.center, to: localTargetPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) itemComponentView.layer.animateScale(from: 1.0, to: targetLayer.bounds.width / itemComponentView.bounds.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) if let actionsComponentView = self.actionsView.view { actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in }) } if let timeSelectionComponentView = self.timeSelectionView.view { timeSelectionComponentView.layer.animatePosition(from: timeSelectionComponentView.layer.position, to: CGPoint(x: timeSelectionComponentView.layer.position.x, y: self.bounds.height + timeSelectionComponentView.bounds.height / 2.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } else { self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) if let actionsComponentView = self.actionsView.view { actionsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completion() }) if let timeSelectionComponentView = self.timeSelectionView.view { timeSelectionComponentView.layer.animatePosition(from: timeSelectionComponentView.layer.position, to: CGPoint(x: timeSelectionComponentView.layer.position.x, y: self.bounds.height + timeSelectionComponentView.bounds.height / 2.0), duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } else { completion() } } } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }