diff --git a/submodules/Display/.gitignore b/submodules/Display/.gitignore new file mode 100644 index 0000000000..6e879fb6f0 --- /dev/null +++ b/submodules/Display/.gitignore @@ -0,0 +1,25 @@ +fastlane/README.md +fastlane/report.xml +fastlane/test_output/* +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.xcscmblueprint +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +.DS_Store +*.dSYM +*.dSYM.zip +*.ipa +*/xcuserdata/* +Display.xcodeproj/* diff --git a/submodules/Display/.gitmodules b/submodules/Display/.gitmodules new file mode 100644 index 0000000000..e69de29bb2 diff --git a/submodules/Display/BUCK b/submodules/Display/BUCK new file mode 100644 index 0000000000..d073d26cc1 --- /dev/null +++ b/submodules/Display/BUCK @@ -0,0 +1,53 @@ +load('//tools:buck_utils.bzl', 'config_with_updated_linker_flags', 'configs_with_config', 'combined_config') +load('//tools:buck_defs.bzl', 'SHARED_CONFIGS', 'EXTENSION_LIB_SPECIFIC_CONFIG') + +apple_library( + name = 'DisplayPrivate', + srcs = glob([ + 'Display/*.m', + ]), + headers = glob([ + 'Display/*.h', + ]), + header_namespace = 'DisplayPrivate', + exported_headers = glob([ + 'Display/*.h', + ], exclude = ['Display/Display.h']), + modular = True, + configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])), + compiler_flags = ['-w'], + preprocessor_flags = ['-fobjc-arc'], + visibility = ['//submodules/Display:Display'], + deps = [ + '//submodules/AsyncDisplayKit:AsyncDisplayKit', + ], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + '$SDKROOT/System/Library/Frameworks/UIKit.framework', + ], +) + +apple_library( + name = 'Display', + srcs = glob([ + 'Display/*.swift', + ]), + configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])), + swift_compiler_flags = [ + '-suppress-warnings', + '-application-extension', + ], + visibility = ['PUBLIC'], + deps = [ + ':DisplayPrivate', + '//submodules/AsyncDisplayKit:AsyncDisplayKit', + '//submodules/SSignalKit:SwiftSignalKit', + ], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + '$SDKROOT/System/Library/Frameworks/UIKit.framework', + '$SDKROOT/System/Library/Frameworks/QuartzCore.framework', + '$SDKROOT/System/Library/Frameworks/CoreText.framework', + '$SDKROOT/System/Library/Frameworks/CoreGraphics.framework', + ], +) diff --git a/submodules/Display/Display/ASTransformLayerNode.swift b/submodules/Display/Display/ASTransformLayerNode.swift new file mode 100644 index 0000000000..e995d3915c --- /dev/null +++ b/submodules/Display/Display/ASTransformLayerNode.swift @@ -0,0 +1,69 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +class ASTransformLayer: CATransformLayer { + override var contents: Any? { + get { + return nil + } set(value) { + + } + } + + override var backgroundColor: CGColor? { + get { + return nil + } set(value) { + + } + } + + override func setNeedsLayout() { + } + + override func layoutSublayers() { + } +} + +class ASTransformView: UIView { + override class var layerClass: AnyClass { + return ASTransformLayer.self + } +} + +open class ASTransformLayerNode: ASDisplayNode { + public override init() { + super.init() + self.setLayerBlock({ + return ASTransformLayer() + }) + } +} + +open class ASTransformViewNode: ASDisplayNode { + public override init() { + super.init() + + self.setViewBlock({ + return ASTransformView() + }) + } +} + +open class ASTransformNode: ASDisplayNode { + public init(layerBacked: Bool = true) { + if layerBacked { + super.init() + self.setLayerBlock({ + return ASTransformLayer() + }) + } else { + super.init() + + self.setViewBlock({ + return ASTransformView() + }) + } + } +} diff --git a/submodules/Display/Display/Accessibility.swift b/submodules/Display/Display/Accessibility.swift new file mode 100644 index 0000000000..77a8835a7f --- /dev/null +++ b/submodules/Display/Display/Accessibility.swift @@ -0,0 +1,21 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public func addAccessibilityChildren(of node: ASDisplayNode, container: Any, to list: inout [Any]) { + if node.isAccessibilityElement { + let element = UIAccessibilityElement(accessibilityContainer: container) + element.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view) + element.accessibilityLabel = node.accessibilityLabel + element.accessibilityValue = node.accessibilityValue + element.accessibilityTraits = node.accessibilityTraits + element.accessibilityHint = node.accessibilityHint + element.accessibilityIdentifier = node.accessibilityIdentifier + + //node.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view) + list.append(element) + } else if let accessibilityElements = node.accessibilityElements { + list.append(contentsOf: accessibilityElements) + } +} + diff --git a/submodules/Display/Display/AccessibilityAreaNode.swift b/submodules/Display/Display/AccessibilityAreaNode.swift new file mode 100644 index 0000000000..6f351dd33e --- /dev/null +++ b/submodules/Display/Display/AccessibilityAreaNode.swift @@ -0,0 +1,21 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public final class AccessibilityAreaNode: ASDisplayNode { + public var activate: (() -> Bool)? + + override public init() { + super.init() + + self.isAccessibilityElement = true + } + + override public func accessibilityActivate() -> Bool { + return self.activate?() ?? false + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} diff --git a/submodules/Display/Display/ActionSheetButtonItem.swift b/submodules/Display/Display/ActionSheetButtonItem.swift new file mode 100644 index 0000000000..ff08e0c293 --- /dev/null +++ b/submodules/Display/Display/ActionSheetButtonItem.swift @@ -0,0 +1,138 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum ActionSheetButtonColor { + case accent + case destructive + case disabled +} + + +public enum ActionSheetButtonFont { + case `default` + case bold +} + +public class ActionSheetButtonItem: ActionSheetItem { + public let title: String + public let color: ActionSheetButtonColor + public let font: ActionSheetButtonFont + public let enabled: Bool + public let action: () -> Void + + public init(title: String, color: ActionSheetButtonColor = .accent, font: ActionSheetButtonFont = .default, enabled: Bool = true, action: @escaping () -> Void) { + self.title = title + self.color = color + self.font = font + self.enabled = enabled + self.action = action + } + + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetButtonNode(theme: theme) + node.setItem(self) + return node + } + + public func updateNode(_ node: ActionSheetItemNode) { + guard let node = node as? ActionSheetButtonNode else { + assertionFailure() + return + } + + node.setItem(self) + } +} + +public class ActionSheetButtonNode: ActionSheetItemNode { + private let theme: ActionSheetControllerTheme + + public static let defaultFont: UIFont = Font.regular(20.0) + public static let boldFont: UIFont = Font.medium(20.0) + + private var item: ActionSheetButtonItem? + + private let button: HighlightTrackingButton + private let label: ASTextNode + + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + + self.button = HighlightTrackingButton() + + self.label = ASTextNode() + self.label.isUserInteractionEnabled = false + self.label.maximumNumberOfLines = 1 + self.label.displaysAsynchronously = false + self.label.truncationMode = .byTruncatingTail + + super.init(theme: theme) + + self.view.addSubview(self.button) + + self.label.isUserInteractionEnabled = false + self.addSubnode(self.label) + + self.button.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor + }) + } + } + } + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + func setItem(_ item: ActionSheetButtonItem) { + self.item = item + + let textColor: UIColor + let textFont: UIFont + switch item.color { + case .accent: + textColor = self.theme.standardActionTextColor + case .destructive: + textColor = self.theme.destructiveActionTextColor + case .disabled: + textColor = self.theme.disabledActionTextColor + } + switch item.font { + case .default: + textFont = ActionSheetButtonNode.defaultFont + case .bold: + textFont = ActionSheetButtonNode.boldFont + } + self.label.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) + + self.button.isEnabled = item.enabled + + self.setNeedsLayout() + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 57.0) + } + + public override func layout() { + super.layout() + + let size = self.bounds.size + + self.button.frame = CGRect(origin: CGPoint(), size: size) + + let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 10.0), height: size.height)) + self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + } + + @objc func buttonPressed() { + if let item = self.item { + item.action() + } + } +} diff --git a/submodules/Display/Display/ActionSheetCheckboxItem.swift b/submodules/Display/Display/ActionSheetCheckboxItem.swift new file mode 100644 index 0000000000..0b3af0046b --- /dev/null +++ b/submodules/Display/Display/ActionSheetCheckboxItem.swift @@ -0,0 +1,147 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum ActionSheetCheckboxStyle { + case `default` + case alignRight +} + +public class ActionSheetCheckboxItem: ActionSheetItem { + public let title: String + public let label: String + public let value: Bool + public let style: ActionSheetCheckboxStyle + public let action: (Bool) -> Void + + public init(title: String, label: String, value: Bool, style: ActionSheetCheckboxStyle = .default, action: @escaping (Bool) -> Void) { + self.title = title + self.label = label + self.value = value + self.style = style + self.action = action + } + + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetCheckboxItemNode(theme: theme) + node.setItem(self) + return node + } + + public func updateNode(_ node: ActionSheetItemNode) { + guard let node = node as? ActionSheetCheckboxItemNode else { + assertionFailure() + return + } + + node.setItem(self) + } +} + +public class ActionSheetCheckboxItemNode: ActionSheetItemNode { + public static let defaultFont: UIFont = Font.regular(20.0) + + private let theme: ActionSheetControllerTheme + + private var item: ActionSheetCheckboxItem? + + private let button: HighlightTrackingButton + private let titleNode: ImmediateTextNode + private let labelNode: ImmediateTextNode + private let checkNode: ASImageNode + + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + + self.button = HighlightTrackingButton() + + self.titleNode = ImmediateTextNode() + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.labelNode = ImmediateTextNode() + self.labelNode.maximumNumberOfLines = 1 + self.labelNode.isUserInteractionEnabled = false + self.labelNode.displaysAsynchronously = false + + self.checkNode = ASImageNode() + self.checkNode.isUserInteractionEnabled = false + self.checkNode.displayWithoutProcessing = true + self.checkNode.displaysAsynchronously = false + self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.controlAccentColor.cgColor) + context.setLineWidth(2.0) + context.move(to: CGPoint(x: 12.0, y: 1.0)) + context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) + context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) + context.strokePath() + }) + + super.init(theme: theme) + + self.view.addSubview(self.button) + self.addSubnode(self.titleNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.checkNode) + + self.button.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor + }) + } + } + } + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + func setItem(_ item: ActionSheetCheckboxItem) { + self.item = item + + self.titleNode.attributedText = NSAttributedString(string: item.title, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.primaryTextColor) + self.labelNode.attributedText = NSAttributedString(string: item.label, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.secondaryTextColor) + self.checkNode.isHidden = !item.value + + self.setNeedsLayout() + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 57.0) + } + + public override func layout() { + super.layout() + + let size = self.bounds.size + + self.button.frame = CGRect(origin: CGPoint(), size: size) + + var titleOrigin: CGFloat = 44.0 + var checkOrigin: CGFloat = 22.0 + if let item = self.item, item.style == .alignRight { + titleOrigin = 24.0 + checkOrigin = size.width - 22.0 + } + + let labelSize = self.labelNode.updateLayout(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height)) + let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + + if let image = self.checkNode.image { + self.checkNode.frame = CGRect(origin: CGPoint(x: floor(checkOrigin - (image.size.width / 2.0)), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + } + } + + @objc func buttonPressed() { + if let item = self.item { + item.action(!item.value) + } + } +} diff --git a/submodules/Display/Display/ActionSheetController.swift b/submodules/Display/Display/ActionSheetController.swift new file mode 100644 index 0000000000..46df40e0ca --- /dev/null +++ b/submodules/Display/Display/ActionSheetController.swift @@ -0,0 +1,82 @@ +import Foundation +import UIKit + +open class ActionSheetController: ViewController, PresentableController { + private var actionSheetNode: ActionSheetControllerNode { + return self.displayNode as! ActionSheetControllerNode + } + + public var theme: ActionSheetControllerTheme { + didSet { + if oldValue != self.theme { + self.actionSheetNode.theme = self.theme + } + } + } + + private var groups: [ActionSheetItemGroup] = [] + + private var isDismissed: Bool = false + + public var dismissed: ((Bool) -> Void)? + + public init(theme: ActionSheetControllerTheme) { + self.theme = theme + + super.init(navigationBarPresentationData: nil) + + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func dismissAnimated() { + if !self.isDismissed { + self.isDismissed = true + self.actionSheetNode.animateOut(cancelled: false) + } + } + + open override func loadDisplayNode() { + self.displayNode = ActionSheetControllerNode(theme: self.theme) + self.displayNodeDidLoad() + + self.actionSheetNode.dismiss = { [weak self] cancelled in + self?.dismissed?(cancelled) + self?.presentingViewController?.dismiss(animated: false) + } + + self.actionSheetNode.setGroups(self.groups) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.actionSheetNode.containerLayoutUpdated(layout, transition: transition) + } + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.viewDidAppear(completion: {}) + } + + public func viewDidAppear(completion: @escaping () -> Void) { + self.actionSheetNode.animateIn(completion: completion) + } + + public func setItemGroups(_ groups: [ActionSheetItemGroup]) { + self.groups = groups + if self.isViewLoaded { + self.actionSheetNode.setGroups(groups) + } + } + + public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) { + if self.isViewLoaded { + self.actionSheetNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f) + } + } +} diff --git a/submodules/Display/Display/ActionSheetControllerNode.swift b/submodules/Display/Display/ActionSheetControllerNode.swift new file mode 100644 index 0000000000..820547892d --- /dev/null +++ b/submodules/Display/Display/ActionSheetControllerNode.swift @@ -0,0 +1,200 @@ +import UIKit +import AsyncDisplayKit + +private let containerInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) + +private class ActionSheetControllerNodeScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } +} + +final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { + var theme: ActionSheetControllerTheme { + didSet { + self.itemGroupsContainerNode.theme = self.theme + self.updateTheme() + } + } + + private let dismissTapView: UIView + + private let leftDimView: UIView + private let rightDimView: UIView + private let topDimView: UIView + private let bottomDimView: UIView + + private let itemGroupsContainerNode: ActionSheetItemGroupsContainerNode + + private let scrollView: UIScrollView + + var dismiss: (Bool) -> Void = { _ in } + + private var validLayout: ContainerViewLayout? + + init(theme: ActionSheetControllerTheme) { + self.theme = theme + + self.scrollView = ActionSheetControllerNodeScrollView() + + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + self.scrollView.alwaysBounceVertical = true + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + + self.dismissTapView = UIView() + + self.leftDimView = UIView() + self.leftDimView.isUserInteractionEnabled = false + + self.rightDimView = UIView() + self.rightDimView.isUserInteractionEnabled = false + + self.topDimView = UIView() + self.topDimView.isUserInteractionEnabled = false + + self.bottomDimView = UIView() + self.bottomDimView.isUserInteractionEnabled = false + + self.itemGroupsContainerNode = ActionSheetItemGroupsContainerNode(theme: self.theme) + + super.init() + + self.scrollView.delegate = self + + self.view.addSubview(self.scrollView) + + self.scrollView.addSubview(self.dismissTapView) + + self.scrollView.addSubview(self.leftDimView) + self.scrollView.addSubview(self.rightDimView) + self.scrollView.addSubview(self.topDimView) + self.scrollView.addSubview(self.bottomDimView) + + self.dismissTapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) + + self.scrollView.addSubnode(self.itemGroupsContainerNode) + + self.updateTheme() + } + + func updateTheme() { + self.leftDimView.backgroundColor = self.theme.dimColor + self.rightDimView.backgroundColor = self.theme.dimColor + self.topDimView.backgroundColor = self.theme.dimColor + self.bottomDimView.backgroundColor = self.theme.dimColor + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.statusBar]) + + let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) + + insets.left = floor((layout.size.width - containerWidth) / 2.0) + insets.right = insets.left + if !insets.bottom.isZero { + insets.bottom -= 12.0 + } + + self.validLayout = layout + + self.scrollView.frame = CGRect(origin: CGPoint(), size: layout.size) + self.dismissTapView.frame = CGRect(origin: CGPoint(), size: layout.size) + + self.itemGroupsContainerNode.measure(CGSize(width: layout.size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: layout.size.height - containerInsets.top - containerInsets.bottom - insets.top - insets.bottom)) + self.itemGroupsContainerNode.frame = CGRect(origin: CGPoint(x: insets.left + containerInsets.left, y: layout.size.height - insets.bottom - containerInsets.bottom - self.itemGroupsContainerNode.calculatedSize.height), size: self.itemGroupsContainerNode.calculatedSize) + self.itemGroupsContainerNode.layout() + + self.updateScrollDimViews(size: layout.size, insets: insets) + } + + func animateIn(completion: @escaping () -> Void) { + let tempDimView = UIView() + tempDimView.backgroundColor = self.theme.dimColor + tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height) + self.view.addSubview(tempDimView) + + for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + self.itemGroupsContainerNode.animateDimViewsAlpha(from: 0.0, to: 1.0, duration: 0.4) + + self.layer.animateBounds(from: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), to: self.bounds, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak tempDimView] _ in + tempDimView?.removeFromSuperview() + completion() + }) + } + + func animateOut(cancelled: Bool) { + let tempDimView = UIView() + tempDimView.backgroundColor = self.theme.dimColor + tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height) + self.view.addSubview(tempDimView) + + for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] { + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } + self.itemGroupsContainerNode.animateDimViewsAlpha(from: 1.0, to: 0.0, duration: 0.3) + + self.layer.animateBounds(from: self.bounds, to: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), duration: 0.35, timingFunction: kCAMediaTimingFunctionEaseOut, removeOnCompletion: false, completion: { [weak self, weak tempDimView] _ in + tempDimView?.removeFromSuperview() + + self?.dismiss(cancelled) + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result + } + + @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.animateOut(cancelled: true) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let layout = self.validLayout { + var insets = layout.insets(options: [.statusBar]) + + let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) + + insets.left = floor((layout.size.width - containerWidth) / 2.0) + insets.right = insets.left + + self.updateScrollDimViews(size: layout.size, insets: insets) + } + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = self.scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.animateOut(cancelled: true) + } + } + + func updateScrollDimViews(size: CGSize, insets: UIEdgeInsets) { + let additionalTopHeight = max(0.0, -self.scrollView.contentOffset.y) + let additionalBottomHeight = -min(0.0, -self.scrollView.contentOffset.y) + + self.topDimView.frame = CGRect(x: containerInsets.left + insets.left, y: -additionalTopHeight, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, self.itemGroupsContainerNode.frame.minY + additionalTopHeight)) + self.bottomDimView.frame = CGRect(x: containerInsets.left + insets.left, y: self.itemGroupsContainerNode.frame.maxY, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, size.height - self.itemGroupsContainerNode.frame.maxY + additionalBottomHeight)) + + self.leftDimView.frame = CGRect(x: 0.0, y: -additionalTopHeight, width: containerInsets.left + insets.left, height: size.height + additionalTopHeight + additionalBottomHeight) + self.rightDimView.frame = CGRect(x: size.width - containerInsets.right - insets.right, y: -additionalTopHeight, width: containerInsets.right + insets.right, height: size.height + additionalTopHeight + additionalBottomHeight) + } + + func setGroups(_ groups: [ActionSheetItemGroup]) { + self.itemGroupsContainerNode.setGroups(groups) + } + + public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) { + self.itemGroupsContainerNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f) + } +} diff --git a/submodules/Display/Display/ActionSheetItem.swift b/submodules/Display/Display/ActionSheetItem.swift new file mode 100644 index 0000000000..291b3478a6 --- /dev/null +++ b/submodules/Display/Display/ActionSheetItem.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol ActionSheetItem { + func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode + func updateNode(_ node: ActionSheetItemNode) -> Void +} diff --git a/submodules/Display/Display/ActionSheetItemGroup.swift b/submodules/Display/Display/ActionSheetItemGroup.swift new file mode 100644 index 0000000000..4db0b3dcd7 --- /dev/null +++ b/submodules/Display/Display/ActionSheetItemGroup.swift @@ -0,0 +1,11 @@ +import UIKit + +public final class ActionSheetItemGroup { + let items: [ActionSheetItem] + let leadingVisibleNodeCount: CGFloat? + + public init(items: [ActionSheetItem], leadingVisibleNodeCount: CGFloat? = nil) { + self.items = items + self.leadingVisibleNodeCount = leadingVisibleNodeCount + } +} diff --git a/submodules/Display/Display/ActionSheetItemGroupNode.swift b/submodules/Display/Display/ActionSheetItemGroupNode.swift new file mode 100644 index 0000000000..771f541177 --- /dev/null +++ b/submodules/Display/Display/ActionSheetItemGroupNode.swift @@ -0,0 +1,223 @@ +import UIKit +import AsyncDisplayKit + +private class ActionSheetItemGroupNodeScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } +} + +final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate { + private let theme: ActionSheetControllerTheme + + private let centerDimView: UIImageView + private let topDimView: UIView + private let bottomDimView: UIView + let trailingDimView: UIView + + private let clippingNode: ASDisplayNode + private let backgroundEffectView: UIVisualEffectView + private let scrollView: UIScrollView + + private var itemNodes: [ActionSheetItemNode] = [] + private var leadingVisibleNodeCount: CGFloat = 100.0 + + var respectInputHeight = true + + init(theme: ActionSheetControllerTheme) { + self.theme = theme + + self.centerDimView = UIImageView() + self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: self.theme.dimColor) + + self.topDimView = UIView() + self.topDimView.backgroundColor = self.theme.dimColor + self.topDimView.isUserInteractionEnabled = false + + self.bottomDimView = UIView() + self.bottomDimView.backgroundColor = self.theme.dimColor + self.bottomDimView.isUserInteractionEnabled = false + + self.trailingDimView = UIView() + self.trailingDimView.backgroundColor = self.theme.dimColor + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + self.clippingNode.cornerRadius = 16.0 + + self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark)) + + self.scrollView = ActionSheetItemGroupNodeScrollView() + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + + super.init() + + self.view.addSubview(self.centerDimView) + self.view.addSubview(self.topDimView) + self.view.addSubview(self.bottomDimView) + self.view.addSubview(self.trailingDimView) + + self.scrollView.delegate = self + + self.clippingNode.view.addSubview(self.backgroundEffectView) + self.clippingNode.view.addSubview(self.scrollView) + + self.addSubnode(self.clippingNode) + } + + func updateItemNodes(_ nodes: [ActionSheetItemNode], leadingVisibleNodeCount: CGFloat = 1000.0) { + for node in self.itemNodes { + if !nodes.contains(where: { $0 === node }) { + node.removeFromSupernode() + } + } + + for node in nodes { + if !self.itemNodes.contains(where: { $0 === node }) { + self.scrollView.addSubnode(node) + } + } + + self.itemNodes = nodes + self.leadingVisibleNodeCount = leadingVisibleNodeCount + self.invalidateCalculatedLayout() + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + var itemNodesHeight: CGFloat = 0.0 + var leadingVisibleNodeSize: CGFloat = 0.0 + + var i = 0 + for node in self.itemNodes { + if CGFloat(0.0).isLess(than: itemNodesHeight) { + itemNodesHeight += UIScreenPixel + } + let size = node.measure(constrainedSize) + itemNodesHeight += size.height + + if ceil(CGFloat(i)).isLessThanOrEqualTo(leadingVisibleNodeCount) { + if CGFloat(0.0).isLess(than: leadingVisibleNodeSize) { + leadingVisibleNodeSize += UIScreenPixel + } + let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i)) + leadingVisibleNodeSize += size.height * factor + } + i += 1 + } + + return CGSize(width: constrainedSize.width, height: min(floorToScreenPixels(itemNodesHeight), constrainedSize.height)) + } + + override func layout() { + let scrollViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize) + var updateOffset = false + if !self.scrollView.frame.equalTo(scrollViewFrame) { + self.scrollView.frame = scrollViewFrame + updateOffset = true + } + + let backgroundEffectViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize) + if !self.backgroundEffectView.frame.equalTo(backgroundEffectViewFrame) { + self.backgroundEffectView.frame = backgroundEffectViewFrame + } + + var itemNodesHeight: CGFloat = 0.0 + var leadingVisibleNodeSize: CGFloat = 0.0 + + var i = 0 + for node in self.itemNodes { + if CGFloat(0.0).isLess(than: itemNodesHeight) { + itemNodesHeight += UIScreenPixel + } + node.frame = CGRect(origin: CGPoint(x: 0.0, y: itemNodesHeight), size: node.calculatedSize) + itemNodesHeight += node.calculatedSize.height + + if CGFloat(i).isLessThanOrEqualTo(leadingVisibleNodeCount) { + if CGFloat(0.0).isLess(than: leadingVisibleNodeSize) { + leadingVisibleNodeSize += UIScreenPixel + } + let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i)) + leadingVisibleNodeSize += node.calculatedSize.height * factor + } + i += 1 + } + + let scrollViewContentSize = CGSize(width: self.calculatedSize.width, height: itemNodesHeight) + if !self.scrollView.contentSize.equalTo(scrollViewContentSize) { + self.scrollView.contentSize = scrollViewContentSize + } + let scrollViewContentInsets = UIEdgeInsets(top: max(0.0, self.calculatedSize.height - leadingVisibleNodeSize), left: 0.0, bottom: 0.0, right: 0.0) + + if !UIEdgeInsetsEqualToEdgeInsets(self.scrollView.contentInset, scrollViewContentInsets) { + self.scrollView.contentInset = scrollViewContentInsets + } + + if updateOffset { + self.scrollView.contentOffset = CGPoint(x: 0.0, y: -scrollViewContentInsets.top) + } + + self.updateOverscroll() + } + + private func currentVerticalOverscroll() -> CGFloat { + var verticalOverscroll: CGFloat = 0.0 + if scrollView.contentOffset.y < 0.0 { + verticalOverscroll = scrollView.contentOffset.y + } else if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.size.height { + verticalOverscroll = scrollView.contentOffset.y - (scrollView.contentSize.height - scrollView.bounds.size.height) + } + return verticalOverscroll + } + + private func currentRealVerticalOverscroll() -> CGFloat { + var verticalOverscroll: CGFloat = 0.0 + if scrollView.contentOffset.y < 0.0 { + verticalOverscroll = scrollView.contentOffset.y + } else if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.size.height { + verticalOverscroll = scrollView.contentOffset.y - (scrollView.contentSize.height - scrollView.bounds.size.height) + } + return verticalOverscroll + } + + private func updateOverscroll() { + let verticalOverscroll = self.currentVerticalOverscroll() + + self.clippingNode.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, min(0.0, verticalOverscroll), 0.0) + let clippingNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -verticalOverscroll)), size: CGSize(width: self.calculatedSize.width, height: self.calculatedSize.height - abs(verticalOverscroll))) + if !self.clippingNode.frame.equalTo(clippingNodeFrame) { + self.clippingNode.frame = clippingNodeFrame + + self.centerDimView.frame = clippingNodeFrame + self.topDimView.frame = CGRect(x: 0.0, y: 0.0, width: clippingNodeFrame.size.width, height: max(0.0, clippingNodeFrame.minY)) + self.bottomDimView.frame = CGRect(x: 0.0, y: clippingNodeFrame.maxY, width: clippingNodeFrame.size.width, height: max(0.0, self.bounds.size.height - clippingNodeFrame.maxY)) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateOverscroll() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.clippingNode.frame.contains(point) { + return super.hitTest(point, with: event) + } else { + return nil + } + } + + func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) { + for node in [self.centerDimView, self.topDimView, self.bottomDimView] { + node.layer.animateAlpha(from: from, to: to, duration: duration) + } + } + + func itemNode(at index: Int) -> ActionSheetItemNode { + return self.itemNodes[index] + } +} diff --git a/submodules/Display/Display/ActionSheetItemGroupsContainerNode.swift b/submodules/Display/Display/ActionSheetItemGroupsContainerNode.swift new file mode 100644 index 0000000000..0e3b9a5d08 --- /dev/null +++ b/submodules/Display/Display/ActionSheetItemGroupsContainerNode.swift @@ -0,0 +1,99 @@ +import UIKit +import AsyncDisplayKit + +private let groupSpacing: CGFloat = 8.0 + +final class ActionSheetItemGroupsContainerNode: ASDisplayNode { + var theme: ActionSheetControllerTheme { + didSet { + self.setGroups(self.groups) + self.setNeedsLayout() + } + } + + private var groups: [ActionSheetItemGroup] = [] + private var groupNodes: [ActionSheetItemGroupNode] = [] + + init(theme: ActionSheetControllerTheme) { + self.theme = theme + + super.init() + } + + func setGroups(_ groups: [ActionSheetItemGroup]) { + self.groups = groups + + for groupNode in self.groupNodes { + groupNode.removeFromSupernode() + } + self.groupNodes.removeAll() + + for group in groups { + let groupNode = ActionSheetItemGroupNode(theme: self.theme) + groupNode.updateItemNodes(group.items.map({ $0.node(theme: self.theme) }), leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0) + self.groupNodes.append(groupNode) + self.addSubnode(groupNode) + } + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + var groupsHeight: CGFloat = 0.0 + + for groupNode in self.groupNodes.reversed() { + if CGFloat(0.0).isLess(than: groupsHeight) { + groupsHeight += groupSpacing + } + + let size = groupNode.measure(CGSize(width: constrainedSize.width, height: max(0.0, constrainedSize.height - groupsHeight))) + groupsHeight += size.height + } + + return CGSize(width: constrainedSize.width, height: min(groupsHeight, constrainedSize.height)) + } + + override func layout() { + var groupsHeight: CGFloat = 0.0 + for i in 0 ..< self.groupNodes.count { + let groupNode = self.groupNodes[i] + + let size = groupNode.calculatedSize + + if i != 0 { + groupsHeight += groupSpacing + self.groupNodes[i - 1].trailingDimView.frame = CGRect(x: 0.0, y: groupNodes[i - 1].bounds.size.height, width: size.width, height: groupSpacing) + } + + groupNode.frame = CGRect(origin: CGPoint(x: 0.0, y: groupsHeight), size: size) + groupNode.trailingDimView.frame = CGRect() + + groupsHeight += size.height + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for groupNode in self.groupNodes { + if groupNode.frame.contains(point) { + return groupNode.hitTest(self.convert(point, to: groupNode), with: event) + } + } + return nil + } + + func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) { + for node in self.groupNodes { + node.animateDimViewsAlpha(from: from, to: to, duration: duration) + } + } + + public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) { + var item = self.groups[groupIndex].items[itemIndex] + let itemNode = self.groupNodes[groupIndex].itemNode(at: itemIndex) + item = f(item) + item.updateNode(itemNode) + + var groupItems = self.groups[groupIndex].items + groupItems[itemIndex] = item + + self.groups[groupIndex] = ActionSheetItemGroup(items: groupItems) + } +} diff --git a/submodules/Display/Display/ActionSheetItemNode.swift b/submodules/Display/Display/ActionSheetItemNode.swift new file mode 100644 index 0000000000..178bfc0f05 --- /dev/null +++ b/submodules/Display/Display/ActionSheetItemNode.swift @@ -0,0 +1,33 @@ +import UIKit +import AsyncDisplayKit + +open class ActionSheetItemNode: ASDisplayNode { + private let theme: ActionSheetControllerTheme + + public let backgroundNode: ASDisplayNode + private let overflowSeparatorNode: ASDisplayNode + + public init(theme: ActionSheetControllerTheme) { + self.theme = theme + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor + + self.overflowSeparatorNode = ASDisplayNode() + self.overflowSeparatorNode.backgroundColor = self.theme.itemHighlightedBackgroundColor + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.overflowSeparatorNode) + } + + open override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 57.0) + } + + open override func layout() { + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: self.calculatedSize) + self.overflowSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.calculatedSize.height), size: CGSize(width: self.calculatedSize.width, height: UIScreenPixel)) + } +} diff --git a/submodules/Display/Display/ActionSheetSwitchItem.swift b/submodules/Display/Display/ActionSheetSwitchItem.swift new file mode 100644 index 0000000000..dc29f57538 --- /dev/null +++ b/submodules/Display/Display/ActionSheetSwitchItem.swift @@ -0,0 +1,104 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public class ActionSheetSwitchItem: ActionSheetItem { + public let title: String + public let isOn: Bool + public let action: (Bool) -> Void + + public init(title: String, isOn: Bool, action: @escaping (Bool) -> Void) { + self.title = title + self.isOn = isOn + self.action = action + } + + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetSwitchNode(theme: theme) + node.setItem(self) + return node + } + + public func updateNode(_ node: ActionSheetItemNode) { + guard let node = node as? ActionSheetSwitchNode else { + assertionFailure() + return + } + + node.setItem(self) + } +} + +public class ActionSheetSwitchNode: ActionSheetItemNode { + private let theme: ActionSheetControllerTheme + + private var item: ActionSheetSwitchItem? + + private let button: HighlightTrackingButton + private let label: ASTextNode + private let switchNode: SwitchNode + + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + + self.button = HighlightTrackingButton() + + self.label = ASTextNode() + self.label.isUserInteractionEnabled = false + self.label.maximumNumberOfLines = 1 + self.label.displaysAsynchronously = false + self.label.truncationMode = .byTruncatingTail + + self.switchNode = SwitchNode() + self.switchNode.frameColor = theme.switchFrameColor + self.switchNode.contentColor = theme.switchContentColor + self.switchNode.handleColor = theme.switchHandleColor + + super.init(theme: theme) + + self.view.addSubview(self.button) + + self.label.isUserInteractionEnabled = false + self.addSubnode(self.label) + self.addSubnode(self.switchNode) + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + self.switchNode.valueUpdated = { [weak self] value in + self?.item?.action(value) + } + } + + func setItem(_ item: ActionSheetSwitchItem) { + self.item = item + + self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: self.theme.primaryTextColor) + + self.switchNode.isOn = item.isOn + + self.setNeedsLayout() + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 57.0) + } + + public override func layout() { + super.layout() + + let size = self.bounds.size + + self.button.frame = CGRect(origin: CGPoint(), size: size) + + let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 51.0 - 16.0 * 2.0), height: size.height)) + self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + + let switchSize = CGSize(width: 51.0, height: 31.0) + self.switchNode.frame = CGRect(origin: CGPoint(x: size.width - 16.0 - switchSize.width, y: floor((size.height - switchSize.height) / 2.0)), size: switchSize) + } + + @objc func buttonPressed() { + let value = !self.switchNode.isOn + self.switchNode.setOn(value, animated: true) + self.item?.action(value) + } +} diff --git a/submodules/Display/Display/ActionSheetTextItem.swift b/submodules/Display/Display/ActionSheetTextItem.swift new file mode 100644 index 0000000000..3577b46f9b --- /dev/null +++ b/submodules/Display/Display/ActionSheetTextItem.swift @@ -0,0 +1,73 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public class ActionSheetTextItem: ActionSheetItem { + public let title: String + + public init(title: String) { + self.title = title + } + + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetTextNode(theme: theme) + node.setItem(self) + return node + } + + public func updateNode(_ node: ActionSheetItemNode) { + guard let node = node as? ActionSheetTextNode else { + assertionFailure() + return + } + + node.setItem(self) + } +} + +public class ActionSheetTextNode: ActionSheetItemNode { + public static let defaultFont: UIFont = Font.regular(13.0) + + private let theme: ActionSheetControllerTheme + + private var item: ActionSheetTextItem? + + private let label: ASTextNode + + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + + self.label = ASTextNode() + self.label.isUserInteractionEnabled = false + self.label.maximumNumberOfLines = 0 + self.label.displaysAsynchronously = false + self.label.truncationMode = .byTruncatingTail + + super.init(theme: theme) + + self.label.isUserInteractionEnabled = false + self.addSubnode(self.label) + } + + func setItem(_ item: ActionSheetTextItem) { + self.item = item + + self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetTextNode.defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) + + self.setNeedsLayout() + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + let labelSize = self.label.measure(CGSize(width: max(1.0, constrainedSize.width - 20.0), height: constrainedSize.height)) + return CGSize(width: constrainedSize.width, height: max(57.0, labelSize.height + 32.0)) + } + + public override func layout() { + super.layout() + + let size = self.bounds.size + + let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 20.0), height: size.height)) + self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + } +} diff --git a/submodules/Display/Display/ActionSheetTheme.swift b/submodules/Display/Display/ActionSheetTheme.swift new file mode 100644 index 0000000000..1911cb8b3d --- /dev/null +++ b/submodules/Display/Display/ActionSheetTheme.swift @@ -0,0 +1,87 @@ +import Foundation +import UIKit + +public enum ActionSheetControllerThemeBackgroundType { + case light + case dark +} + +public final class ActionSheetControllerTheme: Equatable { + public let dimColor: UIColor + public let backgroundType: ActionSheetControllerThemeBackgroundType + public let itemBackgroundColor: UIColor + public let itemHighlightedBackgroundColor: UIColor + public let standardActionTextColor: UIColor + public let destructiveActionTextColor: UIColor + public let disabledActionTextColor: UIColor + public let primaryTextColor: UIColor + public let secondaryTextColor: UIColor + public let controlAccentColor: UIColor + public let controlColor: UIColor + public let switchFrameColor: UIColor + public let switchContentColor: UIColor + public let switchHandleColor: UIColor + + public init(dimColor: UIColor, backgroundType: ActionSheetControllerThemeBackgroundType, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, controlColor: UIColor, switchFrameColor: UIColor, switchContentColor: UIColor, switchHandleColor: UIColor) { + self.dimColor = dimColor + self.backgroundType = backgroundType + self.itemBackgroundColor = itemBackgroundColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.standardActionTextColor = standardActionTextColor + self.destructiveActionTextColor = destructiveActionTextColor + self.disabledActionTextColor = disabledActionTextColor + self.primaryTextColor = primaryTextColor + self.secondaryTextColor = secondaryTextColor + self.controlAccentColor = controlAccentColor + self.controlColor = controlColor + self.switchFrameColor = switchFrameColor + self.switchContentColor = switchContentColor + self.switchHandleColor = switchHandleColor + } + + public static func ==(lhs: ActionSheetControllerTheme, rhs: ActionSheetControllerTheme) -> Bool { + if lhs.dimColor != rhs.dimColor { + return false + } + if lhs.backgroundType != rhs.backgroundType { + return false + } + if lhs.itemBackgroundColor != rhs.itemBackgroundColor { + return false + } + if lhs.itemHighlightedBackgroundColor != rhs.itemHighlightedBackgroundColor { + return false + } + if lhs.standardActionTextColor != rhs.standardActionTextColor { + return false + } + if lhs.destructiveActionTextColor != rhs.destructiveActionTextColor { + return false + } + if lhs.disabledActionTextColor != rhs.disabledActionTextColor { + return false + } + if lhs.primaryTextColor != rhs.primaryTextColor { + return false + } + if lhs.secondaryTextColor != rhs.secondaryTextColor { + return false + } + if lhs.controlAccentColor != rhs.controlAccentColor { + return false + } + if lhs.controlColor != rhs.controlColor { + return false + } + if lhs.switchFrameColor != rhs.switchFrameColor { + return false + } + if lhs.switchContentColor != rhs.switchContentColor { + return false + } + if lhs.switchHandleColor != rhs.switchHandleColor { + return false + } + return true + } +} diff --git a/submodules/Display/Display/AlertContentNode.swift b/submodules/Display/Display/AlertContentNode.swift new file mode 100644 index 0000000000..7659a9ab30 --- /dev/null +++ b/submodules/Display/Display/AlertContentNode.swift @@ -0,0 +1,21 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +open class AlertContentNode: ASDisplayNode { + open var requestLayout: ((ContainedViewLayoutTransition) -> Void)? + + open var dismissOnOutsideTap: Bool { + return true + } + + open func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + assertionFailure() + + return CGSize() + } + + open func updateTheme(_ theme: AlertControllerTheme) { + + } +} diff --git a/submodules/Display/Display/AlertController.swift b/submodules/Display/Display/AlertController.swift new file mode 100644 index 0000000000..7e74a95ee9 --- /dev/null +++ b/submodules/Display/Display/AlertController.swift @@ -0,0 +1,133 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum AlertControllerThemeBackgroundType { + case light + case dark +} + +public final class AlertControllerTheme: Equatable { + public let backgroundType: ActionSheetControllerThemeBackgroundType + public let backgroundColor: UIColor + public let separatorColor: UIColor + public let highlightedItemColor: UIColor + public let primaryColor: UIColor + public let secondaryColor: UIColor + public let accentColor: UIColor + public let destructiveColor: UIColor + public let disabledColor: UIColor + + public init(backgroundType: ActionSheetControllerThemeBackgroundType, backgroundColor: UIColor, separatorColor: UIColor, highlightedItemColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disabledColor: UIColor) { + self.backgroundType = backgroundType + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + self.highlightedItemColor = highlightedItemColor + self.primaryColor = primaryColor + self.secondaryColor = secondaryColor + self.accentColor = accentColor + self.destructiveColor = destructiveColor + self.disabledColor = disabledColor + } + + public static func ==(lhs: AlertControllerTheme, rhs: AlertControllerTheme) -> Bool { + if lhs.backgroundType != rhs.backgroundType { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.separatorColor != rhs.separatorColor { + return false + } + if lhs.highlightedItemColor != rhs.highlightedItemColor { + return false + } + if lhs.primaryColor != rhs.primaryColor { + return false + } + if lhs.secondaryColor != rhs.secondaryColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.destructiveColor != rhs.destructiveColor { + return false + } + if lhs.disabledColor != rhs.disabledColor { + return false + } + return true + } +} + +open class AlertController: ViewController { + private var controllerNode: AlertControllerNode { + return self.displayNode as! AlertControllerNode + } + + public var theme: AlertControllerTheme { + didSet { + if oldValue != self.theme { + self.controllerNode.updateTheme(self.theme) + } + } + } + private let contentNode: AlertContentNode + private let allowInputInset: Bool + + public var dismissed: (() -> Void)? + + public init(theme: AlertControllerTheme, contentNode: AlertContentNode, allowInputInset: Bool = true) { + self.theme = theme + self.contentNode = contentNode + self.allowInputInset = allowInputInset + + super.init(navigationBarPresentationData: nil) + + self.blocksBackgroundWhenInOverlay = true + + self.statusBar.statusBarStyle = .Ignore + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func loadDisplayNode() { + self.displayNode = AlertControllerNode(contentNode: self.contentNode, theme: self.theme, allowInputInset: self.allowInputInset) + self.displayNodeDidLoad() + + self.controllerNode.dismiss = { [weak self] in + if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap { + strongSelf.controllerNode.animateOut { + self?.dismiss() + } + } + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.controllerNode.animateIn() + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + + override open func dismiss(completion: (() -> Void)? = nil) { + self.dismissed?() + self.presentingViewController?.dismiss(animated: false, completion: completion) + } + + public func dismissAnimated() { + self.controllerNode.animateOut { [weak self] in + self?.dismiss() + } + } +} diff --git a/submodules/Display/Display/AlertControllerNode.swift b/submodules/Display/Display/AlertControllerNode.swift new file mode 100644 index 0000000000..30ae994d00 --- /dev/null +++ b/submodules/Display/Display/AlertControllerNode.swift @@ -0,0 +1,147 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class AlertControllerNode: ASDisplayNode { + private let centerDimView: UIImageView + private let topDimView: UIView + private let bottomDimView: UIView + private let leftDimView: UIView + private let rightDimView: UIView + + private let containerNode: ASDisplayNode + private let effectNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentNode: AlertContentNode + private let allowInputInset: Bool + + private var containerLayout: ContainerViewLayout? + + var dismiss: (() -> Void)? + + init(contentNode: AlertContentNode, theme: AlertControllerTheme, allowInputInset: Bool) { + self.allowInputInset = allowInputInset + + let dimColor = UIColor(white: 0.0, alpha: 0.5) + + self.centerDimView = UIImageView() + self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: dimColor) + + self.topDimView = UIView() + self.topDimView.backgroundColor = dimColor + + self.bottomDimView = UIView() + self.bottomDimView.backgroundColor = dimColor + + self.leftDimView = UIView() + self.leftDimView.backgroundColor = dimColor + + self.rightDimView = UIView() + self.rightDimView.backgroundColor = dimColor + + self.containerNode = ASDisplayNode() + self.containerNode.layer.cornerRadius = 14.0 + self.containerNode.layer.masksToBounds = true + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = theme.backgroundColor + + self.effectNode = ASDisplayNode(viewBlock: { + return UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)) + }) + + self.contentNode = contentNode + + super.init() + + self.view.addSubview(self.centerDimView) + self.view.addSubview(self.topDimView) + self.view.addSubview(self.bottomDimView) + self.view.addSubview(self.leftDimView) + self.view.addSubview(self.rightDimView) + + self.containerNode.addSubnode(self.effectNode) + self.containerNode.addSubnode(self.backgroundNode) + self.containerNode.addSubnode(self.contentNode) + self.addSubnode(self.containerNode) + + self.contentNode.requestLayout = { [weak self] transition in + if let strongSelf = self, let containerLayout = self?.containerLayout { + strongSelf.containerLayoutUpdated(containerLayout, transition: transition) + } + } + } + + override func didLoad() { + super.didLoad() + + self.topDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:)))) + self.bottomDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:)))) + self.leftDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:)))) + self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:)))) + } + + func updateTheme(_ theme: AlertControllerTheme) { + if let effectView = self.effectNode.view as? UIVisualEffectView { + effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark) + } + self.backgroundNode.backgroundColor = theme.backgroundColor + self.contentNode.updateTheme(theme) + } + + func animateIn() { + self.centerDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.topDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) + } + + func animateOut(completion: @escaping () -> Void) { + self.centerDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.topDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.bottomDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.leftDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.rightDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.containerLayout = layout + + var insetOptions: ContainerViewLayoutInsetOptions = [.statusBar] + if self.allowInputInset { + insetOptions.insert(.input) + } + var insets = layout.insets(options: insetOptions) + let maxWidth = min(240.0, layout.size.width - 70.0) + insets.left = floor((layout.size.width - maxWidth) / 2.0) + insets.right = floor((layout.size.width - maxWidth) / 2.0) + let contentAvailableFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: layout.size.width - insets.right, height: layout.size.height - insets.top - insets.bottom)) + let contentSize = self.contentNode.updateLayout(size: contentAvailableFrame.size, transition: transition) + let containerSize = CGSize(width: contentSize.width, height: contentSize.height) + let containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: contentAvailableFrame.minY + floor((contentAvailableFrame.size.height - containerSize.height) / 2.0)), size: containerSize) + + transition.updateFrame(view: self.centerDimView, frame: containerFrame) + transition.updateFrame(view: self.topDimView, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: containerFrame.minY))) + transition.updateFrame(view: self.bottomDimView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.maxY), size: CGSize(width: layout.size.width, height: layout.size.height - containerFrame.maxY))) + transition.updateFrame(view: self.leftDimView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.minY), size: CGSize(width: containerFrame.minX, height: containerFrame.height))) + transition.updateFrame(view: self.rightDimView, frame: CGRect(origin: CGPoint(x: containerFrame.maxX, y: containerFrame.minY), size: CGSize(width: layout.size.width - containerFrame.maxX, height: containerFrame.height))) + + transition.updateFrame(node: self.containerNode, frame: containerFrame) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size)) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size)) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size)) + } + + @objc func dimmingNodeTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismiss?() + } + } +} diff --git a/submodules/Display/Display/CAAnimationUtils.swift b/submodules/Display/Display/CAAnimationUtils.swift new file mode 100644 index 0000000000..44cfa25f4e --- /dev/null +++ b/submodules/Display/Display/CAAnimationUtils.swift @@ -0,0 +1,326 @@ +import UIKit + +#if BUCK +import DisplayPrivate +#endif + +@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate { + private let keyPath: String? + var completion: ((Bool) -> Void)? + + init(animation: CAAnimation, completion: ((Bool) -> Void)?) { + if let animation = animation as? CABasicAnimation { + self.keyPath = animation.keyPath + } else { + self.keyPath = nil + } + self.completion = completion + + super.init() + } + + @objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { + if let anim = anim as? CABasicAnimation { + if anim.keyPath != self.keyPath { + return + } + } + if let completion = self.completion { + completion(flag) + } + } +} + +private let completionKey = "CAAnimationUtils_completion" + +public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve" + +public extension CAAnimation { + public var completion: ((Bool) -> Void)? { + get { + if let delegate = self.delegate as? CALayerAnimationDelegate { + return delegate.completion + } else { + return nil + } + } set(value) { + if let delegate = self.delegate as? CALayerAnimationDelegate { + delegate.completion = value + } else { + self.delegate = CALayerAnimationDelegate(animation: self, completion: value) + } + } + } +} + +public extension CALayer { + public func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation { + if timingFunction == kCAMediaTimingFunctionSpring { + let animation = makeSpringAnimation(keyPath) + animation.fromValue = from + animation.toValue = to + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = kCAFillModeForwards + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + animation.speed = speed * Float(animation.duration / duration) + animation.isAdditive = additive + + if !delay.isZero { + animation.beginTime = CACurrentMediaTime() + delay + animation.fillMode = kCAFillModeBoth + } + + return animation + } else { + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let animation = CABasicAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.duration = duration + if let mediaTimingFunction = mediaTimingFunction { + animation.timingFunction = mediaTimingFunction + } else { + animation.timingFunction = CAMediaTimingFunction(name: timingFunction) + } + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = kCAFillModeForwards + animation.speed = speed + animation.isAdditive = additive + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + if !delay.isZero { + animation.beginTime = CACurrentMediaTime() + delay + animation.fillMode = kCAFillModeBoth + } + + return animation + } + } + + public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + self.add(animation, forKey: additive ? nil : keyPath) + } + + public func animateGroup(_ animations: [CAAnimation], key: String) { + let animationGroup = CAAnimationGroup() + var timeOffset = 0.0 + for animation in animations { + animation.beginTime = animation.beginTime + timeOffset + timeOffset += animation.duration / Double(animation.speed) + } + animationGroup.animations = animations + animationGroup.duration = timeOffset + self.add(animationGroup, forKey: key) + } + + public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let animation = CAKeyframeAnimation(keyPath: keyPath) + animation.values = values + var keyTimes: [NSNumber] = [] + for i in 0 ..< values.count { + if i == 0 { + keyTimes.append(0.0) + } else if i == values.count - 1 { + keyTimes.append(1.0) + } else { + keyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber) + } + } + animation.keyTimes = keyTimes + animation.speed = speed + animation.duration = duration + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + self.add(animation, forKey: keyPath) + } + + public func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, initialVelocity: CGFloat = 0.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + let animation: CABasicAnimation + if #available(iOS 9.0, *) { + animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping) + } else { + animation = makeSpringAnimation(keyPath) + } + animation.fromValue = from + animation.toValue = to + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = kCAFillModeForwards + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + animation.speed = speed * Float(animation.duration / duration) + animation.isAdditive = additive + + self.add(animation, forKey: keyPath) + } + + public func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let animation = CABasicAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.duration = duration + if let mediaTimingFunction = mediaTimingFunction { + animation.timingFunction = mediaTimingFunction + } else { + animation.timingFunction = CAMediaTimingFunction(name: timingFunction) + } + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = kCAFillModeForwards + animation.speed = speed + animation.isAdditive = true + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + self.add(animation, forKey: key) + } + + public func animateAlpha(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) { + self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) + } + + public func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) + } + + public func animateRotation(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.rotation.z", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) + } + + func animatePosition(from: CGPoint, to: CGPoint, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if from == to && !force { + if let completion = completion { + completion(true) + } + return + } + self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "position", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + } + + func animateBounds(from: CGRect, to: CGRect, duration: Double, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if from == to && !force { + if let completion = completion { + completion(true) + } + return + } + self.animate(from: NSValue(cgRect: from), to: NSValue(cgRect: to), keyPath: "bounds", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + } + + public func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) + } + + public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) + } + + public func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) { + self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true) + } + + public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) { + self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true) + } + + public func animatePositionKeyframes(values: [CGPoint], duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animateKeyframes(values: values.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position") + } + + public func animateFrame(from: CGRect, to: CGRect, duration: Double, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if from == to && !force { + if let completion = completion { + completion(true) + } + return + } + var interrupted = false + var completedPosition = false + var completedBounds = false + let partialCompletion: () -> Void = { + if interrupted || (completedPosition && completedBounds) { + if let completion = completion { + completion(!interrupted) + } + } + } + + var fromPosition = CGPoint(x: from.midX, y: from.midY) + var toPosition = CGPoint(x: to.midX, y: to.midY) + + var fromBounds = CGRect(origin: self.bounds.origin, size: from.size) + var toBounds = CGRect(origin: self.bounds.origin, size: to.size) + + if additive { + fromPosition.x = -(toPosition.x - fromPosition.x) + fromPosition.y = -(toPosition.y - fromPosition.y) + toPosition = CGPoint() + + fromBounds.size.width = -(toBounds.width - fromBounds.width) + fromBounds.size.height = -(toBounds.height - fromBounds.height) + toBounds = CGRect() + } + + self.animatePosition(from: fromPosition, to: toPosition, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in + if !value { + interrupted = true + } + completedPosition = true + partialCompletion() + }) + self.animateBounds(from: fromBounds, to: toBounds, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in + if !value { + interrupted = true + } + completedBounds = true + partialCompletion() + }) + } + + public func cancelAnimationsRecursive(key: String) { + self.removeAnimation(forKey: key) + if let sublayers = self.sublayers { + for layer in sublayers { + layer.cancelAnimationsRecursive(key: key) + } + } + } +} diff --git a/submodules/Display/Display/CASeeThroughTracingLayer.h b/submodules/Display/Display/CASeeThroughTracingLayer.h new file mode 100644 index 0000000000..8c6ff3de8e --- /dev/null +++ b/submodules/Display/Display/CASeeThroughTracingLayer.h @@ -0,0 +1,9 @@ +#import + +@interface CASeeThroughTracingLayer : CALayer + +@end + +@interface CASeeThroughTracingView : UIView + +@end diff --git a/submodules/Display/Display/CASeeThroughTracingLayer.m b/submodules/Display/Display/CASeeThroughTracingLayer.m new file mode 100644 index 0000000000..79ff8d3631 --- /dev/null +++ b/submodules/Display/Display/CASeeThroughTracingLayer.m @@ -0,0 +1,57 @@ +#import "CASeeThroughTracingLayer.h" + +@interface CASeeThroughTracingLayer () { + CGPoint _parentOffset; +} + +@end + +@implementation CASeeThroughTracingLayer + +- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key { + [super addAnimation:anim forKey:key]; +} + +- (void)setFrame:(CGRect)frame { + [super setFrame:frame]; + + [self _mirrorTransformToSublayers]; +} + +- (void)setBounds:(CGRect)bounds { + [super setBounds:bounds]; + + [self _mirrorTransformToSublayers]; +} + +- (void)setPosition:(CGPoint)position { + [super setPosition:position]; + + [self _mirrorTransformToSublayers]; +} + +- (void)_mirrorTransformToSublayers { + CGRect bounds = self.bounds; + CGPoint position = self.position; + + CGPoint sublayerParentOffset = _parentOffset; + sublayerParentOffset.x += position.x - (bounds.size.width) / 2.0f; + sublayerParentOffset.y += position.y - (bounds.size.width) / 2.0f; + + for (CALayer *sublayer in self.sublayers) { + if ([sublayer isKindOfClass:[CASeeThroughTracingLayer class]]) { + ((CASeeThroughTracingLayer *)sublayer)->_parentOffset = sublayerParentOffset; + [(CASeeThroughTracingLayer *)sublayer _mirrorTransformToSublayers]; + } + } +} + +@end + +@implementation CASeeThroughTracingView + ++ (Class)layerClass { + return [CASeeThroughTracingLayer class]; +} + +@end diff --git a/submodules/Display/Display/CATracingLayer.h b/submodules/Display/Display/CATracingLayer.h new file mode 100644 index 0000000000..22037b19a4 --- /dev/null +++ b/submodules/Display/Display/CATracingLayer.h @@ -0,0 +1,34 @@ +#import + +@interface CATracingLayer : CALayer + +@end + +@interface CATracingLayerInfo : NSObject + +@property (nonatomic, readonly) bool shouldBeAdjustedToInverseTransform; +@property (nonatomic, weak, readonly) id _Nullable userData; +@property (nonatomic, readonly) int32_t tracingTag; +@property (nonatomic, readonly) int32_t disableChildrenTracingTags; + +- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag disableChildrenTracingTags:(int32_t)disableChildrenTracingTags; + +@end + +@interface CALayer (Tracing) + +- (CATracingLayerInfo * _Nullable)traceableInfo; +- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info; + +- (bool)hasPositionOrOpacityAnimations; +- (bool)hasPositionAnimations; + +- (void)setInvalidateTracingSublayers:(void (^_Nullable)())block; +- (NSArray *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag; +- (void)adjustTraceableLayerTransforms:(CGSize)offset; + +- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget; + +- (void)invalidateUpTheTree; + +@end diff --git a/submodules/Display/Display/CATracingLayer.m b/submodules/Display/Display/CATracingLayer.m new file mode 100644 index 0000000000..dd463b5b56 --- /dev/null +++ b/submodules/Display/Display/CATracingLayer.m @@ -0,0 +1,364 @@ +#import "CATracingLayer.h" + +#import "RuntimeUtils.h" + +static void *CATracingLayerInvalidatedKey = &CATracingLayerInvalidatedKey; +static void *CATracingLayerIsInvalidatedBlock = &CATracingLayerIsInvalidatedBlock; +static void *CATracingLayerTraceableInfoKey = &CATracingLayerTraceableInfoKey; +static void *CATracingLayerPositionAnimationMirrorTarget = &CATracingLayerPositionAnimationMirrorTarget; + +@implementation CALayer (Tracing) + +- (void)setInvalidateTracingSublayers:(void (^_Nullable)())block { + [self setAssociatedObject:[block copy] forKey:CATracingLayerIsInvalidatedBlock]; +} + +- (void (^_Nullable)())invalidateTracingSublayers { + return [self associatedObjectForKey:CATracingLayerIsInvalidatedBlock]; +} + +- (bool)isTraceable { + return [self associatedObjectForKey:CATracingLayerTraceableInfoKey] != nil || [self isKindOfClass:[CATracingLayer class]]; +} + +- (CATracingLayerInfo * _Nullable)traceableInfo { + return [self associatedObjectForKey:CATracingLayerTraceableInfoKey]; +} + +- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info { + [self setAssociatedObject:info forKey:CATracingLayerTraceableInfoKey]; +} + +- (bool)hasPositionOrOpacityAnimations { + return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil || [self animationForKey:@"sublayerTransform"] != nil || [self animationForKey:@"opacity"] != nil; +} + +- (bool)hasPositionAnimations { + return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil; +} + +static void traceLayerSurfaces(int32_t tracingTag, int depth, CALayer * _Nonnull layer, NSMutableDictionary *> *layersByDepth, bool skipIfNoTraceableSublayers) { + bool hadTraceableSublayers = false; + for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) { + CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo]; + if (sublayerTraceableInfo != nil && sublayerTraceableInfo.tracingTag == tracingTag) { + NSMutableArray *array = layersByDepth[@(depth)]; + if (array == nil) { + array = [[NSMutableArray alloc] init]; + layersByDepth[@(depth)] = array; + } + [array addObject:sublayer]; + hadTraceableSublayers = true; + } + if (sublayerTraceableInfo.disableChildrenTracingTags & tracingTag) { + return; + } + } + + if (!skipIfNoTraceableSublayers || !hadTraceableSublayers) { + for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) { + if ([sublayer isKindOfClass:[CATracingLayer class]]) { + traceLayerSurfaces(tracingTag, depth + 1, sublayer, layersByDepth, hadTraceableSublayers); + } + } + } +} + +- (NSArray *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag { + NSMutableDictionary *> *layersByDepth = [[NSMutableDictionary alloc] init]; + + traceLayerSurfaces(tracingTag, 0, self, layersByDepth, false); + + NSMutableArray *> *result = [[NSMutableArray alloc] init]; + + for (id key in [[layersByDepth allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [result addObject:layersByDepth[key]]; + } + + return result; +} + +- (void)adjustTraceableLayerTransforms:(CGSize)offset { + CGRect frame = self.frame; + CGSize sublayerOffset = CGSizeMake(frame.origin.x + offset.width, frame.origin.y + offset.height); + for (CALayer *sublayer in self.sublayers) { + CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo]; + if (sublayerTraceableInfo != nil && sublayerTraceableInfo.shouldBeAdjustedToInverseTransform) { + sublayer.sublayerTransform = CATransform3DMakeTranslation(-sublayerOffset.width, -sublayerOffset.height, 0.0f); + } else if ([sublayer isKindOfClass:[CATracingLayer class]]) { + [(CATracingLayer *)sublayer adjustTraceableLayerTransforms:sublayerOffset]; + } + } +} + +- (CALayer * _Nullable)animationMirrorTarget { + return [self associatedObjectForKey:CATracingLayerPositionAnimationMirrorTarget]; +} + +- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget { + [self setAssociatedObject:animationMirrorTarget forKey:CATracingLayerPositionAnimationMirrorTarget associationPolicy:NSObjectAssociationPolicyRetain]; +} + +- (void)invalidateUpTheTree { + CALayer *superlayer = self; + while (true) { + if (superlayer == nil) { + break; + } + + void (^block)() = [superlayer invalidateTracingSublayers]; + if (block != nil) { + block(); + } + + superlayer = superlayer.superlayer; + } +} + +@end + +@interface CATracingLayerAnimationDelegate : NSObject { + id _delegate; + void (^_animationStopped)(); +} + +@end + +@implementation CATracingLayerAnimationDelegate + +- (instancetype)initWithDelegate:(id)delegate animationStopped:(void (^_Nonnull)())animationStopped { + _delegate = delegate; + _animationStopped = [animationStopped copy]; + return self; +} + +- (void)animationDidStart:(CAAnimation *)anim { + if ([_delegate respondsToSelector:@selector(animationDidStart:)]) { + [(id)_delegate animationDidStart:anim]; + } +} + +- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { + if ([_delegate respondsToSelector:@selector(animationDidStop:finished:)]) { + [(id)_delegate animationDidStop:anim finished:flag]; + } + + if (_animationStopped) { + _animationStopped(); + } +} + +@end + +@interface CATracingLayer () + +@property (nonatomic) bool isInvalidated; + +@end + +@implementation CATracingLayer + +- (void)setNeedsDisplay { +} + +- (void)displayIfNeeded { +} + +- (bool)isInvalidated { + return [[self associatedObjectForKey:CATracingLayerInvalidatedKey] intValue] != 0; +} + +- (void)setIsInvalidated:(bool)isInvalidated { + [self setAssociatedObject: isInvalidated ? @1 : @0 forKey:CATracingLayerInvalidatedKey]; +} + +- (void)setPosition:(CGPoint)position { + [super setPosition:position]; + + [self invalidateUpTheTree]; +} + +- (void)setOpacity:(float)opacity { + [super setOpacity:opacity]; + + [self invalidateUpTheTree]; +} + +- (void)addSublayer:(CALayer *)layer { + [super addSublayer:layer]; + + if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) { + [self invalidateUpTheTree]; + } +} + +- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx { + [super insertSublayer:layer atIndex:idx]; + + if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) { + [self invalidateUpTheTree]; + } +} + +- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling { + [super insertSublayer:layer below:sibling]; + + if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) { + [self invalidateUpTheTree]; + } +} + +- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling { + [super insertSublayer:layer above:sibling]; + + if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) { + [self invalidateUpTheTree]; + } +} + +- (void)replaceSublayer:(CALayer *)layer with:(CALayer *)layer2 { + [super replaceSublayer:layer with:layer2]; + + if ([layer isTraceable] || [layer2 isTraceable]) { + [self invalidateUpTheTree]; + } +} + +- (void)removeFromSuperlayer { + if ([self isTraceable]) { + [self invalidateUpTheTree]; + } + + [super removeFromSuperlayer]; +} + +- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key { + if ([anim isKindOfClass:[CABasicAnimation class]]) { + if (false && [key isEqualToString:@"bounds.origin.y"]) { + CABasicAnimation *animCopy = [anim copy]; + CGFloat from = [animCopy.fromValue floatValue]; + CGFloat to = [animCopy.toValue floatValue]; + + animCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0, to - from, 0.0f)]; + animCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; + animCopy.keyPath = @"sublayerTransform"; + + __weak CATracingLayer *weakSelf = self; + anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{ + __strong CATracingLayer *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf invalidateUpTheTree]; + } + }]; + + [super addAnimation:anim forKey:key]; + + CABasicAnimation *positionAnimCopy = [animCopy copy]; + positionAnimCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0, 0.0, 0.0f)]; + positionAnimCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; + positionAnimCopy.additive = true; + positionAnimCopy.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{ + __strong CATracingLayer *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf invalidateUpTheTree]; + } + }]; + + [self invalidateUpTheTree]; + + [self mirrorAnimationDownTheTree:animCopy key:@"sublayerTransform"]; + [self mirrorPositionAnimationDownTheTree:positionAnimCopy key:@"sublayerTransform"]; + } else if ([key isEqualToString:@"position"]) { + CABasicAnimation *animCopy = [anim copy]; + CGPoint from = [animCopy.fromValue CGPointValue]; + CGPoint to = [animCopy.toValue CGPointValue]; + + animCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(to.x - from.x, to.y - from.y, 0.0f)]; + animCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; + animCopy.keyPath = @"sublayerTransform"; + + __weak CATracingLayer *weakSelf = self; + anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{ + __strong CATracingLayer *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf invalidateUpTheTree]; + } + }]; + + [super addAnimation:anim forKey:key]; + + CABasicAnimation *positionAnimCopy = [animCopy copy]; + positionAnimCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-to.x + from.x, 0.0, 0.0f)]; + positionAnimCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; + positionAnimCopy.additive = true; + positionAnimCopy.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{ + __strong CATracingLayer *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf invalidateUpTheTree]; + } + }]; + + [self invalidateUpTheTree]; + + [self mirrorAnimationDownTheTree:animCopy key:@"sublayerTransform"]; + [self mirrorPositionAnimationDownTheTree:positionAnimCopy key:@"sublayerTransform"]; + } else if ([key isEqualToString:@"opacity"]) { + __weak CATracingLayer *weakSelf = self; + anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{ + __strong CATracingLayer *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf invalidateUpTheTree]; + } + }]; + + [super addAnimation:anim forKey:key]; + + [self invalidateUpTheTree]; + } else { + [super addAnimation:anim forKey:key]; + } + } else { + [super addAnimation:anim forKey:key]; + } +} + +- (void)mirrorPositionAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key { + if ([animation isKindOfClass:[CABasicAnimation class]]) { + if ([((CABasicAnimation *)animation).keyPath isEqualToString:@"sublayerTransform"]) { + CALayer *positionAnimationMirrorTarget = [self animationMirrorTarget]; + if (positionAnimationMirrorTarget != nil) { + [positionAnimationMirrorTarget addAnimation:[animation copy] forKey:key]; + } + } + } +} + +- (void)mirrorAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key { + for (CALayer *sublayer in self.sublayers) { + CATracingLayerInfo *traceableInfo = [sublayer traceableInfo]; + if (traceableInfo != nil && traceableInfo.shouldBeAdjustedToInverseTransform) { + [sublayer addAnimation:[animation copy] forKey:key]; + } + + if ([sublayer isKindOfClass:[CATracingLayer class]]) { + [(CATracingLayer *)sublayer mirrorAnimationDownTheTree:animation key:key]; + } + } +} + +@end + +@implementation CATracingLayerInfo + +- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag disableChildrenTracingTags:(int32_t)disableChildrenTracingTags { + self = [super init]; + if (self != nil) { + _shouldBeAdjustedToInverseTransform = shouldBeAdjustedToInverseTransform; + _userData = userData; + _tracingTag = tracingTag; + _disableChildrenTracingTags = disableChildrenTracingTags; + } + return self; +} + +@end diff --git a/submodules/Display/Display/ChildWindowHostView.swift b/submodules/Display/Display/ChildWindowHostView.swift new file mode 100644 index 0000000000..3789c0dd71 --- /dev/null +++ b/submodules/Display/Display/ChildWindowHostView.swift @@ -0,0 +1,117 @@ +import Foundation +import UIKit + +private final class ChildWindowHostView: UIView, WindowHost { + var updateSize: ((CGSize) -> Void)? + var layoutSubviewsEvent: (() -> Void)? + var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? + var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)? + var invalidateDeferScreenEdgeGestureImpl: (() -> Void)? + var invalidatePreferNavigationUIHiddenImpl: (() -> Void)? + var cancelInteractiveKeyboardGesturesImpl: (() -> Void)? + var forEachControllerImpl: (((ContainableController) -> Void) -> Void)? + var getAccessibilityElementsImpl: (() -> [Any]?)? + + override var frame: CGRect { + didSet { + if self.frame.size != oldValue.size { + self.updateSize?(self.frame.size) + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.layoutSubviewsEvent?() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return self.hitTestImpl?(point, event) + } + + func invalidateDeferScreenEdgeGestures() { + self.invalidateDeferScreenEdgeGestureImpl?() + } + + func invalidatePreferNavigationUIHidden() { + self.invalidatePreferNavigationUIHiddenImpl?() + } + + func cancelInteractiveKeyboardGestures() { + self.cancelInteractiveKeyboardGesturesImpl?() + } + + func forEachController(_ f: (ContainableController) -> Void) { + self.forEachControllerImpl?(f) + } + + func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) { + self.presentController?(controller, level, blockInteraction, completion) + } + + func presentInGlobalOverlay(_ controller: ContainableController) { + } +} + +public func childWindowHostView(parent: UIView) -> WindowHostView { + let view = ChildWindowHostView() + view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + let hostView = WindowHostView(containerView: view, eventView: view, isRotating: { + return false + }, updateSupportedInterfaceOrientations: { orientations in + }, updateDeferScreenEdgeGestures: { edges in + }, updatePreferNavigationUIHidden: { value in + }) + + view.updateSize = { [weak hostView] size in + hostView?.updateSize?(size, 0.0) + } + + view.layoutSubviewsEvent = { [weak hostView] in + hostView?.layoutSubviews?() + } + + /*window.updateIsUpdatingOrientationLayout = { [weak hostView] value in + hostView?.isUpdatingOrientationLayout = value + } + + window.updateToInterfaceOrientation = { [weak hostView] in + hostView?.updateToInterfaceOrientation?() + }*/ + + view.presentController = { [weak hostView] controller, level, block, f in + hostView?.present?(controller, level, block, f) + } + + /*view.presentNativeImpl = { [weak hostView] controller in + hostView?.presentNative?(controller) + }*/ + + view.hitTestImpl = { [weak hostView] point, event in + return hostView?.hitTest?(point, event) + } + + view.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in + return hostView?.invalidateDeferScreenEdgeGesture?() + } + + view.invalidatePreferNavigationUIHiddenImpl = { [weak hostView] in + return hostView?.invalidatePreferNavigationUIHidden?() + } + + view.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in + hostView?.cancelInteractiveKeyboardGestures?() + } + + view.forEachControllerImpl = { [weak hostView] f in + hostView?.forEachController?(f) + } + + view.getAccessibilityElementsImpl = { [weak hostView] in + return hostView?.getAccessibilityElements?() + } + + return hostView +} diff --git a/submodules/Display/Display/CollectionIndexNode.swift b/submodules/Display/Display/CollectionIndexNode.swift new file mode 100644 index 0000000000..60fd7bbe36 --- /dev/null +++ b/submodules/Display/Display/CollectionIndexNode.swift @@ -0,0 +1,166 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +private let titleFont = Font.bold(11.0) + +public final class CollectionIndexNode: ASDisplayNode { + public static let searchIndex: String = "_$search$_" + + private var currentSize: CGSize? + private var currentSections: [String] = [] + private var currentColor: UIColor? + private var titleNodes: [String: (node: ImmediateTextNode, size: CGSize)] = [:] + private var scrollFeedback: HapticFeedback? + + private var currentSelectedIndex: String? + public var indexSelected: ((String) -> Void)? + + override public init() { + super.init() + } + + override public func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + public func update(size: CGSize, color: UIColor, sections: [String], transition: ContainedViewLayoutTransition) { + if self.currentColor == nil || !color.isEqual(self.currentColor) { + self.currentColor = color + for (title, nodeAndSize) in self.titleNodes { + nodeAndSize.node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color) + let _ = nodeAndSize.node.updateLayout(CGSize(width: 100.0, height: 100.0)) + } + } + + if self.currentSize == size && self.currentSections == sections { + return + } + + self.currentSize = size + self.currentSections = sections + + let itemHeight: CGFloat = 15.0 + let verticalInset: CGFloat = 10.0 + let maxHeight = size.height - verticalInset * 2.0 + + let maxItemCount = min(sections.count, Int(floor(maxHeight / itemHeight))) + let skipCount: Int + if sections.isEmpty { + skipCount = 1 + } else { + skipCount = Int(ceil(CGFloat(sections.count) / CGFloat(maxItemCount))) + } + let actualCount: CGFloat = ceil(CGFloat(sections.count) / CGFloat(skipCount)) + + let totalHeight = actualCount * itemHeight + let verticalOrigin = verticalInset + floor((maxHeight - totalHeight) / 2.0) + + var validTitles = Set() + + var currentIndex = 0 + var displayIndex = 0 + var addedLastTitle = false + + let addTitle: (Int) -> Void = { index in + let title = sections[index] + let nodeAndSize: (node: ImmediateTextNode, size: CGSize) + var animate = false + if let current = self.titleNodes[title] { + animate = true + nodeAndSize = current + } else { + let node = ImmediateTextNode() + node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color) + let nodeSize = node.updateLayout(CGSize(width: 100.0, height: 100.0)) + nodeAndSize = (node, nodeSize) + self.addSubnode(node) + self.titleNodes[title] = nodeAndSize + } + validTitles.insert(title) + let previousPosition = nodeAndSize.node.position + nodeAndSize.node.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeAndSize.size.width) / 2.0), y: verticalOrigin + itemHeight * CGFloat(displayIndex) + floor((itemHeight - nodeAndSize.size.height) / 2.0)), size: nodeAndSize.size) + if animate { + transition.animatePosition(node: nodeAndSize.node, from: previousPosition) + } + + currentIndex += skipCount + displayIndex += 1 + } + + while currentIndex < sections.count { + if currentIndex == sections.count - 1 { + addedLastTitle = true + } + addTitle(currentIndex) + } + + if !addedLastTitle && sections.count > 0 { + addTitle(sections.count - 1) + } + + var removeTitles: [String] = [] + for title in self.titleNodes.keys { + if !validTitles.contains(title) { + removeTitles.append(title) + } + } + + for title in removeTitles { + self.titleNodes.removeValue(forKey: title)?.node.removeFromSupernode() + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.isUserInteractionEnabled, self.bounds.insetBy(dx: -5.0, dy: 0.0).contains(point) { + return self.view + } else { + return nil + } + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + var locationTitleAndPosition: (String, CGFloat)? + let location = recognizer.location(in: self.view) + for (title, nodeAndSize) in self.titleNodes { + let nodeFrame = nodeAndSize.node.frame + if location.y >= nodeFrame.minY - 5.0 && location.y <= nodeFrame.maxY + 5.0 { + if let currentTitleAndPosition = locationTitleAndPosition { + let distance = abs(nodeFrame.midY - location.y) + let previousDistance = abs(currentTitleAndPosition.1 - location.y) + if distance < previousDistance { + locationTitleAndPosition = (title, nodeFrame.midY) + } + } else { + locationTitleAndPosition = (title, nodeFrame.midY) + } + } + } + let locationTitle = locationTitleAndPosition?.0 + switch recognizer.state { + case .began: + self.currentSelectedIndex = locationTitle + if let locationTitle = locationTitle { + self.indexSelected?(locationTitle) + } + case .changed: + if locationTitle != self.currentSelectedIndex { + self.currentSelectedIndex = locationTitle + if let locationTitle = locationTitle { + self.indexSelected?(locationTitle) + + if self.scrollFeedback == nil { + self.scrollFeedback = HapticFeedback() + } + self.scrollFeedback?.tap() + } + } + case .cancelled, .ended: + self.currentSelectedIndex = nil + default: + break + } + } +} diff --git a/submodules/Display/Display/ContainableController.swift b/submodules/Display/Display/ContainableController.swift new file mode 100644 index 0000000000..6e940cec68 --- /dev/null +++ b/submodules/Display/Display/ContainableController.swift @@ -0,0 +1,27 @@ +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +public protocol PresentableController: class { + func viewDidAppear(completion: @escaping () -> Void) +} + +public protocol ContainableController: class { + var view: UIView! { get } + var displayNode: ASDisplayNode { get } + var isViewLoaded: Bool { get } + var isOpaqueWhenInOverlay: Bool { get } + var blocksBackgroundWhenInOverlay: Bool { get } + var ready: Promise { get } + + func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations + var deferScreenEdgeGestures: UIRectEdge { get } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) + func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) + + func viewWillAppear(_ animated: Bool) + func viewWillDisappear(_ animated: Bool) + func viewDidAppear(_ animated: Bool) + func viewDidDisappear(_ animated: Bool) +} diff --git a/submodules/Display/Display/ContainedViewLayoutTransition.swift b/submodules/Display/Display/ContainedViewLayoutTransition.swift new file mode 100644 index 0000000000..d7f3581ad2 --- /dev/null +++ b/submodules/Display/Display/ContainedViewLayoutTransition.swift @@ -0,0 +1,675 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum ContainedViewLayoutTransitionCurve { + case easeInOut + case spring + case custom(Float, Float, Float, Float) +} + +public extension ContainedViewLayoutTransitionCurve { + var timingFunction: String { + switch self { + case .easeInOut: + return kCAMediaTimingFunctionEaseInEaseOut + case .spring: + return kCAMediaTimingFunctionSpring + case .custom: + return kCAMediaTimingFunctionEaseInEaseOut + } + } + + var mediaTimingFunction: CAMediaTimingFunction? { + switch self { + case .easeInOut: + return nil + case .spring: + return nil + case let .custom(p1, p2, p3, p4): + return CAMediaTimingFunction(controlPoints: p1, p2, p3, p4) + } + } + + #if os(iOS) + var viewAnimationOptions: UIViewAnimationOptions { + switch self { + case .easeInOut: + return [.curveEaseInOut] + case .spring: + return UIViewAnimationOptions(rawValue: 7 << 16) + case .custom: + return [] + } + } + #endif +} + +public enum ContainedViewLayoutTransition { + case immediate + case animated(duration: Double, curve: ContainedViewLayoutTransitionCurve) + + public var isAnimated: Bool { + if case .immediate = self { + return false + } else { + return true + } + } +} + +public extension ContainedViewLayoutTransition { + func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if node.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + node.frame = frame + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousFrame = node.frame + node.frame = frame + node.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if node.bounds.equalTo(bounds) && !force { + completion?(true) + } else { + switch self { + case .immediate: + node.bounds = bounds + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousBounds = node.bounds + node.bounds = bounds + node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func updateBounds(layer: CALayer, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if layer.bounds.equalTo(bounds) && !force { + completion?(true) + } else { + switch self { + case .immediate: + layer.bounds = bounds + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousBounds = layer.bounds + layer.bounds = bounds + layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func updatePosition(node: ASDisplayNode, position: CGPoint, completion: ((Bool) -> Void)? = nil) { + if node.position.equalTo(position) { + completion?(true) + } else { + switch self { + case .immediate: + node.position = position + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousPosition = node.position + node.position = position + node.layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) { + if layer.position.equalTo(position) { + completion?(true) + } else { + switch self { + case .immediate: + layer.position = position + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousPosition = layer.position + layer.position = position + layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func animatePosition(node: ASDisplayNode, from position: CGPoint, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.animatePosition(from: position, to: node.position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func animatePosition(node: ASDisplayNode, to position: CGPoint, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + if node.position.equalTo(position) { + completion?(true) + } else { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.animatePosition(from: node.position, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func animateFrame(node: ASDisplayNode, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.animateFrame(from: frame, to: toFrame ?? node.layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func animateBounds(layer: CALayer, from bounds: CGRect, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.animateBounds(from: bounds, to: layer.bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func animateOffsetAdditive(node: ASDisplayNode, offset: CGFloat) { + switch self { + case .immediate: + break + case let .animated(duration, curve): + node.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) + } + } + + func animateHorizontalOffsetAdditive(node: ASDisplayNode, offset: CGFloat) { + switch self { + case .immediate: + break + case let .animated(duration, curve): + node.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) + } + } + + func animateOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) { + switch self { + case .immediate: + completion?() + case let .animated(duration, curve): + layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in + completion?() + }) + } + } + + func animatePositionAdditive(node: ASDisplayNode, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { + switch self { + case .immediate: + break + case let .animated(duration, curve): + node.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) + } + } + + func animatePositionAdditive(layer: CALayer, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { + switch self { + case .immediate: + break + case let .animated(duration, curve): + layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) + } + } + + func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { + switch self { + case .immediate: + break + case let .animated(duration, curve): + node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in + completion?() + }) + } + } + + func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { + switch self { + case .immediate: + break + case let .animated(duration, curve): + layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in + completion?() + }) + } + } + + func updateFrame(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if view.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + view.frame = frame + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousFrame = view.frame + view.frame = frame + view.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) { + if layer.frame.equalTo(frame) { + completion?(true) + } else { + switch self { + case .immediate: + layer.frame = frame + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousFrame = layer.frame + layer.frame = frame + layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + + func updateAlpha(node: ASDisplayNode, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + if node.alpha.isEqual(to: alpha) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.alpha = alpha + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousAlpha = node.alpha + node.alpha = alpha + node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + if layer.opacity.isEqual(to: Float(alpha)) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + layer.opacity = Float(alpha) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousAlpha = layer.opacity + layer.opacity = Float(alpha) + layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateBackgroundColor(node: ASDisplayNode, color: UIColor, completion: ((Bool) -> Void)? = nil) { + if let nodeColor = node.backgroundColor, nodeColor.isEqual(color) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.backgroundColor = color + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + if let nodeColor = node.backgroundColor { + node.backgroundColor = color + node.layer.animate(from: nodeColor.cgColor, to: color.cgColor, keyPath: "backgroundColor", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } else { + node.backgroundColor = color + if let completion = completion { + completion(true) + } + } + } + } + + func updateCornerRadius(node: ASDisplayNode, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) { + if node.cornerRadius.isEqual(to: cornerRadius) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.cornerRadius = cornerRadius + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousCornerRadius = node.cornerRadius + node.cornerRadius = cornerRadius + node.layer.animate(from: NSNumber(value: Float(previousCornerRadius)), to: NSNumber(value: Float(cornerRadius)), keyPath: "cornerRadius", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) { + let t = node.layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: fromScale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateTransformScale(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + let t = node.layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: scale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + node.layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateTransformScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + let t = layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: scale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) + return + } + let t = node.layer.sublayerTransform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: scale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + return + } + let t = node.layer.sublayerTransform + let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23)) + if t.m22 < 0.0 { + currentScaleY = -currentScaleY + } + if CGPoint(x: currentScaleX, y: currentScaleY) == scale { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + return + } + let t = node.layer.transform + let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23)) + if t.m22 < 0.0 { + currentScaleY = -currentScaleY + } + if CGPoint(x: currentScaleX, y: currentScaleY) == scale { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateSublayerTransformOffset(layer: CALayer, offset: CGPoint, completion: ((Bool) -> Void)? = nil) { + let t = layer.transform + let currentOffset = CGPoint(x: t.m41, y: t.m42) + if currentOffset == offset { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0) + layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } +} + +#if os(iOS) + +public extension ContainedViewLayoutTransition { + public func animateView(_ f: @escaping () -> Void) { + switch self { + case .immediate: + f() + case let .animated(duration, curve): + UIView.animate(withDuration: duration, delay: 0.0, options: curve.viewAnimationOptions, animations: { + f() + }, completion: nil) + } + } +} + +#endif diff --git a/submodules/Display/Display/ContainerViewLayout.swift b/submodules/Display/Display/ContainerViewLayout.swift new file mode 100644 index 0000000000..a516e86638 --- /dev/null +++ b/submodules/Display/Display/ContainerViewLayout.swift @@ -0,0 +1,87 @@ +import UIKit + +public struct ContainerViewLayoutInsetOptions: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let statusBar = ContainerViewLayoutInsetOptions(rawValue: 1 << 0) + public static let input = ContainerViewLayoutInsetOptions(rawValue: 1 << 1) +} + +public enum ContainerViewLayoutSizeClass { + case compact + case regular +} + +public struct LayoutMetrics: Equatable { + public let widthClass: ContainerViewLayoutSizeClass + public let heightClass: ContainerViewLayoutSizeClass + + public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) { + self.widthClass = widthClass + self.heightClass = heightClass + } + + public init() { + self.widthClass = .compact + self.heightClass = .compact + } +} + +public struct ContainerViewLayout: Equatable { + public let size: CGSize + public let metrics: LayoutMetrics + public let intrinsicInsets: UIEdgeInsets + public let safeInsets: UIEdgeInsets + public let statusBarHeight: CGFloat? + public var inputHeight: CGFloat? + public let standardInputHeight: CGFloat + public let inputHeightIsInteractivellyChanging: Bool + public let inVoiceOver: Bool + + public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, standardInputHeight: CGFloat, inputHeightIsInteractivellyChanging: Bool, inVoiceOver: Bool) { + self.size = size + self.metrics = metrics + self.intrinsicInsets = intrinsicInsets + self.safeInsets = safeInsets + self.statusBarHeight = statusBarHeight + self.inputHeight = inputHeight + self.standardInputHeight = standardInputHeight + self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging + self.inVoiceOver = inVoiceOver + } + + public func insets(options: ContainerViewLayoutInsetOptions) -> UIEdgeInsets { + var insets = self.intrinsicInsets + if let statusBarHeight = self.statusBarHeight , options.contains(.statusBar) { + insets.top += statusBarHeight + } + if let inputHeight = self.inputHeight , options.contains(.input) { + insets.bottom = max(inputHeight, insets.bottom) + } + return insets + } + + public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout { + return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: UIEdgeInsets(top: self.intrinsicInsets.top + insets.top, left: self.intrinsicInsets.left + insets.left, bottom: self.intrinsicInsets.bottom + insets.bottom, right: self.intrinsicInsets.right + insets.right), safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) + } + + public func withUpdatedSize(_ size: CGSize) -> ContainerViewLayout { + return ContainerViewLayout(size: size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) + } + + public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout { + return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) + } + + public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout { + return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) + } +} diff --git a/submodules/Display/Display/ContextMenuAction.swift b/submodules/Display/Display/ContextMenuAction.swift new file mode 100644 index 0000000000..3646cc5c94 --- /dev/null +++ b/submodules/Display/Display/ContextMenuAction.swift @@ -0,0 +1,16 @@ +import UIKit + +public enum ContextMenuActionContent { + case text(title: String, accessibilityLabel: String) + case icon(UIImage) +} + +public struct ContextMenuAction { + public let content: ContextMenuActionContent + public let action: () -> Void + + public init(content: ContextMenuActionContent, action: @escaping () -> Void) { + self.content = content + self.action = action + } +} diff --git a/submodules/Display/Display/ContextMenuActionNode.swift b/submodules/Display/Display/ContextMenuActionNode.swift new file mode 100644 index 0000000000..135e5c9c06 --- /dev/null +++ b/submodules/Display/Display/ContextMenuActionNode.swift @@ -0,0 +1,118 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final private class ContextMenuActionButton: HighlightTrackingButton { + override func convert(_ point: CGPoint, from view: UIView?) -> CGPoint { + if view is UIWindow { + return super.convert(point, from: nil) + } else { + return super.convert(point, from: view) + } + } +} + +final class ContextMenuActionNode: ASDisplayNode { + private let textNode: ImmediateTextNode? + private var textSize: CGSize? + private let iconNode: ASImageNode? + private let action: () -> Void + private let button: ContextMenuActionButton + private let actionArea: AccessibilityAreaNode + + var dismiss: (() -> Void)? + + init(action: ContextMenuAction) { + self.actionArea = AccessibilityAreaNode() + self.actionArea.accessibilityTraits = UIAccessibilityTraitButton + + switch action.content { + case let .text(title, accessibilityLabel): + self.actionArea.accessibilityLabel = accessibilityLabel + + let textNode = ImmediateTextNode() + textNode.isUserInteractionEnabled = false + textNode.displaysAsynchronously = false + textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: UIColor.white) + textNode.isAccessibilityElement = false + + self.textNode = textNode + self.iconNode = nil + case let .icon(image): + let iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + iconNode.image = image + + self.iconNode = iconNode + self.textNode = nil + } + self.action = action.action + + self.button = ContextMenuActionButton() + self.button.isAccessibilityElement = false + + super.init() + + self.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + if let textNode = self.textNode { + self.addSubnode(textNode) + } + if let iconNode = self.iconNode { + self.addSubnode(iconNode) + } + + self.button.highligthedChanged = { [weak self] highlighted in + self?.backgroundColor = highlighted ? UIColor(white: 0.0, alpha: 0.4) : UIColor(white: 0.0, alpha: 0.8) + } + self.view.addSubview(self.button) + self.addSubnode(self.actionArea) + + self.actionArea.activate = { [weak self] in + self?.buttonPressed() + return true + } + } + + override func didLoad() { + super.didLoad() + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) + } + + @objc private func buttonPressed() { + self.backgroundColor = UIColor(white: 0.0, alpha: 0.4) + + self.action() + if let dismiss = self.dismiss { + dismiss() + } + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + if let textNode = self.textNode { + let textSize = textNode.updateLayout(constrainedSize) + self.textSize = textSize + return CGSize(width: textSize.width + 36.0, height: 54.0) + } else if let iconNode = self.iconNode, let image = iconNode.image { + return CGSize(width: image.size.width + 36.0, height: 54.0) + } else { + return CGSize(width: 36.0, height: 54.0) + } + } + + override func layout() { + super.layout() + + self.button.frame = self.bounds + self.actionArea.frame = self.bounds + + if let textNode = self.textNode, let textSize = self.textSize { + textNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - textSize.width) / 2.0), y: floor((self.bounds.size.height - textSize.height) / 2.0)), size: textSize) + } + if let iconNode = self.iconNode, let image = iconNode.image { + let iconSize = image.size + iconNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - iconSize.width) / 2.0), y: floor((self.bounds.size.height - iconSize.height) / 2.0)), size: iconSize) + } + } +} diff --git a/submodules/Display/Display/ContextMenuContainerNode.swift b/submodules/Display/Display/ContextMenuContainerNode.swift new file mode 100644 index 0000000000..b8b5a282ca --- /dev/null +++ b/submodules/Display/Display/ContextMenuContainerNode.swift @@ -0,0 +1,90 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +private struct CachedMaskParams: Equatable { + let size: CGSize + let relativeArrowPosition: CGFloat + let arrowOnBottom: Bool +} + +private final class ContextMenuContainerMaskView: UIView { + override class var layerClass: AnyClass { + return CAShapeLayer.self + } +} + +public final class ContextMenuContainerNode: ASDisplayNode { + private var cachedMaskParams: CachedMaskParams? + private let maskView = ContextMenuContainerMaskView() + + public var relativeArrowPosition: (CGFloat, Bool)? + + //private let effectView: UIVisualEffectView + + override public init() { + //self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + + super.init() + + self.backgroundColor = UIColor(rgb: 0xeaecec) + //self.view.addSubview(self.effectView) + //self.effectView.mask = self.maskView + self.view.mask = self.maskView + } + + override public func didLoad() { + super.didLoad() + + self.layer.allowsGroupOpacity = true + } + + override public func layout() { + super.layout() + + self.updateLayout(transition: .immediate) + } + + public func updateLayout(transition: ContainedViewLayoutTransition) { + //self.effectView.frame = self.bounds + + let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true) + if self.cachedMaskParams != maskParams { + let path = UIBezierPath() + let cornerRadius: CGFloat = 6.0 + let verticalInset: CGFloat = 9.0 + let arrowWidth: CGFloat = 18.0 + let requestedArrowPosition = maskParams.relativeArrowPosition + let arrowPosition = max(cornerRadius + arrowWidth / 2.0, min(maskParams.size.width - cornerRadius - arrowWidth / 2.0, requestedArrowPosition)) + let arrowOnBottom = maskParams.arrowOnBottom + + path.move(to: CGPoint(x: 0.0, y: verticalInset + cornerRadius)) + path.addArc(withCenter: CGPoint(x: cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: CGFloat(3.0 * CGFloat.pi / 2.0), clockwise: true) + if !arrowOnBottom { + path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: verticalInset)) + path.addLine(to: CGPoint(x: arrowPosition, y: 0.0)) + path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: verticalInset)) + } + path.addLine(to: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset)) + path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat(3.0 * CGFloat.pi / 2.0), endAngle: 0.0, clockwise: true) + path.addLine(to: CGPoint(x: maskParams.size.width, y: maskParams.size.height - cornerRadius - verticalInset)) + path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: 0.0, endAngle: CGFloat(CGFloat.pi / 2.0), clockwise: true) + if arrowOnBottom { + path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: maskParams.size.height - verticalInset)) + path.addLine(to: CGPoint(x: arrowPosition, y: maskParams.size.height)) + path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: maskParams.size.height - verticalInset)) + } + path.addLine(to: CGPoint(x: cornerRadius, y: maskParams.size.height - verticalInset)) + path.addArc(withCenter: CGPoint(x: cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: CGFloat(CGFloat.pi / 2.0), endAngle: CGFloat(M_PI), clockwise: true) + path.close() + + self.cachedMaskParams = maskParams + if let layer = self.maskView.layer as? CAShapeLayer { + if case let .animated(duration, curve) = transition, let previousPath = layer.path { + layer.animate(from: previousPath, to: path.cgPath, keyPath: "path", timingFunction: curve.timingFunction, duration: duration) + } + layer.path = path.cgPath + } + } + } +} diff --git a/submodules/Display/Display/ContextMenuController.swift b/submodules/Display/Display/ContextMenuController.swift new file mode 100644 index 0000000000..4e71c8d903 --- /dev/null +++ b/submodules/Display/Display/ContextMenuController.swift @@ -0,0 +1,96 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public final class ContextMenuControllerPresentationArguments { + fileprivate let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)? + + public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?) { + self.sourceNodeAndRect = sourceNodeAndRect + } +} + +public final class ContextMenuController: ViewController, KeyShortcutResponder { + private var contextMenuNode: ContextMenuNode { + return self.displayNode as! ContextMenuNode + } + + public var keyShortcuts: [KeyShortcut] { + return [KeyShortcut(input: UIKeyInputEscape, action: { [weak self] in + if let strongSelf = self { + strongSelf.dismiss() + } + })] + } + private let actions: [ContextMenuAction] + private let catchTapsOutside: Bool + private let hasHapticFeedback: Bool + + private var layout: ContainerViewLayout? + + public var dismissed: (() -> Void)? + + public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false) { + self.actions = actions + self.catchTapsOutside = catchTapsOutside + self.hasHapticFeedback = hasHapticFeedback + + super.init(navigationBarPresentationData: nil) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = ContextMenuNode(actions: self.actions, dismiss: { [weak self] in + self?.dismissed?() + self?.contextMenuNode.animateOut { + self?.presentingViewController?.dismiss(animated: false) + } + }, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback) + self.displayNodeDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.contextMenuNode.animateIn() + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.dismissed?() + self.contextMenuNode.animateOut { [weak self] in + self?.presentingViewController?.dismiss(animated: false) + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if self.layout != nil && self.layout! != layout { + self.dismissed?() + self.contextMenuNode.animateOut { [weak self] in + self?.presentingViewController?.dismiss(animated: false) + } + } else { + self.layout = layout + + if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() { + self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil) + self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil) + } else { + self.contextMenuNode.sourceRect = nil + self.contextMenuNode.containerRect = nil + } + + self.contextMenuNode.containerLayoutUpdated(layout, transition: transition) + } + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.contextMenuNode.animateIn() + } +} diff --git a/submodules/Display/Display/ContextMenuNode.swift b/submodules/Display/Display/ContextMenuNode.swift new file mode 100644 index 0000000000..447e1fadcf --- /dev/null +++ b/submodules/Display/Display/ContextMenuNode.swift @@ -0,0 +1,264 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +private func generateShadowImage() -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(white: 0.18, alpha: 1.0).cgColor) + context.setFillColor(UIColor(white: 0.18, alpha: 1.0).cgColor) + context.fill(CGRect(origin: CGPoint(x: -15.0, y: 0.0), size: CGSize(width: 30.0, height: 1.0))) + }) +} + +private final class ContextMenuContentScrollNode: ASDisplayNode { + var contentWidth: CGFloat = 0.0 + + private var initialOffset: CGFloat = 0.0 + + private let leftShadow: ASImageNode + private let rightShadow: ASImageNode + private let leftOverscrollNode: ASDisplayNode + private let rightOverscrollNode: ASDisplayNode + let contentNode: ASDisplayNode + + override init() { + self.contentNode = ASDisplayNode() + + let shadowImage = generateShadowImage() + + self.leftShadow = ASImageNode() + self.leftShadow.displayWithoutProcessing = true + self.leftShadow.displaysAsynchronously = false + self.leftShadow.image = shadowImage + self.rightShadow = ASImageNode() + self.rightShadow.displayWithoutProcessing = true + self.rightShadow.displaysAsynchronously = false + self.rightShadow.image = shadowImage + self.rightShadow.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + + self.leftOverscrollNode = ASDisplayNode() + self.leftOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + self.rightOverscrollNode = ASDisplayNode() + self.rightOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + + super.init() + + self.contentNode.addSubnode(self.leftOverscrollNode) + self.contentNode.addSubnode(self.rightOverscrollNode) + self.addSubnode(self.contentNode) + + self.addSubnode(self.leftShadow) + self.addSubnode(self.rightShadow) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.view.addGestureRecognizer(panRecognizer) + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.initialOffset = self.contentNode.bounds.origin.x + case .changed: + var bounds = self.contentNode.bounds + bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x + if bounds.origin.x > self.contentWidth - bounds.size.width { + let delta = bounds.origin.x - (self.contentWidth - bounds.size.width) + bounds.origin.x = self.contentWidth - bounds.size.width + ((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + if bounds.origin.x < 0.0 { + let delta = -bounds.origin.x + bounds.origin.x = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + self.contentNode.bounds = bounds + self.updateShadows(.immediate) + case .ended, .cancelled: + var bounds = self.contentNode.bounds + bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x + + var duration = 0.4 + + if abs(bounds.origin.x - self.initialOffset) > 10.0 || abs(recognizer.velocity(in: self.view).x) > 100.0 { + duration = 0.2 + if bounds.origin.x < self.initialOffset { + bounds.origin.x = 0.0 + } else { + bounds.origin.x = self.contentWidth - bounds.size.width + } + } else { + bounds.origin.x = self.initialOffset + } + + if bounds.origin.x > self.contentWidth - bounds.size.width { + bounds.origin.x = self.contentWidth - bounds.size.width + } + if bounds.origin.x < 0.0 { + bounds.origin.x = 0.0 + } + let previousBounds = self.contentNode.bounds + self.contentNode.bounds = bounds + self.contentNode.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.updateShadows(.animated(duration: duration, curve: .spring)) + default: + break + } + } + + override func layout() { + let bounds = self.bounds + self.contentNode.frame = bounds + self.leftShadow.frame = CGRect(origin: CGPoint(), size: CGSize(width: 30.0, height: bounds.height)) + self.rightShadow.frame = CGRect(origin: CGPoint(x: bounds.size.width - 30.0, y: 0.0), size: CGSize(width: 30.0, height: bounds.height)) + self.leftOverscrollNode.frame = bounds.offsetBy(dx: -bounds.width, dy: 0.0) + self.rightOverscrollNode.frame = bounds.offsetBy(dx: self.contentWidth, dy: 0.0) + self.updateShadows(.immediate) + } + + private func updateShadows(_ transition: ContainedViewLayoutTransition) { + let bounds = self.contentNode.bounds + + let leftAlpha = max(0.0, min(1.0, bounds.minX / 20.0)) + transition.updateAlpha(node: self.leftShadow, alpha: leftAlpha) + + let rightAlpha = max(0.0, min(1.0, (self.contentWidth - bounds.maxX) / 20.0)) + transition.updateAlpha(node: self.rightShadow, alpha: rightAlpha) + } +} + +final class ContextMenuNode: ASDisplayNode { + private let actions: [ContextMenuAction] + private let dismiss: () -> Void + + private let containerNode: ContextMenuContainerNode + private let scrollNode: ContextMenuContentScrollNode + private let actionNodes: [ContextMenuActionNode] + + var sourceRect: CGRect? + var containerRect: CGRect? + var arrowOnBottom: Bool = true + + private var dismissedByTouchOutside = false + private let catchTapsOutside: Bool + + private let feedback: HapticFeedback? + + init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, catchTapsOutside: Bool, hasHapticFeedback: Bool = false) { + self.actions = actions + self.dismiss = dismiss + self.catchTapsOutside = catchTapsOutside + + self.containerNode = ContextMenuContainerNode() + self.scrollNode = ContextMenuContentScrollNode() + + self.actionNodes = actions.map { action in + return ContextMenuActionNode(action: action) + } + + if hasHapticFeedback { + self.feedback = HapticFeedback() + self.feedback?.prepareImpact(.light) + } else { + self.feedback = nil + } + + super.init() + + self.containerNode.addSubnode(self.scrollNode) + + self.addSubnode(self.containerNode) + let dismissNode = { + dismiss() + } + for actionNode in self.actionNodes { + actionNode.dismiss = dismissNode + self.scrollNode.contentNode.addSubnode(actionNode) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var unboundActionsWidth: CGFloat = 0.0 + let actionSeparatorWidth: CGFloat = UIScreenPixel + for actionNode in self.actionNodes { + if !unboundActionsWidth.isZero { + unboundActionsWidth += actionSeparatorWidth + } + let actionSize = actionNode.measure(CGSize(width: layout.size.width, height: 54.0)) + actionNode.frame = CGRect(origin: CGPoint(x: unboundActionsWidth, y: 0.0), size: actionSize) + unboundActionsWidth += actionSize.width + } + + let maxActionsWidth = layout.size.width - 20.0 + let actionsWidth = min(unboundActionsWidth, maxActionsWidth) + + let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize()) + let containerRect: CGRect = self.containerRect ?? self.bounds + + let insets = layout.insets(options: [.statusBar, .input]) + + let verticalOrigin: CGFloat + var arrowOnBottom = true + if sourceRect.minY - 54.0 > containerRect.minY + insets.top { + verticalOrigin = sourceRect.minY - 54.0 + } else { + verticalOrigin = min(containerRect.maxY - insets.bottom - 54.0, sourceRect.maxY) + arrowOnBottom = false + } + self.arrowOnBottom = arrowOnBottom + + let horizontalOrigin: CGFloat = floor(max(8.0, min(max(sourceRect.minX + 8.0, sourceRect.midX - actionsWidth / 2.0), layout.size.width - actionsWidth - 8.0))) + + self.containerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: CGSize(width: actionsWidth, height: 54.0)) + self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom) + + self.scrollNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: actionsWidth, height: 54.0)) + self.scrollNode.contentWidth = unboundActionsWidth + + self.containerNode.layout() + self.scrollNode.layout() + } + + func animateIn() { + self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4) + + let containerPosition = self.containerNode.layer.position + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4) + + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + if let feedback = self.feedback { + feedback.impact(.light) + } + } + + func animateOut(completion: @escaping () -> Void) { + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let event = event { + var eventIsPresses = false + if #available(iOSApplicationExtension 9.0, *) { + eventIsPresses = event.type == .presses + } + if event.type == .touches || eventIsPresses { + if !self.containerNode.frame.contains(point) { + if !self.dismissedByTouchOutside { + self.dismissedByTouchOutside = true + self.dismiss() + } + if self.catchTapsOutside { + return self.view + } + return nil + } + } + } + return super.hitTest(point, with: event) + } +} diff --git a/submodules/Display/Display/DeviceMetrics.swift b/submodules/Display/Display/DeviceMetrics.swift new file mode 100644 index 0000000000..7e7f03e235 --- /dev/null +++ b/submodules/Display/Display/DeviceMetrics.swift @@ -0,0 +1,184 @@ +import UIKit + +public enum DeviceMetrics: CaseIterable { + case iPhone4 + case iPhone5 + case iPhone6 + case iPhone6Plus + case iPhoneX + case iPhoneXSMax + case iPad + case iPadPro10Inch + case iPadPro11Inch + case iPadPro + case iPadPro3rdGen + + public static func forScreenSize(_ size: CGSize, hintHasOnScreenNavigation: Bool = false) -> DeviceMetrics? { + let additionalSize = CGSize(width: size.width, height: size.height + 20.0) + for device in DeviceMetrics.allCases { + let width = device.screenSize.width + let height = device.screenSize.height + + if ((size.width.isEqual(to: width) && size.height.isEqual(to: height)) || size.height.isEqual(to: width) && size.width.isEqual(to: height)) || ((additionalSize.width.isEqual(to: width) && additionalSize.height.isEqual(to: height)) || additionalSize.height.isEqual(to: width) && additionalSize.width.isEqual(to: height)) { + if hintHasOnScreenNavigation && device.onScreenNavigationHeight(inLandscape: false) == nil { + continue + } + return device + } + } + + + return nil + } + + var screenSize: CGSize { + switch self { + case .iPhone4: + return CGSize(width: 320.0, height: 480.0) + case .iPhone5: + return CGSize(width: 320.0, height: 568.0) + case .iPhone6: + return CGSize(width: 375.0, height: 667.0) + case .iPhone6Plus: + return CGSize(width: 414.0, height: 736.0) + case .iPhoneX: + return CGSize(width: 375.0, height: 812.0) + case .iPhoneXSMax: + return CGSize(width: 414.0, height: 896.0) + case .iPad: + return CGSize(width: 768.0, height: 1024.0) + case .iPadPro10Inch: + return CGSize(width: 834.0, height: 1112.0) + case .iPadPro11Inch: + return CGSize(width: 834.0, height: 1194.0) + case .iPadPro, .iPadPro3rdGen: + return CGSize(width: 1024.0, height: 1366.0) + } + } + + func safeAreaInsets(inLandscape: Bool) -> UIEdgeInsets { + switch self { + case .iPhoneX, .iPhoneXSMax: + return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) + default: + return UIEdgeInsets.zero + } + } + + func onScreenNavigationHeight(inLandscape: Bool) -> CGFloat? { + switch self { + case .iPhoneX, .iPhoneXSMax: + return inLandscape ? 21.0 : 34.0 + case .iPadPro3rdGen, .iPadPro11Inch: + return 21.0 + default: + return nil + } + } + + var statusBarHeight: CGFloat { + switch self { + case .iPhoneX, .iPhoneXSMax: + return 44.0 + case .iPadPro11Inch, .iPadPro3rdGen: + return 24.0 + default: + return 20.0 + } + } + + public func standardInputHeight(inLandscape: Bool) -> CGFloat { + if inLandscape { + switch self { + case .iPhone4, .iPhone5: + return 162.0 + case .iPhone6, .iPhone6Plus: + return 163.0 + case .iPhoneX, .iPhoneXSMax: + return 172.0 + case .iPad, .iPadPro10Inch: + return 348.0 + case .iPadPro11Inch: + return 368.0 + case .iPadPro: + return 421.0 + case .iPadPro3rdGen: + return 441.0 + } + } else { + switch self { + case .iPhone4, .iPhone5, .iPhone6: + return 216.0 + case .iPhone6Plus: + return 227.0 + case .iPhoneX: + return 291.0 + case .iPhoneXSMax: + return 302.0 + case .iPad, .iPadPro10Inch: + return 263.0 + case .iPadPro11Inch: + return 283.0 + case .iPadPro: + return 328.0 + case .iPadPro3rdGen: + return 348.0 + } + } + } + + func predictiveInputHeight(inLandscape: Bool) -> CGFloat { + if inLandscape { + switch self { + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax: + return 37.0 + case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen: + return 50.0 + } + } else { + switch self { + case .iPhone4, .iPhone5: + return 37.0 + case .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax: + return 44.0 + case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen: + return 50.0 + } + } + } + + public func previewingContentSize(inLandscape: Bool) -> CGSize { + let screenSize = self.screenSize + if inLandscape { + switch self { + case .iPhone5: + return CGSize(width: screenSize.height, height: screenSize.width - 10.0) + case .iPhone6: + return CGSize(width: screenSize.height, height: screenSize.width - 22.0) + case .iPhone6Plus: + return CGSize(width: screenSize.height, height: screenSize.width - 22.0) + case .iPhoneX: + return CGSize(width: screenSize.height, height: screenSize.width + 48.0) + case .iPhoneXSMax: + return CGSize(width: screenSize.height, height: screenSize.width - 30.0) + default: + return CGSize(width: screenSize.height, height: screenSize.width - 10.0) + } + } else { + switch self { + case .iPhone5: + return CGSize(width: screenSize.width, height: screenSize.height - 50.0) + case .iPhone6: + return CGSize(width: screenSize.width, height: screenSize.height - 97.0) + case .iPhone6Plus: + return CGSize(width: screenSize.width, height: screenSize.height - 95.0) + case .iPhoneX: + return CGSize(width: screenSize.width, height: screenSize.height - 154.0) + case .iPhoneXSMax: + return CGSize(width: screenSize.width, height: screenSize.height - 84.0) + default: + return CGSize(width: screenSize.width, height: screenSize.height - 50.0) + } + } + } +} diff --git a/submodules/Display/Display/Display.h b/submodules/Display/Display/Display.h new file mode 100644 index 0000000000..59ea13a08f --- /dev/null +++ b/submodules/Display/Display/Display.h @@ -0,0 +1,34 @@ +// +// Display.h +// Display +// +// Created by Peter on 29/07/15. +// Copyright © 2015 Telegram. All rights reserved. +// + +#import + +//! Project version number for Display. +FOUNDATION_EXPORT double DisplayVersionNumber; + +//! Project version string for Display. +FOUNDATION_EXPORT const unsigned char DisplayVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/submodules/Display/Display/DisplayLinkDispatcher.swift b/submodules/Display/Display/DisplayLinkDispatcher.swift new file mode 100644 index 0000000000..2519977ad3 --- /dev/null +++ b/submodules/Display/Display/DisplayLinkDispatcher.swift @@ -0,0 +1,47 @@ +import Foundation +import UIKit + +public class DisplayLinkDispatcher: NSObject { + private var displayLink: CADisplayLink! + private var blocksToDispatch: [() -> Void] = [] + private let limit: Int + + public init(limit: Int = 0) { + self.limit = limit + + super.init() + + if #available(iOS 10.0, *) { + //self.displayLink.preferredFramesPerSecond = 60 + } else { + self.displayLink = CADisplayLink(target: self, selector: #selector(self.run)) + self.displayLink.isPaused = true + self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) + } + } + + public func dispatch(f: @escaping () -> Void) { + if self.displayLink == nil { + if Thread.isMainThread { + f() + } else { + DispatchQueue.main.async(execute: f) + } + } else { + self.blocksToDispatch.append(f) + self.displayLink.isPaused = false + } + } + + @objc func run() { + for _ in 0 ..< (self.limit == 0 ? 1000 : self.limit) { + if self.blocksToDispatch.count == 0 { + self.displayLink.isPaused = true + break + } else { + let f = self.blocksToDispatch.removeFirst() + f() + } + } + } +} diff --git a/submodules/Display/Display/EditableTextNode.swift b/submodules/Display/Display/EditableTextNode.swift new file mode 100644 index 0000000000..aa1f1add3c --- /dev/null +++ b/submodules/Display/Display/EditableTextNode.swift @@ -0,0 +1,24 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public class EditableTextNode: ASEditableTextNode { + override public var keyboardAppearance: UIKeyboardAppearance { + get { + return super.keyboardAppearance + } + set { + guard newValue != self.keyboardAppearance else { + return + } + let resigning = self.isFirstResponder() + if resigning { + self.resignFirstResponder() + } + super.keyboardAppearance = newValue + if resigning { + self.becomeFirstResponder() + } + } + } +} diff --git a/submodules/Display/Display/FBAnimationPerformanceTracker.h b/submodules/Display/Display/FBAnimationPerformanceTracker.h new file mode 100644 index 0000000000..f9cad92ae9 --- /dev/null +++ b/submodules/Display/Display/FBAnimationPerformanceTracker.h @@ -0,0 +1,136 @@ +#import + +/* + * This is an example provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * + * FBAnimationPerformanceTracker + * ----------------------------------------------------------------------- + * + * This class provides animation performance tracking functionality. It basically + * measures the app's frame rate during an operation, and reports this information. + * + * 1) In Foo's designated initializer, construct a tracker object + * + * 2) Add calls to -start and -stop in appropriate places, e.g. for a ScrollView + * + * - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + * [_apTracker start]; + * } + * + * - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView + * { + * if (!scrollView.dragging) { + * [_apTracker stop]; + * } + * } + * + * - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + * if (!decelerate) { + * [_apTracker stop]; + * } + * } + * + * Notes + * ----- + * [] The tracker operates by creating a CADisplayLink object to measure the frame rate of the display + * during start/stop interval. + * + * [] Calls to -stop that were not preceded by a matching call to -start have no effect. + * + * [] 2 calls to -start in a row will trash the data accumulated so far and not log anything. + * + * + * Configuration object for the core tracker + * + * =============================================================================== + * I highly recommend for you to use the standard configuration provided + * These are essentially here so that the computation of the metric is transparent + * and you can feel confident in what the numbers mean. + * =============================================================================== + */ +struct FBAnimationPerformanceTrackerConfig +{ + // Number of frame drop that defines a "small" drop event. By default, 1. + NSInteger smallDropEventFrameNumber; + // Number of frame drop that defines a "large" drop event. By default, 4. + NSInteger largeDropEventFrameNumber; + // Number of maximum frame drops to which the drop will be trimmed down to. Currently 15. + NSInteger maxFrameDropAccount; + + // If YES, will report stack traces + BOOL reportStackTraces; +}; +typedef struct FBAnimationPerformanceTrackerConfig FBAnimationPerformanceTrackerConfig; + + +@protocol FBAnimationPerformanceTrackerDelegate + +/** + * Core Metric + * + * You are responsible for the aggregation of these metrics (it being on the client or the server). I recommend to implement both + * to limit the payload you are sending to the server. + * + * The final recommended metric being: - SUM(duration) / SUM(smallDropEvent) aka the number of seconds between one frame drop or more + * - SUM(duration) / SUM(largeDropEvent) aka the number of seconds between four frame drops or more + * + * The first metric will tell you how smooth is your scroll view. + * The second metric will tell you how clowny your scroll view can get. + * + * Every time stop is called, this event will fire reporting the performance. + * + * NOTE on this metric: + * - It has been tested at scale on many Facebook apps. + * - It follows the curves of devices. + * - You will need about 100K calls for the number to converge. + * - It is perfectly correlated to X = Percentage of time spent at 60fps. Number of seconds between one frame drop = 1 / ( 1 - Time spent at 60 fps) + * - We report fraction of drops. 7 frame drop = 1.75 of a large frame drop if a large drop is 4 frame drop. + * This is to preserve the correlation mentionned above. + */ +- (void)reportDurationInMS:(NSInteger)duration smallDropEvent:(double)smallDropEvent largeDropEvent:(double)largeDropEvent; + +/** + * Stack traces + * + * Dark magic of the animation tracker. In case of a frame drop, this will return a stack trace. + * This will NOT be reported on the main-thread, but off-main thread to save a few CPU cycles. + * + * The slide is constant value that needs to be reported with the stack for processing. + * This currently only allows for symbolication of your own image. + * + * Future work includes symbolicating all modules. I personnaly find it usually + * good enough to know the name of the module. + * + * The stack will have the following format: + * Foundation:0x123|MyApp:0x234|MyApp:0x345| + * + * The slide will have the following format: + * 0x456 + */ +- (void)reportStackTrace:(NSString *)stack withSlide:(NSString *)slide; + +@end + +@interface FBAnimationPerformanceTracker : NSObject + +- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config; + ++ (FBAnimationPerformanceTrackerConfig)standardConfig; + +@property (weak, nonatomic, readwrite) id delegate; + +- (void)start; +- (void)stop; + +@end diff --git a/submodules/Display/Display/FBAnimationPerformanceTracker.mm b/submodules/Display/Display/FBAnimationPerformanceTracker.mm new file mode 100644 index 0000000000..7257c338b8 --- /dev/null +++ b/submodules/Display/Display/FBAnimationPerformanceTracker.mm @@ -0,0 +1,412 @@ +// +// FBAnimationPerformanceTracker.m +// Display +// +// Created by Peter on 3/16/16. +// Copyright © 2016 Telegram. All rights reserved. +// + +#import "FBAnimationPerformanceTracker.h" + +#import +#import +#import + +#import + +#import + +#import "execinfo.h" + +#include + +static BOOL _signalSetup; +static pthread_t _mainThread; +static NSThread *_trackerThread; + +static std::map> _imageNames; + +#ifdef __LP64__ +typedef mach_header_64 fb_mach_header; +typedef segment_command_64 fb_mach_segment_command; +#define LC_SEGMENT_ARCH LC_SEGMENT_64 +#else +typedef mach_header fb_mach_header; +typedef segment_command fb_mach_segment_command; +#define LC_SEGMENT_ARCH LC_SEGMENT +#endif + +static volatile BOOL _scrolling; +pthread_mutex_t _scrollingMutex; +pthread_cond_t _scrollingCondVariable; +dispatch_queue_t _symbolicationQueue; + +// We record at most 16 frames since I cap the number of frames dropped measured at 15. +// Past 15, something went very wrong (massive contention, priority inversion, rpc call going wrong...) . +// It will only pollute the data to get more. +static const int callstack_max_number = 16; + +static int callstack_i; +static bool callstack_dirty; +static int callstack_size[callstack_max_number]; +static void *callstacks[callstack_max_number][128]; +uint64_t callstack_time_capture; + +static void _callstack_signal_handler(int signr, siginfo_t *info, void *secret) +{ + // This is run on the main thread every 16 ms or so during scroll. + + // Signals are run one by one so there is no risk of concurrency of a signal + // by the same signal. + + // The backtrace call is technically signal-safe on Unix-based system + // See: http://www.unix.com/man-page/all/3c/walkcontext/ + + // WARNING: this is signal handler, no memory allocation is safe. + // Essentially nothing is safe unless specified it is. + callstack_size[callstack_i] = backtrace(callstacks[callstack_i], 128); + callstack_i = (callstack_i + 1) & (callstack_max_number - 1); // & is a cheap modulo (only works for power of 2) + callstack_dirty = true; +} + +@interface FBCallstack : NSObject +@property (nonatomic, readonly, assign) int size; +@property (nonatomic, readonly, assign) void **callstack; +- (instancetype)initWithSize:(int)size callstack:(void *)callstack; +@end + +@implementation FBCallstack +- (instancetype)initWithSize:(int)size callstack:(void *)callstack +{ + if (self = [super init]) { + _size = size; + _callstack = (void **)malloc(size * sizeof(void *)); + memcpy(_callstack, callstack, size * sizeof(void *)); + } + return self; +} + +- (void)dealloc +{ + free(_callstack); +} +@end + +@implementation FBAnimationPerformanceTracker +{ + FBAnimationPerformanceTrackerConfig _config; + + BOOL _tracking; + BOOL _firstUpdate; + NSTimeInterval _previousFrameTimestamp; + CADisplayLink *_displayLink; + BOOL _prepared; + + // numbers used to track the performance metrics + double _durationTotal; + double _maxFrameTime; + double _smallDrops; + double _largeDrops; +} + +- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config +{ + if (self = [super init]) { + // Stack trace logging is not working well in debug mode + // We don't want the data anyway. So let's bail. +#if defined(DEBUG) + config.reportStackTraces = NO; +#endif + _config = config; + if (config.reportStackTraces) { + [self _setupSignal]; + } + } + return self; +} + ++ (FBAnimationPerformanceTrackerConfig)standardConfig +{ + FBAnimationPerformanceTrackerConfig config = { + .smallDropEventFrameNumber = 1, + .largeDropEventFrameNumber = 4, + .maxFrameDropAccount = 15, + .reportStackTraces = NO + }; + return config; +} + ++ (void)_trackerLoop +{ + while (true) { + // If you are confused by this part, + // Check out https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables + + // Lock the mutex + pthread_mutex_lock(&_scrollingMutex); + while (!_scrolling) { + // Unlock the mutex and sleep until the conditional variable is signaled + pthread_cond_wait(&_scrollingCondVariable, &_scrollingMutex); + // The conditional variable was signaled, but we need to check _scrolling + // As nothing guarantees that it is still true + } + // _scrolling is true, go ahead and capture traces for a while. + pthread_mutex_unlock(&_scrollingMutex); + + // We are scrolling, yay, capture traces + while (_scrolling) { + usleep(16000); + + // Here I use SIGPROF which is a signal supposed to be used for profiling + // I haven't stumbled upon any collision so far. + // There is no guarantee that it won't impact the system in unpredicted ways. + // Use wisely. + + pthread_kill(_mainThread, SIGPROF); + } + } +} + +- (void)_setupSignal +{ + if (!_signalSetup) { + // The signal hook should be setup once and only once + _signalSetup = YES; + + // I actually don't know if the main thread can die. If it does, well, + // this is not going to work. + // UPDATE 4/2015: on iOS8, it looks like the main-thread never dies, and this pointer is correct + _mainThread = pthread_self(); + + callstack_i = 0; + + // Setup the signal + struct sigaction sa; + sigfillset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = _callstack_signal_handler; + sigaction(SIGPROF, &sa, NULL); + + pthread_mutex_init(&_scrollingMutex, NULL); + pthread_cond_init (&_scrollingCondVariable, NULL); + + // Setup the signal firing loop + _trackerThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(_trackerLoop) object:nil]; + // We wanna be higher priority than the main thread + // On iOS8 : this will roughly stick us at priority 61, while the main thread oscillates between 20 and 47 + _trackerThread.threadPriority = 1.0; + [_trackerThread start]; + + _symbolicationQueue = dispatch_queue_create("com.facebook.symbolication", DISPATCH_QUEUE_SERIAL); + dispatch_async(_symbolicationQueue, ^(void) {[self _setupSymbolication];}); + } +} + +- (void)_setupSymbolication +{ + // This extract the starting slide of every module in the app + // This is used to know which module an instruction pointer belongs to. + + // These operations is NOT thread-safe according to Apple docs + // Do not call this multiple times + int images = _dyld_image_count(); + + for (int i = 0; i < images; i ++) { + intptr_t imageSlide = _dyld_get_image_vmaddr_slide(i); + + // Here we extract the module name from the full path + // Typically it looks something like: /path/to/lib/UIKit + // And I just extract UIKit + NSString *fullName = [NSString stringWithUTF8String:_dyld_get_image_name(i)]; + NSRange range = [fullName rangeOfString:@"/" options:NSBackwardsSearch]; + NSUInteger startP = (range.location != NSNotFound) ? range.location + 1 : 0; + NSString *imageName = [fullName substringFromIndex:startP]; + + // This is parsing the mach header in order to extract the slide. + // See https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachORuntime/index.html + // For the structure of mach headers + fb_mach_header *header = (fb_mach_header*)_dyld_get_image_header(i); + if (!header) { + continue; + } + + const struct load_command *cmd = + reinterpret_cast(header + 1); + + for (unsigned int c = 0; cmd && (c < header->ncmds); c++) { + if (cmd->cmd == LC_SEGMENT_ARCH) { + const fb_mach_segment_command *seg = + reinterpret_cast(cmd); + + if (!strcmp(seg->segname, "__TEXT")) { + _imageNames[(void *)(seg->vmaddr + imageSlide)] = imageName; + break; + } + } + cmd = reinterpret_cast((char *)cmd + cmd->cmdsize); + } + } +} + +- (void)dealloc +{ + if (_prepared) { + [self _tearDownCADisplayLink]; + } +} + +#pragma mark - Tracking + +- (void)start +{ + if (!_tracking) { + if ([self prepare]) { + _displayLink.paused = NO; + _tracking = YES; + [self _reset]; + + if (_config.reportStackTraces) { + pthread_mutex_lock(&_scrollingMutex); + _scrolling = YES; + // Signal the tracker thread to start firing the signals + pthread_cond_signal(&_scrollingCondVariable); + pthread_mutex_unlock(&_scrollingMutex); + } + } + } +} + +- (void)stop +{ + if (_tracking) { + _tracking = NO; + _displayLink.paused = YES; + if (_durationTotal > 0) { + [_delegate reportDurationInMS:round(1000.0 * _durationTotal) smallDropEvent:_smallDrops largeDropEvent:_largeDrops]; + if (_config.reportStackTraces) { + pthread_mutex_lock(&_scrollingMutex); + _scrolling = NO; + pthread_mutex_unlock(&_scrollingMutex); + } + } + } +} + +- (BOOL)prepare +{ + if (_prepared) { + return YES; + } + + [self _setUpCADisplayLink]; + _prepared = YES; + + return YES; +} + +- (void)_setUpCADisplayLink +{ + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + _displayLink.paused = YES; +} + +- (void)_tearDownCADisplayLink +{ + [_displayLink invalidate]; + _displayLink = nil; +} + +- (void)_reset +{ + _firstUpdate = YES; + _previousFrameTimestamp = 0.0; + _durationTotal = 0; + _maxFrameTime = 0; + _largeDrops = 0; + _smallDrops = 0; +} + +- (void)_addFrameTime:(NSTimeInterval)actualFrameTime singleFrameTime:(NSTimeInterval)singleFrameTime +{ + _maxFrameTime = MAX(actualFrameTime, _maxFrameTime); + + NSInteger frameDropped = round(actualFrameTime / singleFrameTime) - 1; + frameDropped = MAX(frameDropped, 0); + // This is to reduce noise. Massive frame drops will just add noise to your data. + frameDropped = MIN(_config.maxFrameDropAccount, frameDropped); + + _durationTotal += (frameDropped + 1) * singleFrameTime; + // We account 2 frame drops as 2 small events. This way the metric correlates perfectly with Time at X fps. + _smallDrops += (frameDropped >= _config.smallDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.smallDropEventFrameNumber : 0.0; + _largeDrops += (frameDropped >= _config.largeDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.largeDropEventFrameNumber : 0.0; + + if (frameDropped >= 1) { + if (_config.reportStackTraces) { + callstack_dirty = false; + for (int ci = 0; ci <= frameDropped ; ci ++) { + // This is computing the previous indexes + // callstack - 1 - ci takes us back ci frames + // I want a positive number so I add callstack_max_number + // And then just modulo it, with & (callstack_max_number - 1) + int callstackPreviousIndex = ((callstack_i - 1 - ci) + callstack_max_number) & (callstack_max_number - 1); + FBCallstack *callstackCopy = [[FBCallstack alloc] initWithSize:callstack_size[callstackPreviousIndex] callstack:callstacks[callstackPreviousIndex]]; + // Check that in between the beginning and the end of the copy the signal did not fire + if (!callstack_dirty) { + // The copy has been made. We are now fine, let's punt the rest off main-thread. + __weak FBAnimationPerformanceTracker *weakSelf = self; + dispatch_async(_symbolicationQueue, ^(void) { + [weakSelf _reportStackTrace:callstackCopy]; + }); + } + } + } + } +} + +- (void)_update +{ + if (!_tracking) { + return; + } + + if (_firstUpdate) { + _firstUpdate = NO; + _previousFrameTimestamp = _displayLink.timestamp; + return; + } + + NSTimeInterval currentTimestamp = _displayLink.timestamp; + NSTimeInterval frameTime = currentTimestamp - _previousFrameTimestamp; + [self _addFrameTime:frameTime singleFrameTime:_displayLink.duration]; + _previousFrameTimestamp = currentTimestamp; +} + +- (void)_reportStackTrace:(FBCallstack *)callstack +{ + static NSString *slide; + static dispatch_once_t slide_predicate; + + dispatch_once(&slide_predicate, ^{ + slide = [NSString stringWithFormat:@"%p", (void *)_dyld_get_image_header(0)]; + }); + + @autoreleasepool { + NSMutableString *stack = [NSMutableString string]; + + for (int j = 2; j < callstack.size; j ++) { + void *instructionPointer = callstack.callstack[j]; + auto it = _imageNames.lower_bound(instructionPointer); + + NSString *imageName = (it != _imageNames.end()) ? it->second : @"???"; + + [stack appendString:imageName]; + [stack appendString:@":"]; + [stack appendString:[NSString stringWithFormat:@"%p", instructionPointer]]; + [stack appendString:@"|"]; + } + + [_delegate reportStackTrace:stack withSlide:slide]; + } +} +@end diff --git a/submodules/Display/Display/Font.swift b/submodules/Display/Display/Font.swift new file mode 100644 index 0000000000..ed0a02001d --- /dev/null +++ b/submodules/Display/Display/Font.swift @@ -0,0 +1,84 @@ +import Foundation +import UIKit + +public struct Font { + public static func regular(_ size: CGFloat) -> UIFont { + return UIFont.systemFont(ofSize: size) + } + + public static func medium(_ size: CGFloat) -> UIFont { + if #available(iOS 8.2, *) { + return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.medium) + } else { + return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil) + } + } + + public static func semibold(_ size: CGFloat) -> UIFont { + if #available(iOS 8.2, *) { + return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.semibold) + } else { + return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil) + } + } + + public static func bold(_ size: CGFloat) -> UIFont { + if #available(iOS 8.2, *) { + return UIFont.boldSystemFont(ofSize: size) + } else { + return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil) + } + } + + public static func light(_ size: CGFloat) -> UIFont { + if #available(iOS 8.2, *) { + return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light) + } else { + return CTFontCreateWithName("HelveticaNeue-Light" as CFString, size, nil) + } + } + + public static func semiboldItalic(_ size: CGFloat) -> UIFont { + if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) { + return UIFont(descriptor: descriptor, size: size) + } else { + return UIFont.italicSystemFont(ofSize: size) + } + } + + public static func monospace(_ size: CGFloat) -> UIFont { + return UIFont(name: "Menlo-Regular", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) + } + + public static func semiboldMonospace(_ size: CGFloat) -> UIFont { + return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) + } + + public static func italicMonospace(_ size: CGFloat) -> UIFont { + return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) + } + + public static func semiboldItalicMonospace(_ size: CGFloat) -> UIFont { + return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) + } + + public static func italic(_ size: CGFloat) -> UIFont { + return UIFont.italicSystemFont(ofSize: size) + } +} + +public extension NSAttributedString { + convenience init(string: String, font: UIFont? = nil, textColor: UIColor = UIColor.black, paragraphAlignment: NSTextAlignment? = nil) { + var attributes: [NSAttributedStringKey: AnyObject] = [:] + if let font = font { + attributes[NSAttributedStringKey.font] = font + } + attributes[NSAttributedStringKey.foregroundColor] = textColor + if let paragraphAlignment = paragraphAlignment { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = paragraphAlignment + attributes[NSAttributedStringKey.paragraphStyle] = paragraphStyle + } + self.init(string: string, attributes: attributes) + } +} diff --git a/submodules/Display/Display/GenerateImage.swift b/submodules/Display/Display/GenerateImage.swift new file mode 100644 index 0000000000..dac84c3a37 --- /dev/null +++ b/submodules/Display/Display/GenerateImage.swift @@ -0,0 +1,493 @@ +import Foundation +import UIKit + +let deviceColorSpace = CGColorSpaceCreateDeviceRGB() +let deviceScale = UIScreen.main.scale + +public func generateImagePixel(_ size: CGSize, pixelGenerator: (CGSize, UnsafeMutablePointer) -> Void) -> UIImage? { + let scale = deviceScale + let scaledSize = CGSize(width: size.width * scale, height: size.height * scale) + let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + let length = bytesPerRow * Int(scaledSize.height) + let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) + guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in + free(bytes) + }) + else { + return nil + } + + pixelGenerator(scaledSize, bytes) + + let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) + + guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) + else { + return nil + } + + return UIImage(cgImage: image, scale: scale, orientation: .up) +} + +public func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) -> Void, opaque: Bool = false, scale: CGFloat? = nil) -> UIImage? { + let selectedScale = scale ?? deviceScale + let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale) + let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + let length = bytesPerRow * Int(scaledSize.height) + let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) + + guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in + free(bytes) + }) + else { + return nil + } + + let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) + + guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { + return nil + } + + context.scaleBy(x: selectedScale, y: selectedScale) + + contextGenerator(size, context) + + guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) + else { + return nil + } + + return UIImage(cgImage: image, scale: selectedScale, orientation: .up) +} + +public func generateImage(_ size: CGSize, opaque: Bool = false, scale: CGFloat? = nil, rotatedContext: (CGSize, CGContext) -> Void) -> UIImage? { + let selectedScale = scale ?? deviceScale + let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale) + let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + let length = bytesPerRow * Int(scaledSize.height) + let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) + + guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in + free(bytes) + }) + else { + return nil + } + + let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) + + guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { + return nil + } + + context.scaleBy(x: selectedScale, y: selectedScale) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + rotatedContext(size, context) + + guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) + else { + return nil + } + + return UIImage(cgImage: image, scale: selectedScale, orientation: .up) +} + +public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? { + return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + + if let strokeColor = strokeColor, let strokeWidth = strokeWidth { + context.setFillColor(strokeColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let color = color { + context.setFillColor(color.cgColor) + } else { + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + } + context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0))) + } else { + if let color = color { + context.setFillColor(color.cgColor) + } else { + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + } + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + } + }) +} + +public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? { + return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + + if let color = color { + context.setStrokeColor(color.cgColor) + } else { + context.setStrokeColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + } + context.setLineWidth(lineWidth) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth))) + }) +} + +public func generateStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? { + let intRadius = Int(radius) + let cap = intRadius == 1 ? 2 : intRadius + return generateFilledCircleImage(diameter: radius * 2.0, color: color, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap) +} + +public func generateStretchableFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? { + let intRadius = Int(diameter / 2.0) + let intDiameter = Int(diameter) + let cap: Int + if intDiameter == 3 { + cap = 1 + } else if intRadius == 1 { + cap = 2 + } else { + cap = intRadius + } + + return generateFilledCircleImage(diameter: diameter, color: color, strokeColor: strokeColor, strokeWidth: strokeWidth, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap) +} + +public func generateVerticallyStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? { + return generateImage(CGSize(width: radius * 2.0, height: radius * 2.0 + radius), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + + if let color = color { + context.setFillColor(color.cgColor) + } else { + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + } + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius + radius, height: radius + radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: radius + radius, height: radius + radius))) + })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)) +} + +public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor: UIColor? = nil) -> UIImage? { + guard let image = image else { + return nil + } + + let imageSize = image.size + + UIGraphicsBeginImageContextWithOptions(imageSize, backgroundColor != nil, image.scale) + if let context = UIGraphicsGetCurrentContext() { + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: imageSize)) + } + + let imageRect = CGRect(origin: CGPoint(), size: imageSize) + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.clip(to: imageRect, mask: image.cgImage!) + context.setFillColor(color.cgColor) + context.fill(imageRect) + context.restoreGState() + } + + let tintedImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return tintedImage +} + +public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = true, scale: CGFloat? = nil) -> UIImage? { + guard let image = image else { + return nil + } + + return generateImage(size, contextGenerator: { size, context in + if !opaque { + context.clear(CGRect(origin: CGPoint(), size: size)) + } + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + }, opaque: opaque, scale: scale) +} + +private func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? { + return generateImage(size, contextGenerator: { size, context in + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + }) +} + +public enum DrawingContextBltMode { + case Alpha +} + +public class DrawingContext { + public let size: CGSize + public let scale: CGFloat + private let scaledSize: CGSize + public let bytesPerRow: Int + private let bitmapInfo: CGBitmapInfo + public let length: Int + public let bytes: UnsafeMutableRawPointer + let provider: CGDataProvider? + + private var _context: CGContext? + + public func withContext(_ f: (CGContext) -> ()) { + if self._context == nil { + if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) { + c.scaleBy(x: scale, y: scale) + self._context = c + } + } + + if let _context = self._context { + _context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) + _context.scaleBy(x: 1.0, y: -1.0) + _context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) + + f(_context) + + _context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) + _context.scaleBy(x: 1.0, y: -1.0) + _context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) + } + } + + public func withFlippedContext(_ f: (CGContext) -> ()) { + if self._context == nil { + if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) { + c.scaleBy(x: scale, y: scale) + self._context = c + } + } + + if let _context = self._context { + f(_context) + } + } + + public init(size: CGSize, scale: CGFloat = 0.0, clear: Bool = false) { + let actualScale: CGFloat + if scale.isZero { + actualScale = deviceScale + } else { + actualScale = scale + } + self.size = size + self.scale = actualScale + self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale) + + self.bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + self.length = bytesPerRow * Int(scaledSize.height) + + self.bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) + + self.bytes = malloc(length)! + if clear { + memset(self.bytes, 0, self.length) + } + self.provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in + free(bytes) + }) + + assert(self.bytesPerRow % 16 == 0) + assert(Int64(Int(bitPattern: self.bytes)) % 16 == 0) + } + + public func generateImage() -> UIImage? { + if self.scaledSize.width.isZero || self.scaledSize.height.isZero { + return nil + } + if let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: false, intent: .defaultIntent) { + return UIImage(cgImage: image, scale: scale, orientation: .up) + } else { + return nil + } + } + + public func colorAt(_ point: CGPoint) -> UIColor { + let x = Int(point.x * self.scale) + let y = Int(point.y * self.scale) + if x >= 0 && x < Int(self.scaledSize.width) && y >= 0 && y < Int(self.scaledSize.height) { + let srcLine = self.bytes.advanced(by: y * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + let pixel = srcLine + x + let colorValue = pixel.pointee + return UIColor(rgb: UInt32(colorValue)) + } else { + return UIColor.clear + } + } + + public func blt(_ other: DrawingContext, at: CGPoint, mode: DrawingContextBltMode = .Alpha) { + if abs(other.scale - self.scale) < CGFloat.ulpOfOne { + let srcX = 0 + var srcY = 0 + let dstX = Int(at.x * self.scale) + var dstY = Int(at.y * self.scale) + if dstX < 0 || dstY < 0 { + return + } + + let width = min(Int(self.size.width * self.scale) - dstX, Int(other.size.width * self.scale)) + let height = min(Int(self.size.height * self.scale) - dstY, Int(other.size.height * self.scale)) + + let maxDstX = dstX + width + let maxDstY = dstY + height + + switch mode { + case .Alpha: + while dstY < maxDstY { + let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) + let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + + var dx = dstX + var sx = srcX + while dx < maxDstX { + let srcPixel = srcLine + sx + let dstPixel = dstLine + dx + + let baseColor = dstPixel.pointee + let baseAlpha = (baseColor >> 24) & 0xff + let baseR = (baseColor >> 16) & 0xff + let baseG = (baseColor >> 8) & 0xff + let baseB = baseColor & 0xff + + let alpha = min(baseAlpha, srcPixel.pointee >> 24) + + let r = (baseR * alpha) / 255 + let g = (baseG * alpha) / 255 + let b = (baseB * alpha) / 255 + + dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + dx += 1 + sx += 1 + } + + dstY += 1 + srcY += 1 + } + } + } + } +} + +public enum ParsingError: Error { + case Generic +} + +public func readCGFloat(_ index: inout UnsafePointer, end: UnsafePointer, separator: UInt8) throws -> CGFloat { + let begin = index + var seenPoint = false + while index <= end { + let c = index.pointee + index = index.successor() + + if c == 46 { // . + if seenPoint { + throw ParsingError.Generic + } else { + seenPoint = true + } + } else if c == separator { + break + } else if !((c >= 48 && c <= 57) || c == 45 || c == 101 || c == 69) { + throw ParsingError.Generic + } + } + + if index == begin { + throw ParsingError.Generic + } + + if let value = NSString(bytes: UnsafeRawPointer(begin), length: index - begin, encoding: String.Encoding.utf8.rawValue)?.floatValue { + return CGFloat(value) + } else { + throw ParsingError.Generic + } +} + +public func drawSvgPath(_ context: CGContext, path: StaticString, strokeOnMove: Bool = false) throws { + var index: UnsafePointer = path.utf8Start + let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount) + while index < end { + let c = index.pointee + index = index.successor() + + if c == 77 { // M + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Move to \(x), \(y)") + context.move(to: CGPoint(x: x, y: y)) + } else if c == 76 { // L + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Line to \(x), \(y)") + context.addLine(to: CGPoint(x: x, y: y)) + + if strokeOnMove { + context.strokePath() + context.move(to: CGPoint(x: x, y: y)) + } + } else if c == 67 { // C + let x1 = try readCGFloat(&index, end: end, separator: 44) + let y1 = try readCGFloat(&index, end: end, separator: 32) + let x2 = try readCGFloat(&index, end: end, separator: 44) + let y2 = try readCGFloat(&index, end: end, separator: 32) + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + context.addCurve(to: CGPoint(x: x, y: y), control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2)) + + //print("Line to \(x), \(y)") + if strokeOnMove { + context.strokePath() + context.move(to: CGPoint(x: x, y: y)) + } + } else if c == 90 { // Z + if index != end && index.pointee != 32 { + throw ParsingError.Generic + } + + //CGContextClosePath(context) + context.fillPath() + //CGContextBeginPath(context) + //print("Close") + } else if c == 83 { // S + if index != end && index.pointee != 32 { + throw ParsingError.Generic + } + + //CGContextClosePath(context) + context.strokePath() + //CGContextBeginPath(context) + //print("Close") + } else if c == 32 { // space + continue + } else { + throw ParsingError.Generic + } + } +} diff --git a/submodules/Display/Display/GlobalOverlayPresentationContext.swift b/submodules/Display/Display/GlobalOverlayPresentationContext.swift new file mode 100644 index 0000000000..7da0302467 --- /dev/null +++ b/submodules/Display/Display/GlobalOverlayPresentationContext.swift @@ -0,0 +1,189 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +private func isViewVisibleInHierarchy(_ view: UIView, _ initial: Bool = true) -> Bool { + guard let window = view.window else { + return false + } + if view.isHidden || view.alpha == 0.0 { + return false + } + if view.superview === window { + return true + } else if let superview = view.superview { + if initial && view.frame.minY >= superview.frame.height { + return false + } else { + return isViewVisibleInHierarchy(superview, false) + } + } else { + return false + } +} + +final class GlobalOverlayPresentationContext { + private let statusBarHost: StatusBarHost? + + private var controllers: [ContainableController] = [] + + private var presentationDisposables = DisposableSet() + private var layout: ContainerViewLayout? + + private var ready: Bool { + return self.currentPresentationView() != nil && self.layout != nil + } + + init(statusBarHost: StatusBarHost?) { + self.statusBarHost = statusBarHost + } + + private func currentPresentationView() -> UIView? { + if let statusBarHost = self.statusBarHost { + if let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) { + return keyboardWindow + } else { + return statusBarHost.statusBarWindow + } + } + return nil + } + + func present(_ controller: ContainableController) { + let controllerReady = controller.ready.get() + |> filter({ $0 }) + |> take(1) + |> deliverOnMainQueue + |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true)) + + if let _ = self.currentPresentationView(), let initialLayout = self.layout { + controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size) + controller.containerLayoutUpdated(initialLayout, transition: .immediate) + + self.presentationDisposables.add(controllerReady.start(next: { [weak self] _ in + if let strongSelf = self { + if strongSelf.controllers.contains(where: { $0 === controller }) { + return + } + + strongSelf.controllers.append(controller) + if let view = strongSelf.currentPresentationView(), let layout = strongSelf.layout { + (controller as? UIViewController)?.navigation_setDismiss({ [weak controller] in + if let strongSelf = self, let controller = controller { + strongSelf.dismiss(controller) + } + }, rootController: nil) + (controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(true) + if layout != initialLayout { + controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + view.addSubview(controller.view) + controller.containerLayoutUpdated(layout, transition: .immediate) + } else { + view.addSubview(controller.view) + } + (controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(false) + view.layer.invalidateUpTheTree() + controller.viewWillAppear(false) + controller.viewDidAppear(false) + } + } + })) + } else { + self.controllers.append(controller) + } + } + + deinit { + self.presentationDisposables.dispose() + } + + private func dismiss(_ controller: ContainableController) { + if let index = self.controllers.index(where: { $0 === controller }) { + self.controllers.remove(at: index) + controller.viewWillDisappear(false) + controller.view.removeFromSuperview() + controller.viewDidDisappear(false) + } + } + + public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let wasReady = self.ready + self.layout = layout + + if wasReady != self.ready { + self.readyChanged(wasReady: wasReady) + } else if self.ready { + for controller in self.controllers { + controller.containerLayoutUpdated(layout, transition: transition) + } + } + } + + private func readyChanged(wasReady: Bool) { + if !wasReady { + self.addViews() + } else { + self.removeViews() + } + } + + private func addViews() { + if let view = self.currentPresentationView(), let layout = self.layout { + for controller in self.controllers { + controller.viewWillAppear(false) + view.addSubview(controller.view) + controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + controller.containerLayoutUpdated(layout, transition: .immediate) + controller.viewDidAppear(false) + } + } + } + + private func removeViews() { + for controller in self.controllers { + controller.viewWillDisappear(false) + controller.view.removeFromSuperview() + controller.viewDidDisappear(false) + } + } + + func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for controller in self.controllers.reversed() { + if controller.isViewLoaded { + if let result = controller.view.hitTest(point, with: event) { + return result + } + } + } + return nil + } + + func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { + if self.ready { + for controller in self.controllers { + controller.updateToInterfaceOrientation(orientation) + } + } + } + + func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations { + var mask = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) + + for controller in self.controllers { + mask = mask.intersection(controller.combinedSupportedOrientations(currentOrientationToLock: currentOrientationToLock)) + } + + return mask + } + + func combinedDeferScreenEdgeGestures() -> UIRectEdge { + var edges: UIRectEdge = [] + + for controller in self.controllers { + edges = edges.union(controller.deferScreenEdgeGestures) + } + + return edges + } +} diff --git a/submodules/Display/Display/GridItem.swift b/submodules/Display/Display/GridItem.swift new file mode 100644 index 0000000000..bace93a89e --- /dev/null +++ b/submodules/Display/Display/GridItem.swift @@ -0,0 +1,34 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public protocol GridSection { + var height: CGFloat { get } + var hashValue: Int { get } + + func isEqual(to: GridSection) -> Bool + func node() -> ASDisplayNode +} + +public protocol GridItem { + var section: GridSection? { get } + func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode + func update(node: GridItemNode) + var aspectRatio: CGFloat { get } + var fillsRowWithHeight: CGFloat? { get } + var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? { get } +} + +public extension GridItem { + var aspectRatio: CGFloat { + return 1.0 + } + + var fillsRowWithHeight: CGFloat? { + return nil + } + + var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? { + return nil + } +} diff --git a/submodules/Display/Display/GridItemNode.swift b/submodules/Display/Display/GridItemNode.swift new file mode 100644 index 0000000000..cfcf3d1384 --- /dev/null +++ b/submodules/Display/Display/GridItemNode.swift @@ -0,0 +1,21 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +open class GridItemNode: ASDisplayNode { + open var isVisibleInGrid = false + open var isGridScrolling = false + + final var cachedFrame: CGRect = CGRect() + override open var frame: CGRect { + get { + return self.cachedFrame + } set(value) { + self.cachedFrame = value + super.frame = value + } + } + + open func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { + } +} diff --git a/submodules/Display/Display/GridNode.swift b/submodules/Display/Display/GridNode.swift new file mode 100644 index 0000000000..72727165aa --- /dev/null +++ b/submodules/Display/Display/GridNode.swift @@ -0,0 +1,1348 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum GridNodeVisibleContentOffset { + case known(CGFloat) + case unknown + case none +} + +public struct GridNodeInsertItem { + public let index: Int + public let item: GridItem + public let previousIndex: Int? + + public init(index: Int, item: GridItem, previousIndex: Int?) { + self.index = index + self.item = item + self.previousIndex = previousIndex + } +} + +public struct GridNodeUpdateItem { + public let index: Int + public let previousIndex: Int + public let item: GridItem + + public init(index: Int, previousIndex: Int, item: GridItem) { + self.index = index + self.previousIndex = previousIndex + self.item = item + } +} + +public enum GridNodeScrollToItemPosition { + case top(CGFloat) + case bottom(CGFloat) + case center(CGFloat) + case visible +} + +public struct GridNodeScrollToItem { + public let index: Int + public let position: GridNodeScrollToItemPosition + public let transition: ContainedViewLayoutTransition + public let directionHint: GridNodePreviousItemsTransitionDirectionHint + public let adjustForSection: Bool + public let adjustForTopInset: Bool + + public init(index: Int, position: GridNodeScrollToItemPosition, transition: ContainedViewLayoutTransition, directionHint: GridNodePreviousItemsTransitionDirectionHint, adjustForSection: Bool, adjustForTopInset: Bool = false) { + self.index = index + self.position = position + self.transition = transition + self.directionHint = directionHint + self.adjustForSection = adjustForSection + self.adjustForTopInset = adjustForTopInset + } +} + +public enum GridNodeLayoutType: Equatable { + case fixed(itemSize: CGSize, fillWidth: Bool?, lineSpacing: CGFloat, itemSpacing: CGFloat?) + case balanced(idealHeight: CGFloat) +} + +public struct GridNodeLayout: Equatable { + public let size: CGSize + public let insets: UIEdgeInsets + public let scrollIndicatorInsets: UIEdgeInsets? + public let preloadSize: CGFloat + public let type: GridNodeLayoutType + + public init(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets? = nil, preloadSize: CGFloat, type: GridNodeLayoutType) { + self.size = size + self.insets = insets + self.scrollIndicatorInsets = scrollIndicatorInsets + self.preloadSize = preloadSize + self.type = type + } +} + +public struct GridNodeUpdateLayout { + public let layout: GridNodeLayout + public let transition: ContainedViewLayoutTransition + + public init(layout: GridNodeLayout, transition: ContainedViewLayoutTransition) { + self.layout = layout + self.transition = transition + } +} + +public enum GridNodeStationaryItems { + case none + case all + case indices(Set) +} + +public struct GridNodeTransaction { + public let deleteItems: [Int] + public let insertItems: [GridNodeInsertItem] + public let updateItems: [GridNodeUpdateItem] + public let scrollToItem: GridNodeScrollToItem? + public let updateLayout: GridNodeUpdateLayout? + public let itemTransition: ContainedViewLayoutTransition + public let stationaryItems: GridNodeStationaryItems + public let updateFirstIndexInSectionOffset: Int? + public let updateOpaqueState: Any? + public let synchronousLoads: Bool + + public init(deleteItems: [Int], insertItems: [GridNodeInsertItem], updateItems: [GridNodeUpdateItem], scrollToItem: GridNodeScrollToItem?, updateLayout: GridNodeUpdateLayout?, itemTransition: ContainedViewLayoutTransition, stationaryItems: GridNodeStationaryItems, updateFirstIndexInSectionOffset: Int?, updateOpaqueState: Any? = nil, synchronousLoads: Bool = false) { + self.deleteItems = deleteItems + self.insertItems = insertItems + self.updateItems = updateItems + self.scrollToItem = scrollToItem + self.updateLayout = updateLayout + self.itemTransition = itemTransition + self.stationaryItems = stationaryItems + self.updateFirstIndexInSectionOffset = updateFirstIndexInSectionOffset + self.updateOpaqueState = updateOpaqueState + self.synchronousLoads = synchronousLoads + } +} + +private struct GridNodePresentationItem { + let index: Int + let frame: CGRect +} + +private struct GridNodePresentationSection { + let section: GridSection + let frame: CGRect +} + +private struct GridNodePresentationLayout { + let layout: GridNodeLayout + let contentOffset: CGPoint + let contentSize: CGSize + let items: [GridNodePresentationItem] + let sections: [GridNodePresentationSection] +} + +public enum GridNodePreviousItemsTransitionDirectionHint { + case up + case down +} + +private struct GridNodePresentationLayoutTransition { + let layout: GridNodePresentationLayout + let directionHint: GridNodePreviousItemsTransitionDirectionHint + let transition: ContainedViewLayoutTransition +} + +public struct GridNodeCurrentPresentationLayout { + public let layout: GridNodeLayout + public let contentOffset: CGPoint + public let contentSize: CGSize +} + +private final class GridNodeItemLayout { + let contentSize: CGSize + let items: [GridNodePresentationItem] + let sections: [GridNodePresentationSection] + + init(contentSize: CGSize, items: [GridNodePresentationItem], sections: [GridNodePresentationSection]) { + self.contentSize = contentSize + self.items = items + self.sections = sections + } +} + +public struct GridNodeDisplayedItemRange: Equatable { + public let loadedRange: Range? + public let visibleRange: Range? +} + +private struct WrappedGridSection: Equatable, Hashable { + let section: GridSection + + init(_ section: GridSection) { + self.section = section + } + + var hashValue: Int { + return self.section.hashValue + } + + static func ==(lhs: WrappedGridSection, rhs: WrappedGridSection) -> Bool { + return lhs.section.isEqual(to: rhs.section) + } +} + +public struct GridNodeVisibleItems { + public let top: (Int, GridItem)? + public let bottom: (Int, GridItem)? + public let topVisible: (Int, GridItem)? + public let bottomVisible: (Int, GridItem)? + public let topSectionVisible: GridSection? + public let count: Int +} + +private struct WrappedGridItemNode: Hashable { + let node: ASDisplayNode + + var hashValue: Int { + return node.hashValue + } + + static func ==(lhs: WrappedGridItemNode, rhs: WrappedGridItemNode) -> Bool { + return lhs.node === rhs.node + } +} + +open class GridNode: GridNodeScroller, UIScrollViewDelegate { + private var gridLayout = GridNodeLayout(size: CGSize(), insets: UIEdgeInsets(), preloadSize: 0.0, type: .fixed(itemSize: CGSize(), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)) + private var firstIndexInSectionOffset: Int = 0 + public private(set) var items: [GridItem] = [] + private var itemNodes: [Int: GridItemNode] = [:] + private var sectionNodes: [WrappedGridSection: ASDisplayNode] = [:] + private var itemLayout = GridNodeItemLayout(contentSize: CGSize(), items: [], sections: []) + + private var applyingContentOffset = false + + public var visibleItemsUpdated: ((GridNodeVisibleItems) -> Void)? + public var presentationLayoutUpdated: ((GridNodeCurrentPresentationLayout, ContainedViewLayoutTransition) -> Void)? + public var scrollingInitiated: (() -> Void)? + public var scrollingCompleted: (() -> Void)? + public var visibleContentOffsetChanged: (GridNodeVisibleContentOffset) -> Void = { _ in } + + public final var floatingSections = false + + public final var initialOffset: CGFloat = 0.0 + + public var showVerticalScrollIndicator: Bool = false { + didSet { + self.scrollView.showsVerticalScrollIndicator = self.showVerticalScrollIndicator + } + } + + public var indicatorStyle: UIScrollViewIndicatorStyle = .default { + didSet { + self.scrollView.indicatorStyle = self.indicatorStyle + } + } + + public private(set) var opaqueState: Any? + + public override init() { + super.init() + + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func transaction(_ transaction: GridNodeTransaction, completion: (GridNodeDisplayedItemRange) -> Void) { + if let updateOpaqueState = transaction.updateOpaqueState { + self.opaqueState = updateOpaqueState + } + + if transaction.deleteItems.isEmpty && transaction.insertItems.isEmpty && transaction.scrollToItem == nil && transaction.updateItems.isEmpty && (transaction.updateLayout == nil || transaction.updateLayout!.layout == self.gridLayout && (transaction.updateFirstIndexInSectionOffset == nil || transaction.updateFirstIndexInSectionOffset == self.firstIndexInSectionOffset)) { + if let presentationLayoutUpdated = self.presentationLayoutUpdated { + presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: self.gridLayout, contentOffset: self.scrollView.contentOffset, contentSize: self.itemLayout.contentSize), transaction.updateLayout?.transition ?? .immediate) + } + completion(self.displayedItemRange()) + return + } + + if let updateFirstIndexInSectionOffset = transaction.updateFirstIndexInSectionOffset { + self.firstIndexInSectionOffset = updateFirstIndexInSectionOffset + } + + var layoutTransactionOffset: CGFloat = 0.0 + if let updateLayout = transaction.updateLayout { + layoutTransactionOffset += updateLayout.layout.insets.top - self.gridLayout.insets.top + self.gridLayout = updateLayout.layout + } + + for updatedItem in transaction.updateItems { + self.items[updatedItem.previousIndex] = updatedItem.item + if let itemNode = self.itemNodes[updatedItem.previousIndex] { + updatedItem.item.update(node: itemNode) + } + } + + var removedNodes: [GridItemNode] = [] + + if !transaction.deleteItems.isEmpty || !transaction.insertItems.isEmpty { + let deleteItems = transaction.deleteItems.sorted() + + for deleteItemIndex in deleteItems.reversed() { + self.items.remove(at: deleteItemIndex) + if let itemNode = self.itemNodes[deleteItemIndex] { + removedNodes.append(itemNode) + self.removeItemNodeWithIndex(deleteItemIndex, removeNode: false) + } else { + self.removeItemNodeWithIndex(deleteItemIndex, removeNode: true) + } + } + + var remappedDeletionItemNodes: [Int: GridItemNode] = [:] + + for (index, itemNode) in self.itemNodes { + var indexOffset = 0 + for deleteIndex in deleteItems { + if deleteIndex < index { + indexOffset += 1 + } else { + break + } + } + + remappedDeletionItemNodes[index - indexOffset] = itemNode + } + + let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) + if self.items.count == 0 && !insertItems.isEmpty { + if insertItems[0].index != 0 { + fatalError("transaction: invalid insert into empty list") + } + } + + for insertedItem in insertItems { + self.items.insert(insertedItem.item, at: insertedItem.index) + } + + let sortedInsertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) + + var remappedInsertionItemNodes: [Int: GridItemNode] = [:] + for (index, itemNode) in remappedDeletionItemNodes { + var indexOffset = 0 + for insertedItem in sortedInsertItems { + if insertedItem.index <= index + indexOffset { + indexOffset += 1 + } + } + + remappedInsertionItemNodes[index + indexOffset] = itemNode + } + + self.itemNodes = remappedInsertionItemNodes + } + + let previousLayoutWasEmpty = self.itemLayout.items.isEmpty + + self.itemLayout = self.generateItemLayout() + + var updateLayoutTransition = transaction.updateLayout?.transition + + let generatedScrollToItem: GridNodeScrollToItem? + if let scrollToItem = transaction.scrollToItem { + generatedScrollToItem = scrollToItem + if updateLayoutTransition == nil { + updateLayoutTransition = scrollToItem.transition + } + } else if previousLayoutWasEmpty { + generatedScrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: true, adjustForTopInset: true) + } else { + generatedScrollToItem = nil + } + + self.applyPresentationLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, layoutTransactionOffset: layoutTransactionOffset, scrollToItem: generatedScrollToItem), removedNodes: removedNodes, updateLayoutTransition: updateLayoutTransition, customScrollToItem: transaction.scrollToItem != nil, itemTransition: transaction.itemTransition, synchronousLoads: transaction.synchronousLoads, updatingLayout: transaction.updateLayout != nil, completion: completion) + } + + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.updateItemNodeVisibilititesAndScrolling() + self.updateVisibleContentOffset() + self.scrollingInitiated?() + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.updateItemNodeVisibilititesAndScrolling() + self.updateVisibleContentOffset() + self.scrollingCompleted?() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.updateItemNodeVisibilititesAndScrolling() + self.updateVisibleContentOffset() + self.scrollingCompleted?() + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.applyingContentOffset { + self.applyPresentationLayoutTransition(self.generatePresentationLayoutTransition(layoutTransactionOffset: 0.0), removedNodes: [], updateLayoutTransition: nil, customScrollToItem: false, itemTransition: .immediate, synchronousLoads: false, updatingLayout: false, completion: { _ in }) + self.updateVisibleContentOffset() + } + } + + private func displayedItemRange() -> GridNodeDisplayedItemRange { + var minIndex: Int? + var maxIndex: Int? + for index in self.itemNodes.keys { + if minIndex == nil || minIndex! > index { + minIndex = index + } + if maxIndex == nil || maxIndex! < index { + maxIndex = index + } + } + + if let minIndex = minIndex, let maxIndex = maxIndex { + return GridNodeDisplayedItemRange(loadedRange: minIndex ..< maxIndex, visibleRange: minIndex ..< maxIndex) + } else { + return GridNodeDisplayedItemRange(loadedRange: nil, visibleRange: nil) + } + } + + private func generateItemLayout() -> GridNodeItemLayout { + if CGFloat(0.0).isLess(than: gridLayout.size.width) && CGFloat(0.0).isLess(than: gridLayout.size.height) { + var contentSize = CGSize(width: gridLayout.size.width, height: 0.0) + var items: [GridNodePresentationItem] = [] + var sections: [GridNodePresentationSection] = [] + + switch gridLayout.type { + case let .fixed(defaultItemSize, fillWidth, lineSpacing, defaultItemSpacing): + let itemInsets = gridLayout.insets + + let effectiveWidth = gridLayout.size.width - itemInsets.left - itemInsets.right + + let itemsInRow = Int(effectiveWidth / defaultItemSize.width) + let itemsInRowWidth = CGFloat(itemsInRow) * defaultItemSize.width + let remainingWidth = max(0.0, effectiveWidth - itemsInRowWidth) + + let itemSpacing = defaultItemSpacing ?? floorToScreenPixels(remainingWidth / CGFloat(itemsInRow + 1)) + let initialSpacing: CGFloat = (fillWidth ?? false) ? 0.0 : itemSpacing + + var incrementedCurrentRow = false + var nextItemOrigin = CGPoint(x: initialSpacing + itemInsets.left, y: 0.0) + var index = 0 + var previousSection: GridSection? + for item in self.items { + var itemSize = defaultItemSize + + let section = item.section + var keepSection = true + if let previousSection = previousSection, let section = section { + keepSection = previousSection.isEqual(to: section) + } else if (previousSection != nil) != (section != nil) { + keepSection = false + } + + if !keepSection { + if incrementedCurrentRow { + nextItemOrigin.x = initialSpacing + itemInsets.left + nextItemOrigin.y += itemSize.height + lineSpacing + incrementedCurrentRow = false + } + + if let section = section { + sections.append(GridNodePresentationSection(section: section, frame: CGRect(origin: CGPoint(x: 0.0, y: nextItemOrigin.y), size: CGSize(width: gridLayout.size.width, height: section.height)))) + nextItemOrigin.y += section.height + contentSize.height += section.height + } + } + previousSection = section + + if let height = item.fillsRowWithHeight { + nextItemOrigin.x = 0.0 + itemSize.width = gridLayout.size.width + itemSize.height = height + } else if let fillsRowWithDynamicHeight = item.fillsRowWithDynamicHeight { + let height = fillsRowWithDynamicHeight(gridLayout.size.width) + nextItemOrigin.x = 0.0 + itemSize.width = gridLayout.size.width + itemSize.height = height + } else if index == 0 { + let itemsInRow = max(1, Int(effectiveWidth) / Int(itemSize.width)) + let normalizedIndexOffset = self.firstIndexInSectionOffset % itemsInRow + nextItemOrigin.x += (itemSize.width + itemSpacing) * CGFloat(normalizedIndexOffset) + } else if let fillWidth = fillWidth, fillWidth { + let nextItemOriginX = nextItemOrigin.x + itemSize.width + itemSpacing + let remainingWidth = remainingWidth - CGFloat(itemsInRow - 1) * itemSpacing + if nextItemOriginX + itemSize.width > self.gridLayout.size.width && remainingWidth > 0.0 { + itemSize.width += remainingWidth + } + } + + if !incrementedCurrentRow { + incrementedCurrentRow = true + contentSize.height += itemSize.height + lineSpacing + } + + items.append(GridNodePresentationItem(index: index, frame: CGRect(origin: nextItemOrigin, size: itemSize))) + index += 1 + + nextItemOrigin.x += itemSize.width + itemSpacing + if nextItemOrigin.x + itemSize.width > gridLayout.size.width { + nextItemOrigin.x = initialSpacing + itemInsets.left + nextItemOrigin.y += itemSize.height + lineSpacing + incrementedCurrentRow = false + } + } + case let .balanced(idealHeight): + var weights: [Int] = [] + for item in self.items { + weights.append(Int(item.aspectRatio * 100)) + } + + var totalItemSize: CGFloat = 0.0 + for i in 0 ..< self.items.count { + totalItemSize += self.items[i].aspectRatio * idealHeight + } + let numberOfRows = max(Int(round(totalItemSize / gridLayout.size.width)), 1) + + let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows) + + var i = 0 + var offset = CGPoint(x: 0.0, y: 0.0) + var previousItemSize: CGFloat = 0.0 + var contentMaxValueInScrollDirection: CGFloat = 0.0 + let maxWidth = gridLayout.size.width + + let minimumInteritemSpacing: CGFloat = 1.0 + let minimumLineSpacing: CGFloat = 1.0 + + let viewportWidth: CGFloat = gridLayout.size.width + + let preferredRowSize = idealHeight + + var rowIndex = -1 + for row in partition { + rowIndex += 1 + + var summedRatios: CGFloat = 0.0 + + var j = i + var n = i + row.count + + while j < n { + summedRatios += self.items[j].aspectRatio + + j += 1 + } + + var rowSize = gridLayout.size.width - (CGFloat(row.count - 1) * minimumInteritemSpacing) + + if rowIndex == partition.count - 1 { + if row.count < 2 { + rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) + } else if row.count < 3 { + rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) + } + } + + j = i + n = i + row.count + + while j < n { + let preferredAspectRatio = self.items[j].aspectRatio + + let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize) + + var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height) + if frame.origin.x + frame.size.width >= maxWidth - 2.0 { + frame.size.width = max(1.0, maxWidth - frame.origin.x) + } + + items.append(GridNodePresentationItem(index: j, frame: frame)) + + offset.x += actualSize.width + minimumInteritemSpacing + previousItemSize = actualSize.height + contentMaxValueInScrollDirection = frame.maxY + + j += 1 + } + + if row.count > 0 { + offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing) + } + + i += row.count + } + contentSize = CGSize(width: gridLayout.size.width, height: contentMaxValueInScrollDirection) + } + + return GridNodeItemLayout(contentSize: contentSize, items: items, sections: sections) + } else { + return GridNodeItemLayout(contentSize: CGSize(), items: [], sections: []) + } + } + + private func generatePresentationLayoutTransition(stationaryItems: GridNodeStationaryItems = .none, layoutTransactionOffset: CGFloat, scrollToItem: GridNodeScrollToItem? = nil) -> GridNodePresentationLayoutTransition { + if CGFloat(0.0).isLess(than: self.gridLayout.size.width) && CGFloat(0.0).isLess(than: self.gridLayout.size.height) { + var transitionDirectionHint: GridNodePreviousItemsTransitionDirectionHint = .up + var transition: ContainedViewLayoutTransition = .immediate + let contentOffset: CGPoint + var updatedStationaryItems = stationaryItems + if let scrollToItem = scrollToItem { + updatedStationaryItems = .none + if case .immediate = transition { + transition = scrollToItem.transition + } + } + switch updatedStationaryItems { + case .none: + if let scrollToItem = scrollToItem { + if self.itemLayout.items.isEmpty { + transitionDirectionHint = scrollToItem.directionHint + transition = scrollToItem.transition + contentOffset = CGPoint(x: 0.0, y: -self.gridLayout.insets.top + self.initialOffset) + } else { + let itemFrame = self.itemLayout.items[scrollToItem.index] + + var additionalOffset: CGFloat = 0.0 + if scrollToItem.adjustForSection { + var adjustForSection: GridSection? + if scrollToItem.index == 0 { + if let itemSection = self.items[scrollToItem.index].section { + adjustForSection = itemSection + } + } else { + let itemSection = self.items[scrollToItem.index].section + let previousSection = self.items[scrollToItem.index - 1].section + if let itemSection = itemSection, let previousSection = previousSection { + if !itemSection.isEqual(to: previousSection) { + adjustForSection = itemSection + } + } else if let itemSection = itemSection { + adjustForSection = itemSection + } + } + + if let adjustForSection = adjustForSection { + additionalOffset = -adjustForSection.height + } + + if scrollToItem.adjustForTopInset { + additionalOffset += -gridLayout.insets.top// + self.initialOffset + } + } else if scrollToItem.adjustForTopInset { + additionalOffset = -gridLayout.insets.top + } + + let displayHeight = max(0.0, self.gridLayout.size.height - self.gridLayout.insets.top - self.gridLayout.insets.bottom) + var verticalOffset: CGFloat = self.scrollView.contentOffset.y + + switch scrollToItem.position { + case let .top(offset): + verticalOffset = itemFrame.frame.minY + additionalOffset + offset + case let .center(offset): + verticalOffset = floor(itemFrame.frame.minY + itemFrame.frame.size.height / 2.0 - displayHeight / 2.0 - self.gridLayout.insets.top) + additionalOffset + offset + case let .bottom(offset): + verticalOffset = itemFrame.frame.maxY - displayHeight + additionalOffset + offset + case .visible: + if verticalOffset + self.gridLayout.insets.top > itemFrame.frame.minY { + //verticalOffset = -self.gridLayout.insets.top + itemFrame.frame.minY + } else if verticalOffset + self.gridLayout.insets.top + displayHeight < itemFrame.frame.maxY { + verticalOffset = -self.gridLayout.insets.top - displayHeight + itemFrame.frame.maxY + } + } + + if verticalOffset > self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height { + verticalOffset = self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height + } + if verticalOffset < -self.gridLayout.insets.top { + verticalOffset = -self.gridLayout.insets.top + } + + transitionDirectionHint = scrollToItem.directionHint + transition = scrollToItem.transition + + contentOffset = CGPoint(x: 0.0, y: verticalOffset) + } + } else { + if !layoutTransactionOffset.isZero { + var verticalOffset = self.scrollView.contentOffset.y - layoutTransactionOffset + if verticalOffset > self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height { + verticalOffset = self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height + } + if verticalOffset < -self.gridLayout.insets.top { + verticalOffset = -self.gridLayout.insets.top + } + contentOffset = CGPoint(x: 0.0, y: verticalOffset) + } else { + contentOffset = self.scrollView.contentOffset + } + } + case let .indices(stationaryItemIndices): + var selectedContentOffset: CGPoint? + for (index, itemNode) in self.itemNodes { + if stationaryItemIndices.contains(index) { + //let currentScreenOffset = itemNode.frame.origin.y - self.scrollView.contentOffset.y + selectedContentOffset = CGPoint(x: 0.0, y: self.itemLayout.items[index].frame.origin.y - itemNode.frame.origin.y + self.scrollView.contentOffset.y) + break + } + } + + if let _ = selectedContentOffset, self.itemNodes.count > 0, let itemNode = self.itemNodes[0], self.scrollView.contentInset.top + self.scrollView.contentOffset.y <= itemNode.frame.maxY { + selectedContentOffset = self.scrollView.contentOffset + } + + if let selectedContentOffset = selectedContentOffset { + contentOffset = selectedContentOffset + } else { + contentOffset = self.scrollView.contentOffset + } + + + case .all: + var selectedContentOffset: CGPoint? + for (index, itemNode) in self.itemNodes { + //let currentScreenOffset = itemNode.frame.origin.y - self.scrollView.contentOffset.y + selectedContentOffset = CGPoint(x: 0.0, y: self.itemLayout.items[index].frame.origin.y - itemNode.frame.origin.y + self.scrollView.contentOffset.y) + break + } + + if let selectedContentOffset = selectedContentOffset { + contentOffset = selectedContentOffset + } else { + contentOffset = self.scrollView.contentOffset + } + } + + let lowerDisplayBound = contentOffset.y - self.gridLayout.insets.top - self.gridLayout.preloadSize + let upperDisplayBound = contentOffset.y + self.gridLayout.insets.bottom + self.gridLayout.size.height + self.gridLayout.preloadSize + + var presentationItems: [GridNodePresentationItem] = [] + + var validSections = Set() + for item in self.itemLayout.items { + if item.frame.maxY < lowerDisplayBound { + continue + } + if item.frame.minY > upperDisplayBound { + break + } + presentationItems.append(item) + if self.floatingSections { + if let section = self.items[item.index].section { + validSections.insert(WrappedGridSection(section)) + } + } + } + + var presentationSections: [GridNodePresentationSection] = [] + for section in self.itemLayout.sections { + if section.frame.origin.y < lowerDisplayBound { + if !validSections.contains(WrappedGridSection(section.section)) { + continue + } + } + if section.frame.origin.y + section.frame.size.height > upperDisplayBound { + break + } + presentationSections.append(section) + } + + return GridNodePresentationLayoutTransition(layout: GridNodePresentationLayout(layout: self.gridLayout, contentOffset: contentOffset, contentSize: self.itemLayout.contentSize, items: presentationItems, sections: presentationSections), directionHint: transitionDirectionHint, transition: transition) + } else { + return GridNodePresentationLayoutTransition(layout: GridNodePresentationLayout(layout: self.gridLayout, contentOffset: CGPoint(), contentSize: self.itemLayout.contentSize, items: [], sections: []), directionHint: .up, transition: .immediate) + } + } + + private func lowestSectionNode() -> ASDisplayNode? { + var lowestHeaderNode: ASDisplayNode? + var lowestHeaderNodeIndex: Int? + for (_, headerNode) in self.sectionNodes { + if let index = self.subnodes?.index(of: headerNode) { + if lowestHeaderNodeIndex == nil || index < lowestHeaderNodeIndex! { + lowestHeaderNodeIndex = index + lowestHeaderNode = headerNode + } + } + } + return lowestHeaderNode + } + + private func applyPresentationLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode], updateLayoutTransition: ContainedViewLayoutTransition?, customScrollToItem: Bool, itemTransition: ContainedViewLayoutTransition, synchronousLoads: Bool, updatingLayout: Bool, completion: (GridNodeDisplayedItemRange) -> Void) { + let boundsTransition: ContainedViewLayoutTransition = updateLayoutTransition ?? .immediate + + var addedNodes = false + let verticalIndicator = self.scrollView.subviews.last as? UIImageView + + var previousItemFrames: [WrappedGridItemNode: CGRect]? + var saveItemFrames = false + switch presentationLayoutTransition.transition { + case .animated: + saveItemFrames = true + case .immediate: + break + } + if case .animated = itemTransition { + saveItemFrames = true + } + + if saveItemFrames { + var itemFrames: [WrappedGridItemNode: CGRect] = [:] + let contentOffset = self.scrollView.contentOffset + for (_, itemNode) in self.itemNodes { + itemFrames[WrappedGridItemNode(node: itemNode)] = itemNode.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) + } + for (_, sectionNode) in self.sectionNodes { + itemFrames[WrappedGridItemNode(node: sectionNode)] = sectionNode.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) + } + for itemNode in removedNodes { + itemFrames[WrappedGridItemNode(node: itemNode)] = itemNode.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) + } + previousItemFrames = itemFrames + } + + self.applyingContentOffset = true + + let previousBounds = self.bounds + self.scrollView.contentSize = presentationLayoutTransition.layout.contentSize + let layoutInsets = presentationLayoutTransition.layout.layout.insets + self.scrollView.contentInset = UIEdgeInsets(top: layoutInsets.top, left: 0.0, bottom: layoutInsets.bottom, right: 0.0) + if let scrollIndicatorInsets = presentationLayoutTransition.layout.layout.scrollIndicatorInsets { + self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets + } else { + self.scrollView.scrollIndicatorInsets = presentationLayoutTransition.layout.layout.insets + } + var boundsOffset: CGFloat = 0.0 + var shouldAnimateBounds = false + if !self.scrollView.contentOffset.equalTo(presentationLayoutTransition.layout.contentOffset) || self.bounds.size != presentationLayoutTransition.layout.layout.size { + let updatedBounds = CGRect(origin: presentationLayoutTransition.layout.contentOffset, size: presentationLayoutTransition.layout.layout.size) + boundsOffset = updatedBounds.origin.y - previousBounds.origin.y + self.bounds = updatedBounds + shouldAnimateBounds = true + } + self.applyingContentOffset = false + + let lowestSectionNode: ASDisplayNode? = self.lowestSectionNode() + + let bounds = self.bounds + var existingItemIndices = Set() + for item in presentationLayoutTransition.layout.items { + existingItemIndices.insert(item.index) + + let itemInBounds = bounds.intersects(item.frame) + + if let itemNode = self.itemNodes[item.index] { + if itemNode.frame != item.frame { + itemNode.frame = item.frame + } + itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads && itemInBounds) + } else { + let itemNode = self.items[item.index].node(layout: presentationLayoutTransition.layout.layout, synchronousLoad: synchronousLoads && itemInBounds) + itemNode.frame = item.frame + self.addItemNode(index: item.index, itemNode: itemNode, lowestSectionNode: lowestSectionNode) + addedNodes = true + itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads) + } + } + + var existingSections = Set() + for i in 0 ..< presentationLayoutTransition.layout.sections.count { + let section = presentationLayoutTransition.layout.sections[i] + + let wrappedSection = WrappedGridSection(section.section) + existingSections.insert(wrappedSection) + + var sectionFrame = section.frame + if self.floatingSections { + var maxY = CGFloat.greatestFiniteMagnitude + if i != presentationLayoutTransition.layout.sections.count - 1 { + maxY = presentationLayoutTransition.layout.sections[i + 1].frame.minY - sectionFrame.height + } + sectionFrame.origin.y = max(sectionFrame.minY, min(maxY, presentationLayoutTransition.layout.contentOffset.y + presentationLayoutTransition.layout.layout.insets.top)) + } + + if let sectionNode = self.sectionNodes[wrappedSection] { + sectionNode.frame = sectionFrame + } else { + let sectionNode = section.section.node() + sectionNode.frame = sectionFrame + self.addSectionNode(section: wrappedSection, sectionNode: sectionNode) + addedNodes = true + } + } + + if let previousItemFrames = previousItemFrames, case let .animated(duration, curve) = presentationLayoutTransition.transition { + let contentOffset = presentationLayoutTransition.layout.contentOffset + + if !updatingLayout { + boundsOffset = 0.0 + shouldAnimateBounds = false + } + + var offset: CGFloat? + for (index, itemNode) in self.itemNodes { + if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)], existingItemIndices.contains(index) { + let currentFrame = itemNode.frame.offsetBy(dx: 0.0, dy: -presentationLayoutTransition.layout.contentOffset.y) + offset = previousFrame.origin.y - currentFrame.origin.y - boundsOffset + break + } + } + + if offset == nil { + var previousUpperBound: CGFloat? + var previousLowerBound: CGFloat? + for (_, frame) in previousItemFrames { + if previousUpperBound == nil || previousUpperBound! > frame.minY { + previousUpperBound = frame.minY + } + if previousLowerBound == nil || previousLowerBound! < frame.maxY { + previousLowerBound = frame.maxY + } + } + + var updatedUpperBound: CGFloat? + var updatedLowerBound: CGFloat? + for item in presentationLayoutTransition.layout.items { + let frame = item.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) + if updatedUpperBound == nil || updatedUpperBound! > frame.minY { + updatedUpperBound = frame.minY + } + if updatedLowerBound == nil || updatedLowerBound! < frame.maxY { + updatedLowerBound = frame.maxY + } + } + for section in presentationLayoutTransition.layout.sections { + let frame = section.frame.offsetBy(dx: 0.0, dy: -contentOffset.y) + if updatedUpperBound == nil || updatedUpperBound! > frame.minY { + updatedUpperBound = frame.minY + } + if updatedLowerBound == nil || updatedLowerBound! < frame.maxY { + updatedLowerBound = frame.maxY + } + } + + if let updatedUpperBound = updatedUpperBound, let updatedLowerBound = updatedLowerBound { + switch presentationLayoutTransition.directionHint { + case .up: + offset = -(updatedLowerBound - (previousUpperBound ?? 0.0)) + case .down: + offset = -(updatedUpperBound - (previousLowerBound ?? presentationLayoutTransition.layout.layout.size.height)) + } + } + } + + if let offset = offset { + let timingFunction = curve.timingFunction + + for (index, itemNode) in self.itemNodes where existingItemIndices.contains(index) { + itemNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (wrappedSection, sectionNode) in self.sectionNodes where existingSections.contains(wrappedSection) { + sectionNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + + for index in self.itemNodes.keys { + if !existingItemIndices.contains(index) { + let itemNode = self.itemNodes[index]! + if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { + self.removeItemNodeWithIndex(index, removeNode: false) + let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) + itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y - boundsOffset), to: CGPoint(x: position.x, y: position.y + contentOffset.y - boundsOffset - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, force: true, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } else { + self.removeItemNodeWithIndex(index, removeNode: true) + } + } + } + + for itemNode in removedNodes { + if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { + let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) + itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, force: true, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } else { + itemNode.removeFromSupernode() + } + } + + for wrappedSection in self.sectionNodes.keys { + if !existingSections.contains(wrappedSection) { + let sectionNode = self.sectionNodes[wrappedSection]! + if let previousFrame = previousItemFrames[WrappedGridItemNode(node: sectionNode)] { + self.removeSectionNodeWithSection(wrappedSection, removeNode: false) + let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) + sectionNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, force: true, completion: { [weak sectionNode] _ in + sectionNode?.removeFromSupernode() + }) + } else { + self.removeSectionNodeWithSection(wrappedSection, removeNode: true) + } + } + } + } else { + for index in self.itemNodes.keys { + if !existingItemIndices.contains(index) { + self.removeItemNodeWithIndex(index) + } + } + + for wrappedSection in self.sectionNodes.keys { + if !existingSections.contains(wrappedSection) { + self.removeSectionNodeWithSection(wrappedSection) + } + } + + for itemNode in removedNodes { + itemNode.removeFromSupernode() + } + } + } else if let previousItemFrames = previousItemFrames, case let .animated(duration, curve) = itemTransition { + let timingFunction = curve.timingFunction + let contentOffset = self.scrollView.contentOffset + + for index in self.itemNodes.keys { + let itemNode = self.itemNodes[index]! + if !existingItemIndices.contains(index) { + if let _ = previousItemFrames[WrappedGridItemNode(node: itemNode)] { + self.removeItemNodeWithIndex(index, removeNode: false) + itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false) + itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } else { + self.removeItemNodeWithIndex(index, removeNode: true) + } + } else if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { + itemNode.layer.animatePosition(from: CGPoint(x: previousFrame.midX, y: previousFrame.midY + contentOffset.y), to: itemNode.layer.position, duration: duration, timingFunction: timingFunction) + } else { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12, timingFunction: kCAMediaTimingFunctionEaseIn) + itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + } + + for itemNode in removedNodes { + if let _ = previousItemFrames[WrappedGridItemNode(node: itemNode)] { + itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, timingFunction: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false) + itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.18, timingFunction: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } else { + itemNode.removeFromSupernode() + } + } + + for wrappedSection in self.sectionNodes.keys { + let sectionNode = self.sectionNodes[wrappedSection]! + if !existingSections.contains(wrappedSection) { + if let _ = previousItemFrames[WrappedGridItemNode(node: sectionNode)] { + self.removeSectionNodeWithSection(wrappedSection, removeNode: false) + sectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak sectionNode] _ in + sectionNode?.removeFromSupernode() + }) + } else { + self.removeSectionNodeWithSection(wrappedSection, removeNode: true) + } + } else if let previousFrame = previousItemFrames[WrappedGridItemNode(node: sectionNode)] { + sectionNode.layer.animatePosition(from: CGPoint(x: previousFrame.midX, y: previousFrame.midY + contentOffset.y), to: sectionNode.layer.position, duration: duration, timingFunction: timingFunction) + } else { + sectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseIn) + } + } + } else { + for index in self.itemNodes.keys { + if !existingItemIndices.contains(index) { + self.removeItemNodeWithIndex(index) + } + } + + for wrappedSection in self.sectionNodes.keys { + if !existingSections.contains(wrappedSection) { + self.removeSectionNodeWithSection(wrappedSection) + } + } + + for itemNode in removedNodes { + itemNode.removeFromSupernode() + } + } + + if shouldAnimateBounds { + boundsTransition.animateBounds(layer: self.layer, from: previousBounds) + } + + completion(self.displayedItemRange()) + + self.updateItemNodeVisibilititesAndScrolling() + self.updateVisibleContentOffset() + + if let visibleItemsUpdated = self.visibleItemsUpdated { + if presentationLayoutTransition.layout.items.count != 0 { + let topIndex = presentationLayoutTransition.layout.items.first!.index + let bottomIndex = presentationLayoutTransition.layout.items.last!.index + + var topVisible: (Int, GridItem) = (topIndex, self.items[topIndex]) + let bottomVisible: (Int, GridItem) = (bottomIndex, self.items[bottomIndex]) + + let lowerDisplayBound = presentationLayoutTransition.layout.contentOffset.y + presentationLayoutTransition.layout.layout.insets.top + //let upperDisplayBound = presentationLayoutTransition.layout.contentOffset.y + self.gridLayout.size.height + + for item in presentationLayoutTransition.layout.items { + if lowerDisplayBound.isLess(than: item.frame.maxY) { + topVisible = (item.index, self.items[item.index]) + break + } + } + + var topSectionVisible: GridSection? + for section in presentationLayoutTransition.layout.sections { + if lowerDisplayBound.isLess(than: section.frame.maxY) { + if self.itemLayout.items[topVisible.0].frame.minY > section.frame.minY { + topSectionVisible = section.section + } + break + } + } + + visibleItemsUpdated(GridNodeVisibleItems(top: (topIndex, self.items[topIndex]), bottom: (bottomIndex, self.items[bottomIndex]), topVisible: topVisible, bottomVisible: bottomVisible, topSectionVisible: topSectionVisible, count: self.items.count)) + } else { + visibleItemsUpdated(GridNodeVisibleItems(top: nil, bottom: nil, topVisible: nil, bottomVisible: nil, topSectionVisible: nil, count: self.items.count)) + } + } + + if addedNodes { + if let verticalIndicator = verticalIndicator, self.scrollView.subviews.last !== verticalIndicator { + verticalIndicator.superview?.bringSubview(toFront: verticalIndicator) + } + } + + if let presentationLayoutUpdated = self.presentationLayoutUpdated { + presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: presentationLayoutTransition.layout.layout, contentOffset: presentationLayoutTransition.layout.contentOffset, contentSize: presentationLayoutTransition.layout.contentSize), updateLayoutTransition ?? presentationLayoutTransition.transition) + } + } + + private func addItemNode(index: Int, itemNode: GridItemNode, lowestSectionNode: ASDisplayNode?) { + assert(self.itemNodes[index] == nil) + self.itemNodes[index] = itemNode + if itemNode.supernode == nil { + if let lowestSectionNode = lowestSectionNode { + self.insertSubnode(itemNode, belowSubnode: lowestSectionNode) + } else { + self.addSubnode(itemNode) + } + } + } + + private func addSectionNode(section: WrappedGridSection, sectionNode: ASDisplayNode) { + assert(self.sectionNodes[section] == nil) + self.sectionNodes[section] = sectionNode + if sectionNode.supernode == nil { + self.addSubnode(sectionNode) + } + } + + private func removeItemNodeWithIndex(_ index: Int, removeNode: Bool = true) { + if let itemNode = self.itemNodes.removeValue(forKey: index) { + if removeNode { + itemNode.removeFromSupernode() + } + } + } + + private func removeSectionNodeWithSection(_ section: WrappedGridSection, removeNode: Bool = true) { + if let sectionNode = self.sectionNodes.removeValue(forKey: section) { + if removeNode { + sectionNode.removeFromSupernode() + } + } + } + + private func updateItemNodeVisibilititesAndScrolling() { + let visibleRect = self.scrollView.bounds + let isScrolling = self.scrollView.isDragging || self.scrollView.isDecelerating + for (_, itemNode) in self.itemNodes { + let visible = itemNode.frame.intersects(visibleRect) + if itemNode.isVisibleInGrid != visible { + itemNode.isVisibleInGrid = visible + } + if itemNode.isGridScrolling != isScrolling { + itemNode.isGridScrolling = isScrolling + } + } + } + + public func visibleContentOffset() -> GridNodeVisibleContentOffset { + var offset: GridNodeVisibleContentOffset = .unknown + + if let supernode = self.supernode { + var topItemIndexAndFrame: (Int, CGRect) = (-1, CGRect()) + for index in self.itemNodes.keys.sorted() { + let itemNode = self.itemNodes[index]! + topItemIndexAndFrame = (index, supernode.convert(itemNode.bounds, from: itemNode)) + break + } + if topItemIndexAndFrame.0 == 0 { + offset = .known(self.scrollView.contentOffset.y + self.scrollView.contentInset.top) + } else if topItemIndexAndFrame.0 == -1 { + offset = .none + } + } + return offset + } + + private func updateVisibleContentOffset() { + self.visibleContentOffsetChanged(self.visibleContentOffset()) + } + + public func forEachItemNode(_ f: (ASDisplayNode) -> Void) { + for (_, node) in self.itemNodes { + f(node) + } + } + + public func forEachRow(_ f: ([ASDisplayNode]) -> Void) { + var row: [ASDisplayNode] = [] + var previousMinY: CGFloat? + for index in self.itemNodes.keys.sorted() { + let itemNode = self.itemNodes[index]! + if let previousMinY = previousMinY, !previousMinY.isEqual(to: itemNode.frame.minY) { + if !row.isEmpty { + f(row) + row.removeAll() + } + } + previousMinY = itemNode.frame.minY + row.append(itemNode) + } + if !row.isEmpty { + f(row) + } + } + + public func itemNodeAtPoint(_ point: CGPoint) -> ASDisplayNode? { + for (_, node) in self.itemNodes { + if node.frame.contains(point) { + return node + } + } + return nil + } +} + +private func NH_LP_TABLE_LOOKUP(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int) -> Int { + return table[i * rowsize + j] +} + +private func NH_LP_TABLE_LOOKUP_SET(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int, _ value: Int) { + table[i * rowsize + j] = value +} + +private func linearPartitionTable(_ weights: [Int], numberOfPartitions: Int) -> [Int] { + let n = weights.count + let k = numberOfPartitions + + let tableSize = n * k; + var tmpTable = Array(repeatElement(0, count: tableSize)) + + let solutionSize = (n - 1) * (k - 1) + var solution = Array(repeatElement(0, count: solutionSize)) + + for i in 0 ..< n { + let offset = i != 0 ? NH_LP_TABLE_LOOKUP(&tmpTable, i - 1, 0, k) : 0 + NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, 0, k, Int(weights[i]) + offset) + } + + for j in 0 ..< k { + NH_LP_TABLE_LOOKUP_SET(&tmpTable, 0, j, k, Int(weights[0])) + } + + for i in 1 ..< n { + for j in 1 ..< k { + var currentMin = 0 + var minX = Int.max + + for x in 0 ..< i { + let c1 = NH_LP_TABLE_LOOKUP(&tmpTable, x, j - 1, k) + let c2 = NH_LP_TABLE_LOOKUP(&tmpTable, i, 0, k) - NH_LP_TABLE_LOOKUP(&tmpTable, x, 0, k) + let cost = max(c1, c2) + + if x == 0 || cost < currentMin { + currentMin = cost; + minX = x + } + } + + NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, j, k, currentMin) + NH_LP_TABLE_LOOKUP_SET(&solution, i - 1, j - 1, k - 1, minX) + } + } + + return solution +} + +private func linearPartitionForWeights(_ weights: [Int], numberOfPartitions: Int) -> [[Int]] { + var n = weights.count + var k = numberOfPartitions + + if k <= 0 { + return [] + } + + if k >= n { + var partition: [[Int]] = [] + for weight in weights { + partition.append([weight]) + } + return partition + } + + if n == 1 { + return [weights] + } + + var solution = linearPartitionTable(weights, numberOfPartitions: numberOfPartitions) + let solutionRowSize = numberOfPartitions - 1 + + k = k - 2; + n = n - 1; + + var answer: [[Int]] = [] + + while k >= 0 { + if n < 1 { + answer.insert([], at: 0) + } else { + var currentAnswer: [Int] = [] + + var i = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize) + 1 + let range = n + 1 + while i < range { + currentAnswer.append(weights[i]) + i += 1 + } + + answer.insert(currentAnswer, at: 0) + + n = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize) + } + + k = k - 1 + } + + var currentAnswer: [Int] = [] + var i = 0 + let range = n + 1 + while i < range { + currentAnswer.append(weights[i]) + i += 1 + } + + answer.insert(currentAnswer, at: 0) + + return answer +} + diff --git a/submodules/Display/Display/GridNodeScroller.swift b/submodules/Display/Display/GridNodeScroller.swift new file mode 100644 index 0000000000..cbd6801478 --- /dev/null +++ b/submodules/Display/Display/GridNodeScroller.swift @@ -0,0 +1,53 @@ +import UIKit +import AsyncDisplayKit + +private class GridNodeScrollerLayer: CALayer { + override func setNeedsDisplay() { + } +} + +private class GridNodeScrollerView: UIScrollView { + override class var layerClass: AnyClass { + return GridNodeScrollerLayer.self + } + + override init(frame: CGRect) { + super.init(frame: frame) + + if #available(iOSApplicationExtension 11.0, *) { + self.contentInsetAdjustmentBehavior = .never + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } +} + +open class GridNodeScroller: ASDisplayNode, UIGestureRecognizerDelegate { + public var scrollView: UIScrollView { + return self.view as! UIScrollView + } + + override init() { + super.init() + + self.setViewBlock({ + return GridNodeScrollerView(frame: CGRect()) + }) + + self.scrollView.scrollsToTop = false + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/Display/Display/HapticFeedback.swift b/submodules/Display/Display/HapticFeedback.swift new file mode 100644 index 0000000000..2cb17e72cd --- /dev/null +++ b/submodules/Display/Display/HapticFeedback.swift @@ -0,0 +1,132 @@ +import Foundation +import UIKit + +public enum ImpactHapticFeedbackStyle: Hashable { + case light + case medium + case heavy +} + +@available(iOSApplicationExtension 10.0, iOS 10.0, *) +private final class HapticFeedbackImpl { + private lazy var impactGenerator: [ImpactHapticFeedbackStyle : UIImpactFeedbackGenerator] = { + [.light: UIImpactFeedbackGenerator(style: .light), + .medium: UIImpactFeedbackGenerator(style: .medium), + .heavy: UIImpactFeedbackGenerator(style: .heavy)] }() + private lazy var selectionGenerator = { UISelectionFeedbackGenerator() }() + private lazy var notificationGenerator = { UINotificationFeedbackGenerator() }() + + func prepareTap() { + self.selectionGenerator.prepare() + } + + func tap() { + self.selectionGenerator.selectionChanged() + } + + func prepareImpact(_ style: ImpactHapticFeedbackStyle) { + self.impactGenerator[style]?.prepare() + } + + func impact(_ style: ImpactHapticFeedbackStyle) { + self.impactGenerator[style]?.impactOccurred() + } + + func success() { + self.notificationGenerator.notificationOccurred(.success) + } + + func prepareError() { + self.notificationGenerator.prepare() + } + + func error() { + self.notificationGenerator.notificationOccurred(.error) + } + + @objc dynamic func f() { + } +} + +public final class HapticFeedback { + private var impl: AnyObject? + + public init() { + } + + deinit { + let impl = self.impl + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + if #available(iOSApplicationExtension 10.0, *) { + if let impl = impl as? HapticFeedbackImpl { + impl.f() + } + } + }) + } + + @available(iOSApplicationExtension 10.0, iOS 10.0, *) + private func withImpl(_ f: (HapticFeedbackImpl) -> Void) { + if self.impl == nil { + self.impl = HapticFeedbackImpl() + } + f(self.impl as! HapticFeedbackImpl) + } + + public func prepareTap() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.prepareTap() + } + } + } + + public func tap() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.tap() + } + } + } + + public func prepareImpact(_ style: ImpactHapticFeedbackStyle = .medium) { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.prepareImpact(style) + } + } + } + + public func impact(_ style: ImpactHapticFeedbackStyle = .medium) { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.impact(style) + } + } + } + + public func success() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.success() + } + } + } + + public func prepareError() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.prepareError() + } + } + } + + public func error() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.error() + } + } + } +} + diff --git a/submodules/Display/Display/HighlightTrackingButton.swift b/submodules/Display/Display/HighlightTrackingButton.swift new file mode 100644 index 0000000000..c48a9b2b33 --- /dev/null +++ b/submodules/Display/Display/HighlightTrackingButton.swift @@ -0,0 +1,48 @@ +import UIKit + +open class HighlightTrackingButton: UIButton { + private var internalHighlighted = false + + public var internalHighligthedChanged: (Bool) -> Void = { _ in } + public var highligthedChanged: (Bool) -> Void = { _ in } + + open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + if !self.internalHighlighted { + self.internalHighlighted = true + self.highligthedChanged(true) + self.internalHighligthedChanged(true) + } + + return super.beginTracking(touch, with: event) + } + + open override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + if self.internalHighlighted { + self.internalHighlighted = false + self.highligthedChanged(false) + self.internalHighligthedChanged(false) + } + + super.endTracking(touch, with: event) + } + + open override func cancelTracking(with event: UIEvent?) { + if self.internalHighlighted { + self.internalHighlighted = false + self.highligthedChanged(false) + self.internalHighligthedChanged(false) + } + + super.cancelTracking(with: event) + } + + open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + if self.internalHighlighted { + self.internalHighlighted = false + self.highligthedChanged(false) + self.internalHighligthedChanged(false) + } + + super.touchesCancelled(touches, with: event) + } +} diff --git a/submodules/Display/Display/HighlightableButton.swift b/submodules/Display/Display/HighlightableButton.swift new file mode 100644 index 0000000000..8d0d6b7908 --- /dev/null +++ b/submodules/Display/Display/HighlightableButton.swift @@ -0,0 +1,87 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +open class HighlightableButton: HighlightTrackingButton { + override public init(frame: CGRect) { + super.init(frame: frame) + + self.adjustsImageWhenHighlighted = false + self.adjustsImageWhenDisabled = false + self.internalHighligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +open class HighlightTrackingButtonNode: ASButtonNode { + private var internalHighlighted = false + + public var highligthedChanged: (Bool) -> Void = { _ in } + + open override func beginTracking(with touch: UITouch, with event: UIEvent?) -> Bool { + if !self.internalHighlighted { + self.internalHighlighted = true + self.highligthedChanged(true) + } + + return super.beginTracking(with: touch, with: event) + } + + open override func endTracking(with touch: UITouch?, with event: UIEvent?) { + if self.internalHighlighted { + self.internalHighlighted = false + self.highligthedChanged(false) + } + + super.endTracking(with: touch, with: event) + } + + open override func cancelTracking(with event: UIEvent?) { + if self.internalHighlighted { + self.internalHighlighted = false + self.highligthedChanged(false) + } + + super.cancelTracking(with: event) + } + + open override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + + if self.internalHighlighted { + self.internalHighlighted = false + self.highligthedChanged(false) + } + } +} + +open class HighlightableButtonNode: HighlightTrackingButtonNode { + override public init() { + super.init() + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } +} diff --git a/submodules/Display/Display/ImmediateTextNode.swift b/submodules/Display/Display/ImmediateTextNode.swift new file mode 100644 index 0000000000..add3df7e25 --- /dev/null +++ b/submodules/Display/Display/ImmediateTextNode.swift @@ -0,0 +1,135 @@ +import Foundation +import UIKit + +public struct ImmediateTextNodeLayoutInfo { + public let size: CGSize + public let truncated: Bool +} + +public class ImmediateTextNode: TextNode { + public var attributedText: NSAttributedString? + public var textAlignment: NSTextAlignment = .natural + public var truncationType: CTLineTruncationType = .end + public var maximumNumberOfLines: Int = 1 + public var lineSpacing: CGFloat = 0.0 + public var insets: UIEdgeInsets = UIEdgeInsets() + + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + private var linkHighlightingNode: LinkHighlightingNode? + + public var linkHighlightColor: UIColor? + + public var trailingLineWidth: CGFloat? + + public var highlightAttributeAction: (([NSAttributedStringKey: Any]) -> NSAttributedStringKey?)? { + didSet { + if self.isNodeLoaded { + self.updateInteractiveActions() + } + } + } + + public var tapAttributeAction: (([NSAttributedStringKey: Any]) -> Void)? + public var longTapAttributeAction: (([NSAttributedStringKey: Any]) -> Void)? + + public func updateLayout(_ constrainedSize: CGSize) -> CGSize { + let makeLayout = TextNode.asyncLayout(self) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets)) + let _ = apply() + if layout.numberOfLines > 1 { + self.trailingLineWidth = layout.trailingLineWidth + } else { + self.trailingLineWidth = nil + } + return layout.size + } + + public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo { + let makeLayout = TextNode.asyncLayout(self) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets)) + let _ = apply() + return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated) + } + + override public func didLoad() { + super.didLoad() + + self.updateInteractiveActions() + } + + private func updateInteractiveActions() { + if self.highlightAttributeAction != nil { + if self.tapRecognizer == nil { + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:))) + tapRecognizer.highlight = { [weak self] point in + if let strongSelf = self { + var rects: [CGRect]? + if let point = point { + if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) { + if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) { + let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index) + if let initialRects = initialRects, case .center = strongSelf.textAlignment { + var mappedRects: [CGRect] = [] + for i in 0 ..< initialRects.count { + let lineRect = initialRects[i].0 + var itemRect = initialRects[i].1 + itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x + mappedRects.append(itemRect) + } + rects = mappedRects + } else { + rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index) + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = strongSelf.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear) + strongSelf.linkHighlightingNode = linkHighlightingNode + strongSelf.addSubnode(linkHighlightingNode) + } + linkHighlightingNode.frame = strongSelf.bounds + linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) + } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { + strongSelf.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } + self.view.addGestureRecognizer(tapRecognizer) + } + } else if let tapRecognizer = self.tapRecognizer { + self.tapRecognizer = nil + self.view.removeGestureRecognizer(tapRecognizer) + } + } + + @objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let (_, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.tapAttributeAction?(attributes) + } + case .longTap: + if let (_, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.longTapAttributeAction?(attributes) + } + default: + break + } + } + default: + break + } + } +} diff --git a/submodules/Display/Display/Info.plist b/submodules/Display/Display/Info.plist new file mode 100644 index 0000000000..d3de8eefb6 --- /dev/null +++ b/submodules/Display/Display/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/submodules/Display/Display/InteractiveTransitionGestureRecognizer.swift b/submodules/Display/Display/InteractiveTransitionGestureRecognizer.swift new file mode 100644 index 0000000000..8d08e8c5bf --- /dev/null +++ b/submodules/Display/Display/InteractiveTransitionGestureRecognizer.swift @@ -0,0 +1,83 @@ +import Foundation +import UIKit + +private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool { + if view.disablesInteractiveTransitionGestureRecognizer { + return true + } + + if let point = point, let test = view.interactiveTransitionGestureRecognizerTest, test(point) { + return true + } + + if let view = view as? ListViewBackingView { + let transform = view.transform + let angle: Double = Double(atan2f(Float(transform.b), Float(transform.a))) + let term1: Double = abs(angle - Double.pi / 2.0) + let term2: Double = abs(angle + Double.pi / 2.0) + let term3: Double = abs(angle - Double.pi * 3.0 / 2.0) + if term1 < 0.001 || term2 < 0.001 || term3 < 0.001 { + return true + } + } + + if let superview = view.superview { + return hasHorizontalGestures(superview, point: point != nil ? view.convert(point!, to: superview) : nil) + } else { + return false + } +} + +class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { + var validatedGesture = false + var firstLocation: CGPoint = CGPoint() + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.maximumNumberOfTouches = 1 + } + + override func reset() { + super.reset() + + validatedGesture = false + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + let touch = touches.first! + self.firstLocation = touch.location(in: self.view) + + if let target = self.view?.hitTest(self.firstLocation, with: event) { + if hasHorizontalGestures(target, point: self.view?.convert(self.firstLocation, to: target)) { + self.state = .cancelled + } + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + let location = touches.first!.location(in: self.view) + let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y) + + let absTranslationX: CGFloat = abs(translation.x) + let absTranslationY: CGFloat = abs(translation.y) + + if !validatedGesture { + if self.firstLocation.x < 16.0 { + validatedGesture = true + } else if translation.x < 0.0 { + self.state = .failed + } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { + self.state = .failed + } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { + validatedGesture = true + } + } + + if validatedGesture { + super.touchesMoved(touches, with: event) + } + } +} diff --git a/submodules/Display/Display/KeyShortcut.swift b/submodules/Display/Display/KeyShortcut.swift new file mode 100644 index 0000000000..ef00181595 --- /dev/null +++ b/submodules/Display/Display/KeyShortcut.swift @@ -0,0 +1,43 @@ +import UIKit + +public struct KeyShortcut: Hashable { + let title: String + let input: String + let modifiers: UIKeyModifierFlags + let action: () -> Void + + public init(title: String = "", input: String = "", modifiers: UIKeyModifierFlags = [], action: @escaping () -> Void = {}) { + self.title = title + self.input = input + self.modifiers = modifiers + self.action = action + } + + public var hashValue: Int { + return input.hashValue ^ modifiers.hashValue + } + + public static func ==(lhs: KeyShortcut, rhs: KeyShortcut) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} + +extension UIKeyModifierFlags: Hashable { + public var hashValue: Int { + return self.rawValue + } +} + +extension KeyShortcut { + var uiKeyCommand: UIKeyCommand { + if #available(iOSApplicationExtension 9.0, *), !self.title.isEmpty { + return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title) + } else { + return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:))) + } + } + + func isEqual(to command: UIKeyCommand) -> Bool { + return self.input == command.input && self.modifiers == command.modifierFlags + } +} diff --git a/submodules/Display/Display/KeyShortcutsController.swift b/submodules/Display/Display/KeyShortcutsController.swift new file mode 100644 index 0000000000..c6984d775e --- /dev/null +++ b/submodules/Display/Display/KeyShortcutsController.swift @@ -0,0 +1,84 @@ +import UIKit + +public protocol KeyShortcutResponder { + var keyShortcuts: [KeyShortcut] { get }; +} + +public class KeyShortcutsController: UIResponder { + private var effectiveShortcuts: [KeyShortcut]? + private var viewControllerEnumerator: ((ContainableController) -> Bool) -> Void + + public static var isAvailable: Bool { + if #available(iOSApplicationExtension 8.0, *), UIDevice.current.userInterfaceIdiom == .pad { + return true + } else { + return false + } + } + + public init(enumerator: @escaping ((ContainableController) -> Bool) -> Void) { + self.viewControllerEnumerator = enumerator + super.init() + } + + public override var keyCommands: [UIKeyCommand]? { + var convertedCommands: [UIKeyCommand] = [] + var shortcuts: [KeyShortcut] = [] + + self.viewControllerEnumerator({ viewController -> Bool in + guard let viewController = viewController as? KeyShortcutResponder else { + return true + } + shortcuts.removeAll(where: { viewController.keyShortcuts.contains($0) }) + shortcuts.append(contentsOf: viewController.keyShortcuts) + return true + }) + + // iOS 8 fix + convertedCommands.append(KeyShortcut(modifiers:[.command]).uiKeyCommand) + convertedCommands.append(KeyShortcut(modifiers:[.alternate]).uiKeyCommand) + + convertedCommands.append(contentsOf: shortcuts.map { $0.uiKeyCommand }) + + self.effectiveShortcuts = shortcuts + + return convertedCommands + } + + @objc func handleKeyCommand(_ command: UIKeyCommand) { + if let shortcut = findShortcut(for: command) { + shortcut.action() + } + } + + private func findShortcut(for command: UIKeyCommand) -> KeyShortcut? { + if let shortcuts = self.effectiveShortcuts { + for shortcut in shortcuts { + if shortcut.isEqual(to: command) { + return shortcut + } + } + } + return nil + } + + public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) { + return true + } else { + return super.canPerformAction(action, withSender: sender) + } + } + + public override func target(forAction action: Selector, withSender sender: Any?) -> Any? { + if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) { + return self + } else { + return super.target(forAction: action, withSender: sender) + } + } + + public override var canBecomeFirstResponder: Bool { + return true + } +} diff --git a/submodules/Display/Display/Keyboard.swift b/submodules/Display/Display/Keyboard.swift new file mode 100644 index 0000000000..44526b8f1d --- /dev/null +++ b/submodules/Display/Display/Keyboard.swift @@ -0,0 +1,11 @@ +import Foundation + +#if BUCK +import DisplayPrivate +#endif + +public enum Keyboard { + public static func applyAutocorrection() { + applyKeyboardAutocorrection() + } +} diff --git a/submodules/Display/Display/KeyboardManager.swift b/submodules/Display/Display/KeyboardManager.swift new file mode 100644 index 0000000000..086f578ea0 --- /dev/null +++ b/submodules/Display/Display/KeyboardManager.swift @@ -0,0 +1,127 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +#if BUCK +import DisplayPrivate +#endif + +struct KeyboardSurface { + let host: UIView +} + +private func getFirstResponder(_ view: UIView) -> UIView? { + if view.isFirstResponder { + return view + } else { + for subview in view.subviews { + if let result = getFirstResponder(subview) { + return result + } + } + return nil + } +} + +class KeyboardManager { + private let host: StatusBarHost + + private weak var previousPositionAnimationMirrorSource: CATracingLayer? + private weak var previousFirstResponderView: UIView? + private var interactiveInputOffset: CGFloat = 0.0 + + var surfaces: [KeyboardSurface] = [] { + didSet { + self.updateSurfaces(oldValue) + } + } + + init(host: StatusBarHost) { + self.host = host + } + + func getCurrentKeyboardHeight() -> CGFloat { + guard let keyboardView = self.host.keyboardView else { + return 0.0 + } + return keyboardView.bounds.height + } + + func updateInteractiveInputOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + guard let keyboardView = self.host.keyboardView else { + return + } + + self.interactiveInputOffset = offset + + let previousBounds = keyboardView.bounds + let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: -offset), size: previousBounds.size) + keyboardView.layer.bounds = updatedBounds + if transition.isAnimated { + transition.animateOffsetAdditive(layer: keyboardView.layer, offset: previousBounds.minY - updatedBounds.minY, completion: completion) + } else { + completion() + } + + //transition.updateSublayerTransformOffset(layer: keyboardView.layer, offset: CGPoint(x: 0.0, y: offset)) + } + + private func updateSurfaces(_ previousSurfaces: [KeyboardSurface]) { + guard let keyboardWindow = self.host.keyboardWindow else { + return + } + + var firstResponderView: UIView? + var firstResponderDisableAutomaticKeyboardHandling: UIResponderDisableAutomaticKeyboardHandling = [] + for surface in self.surfaces { + if let view = getFirstResponder(surface.host) { + firstResponderView = surface.host + firstResponderDisableAutomaticKeyboardHandling = view.disableAutomaticKeyboardHandling + break + } + } + + if let firstResponderView = firstResponderView { + let containerOrigin = firstResponderView.convert(CGPoint(), to: nil) + var filteredTranslation = containerOrigin.x + if firstResponderDisableAutomaticKeyboardHandling.contains(.forward) { + filteredTranslation = max(0.0, filteredTranslation) + } + if firstResponderDisableAutomaticKeyboardHandling.contains(.backward) { + filteredTranslation = min(0.0, filteredTranslation) + } + let horizontalTranslation = CATransform3DMakeTranslation(filteredTranslation, 0.0, 0.0) + let currentTransform = keyboardWindow.layer.sublayerTransform + if !CATransform3DEqualToTransform(horizontalTranslation, currentTransform) { + //print("set to \(CGPoint(x: containerOrigin.x, y: self.interactiveInputOffset))") + keyboardWindow.layer.sublayerTransform = horizontalTranslation + } + if let tracingLayer = firstResponderView.layer as? CATracingLayer, firstResponderDisableAutomaticKeyboardHandling.isEmpty { + if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource, previousPositionAnimationMirrorSource !== tracingLayer { + previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) + } + tracingLayer.setPositionAnimationMirrorTarget(keyboardWindow.layer) + self.previousPositionAnimationMirrorSource = tracingLayer + } else if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource { + previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) + self.previousPositionAnimationMirrorSource = nil + } + } else { + keyboardWindow.layer.sublayerTransform = CATransform3DIdentity + if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource { + previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) + self.previousPositionAnimationMirrorSource = nil + } + if let previousFirstResponderView = previousFirstResponderView { + if previousFirstResponderView.window == nil { + keyboardWindow.isHidden = true + keyboardWindow.layer.cancelAnimationsRecursive(key: "position") + keyboardWindow.layer.cancelAnimationsRecursive(key: "bounds") + keyboardWindow.isHidden = false + } + } + } + + self.previousFirstResponderView = firstResponderView + } +} diff --git a/submodules/Display/Display/LayoutSizes.swift b/submodules/Display/Display/LayoutSizes.swift new file mode 100644 index 0000000000..74d89e96f8 --- /dev/null +++ b/submodules/Display/Display/LayoutSizes.swift @@ -0,0 +1,10 @@ +import Foundation +import UIKit + +public func horizontalContainerFillingSizeForLayout(layout: ContainerViewLayout, sideInset: CGFloat) -> CGFloat { + if case .regular = layout.metrics.widthClass { + return min(layout.size.width, 414.0) - sideInset * 2.0 + } else { + return layout.size.width - sideInset * 2.0 + } +} diff --git a/submodules/Display/Display/LegacyPresentedController.swift b/submodules/Display/Display/LegacyPresentedController.swift new file mode 100644 index 0000000000..496b976d23 --- /dev/null +++ b/submodules/Display/Display/LegacyPresentedController.swift @@ -0,0 +1,152 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +#if BUCK +import DisplayPrivate +#endif + +public enum LegacyPresentedControllerPresentation { + case custom + case modal +} + +private func passControllerAppearanceAnimated(presentation: LegacyPresentedControllerPresentation) -> Bool { + switch presentation { + case .custom: + return false + case .modal: + return true + } +} + +open class LegacyPresentedController: ViewController { + private let legacyController: UIViewController + private let presentation: LegacyPresentedControllerPresentation + + private var controllerNode: LegacyPresentedControllerNode { + return self.displayNode as! LegacyPresentedControllerNode + } + private var loadedController = false + + var controllerLoaded: (() -> Void)? + + private let asPresentable = true + + public init(legacyController: UIViewController, presentation: LegacyPresentedControllerPresentation) { + self.legacyController = legacyController + self.presentation = presentation + + super.init(navigationBarPresentationData: nil) + + /*legacyController.navigation_setDismiss { [weak self] in + self?.dismiss() + }*/ + if !asPresentable { + self.addChildViewController(legacyController) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func loadDisplayNode() { + self.displayNode = LegacyPresentedControllerNode() + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if self.ignoreAppearanceMethodInvocations() { + return + } + + if !loadedController && !asPresentable { + loadedController = true + + self.controllerNode.controllerView = self.legacyController.view + self.controllerNode.view.addSubview(self.legacyController.view) + self.legacyController.didMove(toParentViewController: self) + + if let controllerLoaded = self.controllerLoaded { + controllerLoaded() + } + } + + if !asPresentable { + self.legacyController.viewWillAppear(animated && passControllerAppearanceAnimated(presentation: self.presentation)) + } + } + + override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if self.ignoreAppearanceMethodInvocations() { + return + } + + if !asPresentable { + self.legacyController.viewWillDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation)) + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if self.ignoreAppearanceMethodInvocations() { + return + } + + if asPresentable { + if !loadedController { + loadedController = true + //self.legacyController.modalPresentationStyle = .currentContext + self.present(self.legacyController, animated: false, completion: nil) + } + } else { + switch self.presentation { + case .modal: + self.controllerNode.animateModalIn() + self.legacyController.viewDidAppear(true) + case .custom: + self.legacyController.viewDidAppear(animated) + } + } + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if !self.asPresentable { + self.legacyController.viewDidDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation)) + } + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + override open func dismiss(completion: (() -> Void)? = nil) { + switch self.presentation { + case .modal: + self.controllerNode.animateModalOut { [weak self] in + /*if let controller = self?.legacyController as? TGViewController { + controller.didDismiss() + } else if let controller = self?.legacyController as? TGNavigationController { + controller.didDismiss() + }*/ + self?.presentingViewController?.dismiss(animated: false, completion: completion) + } + case .custom: + /*if let controller = self.legacyController as? TGViewController { + controller.didDismiss() + } else if let controller = self.legacyController as? TGNavigationController { + controller.didDismiss() + }*/ + self.presentingViewController?.dismiss(animated: false, completion: completion) + } + } +} diff --git a/submodules/Display/Display/LegacyPresentedControllerNode.swift b/submodules/Display/Display/LegacyPresentedControllerNode.swift new file mode 100644 index 0000000000..4aa8fc293d --- /dev/null +++ b/submodules/Display/Display/LegacyPresentedControllerNode.swift @@ -0,0 +1,40 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class LegacyPresentedControllerNode: ASDisplayNode { + private var containerLayout: ContainerViewLayout? + + var controllerView: UIView? { + didSet { + if let controllerView = self.controllerView, let containerLayout = self.containerLayout { + controllerView.frame = CGRect(origin: CGPoint(), size: containerLayout.size) + } + } + } + + override init() { + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = layout + if let controllerView = self.controllerView { + controllerView.frame = CGRect(origin: CGPoint(), size: layout.size) + } + } + + func animateModalIn() { + 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 animateModalOut(completion: @escaping () -> Void) { + 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: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in + completion() + }) + } +} diff --git a/submodules/Display/Display/LinkHighlightingNode.swift b/submodules/Display/Display/LinkHighlightingNode.swift new file mode 100644 index 0000000000..5600a3cbd2 --- /dev/null +++ b/submodules/Display/Display/LinkHighlightingNode.swift @@ -0,0 +1,237 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +private enum CornerType { + case topLeft + case topRight + case bottomLeft + case bottomRight +} + +private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } +} + +private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } +} + +private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) { + if rects.isEmpty { + return (CGPoint(), nil) + } + + var topLeft = rects[0].origin + var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) + for i in 1 ..< rects.count { + topLeft.x = min(topLeft.x, rects[i].origin.x) + topLeft.y = min(topLeft.y, rects[i].origin.y) + bottomRight.x = max(bottomRight.x, rects[i].maxX) + bottomRight.y = max(bottomRight.y, rects[i].maxY) + } + + topLeft.x -= inset + topLeft.y -= inset + bottomRight.x += inset * 2.0 + bottomRight.y += inset * 2.0 + + return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + + context.setBlendMode(.copy) + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset) + context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y)) + } + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + + var previous: CGRect? + if i != 0 { + previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + var next: CGRect? + if i != rects.count - 1 { + next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + if let previous = previous { + if previous.contains(rect.topLeft) { + if abs(rect.topLeft.x - previous.minX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) + } + if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.topRight.x - previous.maxX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) + } + + if let next = next { + if next.contains(rect.bottomLeft) { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) + } + if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) + } + } + })) + +} + +public final class LinkHighlightingNode: ASDisplayNode { + private var rects: [CGRect] = [] + private let imageNode: ASImageNode + + public var innerRadius: CGFloat = 4.0 + public var outerRadius: CGFloat = 4.0 + public var inset: CGFloat = 2.0 + + private var _color: UIColor + public var color: UIColor { + get { + return _color + } set(value) { + self._color = value + if !self.rects.isEmpty { + self.updateImage() + } + } + } + + public init(color: UIColor) { + self._color = color + + self.imageNode = ASImageNode() + self.imageNode.isUserInteractionEnabled = false + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + + super.init() + + self.addSubnode(self.imageNode) + } + + public func updateRects(_ rects: [CGRect]) { + if self.rects != rects { + self.rects = rects + + self.updateImage() + } + } + + private func updateImage() { + if rects.isEmpty { + self.imageNode.image = nil + } + let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius) + + if let image = image { + self.imageNode.image = image + self.imageNode.frame = CGRect(origin: offset, size: image.size) + } + } + + public func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void { + let currentRects = self.rects + let currentColor = self._color + let currentInnerRadius = self.innerRadius + let currentOuterRadius = self.outerRadius + let currentInset = self.inset + + return { [weak self] color, rects, innerRadius, outerRadius, inset in + var updatedImage: (CGPoint, UIImage?)? + if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset { + updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius) + } + + return { + if let strongSelf = self { + strongSelf._color = color + strongSelf.rects = rects + strongSelf.innerRadius = innerRadius + strongSelf.outerRadius = outerRadius + strongSelf.inset = inset + + if let (offset, maybeImage) = updatedImage, let image = maybeImage { + strongSelf.imageNode.image = image + strongSelf.imageNode.frame = CGRect(origin: offset, size: image.size) + } + } + } + } + } +} diff --git a/submodules/Display/Display/ListView.swift b/submodules/Display/Display/ListView.swift new file mode 100644 index 0000000000..024cd5d560 --- /dev/null +++ b/submodules/Display/Display/ListView.swift @@ -0,0 +1,3980 @@ +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +#if BUCK +import DisplayPrivate +#endif + +private let useBackgroundDeallocation = false + +private let infiniteScrollSize: CGFloat = 10000.0 +private let insertionAnimationDuration: Double = 0.4 + +private final class ListViewBackingLayer: CALayer { + override func setNeedsLayout() { + } + + override func layoutSublayers() { + } + + override func setNeedsDisplay() { + } + + override func displayIfNeeded() { + } + + override func needsDisplay() -> Bool { + return false + } + + override func display() { + } +} + +final class ListViewBackingView: UIView { + weak var target: ListView? + + override class var layerClass: AnyClass { + return ListViewBackingLayer.self + } + + override func setNeedsLayout() { + } + + override func layoutSubviews() { + } + + override func setNeedsDisplay() { + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + self.target?.touchesBegan(touches, with: event) + } + + override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + self.target?.touchesCancelled(touches, with: event) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + self.target?.touchesMoved(touches, with: event) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + self.target?.touchesEnded(touches, with: event) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.isHidden, let target = self.target { + if target.limitHitTestToNodes, !target.internalHitTest(point, with: event) { + return nil + } + if let result = target.headerHitTest(point, with: event) { + return result + } + } + return super.hitTest(point, with: event) + } + + override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool { + return self.target?.accessibilityScroll(direction) ?? false + } +} + +private final class ListViewTimerProxy: NSObject { + private let action: () -> () + + init(_ action: @escaping () -> ()) { + self.action = action + super.init() + } + + @objc func timerEvent() { + self.action() + } +} + +public enum ListViewVisibleContentOffset { + case known(CGFloat) + case unknown + case none +} + +public enum ListViewScrollDirection { + case up + case down +} + +public struct ListViewKeepTopItemOverscrollBackground { + public let color: UIColor + public let direction: Bool + + public init(color: UIColor, direction: Bool) { + self.color = color + self.direction = direction + } + + fileprivate func isEqual(to: ListViewKeepTopItemOverscrollBackground) -> Bool { + if !self.color.isEqual(to.color) { + return false + } + if self.direction != to.direction { + return false + } + return true + } +} + +public enum GeneralScrollDirection { + case up + case down +} + +open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate { + private final let scroller: ListViewScroller + private final var visibleSize: CGSize = CGSize() + public private(set) final var insets = UIEdgeInsets() + public final var visualInsets: UIEdgeInsets? + public private(set) final var headerInsets = UIEdgeInsets() + public private(set) final var scrollIndicatorInsets = UIEdgeInsets() + private final var ensureTopInsetForOverlayHighlightedItems: CGFloat? + private final var lastContentOffset: CGPoint = CGPoint() + private final var lastContentOffsetTimestamp: CFAbsoluteTime = 0.0 + private final var ignoreScrollingEvents: Bool = false + + private final var displayLink: CADisplayLink! + private final var needsAnimations = false + + public final var dynamicBounceEnabled = true + public final var rotated = false + public final var experimentalSnapScrollToItem = false + + private final var invisibleInset: CGFloat = 500.0 + public var preloadPages: Bool = true { + didSet { + if self.preloadPages != oldValue { + self.invisibleInset = self.preloadPages ? 500.0 : 20.0 + //self.invisibleInset = self.preloadPages ? 20.0 : 20.0 + if self.preloadPages { + self.enqueueUpdateVisibleItems(synchronous: false) + } + } + } + } + + public final var stackFromBottom: Bool = false + public final var stackFromBottomInsetItemFactor: CGFloat = 0.0 + public final var limitHitTestToNodes: Bool = false + public final var keepTopItemOverscrollBackground: ListViewKeepTopItemOverscrollBackground? { + didSet { + if let value = self.keepTopItemOverscrollBackground { + self.topItemOverscrollBackground?.color = value.color + } + self.updateTopItemOverscrollBackground(transition: .immediate) + } + } + public final var keepBottomItemOverscrollBackground: UIColor? { + didSet { + if let color = self.keepBottomItemOverscrollBackground { + self.bottomItemOverscrollBackground?.backgroundColor = color + } + self.updateBottomItemOverscrollBackground() + } + } + public final var snapToBottomInsetUntilFirstInteraction: Bool = false + + public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? { + didSet { + + } + } + + private var topItemOverscrollBackground: ListViewOverscrollBackgroundNode? + private var bottomItemOverscrollBackground: ASDisplayNode? + + private var itemHighlightOverlayBackground: ASDisplayNode? + + private var verticalScrollIndicator: ASImageNode? + public var verticalScrollIndicatorColor: UIColor? { + didSet { + if let fillColor = self.verticalScrollIndicatorColor { + if self.verticalScrollIndicator == nil { + let verticalScrollIndicator = ASImageNode() + verticalScrollIndicator.isUserInteractionEnabled = false + verticalScrollIndicator.alpha = 0.0 + verticalScrollIndicator.image = generateStretchableFilledCircleImage(diameter: 3.0, color: fillColor) + self.verticalScrollIndicator = verticalScrollIndicator + self.addSubnode(verticalScrollIndicator) + } + } else { + self.verticalScrollIndicator?.removeFromSupernode() + self.verticalScrollIndicator = nil + } + } + } + public final var verticalScrollIndicatorFollowsOverscroll: Bool = false + + private var touchesPosition = CGPoint() + public private(set) var isTracking = false + public private(set) var trackingOffset: CGFloat = 0.0 + public private(set) var beganTrackingAtTopOrigin = false + public private(set) var isDeceleratingAfterTracking = false + + private final var transactionQueue: ListViewTransactionQueue + private final var transactionOffset: CGFloat = 0.0 + + private final var enqueuedUpdateVisibleItems = false + + private final var createdItemNodes = 0 + + public final var synchronousNodes = false + public final var debugInfo = false + + private final var items: [ListViewItem] = [] + private final var itemNodes: [ListViewItemNode] = [] + private final var itemHeaderNodes: [Int64: ListViewItemHeaderNode] = [:] + + public final var displayedItemRangeChanged: (ListViewDisplayedItemRange, Any?) -> Void = { _, _ in } + public private(set) final var displayedItemRange: ListViewDisplayedItemRange = ListViewDisplayedItemRange(loadedRange: nil, visibleRange: nil) + + public private(set) final var opaqueTransactionState: Any? + + public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in } + public final var visibleBottomContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in } + public final var beganInteractiveDragging: () -> Void = { } + public final var didEndScrolling: (() -> Void)? + + private var currentGeneralScrollDirection: GeneralScrollDirection? + public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in } + + public final var reorderItem: (Int, Int, Any?) -> Signal = { _, _, _ in return .single(false) } + + private final var animations: [ListViewAnimation] = [] + private final var actionsForVSync: [() -> ()] = [] + private final var inVSync = false + + private let frictionSlider = UISlider() + private let springSlider = UISlider() + private let freeResistanceSlider = UISlider() + private let scrollingResistanceSlider = UISlider() + + private var selectionTouchLocation: CGPoint? + private var selectionTouchDelayTimer: Foundation.Timer? + private var selectionLongTapDelayTimer: Foundation.Timer? + private var flashNodesDelayTimer: Foundation.Timer? + private var flashScrollIndicatorTimer: Foundation.Timer? + private var highlightedItemIndex: Int? + private var scrolledToItem: (Int, ListViewScrollPosition)? + private var reorderNode: ListViewReorderingItemNode? + private var reorderFeedback: HapticFeedback? + private var reorderFeedbackDisposable: MetaDisposable? + + private let waitingForNodesDisposable = MetaDisposable() + + /*override open var accessibilityElements: [Any]? { + get { + var accessibilityElements: [Any] = [] + self.forEachItemNode({ itemNode in + addAccessibilityChildren(of: itemNode, container: self, to: &accessibilityElements) + }) + return accessibilityElements + } set(value) { + } + }*/ + + override public init() { + class DisplayLinkProxy: NSObject { + weak var target: ListView? + init(target: ListView) { + self.target = target + } + + @objc func displayLinkEvent() { + self.target?.displayLinkEvent() + } + } + + self.transactionQueue = ListViewTransactionQueue() + + self.scroller = ListViewScroller() + + super.init() + + self.isAccessibilityContainer = true + + self.setViewBlock({ () -> UIView in + return ListViewBackingView() + }) + + self.clipsToBounds = true + + (self.view as! ListViewBackingView).target = self + + self.transactionQueue.transactionCompleted = { [weak self] in + if let strongSelf = self { + strongSelf.updateVisibleItemRange() + } + } + + self.scroller.alwaysBounceVertical = true + self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0) + self.scroller.isHidden = true + self.scroller.delegate = self + self.view.addSubview(self.scroller) + self.scroller.panGestureRecognizer.cancelsTouchesInView = true + self.view.addGestureRecognizer(self.scroller.panGestureRecognizer) + + let trackingRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.trackingGesture(_:))) + trackingRecognizer.delegate = self + self.view.addGestureRecognizer(trackingRecognizer) + + self.view.addGestureRecognizer(ListViewReorderingGestureRecognizer(shouldBegin: { [weak self] point in + if let strongSelf = self { + if let index = strongSelf.itemIndexAtPoint(point) { + for i in 0 ..< strongSelf.itemNodes.count { + if strongSelf.itemNodes[i].index == index { + let itemNode = strongSelf.itemNodes[i] + let itemNodeFrame = itemNode.frame + let itemNodeBounds = itemNode.bounds + if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) { + strongSelf.beginReordering(itemNode: itemNode) + return true + } + break + } + } + } + } + return false + }, ended: { [weak self] in + self?.endReordering() + }, moved: { [weak self] offset in + self?.updateReordering(offset: offset) + })) + + self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) + self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) + + if #available(iOS 10.0, *) { + self.displayLink.preferredFramesPerSecond = 60 + } + + self.displayLink.isPaused = true + } + + deinit { + self.pauseAnimations() + self.displayLink.invalidate() + + if useBackgroundDeallocation { + assertionFailure() + /*for itemNode in self.itemNodes { + ASDeallocQueue.sharedDeallocation.releaseObject(inBackground: UnsafeMutablePointer(itemNode)) + } + for itemHeaderNode in self.itemHeaderNodes { + ASDeallocQueue.sharedDeallocatio.releaseObject(inBackground: itemHeaderNode) + }*/ + } else { + for i in (0 ..< self.itemNodes.count).reversed() { + var itemNode: AnyObject? = self.itemNodes[i] + self.itemNodes.remove(at: i) + ASPerformMainThreadDeallocation(&itemNode) + } + for key in self.itemHeaderNodes.keys { + var itemHeaderNode: AnyObject? = self.itemHeaderNodes[key] + self.itemHeaderNodes.removeValue(forKey: key) + ASPerformMainThreadDeallocation(&itemHeaderNode) + } + } + + self.waitingForNodesDisposable.dispose() + self.reorderFeedbackDisposable?.dispose() + } + + private func displayLinkEvent() { + self.updateAnimations() + } + + private func setNeedsAnimations() { + if !self.needsAnimations { + self.needsAnimations = true + self.displayLink.isPaused = false + } + } + + private func pauseAnimations() { + if self.needsAnimations { + self.needsAnimations = false + self.displayLink.isPaused = true + } + } + + private func dispatchOnVSync(forceNext: Bool = false, action: @escaping () -> ()) { + Queue.mainQueue().async { + if !forceNext && self.inVSync { + action() + } else { + action() + //self.actionsForVSync.append(action) + //self.setNeedsAnimations() + } + } + } + + private func beginReordering(itemNode: ListViewItemNode) { + if let reorderNode = self.reorderNode { + reorderNode.removeFromSupernode() + } + let reorderNode = ListViewReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin) + self.reorderNode = reorderNode + if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(reorderNode, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(reorderNode) + } + itemNode.isHidden = true + } + + private func endReordering() { + if let reorderNode = self.reorderNode { + self.reorderNode = nil + if let itemNode = reorderNode.itemNode, itemNode.supernode == self { + self.reorderItemNodeToFront(itemNode) + reorderNode.animateCompletion(completion: { [weak itemNode, weak reorderNode] in + //itemNode?.isHidden = false + reorderNode?.removeFromSupernode() + }) + self.setNeedsAnimations() + } else { + reorderNode.removeFromSupernode() + } + } + } + + private func updateReordering(offset: CGFloat) { + if let reorderNode = self.reorderNode { + reorderNode.updateOffset(offset: offset) + self.checkItemReordering() + } + } + + private func checkItemReordering() { + if let reorderNode = self.reorderNode, let reorderItemNode = reorderNode.itemNode, let reorderItemIndex = reorderItemNode.index, reorderItemNode.supernode == self { + guard let verticalTopOffset = reorderNode.currentOffset() else { + return + } + let verticalOffset = verticalTopOffset + var closestIndex: (Int, CGFloat)? + for i in 0 ..< self.itemNodes.count { + if let itemNodeIndex = self.itemNodes[i].index, itemNodeIndex != reorderItemIndex { + let itemFrame = self.itemNodes[i].apparentContentFrame + let itemOffset = itemFrame.midY + let deltaOffset = itemOffset - verticalOffset + if let (_, closestOffset) = closestIndex { + if abs(deltaOffset) < abs(closestOffset) { + closestIndex = (itemNodeIndex, deltaOffset) + } + } else { + closestIndex = (itemNodeIndex, deltaOffset) + } + } + } + if let (closestIndexValue, offset) = closestIndex { + //print("closest \(closestIndexValue) offset \(offset)") + var toIndex: Int + if offset > 0 { + toIndex = closestIndexValue + if toIndex > reorderItemIndex { + toIndex -= 1 + } + } else { + toIndex = closestIndexValue + 1 + if toIndex > reorderItemIndex { + toIndex -= 1 + } + } + if toIndex != reorderItemNode.index { + if reorderNode.currentState?.0 != reorderItemIndex || reorderNode.currentState?.1 != toIndex { + reorderNode.currentState = (reorderItemIndex, toIndex) + //print("reorder \(reorderItemIndex) to \(toIndex) offset \(offset)") + if self.reorderFeedbackDisposable == nil { + self.reorderFeedbackDisposable = MetaDisposable() + } + self.reorderFeedbackDisposable?.set((self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState) + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self, value else { + return + } + if strongSelf.reorderFeedback == nil { + strongSelf.reorderFeedback = HapticFeedback() + } + strongSelf.reorderFeedback?.tap() + })) + } + } + } + + self.setNeedsAnimations() + } + } + + public func flashHeaderItems(duration: Double = 2.0) { + self.resetHeaderItemsFlashTimer(start: true, duration: duration) + } + + private func resetHeaderItemsFlashTimer(start: Bool, duration: Double = 0.3) { + if let flashNodesDelayTimer = self.flashNodesDelayTimer { + flashNodesDelayTimer.invalidate() + self.flashNodesDelayTimer = nil + } + + if start { + let timer = Timer(timeInterval: duration, target: ListViewTimerProxy { [weak self] in + if let strongSelf = self { + if let flashNodesDelayTimer = strongSelf.flashNodesDelayTimer { + flashNodesDelayTimer.invalidate() + strongSelf.flashNodesDelayTimer = nil + strongSelf.updateHeaderItemsFlashing(animated: true) + } + } + }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) + self.flashNodesDelayTimer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + self.updateHeaderItemsFlashing(animated: true) + } + } + + private func resetScrollIndicatorFlashTimer(start: Bool) { + if let flashScrollIndicatorTimer = self.flashScrollIndicatorTimer { + flashScrollIndicatorTimer.invalidate() + self.flashScrollIndicatorTimer = nil + } + + if start { + let timer = Timer(timeInterval: 0.1, target: ListViewTimerProxy { [weak self] in + if let strongSelf = self { + if let flashScrollIndicatorTimer = strongSelf.flashScrollIndicatorTimer { + flashScrollIndicatorTimer.invalidate() + strongSelf.flashScrollIndicatorTimer = nil + strongSelf.verticalScrollIndicator?.alpha = 0.0 + strongSelf.verticalScrollIndicator?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) + self.flashScrollIndicatorTimer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + } else { + self.verticalScrollIndicator?.layer.removeAnimation(forKey: "opacity") + self.verticalScrollIndicator?.alpha = 1.0 + } + } + + private func headerItemsAreFlashing() -> Bool { + //print("\(self.scroller.isDragging) || (\(self.scroller.isDecelerating) && \(self.isDeceleratingAfterTracking)) || \(self.flashNodesDelayTimer != nil)") + return self.scroller.isDragging || (self.isDeceleratingAfterTracking) || self.flashNodesDelayTimer != nil + } + + private func updateHeaderItemsFlashing(animated: Bool) { + let flashing = self.headerItemsAreFlashing() + for (_, headerNode) in self.itemHeaderNodes { + headerNode.updateFlashingOnScrolling(flashing, animated: animated) + } + } + + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.lastContentOffsetTimestamp = 0.0 + self.resetHeaderItemsFlashTimer(start: false) + self.updateHeaderItemsFlashing(animated: true) + self.resetScrollIndicatorFlashTimer(start: false) + + if self.snapToBottomInsetUntilFirstInteraction { + self.snapToBottomInsetUntilFirstInteraction = false + } + self.scrolledToItem = nil + + self.beganInteractiveDragging() + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if decelerate { + self.lastContentOffsetTimestamp = CACurrentMediaTime() + self.isDeceleratingAfterTracking = true + self.updateHeaderItemsFlashing(animated: true) + self.resetScrollIndicatorFlashTimer(start: false) + } else { + self.isDeceleratingAfterTracking = false + self.resetHeaderItemsFlashTimer(start: true) + self.updateHeaderItemsFlashing(animated: true) + self.resetScrollIndicatorFlashTimer(start: true) + + self.lastContentOffsetTimestamp = 0.0 + self.didEndScrolling?() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.lastContentOffsetTimestamp = 0.0 + self.isDeceleratingAfterTracking = false + self.resetHeaderItemsFlashTimer(start: true) + self.updateHeaderItemsFlashing(animated: true) + self.resetScrollIndicatorFlashTimer(start: true) + self.didEndScrolling?() + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrollViewDidScroll(scrollView, synchronous: false) + } + + private var generalAccumulatedDeltaY: CGFloat = 0.0 + + private func updateScrollViewDidScroll(_ scrollView: UIScrollView, synchronous: Bool) { + if self.ignoreScrollingEvents || scroller !== self.scroller { + return + } + + //CATransaction.begin() + //CATransaction.setDisableActions(true) + + let deltaY = scrollView.contentOffset.y - self.lastContentOffset.y + self.generalAccumulatedDeltaY += deltaY + if abs(self.generalAccumulatedDeltaY) > 14.0 { + let direction: GeneralScrollDirection = self.generalAccumulatedDeltaY < 0 ? .up : .down + self.generalAccumulatedDeltaY = 0.0 + if self.currentGeneralScrollDirection != direction { + self.currentGeneralScrollDirection = direction + self.generalScrollDirectionUpdated(direction) + } + } + + self.lastContentOffset = scrollView.contentOffset + if !self.lastContentOffsetTimestamp.isZero { + self.lastContentOffsetTimestamp = CACurrentMediaTime() + } + + self.transactionOffset += -deltaY + + if self.isTracking { + self.trackingOffset += -deltaY + } + + self.enqueueUpdateVisibleItems(synchronous: synchronous) + + var useScrollDynamics = false + + let anchor: CGFloat + if self.isTracking { + anchor = self.touchesPosition.y + } else if deltaY < 0.0 { + anchor = self.visibleSize.height + } else { + anchor = 0.0 + } + + for itemNode in self.itemNodes { + let position = itemNode.position + itemNode.position = CGPoint(x: position.x, y: position.y - deltaY) + + if self.dynamicBounceEnabled && itemNode.wantsScrollDynamics { + useScrollDynamics = true + + var distance: CGFloat + let itemFrame = itemNode.apparentFrame + if anchor < itemFrame.origin.y { + distance = abs(itemFrame.origin.y - anchor) + } else if anchor > itemFrame.origin.y + itemFrame.size.height { + distance = abs(anchor - (itemFrame.origin.y + itemFrame.size.height)) + } else { + distance = 0.0 + } + + let factor: CGFloat = max(0.08, abs(distance) / self.visibleSize.height) + + let resistance: CGFloat = testSpringFreeResistance + + itemNode.addScrollingOffset(deltaY * factor * resistance) + } + } + + if !self.snapToBounds(snapTopItem: false, stackFromBottom: self.stackFromBottom).offset.isZero { + self.updateVisibleContentOffset() + } + self.updateScroller(transition: .immediate) + + self.updateItemHeaders(leftInset: self.insets.left, rightInset: self.insets.right) + + for (_, headerNode) in self.itemHeaderNodes { + if self.dynamicBounceEnabled && headerNode.wantsScrollDynamics { + useScrollDynamics = true + + var distance: CGFloat + let itemFrame = headerNode.frame + if anchor < itemFrame.origin.y { + distance = abs(itemFrame.origin.y - anchor) + } else if anchor > itemFrame.origin.y + itemFrame.size.height { + distance = abs(anchor - (itemFrame.origin.y + itemFrame.size.height)) + } else { + distance = 0.0 + } + + let factor: CGFloat = max(0.08, abs(distance) / self.visibleSize.height) + + let resistance: CGFloat = testSpringFreeResistance + + headerNode.addScrollingOffset(deltaY * factor * resistance) + } + } + + if useScrollDynamics { + self.setNeedsAnimations() + } + + self.updateVisibleContentOffset() + self.updateVisibleItemRange() + self.updateItemNodesVisibilities(onlyPositive: false) + + //CATransaction.commit() + } + + private func calculateAdditionalTopInverseInset() -> CGFloat { + var additionalInverseTopInset: CGFloat = 0.0 + if !self.stackFromBottomInsetItemFactor.isZero { + var remainingFactor = self.stackFromBottomInsetItemFactor + for itemNode in self.itemNodes { + if remainingFactor.isLessThanOrEqualTo(0.0) { + break + } + + let itemFactor: CGFloat + if CGFloat(1.0).isLessThanOrEqualTo(remainingFactor) { + itemFactor = 1.0 + } else { + itemFactor = remainingFactor + } + + additionalInverseTopInset += floor(itemNode.apparentBounds.height * itemFactor) + + remainingFactor -= 1.0 + } + } + return additionalInverseTopInset + } + + private func snapToBounds(snapTopItem: Bool, stackFromBottom: Bool, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, scrollToItem: ListViewScrollToItem? = nil) -> (snappedTopInset: CGFloat, offset: CGFloat) { + if self.itemNodes.count == 0 { + return (0.0, 0.0) + } + + var overscroll: CGFloat = 0.0 + if self.scroller.contentOffset.y < 0.0 { + overscroll = self.scroller.contentOffset.y + } else if self.scroller.contentOffset.y > max(0.0, self.scroller.contentSize.height - self.scroller.bounds.size.height) { + overscroll = self.scroller.contentOffset.y - max(0.0, (self.scroller.contentSize.height - self.scroller.bounds.size.height)) + } + + var completeHeight: CGFloat = 0.0 + var topItemFound = false + var bottomItemFound = false + var topItemEdge: CGFloat = 0.0 + var bottomItemEdge: CGFloat = 0.0 + + for i in 0 ..< self.itemNodes.count { + if let index = itemNodes[i].index { + if index == 0 { + topItemFound = true + } + break + } + } + + var effectiveInsets = self.insets + if topItemFound && !self.stackFromBottomInsetItemFactor.isZero { + let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() + effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) + } + + if topItemFound { + topItemEdge = itemNodes[0].apparentFrame.origin.y + } + + var bottomItemNode: ListViewItemNode? + for i in (0 ..< self.itemNodes.count).reversed() { + if let index = itemNodes[i].index { + if index == self.items.count - 1 { + bottomItemNode = itemNodes[i] + bottomItemFound = true + } + break + } + } + + if bottomItemFound { + bottomItemEdge = itemNodes[itemNodes.count - 1].apparentFrame.maxY + } + + if topItemFound && bottomItemFound { + for itemNode in self.itemNodes { + completeHeight += itemNode.apparentBounds.height + } + } + + var transition: ContainedViewLayoutTransition = .immediate + if let updateSizeAndInsets = updateSizeAndInsets { + if !updateSizeAndInsets.duration.isZero { + switch updateSizeAndInsets.curve { + case let .Spring(duration): + transition = .animated(duration: duration, curve: .spring) + case let .Default(duration): + transition = .animated(duration: max(updateSizeAndInsets.duration, duration ?? 0.3), curve: .easeInOut) + } + } + } else if let scrollToItem = scrollToItem { + switch scrollToItem.curve { + case let .Spring(duration): + transition = .animated(duration: duration, curve: .spring) + case let .Default(duration): + if let duration = duration, duration.isZero { + transition = .immediate + } else { + transition = .animated(duration: duration ?? 0.3, curve: .easeInOut) + } + } + } + + var offset: CGFloat = 0.0 + if topItemFound && bottomItemFound { + let visibleAreaHeight = self.visibleSize.height - effectiveInsets.bottom - effectiveInsets.top + if self.stackFromBottom { + if visibleAreaHeight > completeHeight { + let areaHeight = completeHeight + if topItemEdge < self.visibleSize.height - effectiveInsets.bottom - areaHeight - overscroll { + offset = self.visibleSize.height - effectiveInsets.bottom - areaHeight - overscroll - topItemEdge + } else if bottomItemEdge > self.visibleSize.height - effectiveInsets.bottom - overscroll { + offset = self.visibleSize.height - effectiveInsets.bottom - overscroll - bottomItemEdge + } + } else { + let areaHeight = min(completeHeight, visibleAreaHeight) + if bottomItemEdge < effectiveInsets.top + areaHeight - overscroll { + offset = effectiveInsets.top + areaHeight - overscroll - bottomItemEdge + } else if topItemEdge > effectiveInsets.top - overscroll { + offset = (effectiveInsets.top - overscroll) - topItemEdge + } + } + } else { + let areaHeight = min(completeHeight, visibleAreaHeight) + if bottomItemEdge < effectiveInsets.top + areaHeight - overscroll { + if snapTopItem && topItemEdge < effectiveInsets.top { + offset = (effectiveInsets.top - overscroll) - topItemEdge + } else { + offset = effectiveInsets.top + areaHeight - overscroll - bottomItemEdge + } + } else if topItemEdge > effectiveInsets.top - overscroll && /*snapTopItem*/ true { + offset = (effectiveInsets.top - overscroll) - topItemEdge + } + } + + if visibleAreaHeight > completeHeight { + if let itemNode = bottomItemNode, itemNode.wantsTrailingItemSpaceUpdates { + itemNode.updateTrailingItemSpace(visibleAreaHeight - completeHeight, transition: transition) + } + } else { + if let itemNode = bottomItemNode, itemNode.wantsTrailingItemSpaceUpdates { + itemNode.updateTrailingItemSpace(0.0, transition: transition) + } + } + } else { + if let itemNode = bottomItemNode, itemNode.wantsTrailingItemSpaceUpdates { + itemNode.updateTrailingItemSpace(0.0, transition: transition) + } + if topItemFound { + if topItemEdge > effectiveInsets.top - overscroll && /*snapTopItem*/ true { + offset = (effectiveInsets.top - overscroll) - topItemEdge + } + } else if bottomItemFound { + if bottomItemEdge < self.visibleSize.height - effectiveInsets.bottom - overscroll { + offset = self.visibleSize.height - effectiveInsets.bottom - overscroll - bottomItemEdge + } + } + } + + if abs(offset) > CGFloat.ulpOfOne { + for itemNode in self.itemNodes { + var frame = itemNode.frame + frame.origin.y += offset + itemNode.frame = frame + if let accessoryItemNode = itemNode.accessoryItemNode { + itemNode.layoutAccessoryItemNode(accessoryItemNode, leftInset: self.insets.left, rightInset: self.insets.right) + } + } + } + + var snappedTopInset: CGFloat = 0.0 + if !self.stackFromBottomInsetItemFactor.isZero && topItemFound { + snappedTopInset = max(0.0, (effectiveInsets.top - self.insets.top) - (topItemEdge + offset)) + } + + return (snappedTopInset, offset) + } + + public func visibleContentOffset() -> ListViewVisibleContentOffset { + var offset: ListViewVisibleContentOffset = .unknown + var topItemIndexAndMinY: (Int, CGFloat) = (-1, 0.0) + + var currentMinY: CGFloat? + for itemNode in self.itemNodes { + if let index = itemNode.index { + let updatedMinY: CGFloat + if let currentMinY = currentMinY { + if itemNode.apparentFrame.minY < currentMinY { + updatedMinY = itemNode.apparentFrame.minY + } else { + updatedMinY = currentMinY + } + } else { + updatedMinY = itemNode.apparentFrame.minY + } + topItemIndexAndMinY = (index, updatedMinY) + break + } else if currentMinY == nil { + currentMinY = itemNode.apparentFrame.minY + } + } + if topItemIndexAndMinY.0 == 0 { + offset = .known(-(topItemIndexAndMinY.1 - self.insets.top)) + } else if topItemIndexAndMinY.0 == -1 { + offset = .none + } + return offset + } + + public func visibleBottomContentOffset() -> ListViewVisibleContentOffset { + var offset: ListViewVisibleContentOffset = .unknown + var bottomItemIndexAndFrame: (Int, CGRect) = (-1, CGRect()) + for itemNode in self.itemNodes.reversed() { + if let index = itemNode.index { + bottomItemIndexAndFrame = (index, itemNode.apparentFrame) + break + } + } + if bottomItemIndexAndFrame.0 == self.items.count - 1 { + offset = .known(bottomItemIndexAndFrame.1.maxY - (self.visibleSize.height - self.insets.bottom)) + } else if bottomItemIndexAndFrame.0 == -1 { + offset = .none + } + return offset + } + + private func updateVisibleContentOffset() { + self.visibleContentOffsetChanged(self.visibleContentOffset()) + self.visibleBottomContentOffsetChanged(self.visibleBottomContentOffset()) + } + + private func stopScrolling() { + let wasIgnoringScrollingEvents = self.ignoreScrollingEvents + self.ignoreScrollingEvents = true + self.scroller.setContentOffset(self.scroller.contentOffset, animated: false) + self.ignoreScrollingEvents = wasIgnoringScrollingEvents + } + + private func updateTopItemOverscrollBackground(transition: ContainedViewLayoutTransition) { + if let value = self.keepTopItemOverscrollBackground { + var applyTransition = transition + + let topItemOverscrollBackground: ListViewOverscrollBackgroundNode + if let current = self.topItemOverscrollBackground { + topItemOverscrollBackground = current + } else { + applyTransition = .immediate + topItemOverscrollBackground = ListViewOverscrollBackgroundNode(color: value.color) + topItemOverscrollBackground.isLayerBacked = true + self.topItemOverscrollBackground = topItemOverscrollBackground + self.insertSubnode(topItemOverscrollBackground, at: 0) + } + var topItemFound = false + var topItemNodeIndex: Int? + if !self.itemNodes.isEmpty { + topItemNodeIndex = self.itemNodes[0].index + } + if topItemNodeIndex == 0 { + topItemFound = true + } + + var backgroundFrame: CGRect + + if topItemFound { + let realTopItemEdge = itemNodes.first!.apparentFrame.origin.y + let realTopItemEdgeOffset = max(0.0, realTopItemEdge) + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.visibleSize.width, height: realTopItemEdgeOffset)) + if value.direction { + backgroundFrame.origin.y = 0.0 + backgroundFrame.size.height = realTopItemEdgeOffset + } else { + backgroundFrame.origin.y = min(self.insets.top, realTopItemEdgeOffset) + backgroundFrame.size.height = max(0.0, self.visibleSize.height - backgroundFrame.origin.y) + 400.0 + } + } else { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.visibleSize.width, height: 0.0)) + if value.direction { + backgroundFrame.origin.y = 0.0 + } else { + backgroundFrame.origin.y = 0.0 + backgroundFrame.size.height = self.visibleSize.height + } + } + + let previousFrame = topItemOverscrollBackground.frame + if !previousFrame.equalTo(backgroundFrame) { + topItemOverscrollBackground.frame = backgroundFrame + + let positionDelta = CGPoint(x: backgroundFrame.minX - previousFrame.minX, y: backgroundFrame.minY - previousFrame.minY) + + applyTransition.animateOffsetAdditive(node: topItemOverscrollBackground, offset: positionDelta.y) + } + + topItemOverscrollBackground.updateLayout(size: backgroundFrame.size, transition: applyTransition) + } else if let topItemOverscrollBackground = self.topItemOverscrollBackground { + self.topItemOverscrollBackground = nil + topItemOverscrollBackground.removeFromSupernode() + } + } + + private func updateFloatingHeaderNode(transition: ContainedViewLayoutTransition) { + guard let updateFloatingHeaderOffset = self.updateFloatingHeaderOffset else { + return + } + + var topItemFound = false + var topItemNodeIndex: Int? + if !self.itemNodes.isEmpty { + topItemNodeIndex = self.itemNodes[0].index + } + if topItemNodeIndex == 0 { + topItemFound = true + } + + var topOffset: CGFloat + + if topItemFound { + let realTopItemEdge = itemNodes.first!.apparentFrame.origin.y + let realTopItemEdgeOffset = max(0.0, realTopItemEdge) + + topOffset = realTopItemEdgeOffset + } else { + if !self.itemNodes.isEmpty { + if self.stackFromBottom { + topOffset = 0.0 + } else { + topOffset = self.visibleSize.height + } + } else { + if self.stackFromBottom { + topOffset = self.visibleSize.height + } else { + topOffset = 0.0 + } + } + } + + updateFloatingHeaderOffset(topOffset, transition) + } + + private func updateBottomItemOverscrollBackground() { + if let color = self.keepBottomItemOverscrollBackground { + var bottomItemFound = false + var lastItemNodeIndex: Int? + if !itemNodes.isEmpty { + lastItemNodeIndex = self.itemNodes[itemNodes.count - 1].index + } + if lastItemNodeIndex == self.items.count - 1 { + bottomItemFound = true + } + + let bottomItemOverscrollBackground: ASDisplayNode + if let currentBottomItemOverscrollBackground = self.bottomItemOverscrollBackground { + bottomItemOverscrollBackground = currentBottomItemOverscrollBackground + } else { + bottomItemOverscrollBackground = ASDisplayNode() + bottomItemOverscrollBackground.backgroundColor = color + bottomItemOverscrollBackground.isLayerBacked = true + self.insertSubnode(bottomItemOverscrollBackground, at: 0) + self.bottomItemOverscrollBackground = bottomItemOverscrollBackground + } + + if bottomItemFound { + let realBottomItemEdge = itemNodes.last!.apparentFrame.origin.y + let realBottomItemEdgeOffset = max(0.0, self.visibleSize.height - realBottomItemEdge) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: self.visibleSize.height - realBottomItemEdgeOffset), size: CGSize(width: self.visibleSize.width, height: self.visibleSize.height)) + if !backgroundFrame.equalTo(bottomItemOverscrollBackground.frame) { + bottomItemOverscrollBackground.frame = backgroundFrame + } + } else { + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: self.visibleSize.height), size: CGSize(width: self.visibleSize.width, height: self.visibleSize.height)) + if !backgroundFrame.equalTo(bottomItemOverscrollBackground.frame) { + bottomItemOverscrollBackground.frame = backgroundFrame + } + } + } else if let bottomItemOverscrollBackground = self.bottomItemOverscrollBackground { + self.bottomItemOverscrollBackground = nil + bottomItemOverscrollBackground.removeFromSupernode() + } + } + + private func updateOverlayHighlight(transition: ContainedViewLayoutTransition) { + var lowestOverlayNode: ListViewItemNode? + + for itemNode in self.itemNodes { + if itemNode.isHighlightedInOverlay { + lowestOverlayNode = itemNode + itemNode.view.superview?.bringSubview(toFront: itemNode.view) + if let verticalScrollIndicator = self.verticalScrollIndicator { + verticalScrollIndicator.view.superview?.bringSubview(toFront: verticalScrollIndicator.view) + } + } + } + + if let lowestOverlayNode = lowestOverlayNode { + let itemHighlightOverlayBackground: ASDisplayNode + if let current = self.itemHighlightOverlayBackground { + itemHighlightOverlayBackground = current + } else { + itemHighlightOverlayBackground = ASDisplayNode() + itemHighlightOverlayBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: -self.visibleSize.height), size: CGSize(width: self.visibleSize.width, height: self.visibleSize.height * 3.0)) + itemHighlightOverlayBackground.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.itemHighlightOverlayBackground = itemHighlightOverlayBackground + self.insertSubnode(itemHighlightOverlayBackground, belowSubnode: lowestOverlayNode) + itemHighlightOverlayBackground.alpha = 0.0 + transition.updateAlpha(node: itemHighlightOverlayBackground, alpha: 1.0) + } + } else if let itemHighlightOverlayBackground = self.itemHighlightOverlayBackground { + self.itemHighlightOverlayBackground = nil + for (_, headerNode) in self.itemHeaderNodes { + //self.view.bringSubview(toFront: headerNode.view) + } + //self.view.bringSubview(toFront: itemHighlightOverlayBackground.view) + for itemNode in self.itemNodes { + //self.view.bringSubview(toFront: itemNode.view) + } + transition.updateAlpha(node: itemHighlightOverlayBackground, alpha: 0.0, completion: { [weak itemHighlightOverlayBackground] _ in + itemHighlightOverlayBackground?.removeFromSupernode() + }) + if let verticalScrollIndicator = self.verticalScrollIndicator { + verticalScrollIndicator.view.superview?.bringSubview(toFront: verticalScrollIndicator.view) + } + } + } + + private func updateScroller(transition: ContainedViewLayoutTransition) { + self.updateOverlayHighlight(transition: transition) + + if self.itemNodes.count == 0 { + return + } + + var topItemFound: Bool = false + var bottomItemFound: Bool = false + var topItemEdge: CGFloat = 0.0 + var bottomItemEdge: CGFloat = 0.0 + + for i in 0 ..< self.itemNodes.count { + if let index = itemNodes[i].index { + if index == 0 { + topItemFound = true + topItemEdge = itemNodes[0].apparentFrame.origin.y + break + } + } + } + + var effectiveInsets = self.insets + if topItemFound && !self.stackFromBottomInsetItemFactor.isZero { + let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() + effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) + } + + var completeHeight = effectiveInsets.top + effectiveInsets.bottom + + if let index = itemNodes[itemNodes.count - 1].index, index == self.items.count - 1 { + bottomItemFound = true + bottomItemEdge = itemNodes[itemNodes.count - 1].apparentFrame.maxY + } + + topItemEdge -= effectiveInsets.top + bottomItemEdge += effectiveInsets.bottom + + if topItemFound && bottomItemFound { + for itemNode in self.itemNodes { + completeHeight += itemNode.apparentBounds.height + } + + if self.stackFromBottom { + let updatedCompleteHeight = max(completeHeight, self.visibleSize.height) + let deltaCompleteHeight = updatedCompleteHeight - completeHeight + topItemEdge -= deltaCompleteHeight + bottomItemEdge -= deltaCompleteHeight + completeHeight = updatedCompleteHeight + } + } + + self.updateTopItemOverscrollBackground(transition: transition) + self.updateBottomItemOverscrollBackground() + self.updateFloatingHeaderNode(transition: transition) + + let wasIgnoringScrollingEvents = self.ignoreScrollingEvents + self.ignoreScrollingEvents = true + if topItemFound && bottomItemFound { + if self.stackFromBottom { + self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) + } else { + self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) + } + self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight) + self.scroller.contentOffset = self.lastContentOffset + } else if topItemFound { + self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) + self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) + self.scroller.contentOffset = self.lastContentOffset + } else if bottomItemFound { + self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) + self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge) + self.scroller.contentOffset = self.lastContentOffset + } + else + { + self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) + self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) + self.scroller.contentOffset = self.lastContentOffset + } + self.ignoreScrollingEvents = wasIgnoringScrollingEvents + } + + private func async(_ f: @escaping () -> Void) { + DispatchQueue.global().async(execute: f) + } + + private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimation: ListViewItemUpdateAnimation, completion: @escaping (QueueLocalObject, ListViewItemNodeLayout, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + if let previousNode = previousNode { + item.updateNode(async: { f in + if synchronous { + f() + } else { + self.async(f) + } + }, node: { + assert(Queue.mainQueue().isCurrent()) + return previousNode.syncWith({ $0 })! + }, params: params, previousItem: previousItem, nextItem: nextItem, animation: updateAnimation, completion: { (layout, apply) in + if Thread.isMainThread { + if synchronous { + completion(previousNode, layout, { + return (nil, { info in + assert(Queue.mainQueue().isCurrent()) + previousNode.with({ $0.index = index }) + apply(info) + }) + }) + } else { + self.async { + completion(previousNode, layout, { + return (nil, { info in + assert(Queue.mainQueue().isCurrent()) + previousNode.with({ $0.index = index }) + apply(info) + }) + }) + } + } + } else { + completion(previousNode, layout, { + return (nil, { info in + assert(Queue.mainQueue().isCurrent()) + previousNode.with({ $0.index = index }) + apply(info) + }) + }) + } + }) + } else { + item.nodeConfiguredForParams(async: { f in + if synchronous { + f() + } else { + self.async(f) + } + }, params: params, synchronousLoads: synchronousLoads, previousItem: previousItem, nextItem: nextItem, completion: { itemNode, apply in + itemNode.index = index + completion(QueueLocalObject(queue: Queue.mainQueue(), generate: { return itemNode }), ListViewItemNodeLayout(contentSize: itemNode.contentSize, insets: itemNode.insets), apply) + }) + } + } + + private func currentState() -> ListViewState { + var nodes: [ListViewStateNode] = [] + nodes.reserveCapacity(self.itemNodes.count) + for node in self.itemNodes { + if let index = node.index { + nodes.append(.Node(index: index, frame: node.apparentFrame, referenceNode: QueueLocalObject(queue: Queue.mainQueue(), generate: { + return node + }))) + } else { + nodes.append(.Placeholder(frame: node.apparentFrame)) + } + } + return ListViewState(insets: self.insets, visibleSize: self.visibleSize, invisibleInset: self.invisibleInset, nodes: nodes, scrollPosition: nil, stationaryOffset: nil, stackFromBottom: self.stackFromBottom) + } + + public func transaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem? = nil, additionalScrollDistance: CGFloat = 0.0, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, stationaryItemRange: (Int, Int)? = nil, updateOpaqueState: Any?, completion: @escaping (ListViewDisplayedItemRange) -> Void = { _ in }) { + if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil && updateSizeAndInsets == nil && additionalScrollDistance.isZero { + if let updateOpaqueState = updateOpaqueState { + self.opaqueTransactionState = updateOpaqueState + } + completion(self.immediateDisplayedItemRange()) + return + } + + self.transactionQueue.addTransaction({ [weak self] transactionCompletion in + if let strongSelf = self { + strongSelf.transactionOffset = 0.0 + strongSelf.deleteAndInsertItemsTransaction(deleteIndices: deleteIndices, insertIndicesAndItems: insertIndicesAndItems, updateIndicesAndItems: updateIndicesAndItems, options: options, scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, updateOpaqueState: updateOpaqueState, completion: { [weak strongSelf] in + completion(strongSelf?.immediateDisplayedItemRange() ?? ListViewDisplayedItemRange(loadedRange: nil, visibleRange: nil)) + + transactionCompletion() + }) + } + }) + } + + private func deleteAndInsertItemsTransaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemRange: (Int, Int)?, updateOpaqueState: Any?, completion: @escaping () -> Void) { + if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil { + if let updateSizeAndInsets = updateSizeAndInsets , (self.items.count == 0 || (updateSizeAndInsets.size == self.visibleSize && updateSizeAndInsets.insets == self.insets)) { + self.visibleSize = updateSizeAndInsets.size + self.insets = updateSizeAndInsets.insets + self.headerInsets = updateSizeAndInsets.headerInsets ?? self.insets + self.scrollIndicatorInsets = updateSizeAndInsets.scrollIndicatorInsets ?? self.insets + self.ensureTopInsetForOverlayHighlightedItems = updateSizeAndInsets.ensureTopInsetForOverlayHighlightedItems + + let wasIgnoringScrollingEvents = self.ignoreScrollingEvents + self.ignoreScrollingEvents = true + self.scroller.frame = CGRect(origin: CGPoint(), size: updateSizeAndInsets.size) + self.scroller.contentSize = CGSize(width: updateSizeAndInsets.size.width, height: infiniteScrollSize * 2.0) + self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) + self.scroller.contentOffset = self.lastContentOffset + self.ignoreScrollingEvents = wasIgnoringScrollingEvents + + self.updateScroller(transition: .immediate) + + if let updateOpaqueState = updateOpaqueState { + self.opaqueTransactionState = updateOpaqueState + } + + completion() + return + } + } + + if !deleteIndices.isEmpty || !insertIndicesAndItems.isEmpty || !updateIndicesAndItems.isEmpty { + self.scrolledToItem = nil + } + + let startTime = CACurrentMediaTime() + var state = self.currentState() + + let widthUpdated: Bool + if let updateSizeAndInsets = updateSizeAndInsets { + widthUpdated = abs(state.visibleSize.width - updateSizeAndInsets.size.width) > CGFloat.ulpOfOne + + state.visibleSize = updateSizeAndInsets.size + state.insets = updateSizeAndInsets.insets + } else { + widthUpdated = false + } + + let sortedDeleteIndices = deleteIndices.sorted(by: {$0.index < $1.index}) + for deleteItem in sortedDeleteIndices.reversed() { + self.items.remove(at: deleteItem.index) + } + + let sortedIndicesAndItems = insertIndicesAndItems.sorted(by: { $0.index < $1.index }) + if self.items.count == 0 && !sortedIndicesAndItems.isEmpty { + if sortedIndicesAndItems[0].index != 0 { + fatalError("deleteAndInsertItems: invalid insert into empty list") + } + } + + var previousNodes: [Int: QueueLocalObject] = [:] + for insertedItem in sortedIndicesAndItems { + if insertedItem.index < 0 || insertedItem.index > self.items.count { + fatalError("insertedItem.index \(insertedItem.index) is out of bounds 0 ... \(self.items.count)") + } + self.items.insert(insertedItem.item, at: insertedItem.index) + if let previousIndex = insertedItem.previousIndex { + for itemNode in self.itemNodes { + if itemNode.index == previousIndex { + previousNodes[insertedItem.index] = QueueLocalObject(queue: Queue.mainQueue(), generate: { return itemNode }) + } + } + } + } + + for updatedItem in updateIndicesAndItems { + self.items[updatedItem.index] = updatedItem.item + for itemNode in self.itemNodes { + if itemNode.index == updatedItem.previousIndex { + previousNodes[updatedItem.index] = QueueLocalObject(queue: Queue.mainQueue(), generate: { return itemNode }) + break + } + } + } + + if let scrollToItem = scrollToItem { + state.scrollPosition = (scrollToItem.index, scrollToItem.position) + } + let itemsCount = self.items.count + state.fixScrollPosition(itemsCount) + + let actions = { + var previousFrames: [Int: CGRect] = [:] + for i in 0 ..< state.nodes.count { + if let index = state.nodes[i].index { + previousFrames[index] = state.nodes[i].frame + } + } + + var operations: [ListViewStateOperation] = [] + + var deleteDirectionHints: [Int: ListViewItemOperationDirectionHint] = [:] + var insertDirectionHints: [Int: ListViewItemOperationDirectionHint] = [:] + + var deleteIndexSet = Set() + for deleteItem in deleteIndices { + deleteIndexSet.insert(deleteItem.index) + if let directionHint = deleteItem.directionHint { + deleteDirectionHints[deleteItem.index] = directionHint + } + } + + var insertedIndexSet = Set() + for insertedItem in sortedIndicesAndItems { + insertedIndexSet.insert(insertedItem.index) + if let directionHint = insertedItem.directionHint { + insertDirectionHints[insertedItem.index] = directionHint + } + } + + let animated = options.contains(.AnimateInsertion) + + var remapDeletion: [Int: Int] = [:] + var updateAdjacentItemsIndices = Set() + + var i = 0 + while i < state.nodes.count { + if let index = state.nodes[i].index { + var indexOffset = 0 + for deleteIndex in sortedDeleteIndices { + if deleteIndex.index < index { + indexOffset += 1 + } else { + break + } + } + + if deleteIndexSet.contains(index) { + previousFrames.removeValue(forKey: index) + state.removeNodeAtIndex(i, direction: deleteDirectionHints[index], animated: animated, operations: &operations) + } else { + let updatedIndex = index - indexOffset + if index != updatedIndex { + remapDeletion[index] = updatedIndex + } + if let previousFrame = previousFrames[index] { + previousFrames.removeValue(forKey: index) + previousFrames[updatedIndex] = previousFrame + } + if deleteIndexSet.contains(index - 1) || deleteIndexSet.contains(index + 1) { + updateAdjacentItemsIndices.insert(updatedIndex) + } + + switch state.nodes[i] { + case let .Node(_, frame, referenceNode): + state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode) + case .Placeholder: + break + } + i += 1 + } + } else { + i += 1 + } + } + + if !remapDeletion.isEmpty { + if self.debugInfo { + //print("remapDeletion \(remapDeletion)") + } + operations.append(.Remap(remapDeletion)) + } + + var remapInsertion: [Int: Int] = [:] + + for i in 0 ..< state.nodes.count { + if let index = state.nodes[i].index { + var indexOffset = 0 + for insertedItem in sortedIndicesAndItems { + if insertedItem.index <= index + indexOffset { + indexOffset += 1 + } + } + if indexOffset != 0 { + let updatedIndex = index + indexOffset + remapInsertion[index] = updatedIndex + + if let previousFrame = previousFrames[index] { + previousFrames.removeValue(forKey: index) + previousFrames[updatedIndex] = previousFrame + } + + switch state.nodes[i] { + case let .Node(_, frame, referenceNode): + state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode) + case .Placeholder: + break + } + } + } + } + + if !remapInsertion.isEmpty { + if self.debugInfo { + print("remapInsertion \(remapInsertion)") + } + operations.append(.Remap(remapInsertion)) + + var remappedUpdateAdjacentItemsIndices = Set() + for index in updateAdjacentItemsIndices { + if let remappedIndex = remapInsertion[index] { + remappedUpdateAdjacentItemsIndices.insert(remappedIndex) + } else { + remappedUpdateAdjacentItemsIndices.insert(index) + } + } + updateAdjacentItemsIndices = remappedUpdateAdjacentItemsIndices + } + + if self.debugInfo { + //print("state \(state.nodes.map({$0.index ?? -1}))") + } + + for node in state.nodes { + if let index = node.index { + if insertedIndexSet.contains(index - 1) || insertedIndexSet.contains(index + 1) { + updateAdjacentItemsIndices.insert(index) + } + } + } + + if let (index, boundary) = stationaryItemRange { + state.setupStationaryOffset(index, boundary: boundary, frames: previousFrames) + } + + if let _ = scrollToItem { + state.fixScrollPosition(itemsCount) + } + + if self.debugInfo { + print("deleteAndInsertItemsTransaction prepare \((CACurrentMediaTime() - startTime) * 1000.0) ms") + } + + self.fillMissingNodes(synchronous: options.contains(.Synchronous), synchronousLoads: options.contains(.PreferSynchronousResourceLoading), animated: animated, inputAnimatedInsertIndices: animated ? insertedIndexSet : Set(), insertDirectionHints: insertDirectionHints, inputState: state, inputPreviousNodes: previousNodes, inputOperations: operations, inputCompletion: { updatedState, operations in + + if self.debugInfo { + print("fillMissingNodes completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") + } + + var updateIndices = updateAdjacentItemsIndices + if widthUpdated { + for case let .Node(index, _, _) in updatedState.nodes { + updateIndices.insert(index) + } + } + + /*if !insertedIndexSet.intersection(updateIndices).isEmpty { + print("int") + }*/ + let explicitelyUpdateIndices = Set(updateIndicesAndItems.map({$0.index})) + /*if !explicitelyUpdateIndices.intersection(updateIndices).isEmpty { + print("int") + }*/ + + updateIndices.subtract(explicitelyUpdateIndices) + + self.updateNodes(synchronous: options.contains(.Synchronous), synchronousLoads: options.contains(.PreferSynchronousResourceLoading), animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: updatedState, previousNodes: previousNodes, inputOperations: operations, completion: { updatedState, operations in + self.updateAdjacent(synchronous: options.contains(.Synchronous), animated: animated, state: updatedState, updateAdjacentItemsIndices: updateIndices, operations: operations, completion: { state, operations in + var updatedState = state + var updatedOperations = operations + updatedState.removeInvisibleNodes(&updatedOperations) + + if self.debugInfo { + print("updateAdjacent completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") + } + + let stationaryItemIndex = updatedState.stationaryOffset?.0 + + let next = { + var updatedOperations = updatedOperations + + var readySignals: [Signal]? + + if options.contains(.PreferSynchronousResourceLoading) { + var currentReadySignals: [Signal] = [] + for i in 0 ..< updatedOperations.count { + if case let .InsertNode(index, offsetDirection, nodeAnimated, node, layout, apply) = updatedOperations[i] { + let (ready, commitApply) = apply() + updatedOperations[i] = .InsertNode(index: index, offsetDirection: offsetDirection, animated: nodeAnimated, node: node, layout: layout, apply: { + return (nil, commitApply) + }) + if let ready = ready { + currentReadySignals.append(ready) + } + } + } + readySignals = currentReadySignals + } + + let beginReplay = { [weak self] in + if let strongSelf = self { + strongSelf.replayOperations(animated: animated, animateAlpha: options.contains(.AnimateAlpha), animateCrossfade: options.contains(.AnimateCrossfade), synchronous: options.contains(.Synchronous), animateTopItemVerticalOrigin: options.contains(.AnimateTopItemPosition), operations: updatedOperations, requestItemInsertionAnimationsIndices: options.contains(.RequestItemInsertionAnimations) ? insertedIndexSet : Set(), scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemIndex: stationaryItemIndex, updateOpaqueState: updateOpaqueState, completion: { + if options.contains(.PreferSynchronousDrawing) { + let startTime = CACurrentMediaTime() + self?.recursivelyEnsureDisplaySynchronously(true) + let deltaTime = CACurrentMediaTime() - startTime + if false { + print("ListView: waited \(deltaTime * 1000.0) ms for nodes to display") + } + } + completion() + }) + } + } + + if let readySignals = readySignals, !readySignals.isEmpty && false { + let readyWithTimeout = combineLatest(readySignals) + |> deliverOnMainQueue + |> timeout(0.2, queue: Queue.mainQueue(), alternate: .single([])) + let startTime = CACurrentMediaTime() + self.waitingForNodesDisposable.set(readyWithTimeout.start(completed: { + let deltaTime = CACurrentMediaTime() - startTime + if false { + print("ListView: waited \(deltaTime * 1000.0) ms for nodes to load") + } + beginReplay() + })) + } else { + beginReplay() + } + } + + if options.contains(.LowLatency) || options.contains(.Synchronous) { + Queue.mainQueue().async { + if self.debugInfo { + print("updateAdjacent LowLatency enqueue \((CACurrentMediaTime() - startTime) * 1000.0) ms") + } + next() + } + } else { + self.dispatchOnVSync { + next() + } + } + }) + }) + }) + } + + if options.contains(.Synchronous) { + actions() + } else { + self.async(actions) + } + } + + private func updateAdjacent(synchronous: Bool, animated: Bool, state: ListViewState, updateAdjacentItemsIndices: Set, operations: [ListViewStateOperation], completion: @escaping (ListViewState, [ListViewStateOperation]) -> Void) { + if updateAdjacentItemsIndices.isEmpty { + completion(state, operations) + } else { + let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None + + var updatedUpdateAdjacentItemsIndices = updateAdjacentItemsIndices + + let nodeIndex = updateAdjacentItemsIndices.first! + updatedUpdateAdjacentItemsIndices.remove(nodeIndex) + + var continueWithoutNode = true + + var i = 0 + for node in state.nodes { + if case let .Node(index, _, referenceNode) = node , index == nodeIndex { + if let referenceNode = referenceNode { + continueWithoutNode = false + self.items[index].updateNode(async: { f in + if synchronous { + f() + } else { + self.async(f) + } + }, node: { + assert(Queue.mainQueue().isCurrent()) + return referenceNode.syncWith({ $0 })! + }, params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right), previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1], animation: updateAnimation, completion: { layout, apply in + var updatedState = state + var updatedOperations = operations + + let heightDelta = layout.size.height - updatedState.nodes[i].frame.size.height + + updatedOperations.append(.UpdateLayout(index: i, layout: layout, apply: { + return (nil, apply) + })) + + if !animated { + let previousFrame = updatedState.nodes[i].frame + updatedState.nodes[i].frame = CGRect(origin: previousFrame.origin, size: layout.size) + if previousFrame.minY < updatedState.insets.top { + for j in 0 ... i { + updatedState.nodes[j].frame = updatedState.nodes[j].frame.offsetBy(dx: 0.0, dy: -heightDelta) + } + } else { + if i != updatedState.nodes.count { + for j in i + 1 ..< updatedState.nodes.count { + updatedState.nodes[j].frame = updatedState.nodes[j].frame.offsetBy(dx: 0.0, dy: heightDelta) + } + } + } + } + + self.updateAdjacent(synchronous: synchronous, animated: animated, state: updatedState, updateAdjacentItemsIndices: updatedUpdateAdjacentItemsIndices, operations: updatedOperations, completion: completion) + }) + } + break + } + i += 1 + } + + if continueWithoutNode { + updateAdjacent(synchronous: synchronous, animated: animated, state: state, updateAdjacentItemsIndices: updatedUpdateAdjacentItemsIndices, operations: operations, completion: completion) + } + } + } + + private func fillMissingNodes(synchronous: Bool, synchronousLoads: Bool, animated: Bool, inputAnimatedInsertIndices: Set, insertDirectionHints: [Int: ListViewItemOperationDirectionHint], inputState: ListViewState, inputPreviousNodes: [Int: QueueLocalObject], inputOperations: [ListViewStateOperation], inputCompletion: @escaping (ListViewState, [ListViewStateOperation]) -> Void) { + let animatedInsertIndices = inputAnimatedInsertIndices + var state = inputState + var previousNodes = inputPreviousNodes + var operations = inputOperations + let completion = inputCompletion + let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None + + if state.nodes.count > 1000 { + print("state.nodes.count > 1000") + } + + while true { + if self.items.count == 0 { + completion(state, operations) + break + } else { + var insertionItemIndexAndDirection: (Int, ListViewInsertionOffsetDirection)? + + if self.debugInfo { + assert(true) + } + + if let insertionPoint = state.insertionPoint(insertDirectionHints, itemCount: self.items.count) { + insertionItemIndexAndDirection = (insertionPoint.index, insertionPoint.direction) + } + + if self.debugInfo { + print("insertionItemIndexAndDirection \(String(describing: insertionItemIndexAndDirection))") + } + + if let insertionItemIndexAndDirection = insertionItemIndexAndDirection { + let index = insertionItemIndexAndDirection.0 + let threadId = pthread_self() + var tailRecurse = false + self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: self.items[index], previousNode: previousNodes[index], index: index, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: self.items.count == index + 1 ? nil : self.items[index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right), updateAnimation: updateAnimation, completion: { (node, layout, apply) in + + if pthread_equal(pthread_self(), threadId) != 0 && !tailRecurse { + tailRecurse = true + state.insertNode(index, node: node, layout: layout, apply: apply, offsetDirection: insertionItemIndexAndDirection.1, animated: animated && animatedInsertIndices.contains(index), operations: &operations, itemCount: self.items.count) + } else { + var updatedState = state + var updatedOperations = operations + updatedState.insertNode(index, node: node, layout: layout, apply: apply, offsetDirection: insertionItemIndexAndDirection.1, animated: animated && animatedInsertIndices.contains(index), operations: &updatedOperations, itemCount: self.items.count) + self.fillMissingNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, animated: animated, inputAnimatedInsertIndices: animatedInsertIndices, insertDirectionHints: insertDirectionHints, inputState: updatedState, inputPreviousNodes: previousNodes, inputOperations: updatedOperations, inputCompletion: completion) + } + }) + if !tailRecurse { + tailRecurse = true + break + } + } else { + completion(state, operations) + break + } + } + } + } + + private func updateNodes(synchronous: Bool, synchronousLoads: Bool, animated: Bool, updateIndicesAndItems: [ListViewUpdateItem], inputState: ListViewState, previousNodes: [Int: QueueLocalObject], inputOperations: [ListViewStateOperation], completion: @escaping (ListViewState, [ListViewStateOperation]) -> Void) { + var state = inputState + var operations = inputOperations + var updateIndicesAndItems = updateIndicesAndItems + + while true { + if updateIndicesAndItems.isEmpty { + completion(state, operations) + break + } else { + let updateItem = updateIndicesAndItems[0] + if let previousNode = previousNodes[updateItem.index] { + self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right), updateAnimation: animated ? .System(duration: insertionAnimationDuration) : .None, completion: { _, layout, apply in + state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, animation: animated ? .System(duration: insertionAnimationDuration) : .None, apply: apply, operations: &operations) + + updateIndicesAndItems.remove(at: 0) + self.updateNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + }) + break + } else { + updateIndicesAndItems.remove(at: 0) + //self.updateNodes(synchronous: synchronous, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + } + } + } + } + + private func referencePointForInsertionAtIndex(_ nodeIndex: Int) -> CGPoint { + var index = 0 + for itemNode in self.itemNodes { + if index == nodeIndex { + return itemNode.apparentFrame.origin + } + index += 1 + } + if self.itemNodes.count == 0 { + return CGPoint(x: 0.0, y: self.insets.top) + } else { + return CGPoint(x: 0.0, y: self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY) + } + } + + private func insertNodeAtIndex(animated: Bool, animateAlpha: Bool, forceAnimateInsertion: Bool, previousFrame: CGRect?, nodeIndex: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> (Signal?, (ListViewItemApply) -> Void), timestamp: Double, listInsets: UIEdgeInsets, visibleBounds: CGRect) { + let insertionOrigin = self.referencePointForInsertionAtIndex(nodeIndex) + + let nodeOrigin: CGPoint + switch offsetDirection { + case .Up: + nodeOrigin = CGPoint(x: insertionOrigin.x, y: insertionOrigin.y - (animated ? 0.0 : layout.size.height)) + case .Down: + nodeOrigin = insertionOrigin + } + + let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + let previousApparentHeight = node.apparentHeight + let previousInsets = node.insets + + node.contentSize = layout.contentSize + node.insets = layout.insets + node.apparentHeight = animated ? 0.0 : layout.size.height + node.frame = nodeFrame + if let accessoryItemNode = node.accessoryItemNode { + node.layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + apply().1(ListViewItemApply(isOnScreen: visibleBounds.intersects(nodeFrame))) + self.itemNodes.insert(node, at: nodeIndex) + + var offsetHeight = node.apparentHeight + var takenAnimation = false + + if let _ = previousFrame, animated && node.index != nil && nodeIndex != self.itemNodes.count - 1 { + let nextNode = self.itemNodes[nodeIndex + 1] + if nextNode.index == nil && nextNode.subnodes == nil || nextNode.subnodes!.isEmpty { + let nextHeight = nextNode.apparentHeight + if abs(nextHeight - previousApparentHeight) < CGFloat.ulpOfOne { + if let animation = nextNode.animationForKey("apparentHeight") { + node.apparentHeight = previousApparentHeight + + offsetHeight = 0.0 + + var offsetPosition = nextNode.position + offsetPosition.y += nextHeight + nextNode.position = offsetPosition + nextNode.apparentHeight = 0.0 + + nextNode.removeApparentHeightAnimation() + + takenAnimation = true + + if abs(layout.size.height - previousApparentHeight) > CGFloat.ulpOfOne { + node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + if let node = node { + node.animateFrameTransition(progress, currentValue) + } + }) + if node.rotated { + node.transitionOffset += previousApparentHeight - layout.size.height + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + } + } + } + } + } + + if node.index == nil { + if node.animationForKey("height") == nil || !(node is ListViewTempItemNode) { + node.addHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + if node.animationForKey("apparentHeight") == nil || !(node is ListViewTempItemNode) { + node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + node.animateRemoved(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor()) + } else if animated { + if takenAnimation { + if let previousFrame = previousFrame { + if self.debugInfo { + assert(true) + } + + let transitionOffsetDelta = nodeFrame.origin.y - previousFrame.origin.y + if node.rotated { + node.transitionOffset -= transitionOffsetDelta - previousApparentHeight + layout.size.height + } else { + node.transitionOffset += transitionOffsetDelta + } + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + if previousInsets != layout.insets { + node.insets = previousInsets + node.addInsetsAnimationToValue(layout.insets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + } + } else { + if !nodeFrame.size.height.isEqual(to: node.apparentHeight) { + node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + if let node = node { + node.animateFrameTransition(progress, currentValue) + } + }) + } + + if let previousFrame = previousFrame { + if self.debugInfo { + assert(true) + } + + let transitionOffsetDelta = nodeFrame.origin.y - previousFrame.origin.y + if node.rotated { + node.transitionOffset -= transitionOffsetDelta - previousApparentHeight + layout.size.height + } else { + node.transitionOffset += transitionOffsetDelta + } + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + if previousInsets != layout.insets { + node.insets = previousInsets + node.addInsetsAnimationToValue(layout.insets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + } else { + if self.debugInfo { + assert(true) + } + if !node.rotated { + if !node.insets.top.isZero { + node.transitionOffset += node.insets.top + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + } + node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), short: false) + } + } + } else if animateAlpha && previousFrame == nil { + if forceAnimateInsertion { + node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), short: true) + } else { + node.animateAdded(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor()) + } + } + + if node.apparentHeight > CGFloat.ulpOfOne { + switch offsetDirection { + case .Up: + var i = nodeIndex - 1 + while i >= 0 { + var frame = self.itemNodes[i].frame + frame.origin.y -= offsetHeight + self.itemNodes[i].frame = frame + if let accessoryItemNode = self.itemNodes[i].accessoryItemNode { + self.itemNodes[i].layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + i -= 1 + } + case .Down: + var i = nodeIndex + 1 + while i < self.itemNodes.count { + var frame = self.itemNodes[i].frame + frame.origin.y += offsetHeight + self.itemNodes[i].frame = frame + if let accessoryItemNode = self.itemNodes[i].accessoryItemNode { + self.itemNodes[i].layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + i += 1 + } + } + } + } + + private func lowestNodeToInsertBelow() -> ASDisplayNode? { + if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self { + //return itemNode + } + var lowestHeaderNode: ASDisplayNode? + lowestHeaderNode = self.verticalScrollIndicator + var lowestHeaderNodeIndex: Int? + for (_, headerNode) in self.itemHeaderNodes { + if let index = self.view.subviews.index(of: headerNode.view) { + if lowestHeaderNodeIndex == nil || index < lowestHeaderNodeIndex! { + lowestHeaderNodeIndex = index + lowestHeaderNode = headerNode + } + } + } + return lowestHeaderNode + } + + private func topItemVerticalOrigin() -> CGFloat? { + var topItemFound = false + + for i in 0 ..< self.itemNodes.count { + if let index = itemNodes[i].index { + if index == 0 { + topItemFound = true + } + break + } + } + + if topItemFound { + return itemNodes[0].apparentFrame.origin.y + } else { + return nil + } + } + + private func bottomItemMaxY() -> CGFloat? { + var bottomItemFound = false + + for i in (0 ..< self.itemNodes.count).reversed() { + if let index = itemNodes[i].index { + if index == self.items.count - 1 { + bottomItemFound = true + break + } + } + } + + if bottomItemFound { + return itemNodes.last!.apparentFrame.maxY + } else { + return nil + } + } + + private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, synchronous: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem originalScrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) { + var scrollToItem: ListViewScrollToItem? + var isExperimentalSnapToScrollToItem = false + if let originalScrollToItem = originalScrollToItem { + scrollToItem = originalScrollToItem + if self.experimentalSnapScrollToItem { + self.scrolledToItem = (originalScrollToItem.index, originalScrollToItem.position) + } + } else if let scrolledToItem = self.scrolledToItem, self.experimentalSnapScrollToItem { + var curve: ListViewAnimationCurve = .Default(duration: nil) + var animated = false + if let updateSizeAndInsets = updateSizeAndInsets { + curve = updateSizeAndInsets.curve + animated = !updateSizeAndInsets.duration.isZero + } + scrollToItem = ListViewScrollToItem(index: scrolledToItem.0, position: scrolledToItem.1, animated: animated, curve: curve, directionHint: .Down) + isExperimentalSnapToScrollToItem = true + } + + weak var highlightedItemNode: ListViewItemNode? + if let highlightedItemIndex = self.highlightedItemIndex { + for itemNode in self.itemNodes { + if itemNode.index == highlightedItemIndex { + highlightedItemNode = itemNode + break + } + } + } + + let timestamp = CACurrentMediaTime() + + let listInsets = updateSizeAndInsets?.insets ?? self.insets + + if let updateOpaqueState = updateOpaqueState { + self.opaqueTransactionState = updateOpaqueState + } + + var previousTopItemVerticalOrigin: CGFloat? + var snapshotView: UIView? + if animateCrossfade { + snapshotView = self.view.snapshotView(afterScreenUpdates: false) + } + if animateTopItemVerticalOrigin { + previousTopItemVerticalOrigin = self.topItemVerticalOrigin() + } + + var previousApparentFrames: [(ListViewItemNode, CGRect)] = [] + for itemNode in self.itemNodes { + previousApparentFrames.append((itemNode, itemNode.apparentFrame)) + } + + var takenPreviousNodes = Set() + for operation in operations { + if case let .InsertNode(_, _, _, node, _, _) = operation { + takenPreviousNodes.insert(node.syncWith({ $0 })!) + } + } + + let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow() + var hadInserts = false + + let visibleBounds = CGRect(origin: CGPoint(), size: self.visibleSize) + + for operation in operations { + switch operation { + case let .InsertNode(index, offsetDirection, nodeAnimated, nodeObject, layout, apply): + let node = nodeObject.syncWith({ $0 })! + var previousFrame: CGRect? + for (previousNode, frame) in previousApparentFrames { + if previousNode === node { + previousFrame = frame + break + } + } + var forceAnimateInsertion = false + if let index = node.index, requestItemInsertionAnimationsIndices.contains(index) { + forceAnimateInsertion = true + } + var updatedPreviousFrame = previousFrame + if let previousFrame = previousFrame, previousFrame.minY >= self.visibleSize.height || previousFrame.maxY < 0.0 { + updatedPreviousFrame = nil + } + + self.insertNodeAtIndex(animated: nodeAnimated, animateAlpha: animateAlpha, forceAnimateInsertion: forceAnimateInsertion, previousFrame: updatedPreviousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds) + hadInserts = true + if let _ = updatedPreviousFrame { + if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self { + self.insertSubnode(node, belowSubnode: itemNode) + } else if let lowestNodeToInsertBelow = lowestNodeToInsertBelow { + self.insertSubnode(node, belowSubnode: lowestNodeToInsertBelow) + } else if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(node, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(node) + } + } else { + if animated { + if let topItemOverscrollBackground = self.topItemOverscrollBackground { + self.insertSubnode(node, aboveSubnode: topItemOverscrollBackground) + } else { + self.insertSubnode(node, at: 0) + } + } else { + if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self { + self.insertSubnode(node, belowSubnode: itemNode) + } else if let lowestNodeToInsertBelow = lowestNodeToInsertBelow { + self.insertSubnode(node, belowSubnode: lowestNodeToInsertBelow) + } else if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(node, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(node) + } + } + } + case let .InsertDisappearingPlaceholder(index, referenceNodeObject, offsetDirection): + var height: CGFloat? + var previousLayout: ListViewItemNodeLayout? + + let referenceNode = referenceNodeObject.syncWith({ $0 })! + + for (node, previousFrame) in previousApparentFrames { + if node === referenceNode { + height = previousFrame.size.height + previousLayout = ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets) + break + } + } + + if let height = height, let previousLayout = previousLayout { + if takenPreviousNodes.contains(referenceNode) { + let tempNode = ListViewTempItemNode(layerBacked: true) + self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: tempNode, layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { return (nil, { _ in }) }, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds) + } else { + referenceNode.index = nil + self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: referenceNode, layout: previousLayout, apply: { return (nil, { _ in }) }, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds) + if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(referenceNode, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(referenceNode) + } + } + } else { + assertionFailure() + } + case let .Remap(mapping): + for node in self.itemNodes { + if let index = node.index { + if let mapped = mapping[index] { + node.index = mapped + } + } + } + case let .Remove(index, offsetDirection): + let apparentFrame = self.itemNodes[index].apparentFrame + let height = apparentFrame.size.height + switch offsetDirection { + case .Up: + if index != self.itemNodes.count - 1 { + for i in index + 1 ..< self.itemNodes.count { + var frame = self.itemNodes[i].frame + frame.origin.y -= height + self.itemNodes[i].frame = frame + if let accessoryItemNode = self.itemNodes[i].accessoryItemNode { + self.itemNodes[i].layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + } + } + case .Down: + if index != 0 { + for i in (0 ..< index).reversed() { + var frame = self.itemNodes[i].frame + frame.origin.y += height + self.itemNodes[i].frame = frame + if let accessoryItemNode = self.itemNodes[i].accessoryItemNode { + self.itemNodes[i].layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + } + } + } + + self.removeItemNodeAtIndex(index) + case let .UpdateLayout(index, layout, apply): + let node = self.itemNodes[index] + + let previousApparentHeight = node.apparentHeight + let previousInsets = node.insets + + node.contentSize = layout.contentSize + node.insets = layout.insets + + let updatedApparentHeight = node.bounds.size.height + let updatedInsets = node.insets + + var apparentFrame = node.apparentFrame + apparentFrame.size.height = updatedApparentHeight + + apply().1(ListViewItemApply(isOnScreen: visibleBounds.intersects(apparentFrame))) + + var offsetRanges = OffsetRanges() + + if animated { + if updatedInsets != previousInsets { + node.insets = previousInsets + node.addInsetsAnimationToValue(updatedInsets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + + if !abs(updatedApparentHeight - previousApparentHeight).isZero { + let currentAnimation = node.animationForKey("apparentHeight") + if let currentAnimation = currentAnimation, let toFloat = currentAnimation.to as? CGFloat, toFloat.isEqual(to: updatedApparentHeight) { + } else { + node.apparentHeight = previousApparentHeight + node.animateFrameTransition(0.0, previousApparentHeight) + node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + if let node = node { + node.animateFrameTransition(progress, currentValue) + } + }) + + if node.rotated && currentAnimation == nil { + let insetPart: CGFloat = previousInsets.bottom - layout.insets.bottom + node.transitionOffset += previousApparentHeight - layout.size.height - insetPart + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + } + } else { + if node.shouldAnimateHorizontalFrameTransition() { + node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + if let node = node { + node.animateFrameTransition(progress, currentValue) + } + }) + } + } + } else { + node.apparentHeight = updatedApparentHeight + + let apparentHeightDelta = updatedApparentHeight - previousApparentHeight + if apparentHeightDelta != 0.0 { + var apparentFrame = node.apparentFrame + apparentFrame.origin.y += offsetRanges.offsetForIndex(index) + if apparentFrame.maxY < self.insets.top { + offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) + } else { + offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: apparentHeightDelta) + } + } + } + + if let accessoryItemNode = node.accessoryItemNode { + node.layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + + var index = 0 + for itemNode in self.itemNodes { + let offset = offsetRanges.offsetForIndex(index) + if offset != 0.0 { + var frame = itemNode.frame + frame.origin.y += offset + itemNode.frame = frame + } + + index += 1 + } + } + + if self.debugInfo { + //print("operation \(self.itemNodes.map({"\($0.index) \(unsafeAddressOf($0))"}))") + } + } + + if hadInserts, let reorderNode = self.reorderNode, reorderNode.supernode != nil { + self.view.bringSubview(toFront: reorderNode.view) + if let verticalScrollIndicator = self.verticalScrollIndicator { + verticalScrollIndicator.view.superview?.bringSubview(toFront: verticalScrollIndicator.view) + } + } + + if self.debugInfo { + //print("replay after \(self.itemNodes.map({"\($0.index) \(unsafeAddressOf($0))"}))") + } + + if let scrollToItem = scrollToItem { + self.stopScrolling() + + for itemNode in self.itemNodes { + if let index = itemNode.index, index == scrollToItem.index { + let insets = self.insets// updateSizeAndInsets?.insets ?? self.insets + + let offset: CGFloat + switch scrollToItem.position { + case let .bottom(additionalOffset): + offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom + additionalOffset + case let .top(additionalOffset): + offset = insets.top - itemNode.apparentFrame.minY - itemNode.scrollPositioningInsets.top + additionalOffset + case let .center(overflow): + let contentAreaHeight = self.visibleSize.height - insets.bottom - insets.top + if itemNode.apparentFrame.size.height <= contentAreaHeight + CGFloat.ulpOfOne { + offset = insets.top + floor(((self.visibleSize.height - insets.bottom - insets.top) - itemNode.frame.size.height) / 2.0) - itemNode.apparentFrame.minY + } else { + switch overflow { + case .top: + offset = insets.top - itemNode.apparentFrame.minY + case .bottom: + offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + } + } + case .visible: + if itemNode.apparentFrame.size.height > self.visibleSize.height - insets.top - insets.bottom { + if itemNode.apparentFrame.maxY > self.visibleSize.height - insets.bottom { + offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom + } else { + offset = 0.0 + } + } else { + if itemNode.apparentFrame.maxY > self.visibleSize.height - insets.bottom { + offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom + } else if itemNode.apparentFrame.minY < insets.top { + offset = insets.top - itemNode.apparentFrame.minY - itemNode.scrollPositioningInsets.top + } else { + offset = 0.0 + } + } + } + + for itemNode in self.itemNodes { + var frame = itemNode.frame + frame.origin.y += offset + itemNode.frame = frame + if let accessoryItemNode = itemNode.accessoryItemNode { + itemNode.layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + } + + break + } + } + } else if let stationaryItemIndex = stationaryItemIndex { + for itemNode in self.itemNodes { + if let index = itemNode.index , index == stationaryItemIndex { + for (previousNode, previousFrame) in previousApparentFrames { + if previousNode === itemNode { + let offset = previousFrame.minY - itemNode.frame.minY + + if abs(offset) > CGFloat.ulpOfOne { + for itemNode in self.itemNodes { + var frame = itemNode.frame + frame.origin.y += offset + itemNode.frame = frame + if let accessoryItemNode = itemNode.accessoryItemNode { + itemNode.layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) + } + } + } + + break + } + } + break + } + } + } else if !additionalScrollDistance.isZero { + self.stopScrolling() + } + + self.debugCheckMonotonity() + + var sizeAndInsetsOffset: CGFloat = 0.0 + + var headerNodesTransition: (ContainedViewLayoutTransition, Bool, CGFloat) = (.immediate, false, 0.0) + + var deferredUpdateVisible = false + var insetTransitionOffset: CGFloat = 0.0 + + if let updateSizeAndInsets = updateSizeAndInsets { + if self.insets != updateSizeAndInsets.insets || self.headerInsets != updateSizeAndInsets.headerInsets || !self.visibleSize.height.isEqual(to: updateSizeAndInsets.size.height) { + let previousVisibleSize = self.visibleSize + self.visibleSize = updateSizeAndInsets.size + + var offsetFix: CGFloat + if self.isTracking || isExperimentalSnapToScrollToItem { + offsetFix = 0.0 + } else if self.snapToBottomInsetUntilFirstInteraction { + offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom + } else { + offsetFix = updateSizeAndInsets.insets.top - self.insets.top + } + + offsetFix += additionalScrollDistance + + self.insets = updateSizeAndInsets.insets + self.headerInsets = updateSizeAndInsets.headerInsets ?? self.insets + self.scrollIndicatorInsets = updateSizeAndInsets.scrollIndicatorInsets ?? self.insets + self.ensureTopInsetForOverlayHighlightedItems = updateSizeAndInsets.ensureTopInsetForOverlayHighlightedItems + self.visibleSize = updateSizeAndInsets.size + + for itemNode in self.itemNodes { + let position = itemNode.position + itemNode.position = CGPoint(x: position.x, y: position.y + offsetFix) + } + + let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets) + + if !snappedTopInset.isZero && (previousVisibleSize.height.isZero || previousApparentFrames.isEmpty) { + offsetFix += snappedTopInset + + for itemNode in self.itemNodes { + let position = itemNode.position + itemNode.position = CGPoint(x: position.x, y: position.y + snappedTopInset) + } + } + + var completeOffset = offsetFix + + if !snapToBoundsOffset.isZero { + self.updateVisibleContentOffset() + } + + sizeAndInsetsOffset = offsetFix + completeOffset += snapToBoundsOffset + + if !updateSizeAndInsets.duration.isZero { + let animation: CABasicAnimation + switch updateSizeAndInsets.curve { + case let .Spring(duration): + headerNodesTransition = (.animated(duration: duration, curve: .spring), false, -completeOffset) + let springAnimation = makeSpringAnimation("sublayerTransform") + springAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0)) + springAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + springAnimation.isRemovedOnCompletion = true + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + if !duration.isZero { + springAnimation.speed = speed * Float(springAnimation.duration / duration) + } + + springAnimation.isAdditive = true + animation = springAnimation + case let .Default(duration): + headerNodesTransition = (.animated(duration: max(duration ?? 0.3, updateSizeAndInsets.duration), curve: .easeInOut), false, -completeOffset) + let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + basicAnimation.duration = updateSizeAndInsets.duration * UIView.animationDurationFactor() + basicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0)) + basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + basicAnimation.isRemovedOnCompletion = true + basicAnimation.isAdditive = true + animation = basicAnimation + } + + deferredUpdateVisible = true + animation.completion = { [weak self] _ in + self?.updateItemNodesVisibilities(onlyPositive: false) + } + self.layer.add(animation, forKey: nil) + } + } else { + self.visibleSize = updateSizeAndInsets.size + + if !self.snapToBounds(snapTopItem: scrollToItem != nil, stackFromBottom: self.stackFromBottom).offset.isZero { + self.updateVisibleContentOffset() + } + } + + if let updatedTopItemVerticalOrigin = self.topItemVerticalOrigin(), let previousTopItemVerticalOrigin = previousTopItemVerticalOrigin, animateTopItemVerticalOrigin, !updatedTopItemVerticalOrigin.isEqual(to: previousTopItemVerticalOrigin) { + self.stopScrolling() + + let completeOffset = updatedTopItemVerticalOrigin - previousTopItemVerticalOrigin + let duration: Double = 0.4 + + if let snapshotView = snapshotView { + snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: completeOffset), size: snapshotView.frame.size) + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + let springAnimation = makeSpringAnimation("sublayerTransform") + springAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0)) + springAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + springAnimation.isRemovedOnCompletion = true + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + springAnimation.speed = speed * Float(springAnimation.duration / duration) + + springAnimation.isAdditive = true + self.layer.add(springAnimation, forKey: nil) + } else { + if let snapshotView = snapshotView { + snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: snapshotView.frame.size) + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + let wasIgnoringScrollingEvents = self.ignoreScrollingEvents + self.ignoreScrollingEvents = true + self.scroller.frame = CGRect(origin: CGPoint(), size: self.visibleSize) + self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) + self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) + self.scroller.contentOffset = self.lastContentOffset + self.ignoreScrollingEvents = wasIgnoringScrollingEvents + } else { + let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, scrollToItem: scrollToItem) + + if !snappedTopInset.isZero && previousApparentFrames.isEmpty { + let offsetFix = snappedTopInset + + for itemNode in self.itemNodes { + let position = itemNode.position + itemNode.position = CGPoint(x: position.x, y: position.y + snappedTopInset) + } + } + + if !snapToBoundsOffset.isZero { + self.updateVisibleContentOffset() + } + + if let snapshotView = snapshotView { + snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: snapshotView.frame.size) + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + var accessoryNodesTransition: ContainedViewLayoutTransition = .immediate + if let scrollToItem = scrollToItem, scrollToItem.animated { + accessoryNodesTransition = .animated(duration: 0.3, curve: .easeInOut) + } + + self.updateAccessoryNodes(transition: accessoryNodesTransition, synchronous: synchronous, currentTimestamp: timestamp, leftInset: listInsets.left, rightInset: listInsets.right) + + if let highlightedItemNode = highlightedItemNode { + if highlightedItemNode.index != self.highlightedItemIndex { + highlightedItemNode.setHighlighted(false, at: CGPoint(), animated: false) + self.highlightedItemIndex = nil + } + } else if self.highlightedItemIndex != nil { + self.highlightedItemIndex = nil + } + + if let scrollToItem = scrollToItem, scrollToItem.animated { + if self.itemNodes.count != 0 { + var offset: CGFloat? + + var temporaryPreviousNodes: [ListViewItemNode] = [] + var previousUpperBound: CGFloat? + var previousLowerBound: CGFloat? + if case .visible = scrollToItem.position { + for (previousNode, previousFrame) in previousApparentFrames { + if previousNode.supernode == nil { + temporaryPreviousNodes.append(previousNode) + previousNode.frame = previousFrame + if previousUpperBound == nil || previousUpperBound! > previousFrame.minY { + previousUpperBound = previousFrame.minY + } + if previousLowerBound == nil || previousLowerBound! < previousFrame.maxY { + previousLowerBound = previousFrame.maxY + } + } else { + if previousNode.canBeUsedAsScrollToItemAnchor { + offset = previousNode.apparentFrame.minY - previousFrame.minY + break + } + } + } + } else { + for (previousNode, previousFrame) in previousApparentFrames { + if previousNode.supernode == nil { + temporaryPreviousNodes.append(previousNode) + previousNode.frame = previousFrame + if previousUpperBound == nil || previousUpperBound! > previousFrame.minY { + previousUpperBound = previousFrame.minY + } + if previousLowerBound == nil || previousLowerBound! < previousFrame.maxY { + previousLowerBound = previousFrame.maxY + } + } else { + if previousNode.canBeUsedAsScrollToItemAnchor { + offset = previousNode.apparentFrame.minY - previousFrame.minY + } + } + } + + if offset == nil { + let updatedUpperBound = self.itemNodes[0].apparentFrame.minY + let updatedLowerBound = max(self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY, self.visibleSize.height) + + switch scrollToItem.directionHint { + case .Up: + offset = updatedLowerBound - (previousUpperBound ?? 0.0) + case .Down: + offset = updatedUpperBound - (previousLowerBound ?? self.visibleSize.height) + } + } + } + + if let offsetValue = offset { + offset = offsetValue - sizeAndInsetsOffset + } + + var previousItemHeaderNodes: [ListViewItemHeaderNode] = [] + let offsetOrZero: CGFloat = offset ?? 0.0 + switch scrollToItem.curve { + case let .Spring(duration): + headerNodesTransition = (.animated(duration: duration, curve: .spring), headerNodesTransition.1, headerNodesTransition.2 - offsetOrZero) + case let .Default(duration): + headerNodesTransition = (.animated(duration: duration ?? 0.3, curve: .easeInOut), true, headerNodesTransition.2 - offsetOrZero) + } + for (_, headerNode) in self.itemHeaderNodes { + previousItemHeaderNodes.append(headerNode) + } + + self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) + + if let offset = offset, !offset.isZero { + let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow() + for itemNode in temporaryPreviousNodes { + itemNode.frame = itemNode.frame.offsetBy(dx: 0.0, dy: offset) + if let lowestNodeToInsertBelow = lowestNodeToInsertBelow { + self.insertSubnode(itemNode, belowSubnode: lowestNodeToInsertBelow) + } else if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(itemNode, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(itemNode) + } + } + + var temporaryHeaderNodes: [ListViewItemHeaderNode] = [] + for headerNode in previousItemHeaderNodes { + if headerNode.supernode == nil { + headerNode.frame = headerNode.frame.offsetBy(dx: 0.0, dy: offset) + temporaryHeaderNodes.append(headerNode) + if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(headerNode, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(headerNode) + } + } + } + + let animation: CABasicAnimation + let reverseAnimation: CABasicAnimation + switch scrollToItem.curve { + case let .Spring(duration): + let springAnimation = makeSpringAnimation("sublayerTransform") + springAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -offset, 0.0)) + springAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + springAnimation.isRemovedOnCompletion = true + springAnimation.isAdditive = true + springAnimation.fillMode = kCAFillModeForwards + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + if !duration.isZero { + springAnimation.speed = speed * Float(springAnimation.duration / duration) + } + + let reverseSpringAnimation = makeSpringAnimation("sublayerTransform") + reverseSpringAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, offset, 0.0)) + reverseSpringAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + reverseSpringAnimation.isRemovedOnCompletion = true + reverseSpringAnimation.isAdditive = true + reverseSpringAnimation.fillMode = kCAFillModeForwards + + reverseSpringAnimation.speed = speed * Float(reverseSpringAnimation.duration / duration) + + animation = springAnimation + reverseAnimation = reverseSpringAnimation + case let .Default(duration): + if let duration = duration { + let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + basicAnimation.duration = duration * UIView.animationDurationFactor() + basicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -offset, 0.0)) + basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + basicAnimation.isRemovedOnCompletion = true + basicAnimation.isAdditive = true + + let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + reverseBasicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + reverseBasicAnimation.duration = duration * UIView.animationDurationFactor() + reverseBasicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, offset, 0.0)) + reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + reverseBasicAnimation.isRemovedOnCompletion = true + reverseBasicAnimation.isAdditive = true + + animation = basicAnimation + reverseAnimation = reverseBasicAnimation + } else { + let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + basicAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.33, 0.52, 0.25, 0.99) + basicAnimation.duration = (duration ?? 0.3) * UIView.animationDurationFactor() + basicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -offset, 0.0)) + basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + basicAnimation.isRemovedOnCompletion = true + basicAnimation.isAdditive = true + + let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + reverseBasicAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.33, 0.52, 0.25, 0.99) + reverseBasicAnimation.duration = (duration ?? 0.3) * UIView.animationDurationFactor() + reverseBasicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, offset, 0.0)) + reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + reverseBasicAnimation.isRemovedOnCompletion = true + reverseBasicAnimation.isAdditive = true + + animation = basicAnimation + reverseAnimation = reverseBasicAnimation + } + } + animation.completion = { _ in + for itemNode in temporaryPreviousNodes { + itemNode.removeFromSupernode() + if useBackgroundDeallocation { + assertionFailure() + //ASDeallocQueue.sharedDeallocation().releaseObject(inBackground: itemNode) + } else { + //ASPerformMainThreadDeallocation(itemNode) + } + } + for headerNode in temporaryHeaderNodes { + headerNode.removeFromSupernode() + if useBackgroundDeallocation { + assertionFailure() + //ASDeallocQueue.sharedDeallocation().releaseObject(inBackground: headerNode) + } else { + //ASPerformMainThreadDeallocation(headerNode) + } + } + } + self.layer.add(animation, forKey: nil) + if let verticalScrollIndicator = self.verticalScrollIndicator { + verticalScrollIndicator.layer.add(reverseAnimation, forKey: nil) + } + } else { + if useBackgroundDeallocation { + assertionFailure() + /*for itemNode in temporaryPreviousNodes { + ASDeallocQueue.sharedDeallocation().releaseObject(inBackground: itemNode) + }*/ + } else { + for itemNode in temporaryPreviousNodes { + //ASPerformMainThreadDeallocation(itemNode) + } + } + } + } + + self.updateItemNodesVisibilities(onlyPositive: deferredUpdateVisible) + + self.updateScroller(transition: headerNodesTransition.0) + + if let topItemOverscrollBackground = self.topItemOverscrollBackground { + headerNodesTransition.0.animatePositionAdditive(node: topItemOverscrollBackground, offset: CGPoint(x: 0.0, y: -headerNodesTransition.2)) + } + + self.setNeedsAnimations() + + self.updateVisibleContentOffset() + + if self.debugInfo { + //let delta = CACurrentMediaTime() - timestamp + //print("replayOperations \(delta * 1000.0) ms") + } + + completion() + } else { + self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) + self.updateItemNodesVisibilities(onlyPositive: deferredUpdateVisible) + + if animated { + self.setNeedsAnimations() + } + + self.updateScroller(transition: headerNodesTransition.0) + + if let topItemOverscrollBackground = self.topItemOverscrollBackground { + headerNodesTransition.0.animatePositionAdditive(node: topItemOverscrollBackground, offset: CGPoint(x: 0.0, y: -headerNodesTransition.2)) + } + + self.updateVisibleContentOffset() + + if self.debugInfo { + //let delta = CACurrentMediaTime() - timestamp + //print("replayOperations \(delta * 1000.0) ms") + } + + for (previousNode, _) in previousApparentFrames { + if previousNode.supernode == nil { + if useBackgroundDeallocation { + assertionFailure() + //ASDeallocQueue.sharedDeallocatio.releaseObject(inBackground: previousNode) + } else { + //ASPerformMainThreadDeallocation(previousNode) + } + } + } + + completion() + } + } + + private func debugCheckMonotonity() { + if self.debugInfo { + var previousMaxY: CGFloat? + for node in self.itemNodes { + if let previousMaxY = previousMaxY , abs(previousMaxY - node.apparentFrame.minY) > CGFloat.ulpOfOne { + print("monotonity violated") + break + } + previousMaxY = node.apparentFrame.maxY + } + } + } + + private func removeItemNodeAtIndex(_ index: Int) { + let node = self.itemNodes[index] + self.itemNodes.remove(at: index) + node.removeFromSupernode() + + node.accessoryItemNode?.removeFromSupernode() + node.setAccessoryItemNode(nil, leftInset: self.insets.left, rightInset: self.insets.right) + node.headerAccessoryItemNode?.removeFromSupernode() + node.headerAccessoryItemNode = nil + } + + private func updateItemHeaders(leftInset: CGFloat, rightInset: CGFloat, transition: (ContainedViewLayoutTransition, Bool, CGFloat) = (.immediate, false, 0.0), animateInsertion: Bool = false) { + let upperDisplayBound = self.headerInsets.top + let lowerDisplayBound = self.visibleSize.height - self.insets.bottom + var visibleHeaderNodes = Set() + + let flashing = self.headerItemsAreFlashing() + + let addHeader: (_ id: Int64, _ upperBound: CGFloat, _ lowerBound: CGFloat, _ item: ListViewItemHeader, _ hasValidNodes: Bool) -> Void = { id, upperBound, lowerBound, item, hasValidNodes in + let itemHeaderHeight: CGFloat = item.height + + let headerFrame: CGRect + let stickLocationDistanceFactor: CGFloat + let stickLocationDistance: CGFloat + switch item.stickDirection { + case .top: + headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBound), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) + stickLocationDistance = 0.0 + stickLocationDistanceFactor = 0.0 + case .bottom: + headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) + stickLocationDistance = lowerBound - headerFrame.maxY + stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) + } + visibleHeaderNodes.insert(id) + if let headerNode = self.itemHeaderNodes[id] { + switch transition.0 { + case .immediate: + headerNode.frame = headerFrame + case let .animated(duration, curve): + let previousFrame = headerNode.frame + headerNode.frame = headerFrame + var offset = headerFrame.minY - previousFrame.minY + transition.2 + if headerNode.isRotated { + offset = -offset + } + switch curve { + case .spring: + transition.0.animateOffsetAdditive(node: headerNode, offset: offset) + case let .custom(p1, p2, p3, p4): + headerNode.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, mediaTimingFunction: CAMediaTimingFunction(controlPoints: p1, p2, p3, p4)) + case .easeInOut: + if transition.1 { + headerNode.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, mediaTimingFunction: CAMediaTimingFunction(controlPoints: 0.33, 0.52, 0.25, 0.99)) + } else { + headerNode.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, mediaTimingFunction: CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)) + } + } + } + + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + headerNode.updateInternalStickLocationDistanceFactor(stickLocationDistanceFactor, animated: true) + headerNode.internalStickLocationDistance = stickLocationDistance + if !hasValidNodes && !headerNode.alpha.isZero { + if animateInsertion { + headerNode.animateRemoved(duration: 0.2) + } + } else if hasValidNodes && headerNode.alpha.isZero { + headerNode.alpha = 1.0 + if animateInsertion { + headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + headerNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + } + } + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: transition.0) + } else { + let headerNode = item.node() + headerNode.updateFlashingOnScrolling(flashing, animated: false) + headerNode.frame = headerFrame + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + headerNode.updateInternalStickLocationDistanceFactor(stickLocationDistanceFactor, animated: false) + self.itemHeaderNodes[id] = headerNode + if let verticalScrollIndicator = self.verticalScrollIndicator { + self.insertSubnode(headerNode, belowSubnode: verticalScrollIndicator) + } else { + self.addSubnode(headerNode) + } + if animateInsertion { + headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + headerNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.3) + } + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } + } + + var previousHeader: (Int64, CGFloat, CGFloat, ListViewItemHeader, Bool)? + for itemNode in self.itemNodes { + let itemFrame = itemNode.apparentFrame + if let itemHeader = itemNode.header() { + if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { + if previousHeaderId == itemHeader.id { + previousHeader = (previousHeaderId, previousUpperBound, itemFrame.maxY, previousHeaderItem, hasValidNodes || itemNode.index != nil) + } else { + addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) + + previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.maxY, itemHeader, itemNode.index != nil) + } + } else { + previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.maxY, itemHeader, itemNode.index != nil) + } + } else { + if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { + addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) + } + previousHeader = nil + } + } + + if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { + addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) + } + + let currentIds = Set(self.itemHeaderNodes.keys) + for id in currentIds.subtracting(visibleHeaderNodes) { + if let headerNode = self.itemHeaderNodes.removeValue(forKey: id) { + headerNode.removeFromSupernode() + } + } + } + + private func updateItemNodesVisibilities(onlyPositive: Bool) { + let visibilityRect = CGRect(origin: CGPoint(x: 0.0, y: self.insets.top), size: CGSize(width: self.visibleSize.width, height: self.visibleSize.height - self.insets.top - self.insets.bottom)) + for itemNode in self.itemNodes { + let itemFrame = itemNode.apparentFrame + var visibility: ListViewItemNodeVisibility = .none + if visibilityRect.intersects(itemFrame) { + let itemContentFrame = itemNode.apparentContentFrame + let intersection = itemContentFrame.intersection(visibilityRect) + let fraction = intersection.height / itemContentFrame.height + visibility = .visible(fraction) + } + var updateVisibility = false + if !onlyPositive { + updateVisibility = true + } + if case .visible = visibility { + updateVisibility = true + } + if updateVisibility { + if visibility != itemNode.visibility { + itemNode.visibility = visibility + } + } + } + } + + private func updateAccessoryNodes(transition: ContainedViewLayoutTransition, synchronous: Bool, currentTimestamp: Double, leftInset: CGFloat, rightInset: CGFloat) { + var totalVisibleHeight: CGFloat = 0.0 + var index = -1 + let count = self.itemNodes.count + for itemNode in self.itemNodes { + index += 1 + totalVisibleHeight += itemNode.apparentHeight + + guard let itemNodeIndex = itemNode.index else { + continue + } + + if let accessoryItem = self.items[itemNodeIndex].accessoryItem { + let previousItem: ListViewItem? = itemNodeIndex == 0 ? nil : self.items[itemNodeIndex - 1] + let previousAccessoryItem = previousItem?.accessoryItem + + if (previousAccessoryItem == nil || !previousAccessoryItem!.isEqualToItem(accessoryItem)) { + if itemNode.accessoryItemNode == nil { + var didStealAccessoryNode = false + if index != count - 1 { + for i in index + 1 ..< count { + let nextItemNode = self.itemNodes[i] + if let nextItemNodeIndex = nextItemNode.index { + let nextItem = self.items[nextItemNodeIndex] + if let nextAccessoryItem = nextItem.accessoryItem , nextAccessoryItem.isEqualToItem(accessoryItem) { + if let nextAccessoryItemNode = nextItemNode.accessoryItemNode { + didStealAccessoryNode = true + + var previousAccessoryItemNodeOrigin = nextAccessoryItemNode.frame.origin + let previousParentOrigin = nextItemNode.frame.origin + previousAccessoryItemNodeOrigin.x += previousParentOrigin.x + previousAccessoryItemNodeOrigin.y += previousParentOrigin.y + previousAccessoryItemNodeOrigin.y -= nextItemNode.bounds.origin.y + previousAccessoryItemNodeOrigin.y -= nextAccessoryItemNode.transitionOffset.y + nextAccessoryItemNode.transitionOffset = CGPoint() + + nextAccessoryItemNode.removeFromSupernode() + itemNode.addSubnode(nextAccessoryItemNode) + + itemNode.setAccessoryItemNode(nextAccessoryItemNode, leftInset: leftInset, rightInset: rightInset) + self.itemNodes[i].setAccessoryItemNode(nil, leftInset: leftInset, rightInset: rightInset) + + var updatedAccessoryItemNodeOrigin = nextAccessoryItemNode.frame.origin + let updatedParentOrigin = itemNode.apparentFrame.origin + updatedAccessoryItemNodeOrigin.x += updatedParentOrigin.x + updatedAccessoryItemNodeOrigin.y += updatedParentOrigin.y + updatedAccessoryItemNodeOrigin.y -= itemNode.bounds.origin.y + //updatedAccessoryItemNodeOrigin.y += itemNode.transitionOffset + + var deltaHeight = itemNode.frame.size.height - nextItemNode.frame.size.height + //deltaHeight = 0.0 + nextAccessoryItemNode.animateTransitionOffset(CGPoint(x: 0.0, y: updatedAccessoryItemNodeOrigin.y - previousAccessoryItemNodeOrigin.y - deltaHeight), beginAt: currentTimestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: listViewAnimationCurveSystem) + + } + } else { + break + } + } + } + } + + if !didStealAccessoryNode { + let accessoryNode = accessoryItem.node(synchronous: synchronous) + itemNode.addSubnode(accessoryNode) + itemNode.setAccessoryItemNode(accessoryNode, leftInset: leftInset, rightInset: rightInset) + } + } + } else { + itemNode.accessoryItemNode?.removeFromSupernode() + itemNode.setAccessoryItemNode(nil, leftInset: leftInset, rightInset: rightInset) + } + } + + if let headerAccessoryItem = self.items[itemNodeIndex].headerAccessoryItem { + let previousItem: ListViewItem? = itemNodeIndex == 0 ? nil : self.items[itemNodeIndex - 1] + let previousHeaderAccessoryItem = previousItem?.headerAccessoryItem + + if (previousHeaderAccessoryItem == nil || !previousHeaderAccessoryItem!.isEqualToItem(headerAccessoryItem)) { + if itemNode.headerAccessoryItemNode == nil { + var didStealHeaderAccessoryNode = false + if index != count - 1 { + for i in index + 1 ..< count { + let nextItemNode = self.itemNodes[i] + if let nextItemNodeIndex = nextItemNode.index { + let nextItem = self.items[nextItemNodeIndex] + if let nextHeaderAccessoryItem = nextItem.headerAccessoryItem , nextHeaderAccessoryItem.isEqualToItem(headerAccessoryItem) { + if let nextHeaderAccessoryItemNode = nextItemNode.headerAccessoryItemNode { + didStealHeaderAccessoryNode = true + + var previousHeaderAccessoryItemNodeOrigin = nextHeaderAccessoryItemNode.frame.origin + let previousParentOrigin = nextItemNode.frame.origin + previousHeaderAccessoryItemNodeOrigin.x += previousParentOrigin.x + previousHeaderAccessoryItemNodeOrigin.y += previousParentOrigin.y + previousHeaderAccessoryItemNodeOrigin.y -= nextItemNode.bounds.origin.y + previousHeaderAccessoryItemNodeOrigin.y -= nextHeaderAccessoryItemNode.transitionOffset.y + nextHeaderAccessoryItemNode.transitionOffset = CGPoint() + + nextHeaderAccessoryItemNode.removeFromSupernode() + itemNode.addSubnode(nextHeaderAccessoryItemNode) + itemNode.headerAccessoryItemNode = nextHeaderAccessoryItemNode + self.itemNodes[i].headerAccessoryItemNode = nil + + var updatedHeaderAccessoryItemNodeOrigin = nextHeaderAccessoryItemNode.frame.origin + let updatedParentOrigin = itemNode.frame.origin + updatedHeaderAccessoryItemNodeOrigin.x += updatedParentOrigin.x + updatedHeaderAccessoryItemNodeOrigin.y += updatedParentOrigin.y + updatedHeaderAccessoryItemNodeOrigin.y -= itemNode.bounds.origin.y + + let deltaHeight = itemNode.frame.size.height - nextItemNode.frame.size.height + + nextHeaderAccessoryItemNode.animateTransitionOffset(CGPoint(x: 0.0, y: updatedHeaderAccessoryItemNodeOrigin.y - previousHeaderAccessoryItemNodeOrigin.y - deltaHeight), beginAt: currentTimestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: listViewAnimationCurveSystem) + } + } else { + break + } + } + } + } + + if !didStealHeaderAccessoryNode { + let headerAccessoryNode = headerAccessoryItem.node(synchronous: synchronous) + itemNode.addSubnode(headerAccessoryNode) + itemNode.headerAccessoryItemNode = headerAccessoryNode + } + } + } else { + itemNode.headerAccessoryItemNode?.removeFromSupernode() + itemNode.headerAccessoryItemNode = nil + } + } + } + + if let verticalScrollIndicator = self.verticalScrollIndicator { + var topIndexAndBoundary: (Int, CGFloat, CGFloat)? + var bottomIndexAndBoundary: (Int, CGFloat, CGFloat)? + for itemNode in self.itemNodes { + if itemNode.apparentFrame.maxY >= self.insets.top, let index = itemNode.index { + topIndexAndBoundary = (index, itemNode.apparentFrame.minY, itemNode.apparentFrame.height) + break + } + } + for itemNode in self.itemNodes.reversed() { + if itemNode.apparentFrame.minY <= self.visibleSize.height - self.insets.bottom, let index = itemNode.index { + bottomIndexAndBoundary = (index, itemNode.apparentFrame.maxY, itemNode.apparentFrame.height) + break + } + } + if let topIndexAndBoundary = topIndexAndBoundary, let bottomIndexAndBoundary = bottomIndexAndBoundary { + let averageRangeItemHeight: CGFloat = 44.0 + + var upperItemsHeight = floor(averageRangeItemHeight * CGFloat(topIndexAndBoundary.0)) + var approximateContentHeight = CGFloat(self.items.count) * averageRangeItemHeight + if topIndexAndBoundary.0 >= 0 && self.items[topIndexAndBoundary.0].approximateHeight.isZero { + upperItemsHeight -= averageRangeItemHeight + approximateContentHeight -= averageRangeItemHeight + } + + var convertedTopBoundary: CGFloat + if topIndexAndBoundary.1 < self.insets.top { + convertedTopBoundary = (topIndexAndBoundary.1 - self.insets.top) * averageRangeItemHeight / topIndexAndBoundary.2 + } else { + convertedTopBoundary = topIndexAndBoundary.1 - self.insets.top + } + convertedTopBoundary -= upperItemsHeight + + let approximateOffset = -convertedTopBoundary + + var convertedBottomBoundary: CGFloat = 0.0 + if bottomIndexAndBoundary.1 > self.visibleSize.height - self.insets.bottom { + convertedBottomBoundary = ((self.visibleSize.height - self.insets.bottom) - bottomIndexAndBoundary.1) * averageRangeItemHeight / bottomIndexAndBoundary.2 + } else { + convertedBottomBoundary = (self.visibleSize.height - self.insets.bottom) - bottomIndexAndBoundary.1 + } + convertedBottomBoundary += CGFloat(bottomIndexAndBoundary.0 + 1) * averageRangeItemHeight + + let approximateVisibleHeight = max(0.0, convertedBottomBoundary - approximateOffset) + + let approximateScrollingProgress = approximateOffset / (approximateContentHeight - approximateVisibleHeight) + + let indicatorSideInset: CGFloat = 3.0 + var indicatorTopInset: CGFloat = 3.0 + if self.verticalScrollIndicatorFollowsOverscroll { + if topIndexAndBoundary.0 == 0 { + indicatorTopInset = max(topIndexAndBoundary.1 + 3.0 - self.insets.top, 3.0) + } + } + let indicatorBottomInset: CGFloat = 3.0 + let minIndicatorContentHeight: CGFloat = 12.0 + let minIndicatorHeight: CGFloat = 6.0 + + let visibleHeightWithoutIndicatorInsets = self.visibleSize.height - self.scrollIndicatorInsets.top - self.scrollIndicatorInsets.bottom - indicatorTopInset - indicatorBottomInset + let indicatorHeight: CGFloat + if approximateContentHeight <= 0 { + indicatorHeight = 0.0 + } else { + indicatorHeight = max(minIndicatorContentHeight, floor(visibleHeightWithoutIndicatorInsets * (self.visibleSize.height - self.insets.top - self.insets.bottom) / approximateContentHeight)) + } + + let upperBound = self.scrollIndicatorInsets.top + indicatorTopInset + let lowerBound = self.visibleSize.height - self.scrollIndicatorInsets.bottom - indicatorTopInset - indicatorBottomInset - indicatorHeight + + let indicatorOffset = ceilToScreenPixels(upperBound * (1.0 - approximateScrollingProgress) + lowerBound * approximateScrollingProgress) + + var indicatorFrame = CGRect(origin: CGPoint(x: self.rotated ? indicatorSideInset : (self.visibleSize.width - 3.0 - indicatorSideInset), y: indicatorOffset), size: CGSize(width: 3.0, height: indicatorHeight)) + if indicatorFrame.minY < self.scrollIndicatorInsets.top + indicatorTopInset { + indicatorFrame.size.height -= self.scrollIndicatorInsets.top + indicatorTopInset - indicatorFrame.minY + indicatorFrame.origin.y = self.scrollIndicatorInsets.top + indicatorTopInset + indicatorFrame.size.height = max(minIndicatorHeight, indicatorFrame.height) + } + if indicatorFrame.maxY > self.visibleSize.height - (self.scrollIndicatorInsets.bottom + indicatorTopInset + indicatorBottomInset) { + indicatorFrame.size.height -= indicatorFrame.maxY - (self.visibleSize.height - (self.scrollIndicatorInsets.bottom + indicatorTopInset)) + indicatorFrame.size.height = max(minIndicatorHeight, indicatorFrame.height) + indicatorFrame.origin.y = self.visibleSize.height - (self.scrollIndicatorInsets.bottom + indicatorBottomInset) - indicatorFrame.height + } + + if indicatorHeight >= visibleHeightWithoutIndicatorInsets { + verticalScrollIndicator.isHidden = true + verticalScrollIndicator.frame = indicatorFrame + } else { + if verticalScrollIndicator.isHidden { + verticalScrollIndicator.isHidden = false + verticalScrollIndicator.frame = indicatorFrame + } else { + verticalScrollIndicator.frame = indicatorFrame + } + } + } else { + verticalScrollIndicator.isHidden = true + } + } + } + + private func enqueueUpdateVisibleItems(synchronous: Bool) { + if !self.enqueuedUpdateVisibleItems { + self.enqueuedUpdateVisibleItems = true + + self.transactionQueue.addTransaction({ [weak self] completion in + if let strongSelf = self { + strongSelf.transactionOffset = 0.0 + strongSelf.updateVisibleItemsTransaction(synchronous: synchronous, completion: { + var repeatUpdate = false + if let strongSelf = self { + repeatUpdate = abs(strongSelf.transactionOffset) > 0.00001 + strongSelf.transactionOffset = 0.0 + strongSelf.enqueuedUpdateVisibleItems = false + } + + completion() + + if repeatUpdate { + strongSelf.enqueueUpdateVisibleItems(synchronous: false) + } + }) + } + }) + } + } + + private func updateVisibleItemsTransaction(synchronous: Bool, completion: @escaping () -> Void) { + if self.items.count == 0 && self.itemNodes.count == 0 { + completion() + return + } + var i = 0 + while i < self.itemNodes.count { + let node = self.itemNodes[i] + if node.index == nil && node.apparentHeight <= CGFloat.ulpOfOne { + self.removeItemNodeAtIndex(i) + if useBackgroundDeallocation { + assertionFailure() + //ASDeallocQueue.sharedDeallocation().releaseObject(inBackground: node) + } else { + //ASPerformMainThreadDeallocation(node) + } + } else { + i += 1 + } + } + + let state = self.currentState() + + let begin: () -> Void = { + self.fillMissingNodes(synchronous: synchronous, synchronousLoads: false, animated: false, inputAnimatedInsertIndices: [], insertDirectionHints: [:], inputState: state, inputPreviousNodes: [:], inputOperations: []) { state, operations in + var updatedState = state + var updatedOperations = operations + updatedState.removeInvisibleNodes(&updatedOperations) + self.dispatchOnVSync { + self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) + } + } + } + if synchronous { + begin() + } else { + self.async { + begin() + } + } + } + + private func updateVisibleItemRange(force: Bool = false) { + let currentRange = self.immediateDisplayedItemRange() + + if currentRange != self.displayedItemRange || force { + self.displayedItemRange = currentRange + self.displayedItemRangeChanged(currentRange, self.opaqueTransactionState) + } + } + + private func immediateDisplayedItemRange() -> ListViewDisplayedItemRange { + var loadedRange: ListViewItemRange? + var visibleRange: ListViewVisibleItemRange? + if self.itemNodes.count != 0 { + var firstIndex: (nodeIndex: Int, index: Int)? + var lastIndex: (nodeIndex: Int, index: Int)? + + var i = 0 + while i < self.itemNodes.count { + if let index = self.itemNodes[i].index { + firstIndex = (i, index) + break + } + i += 1 + } + i = self.itemNodes.count - 1 + while i >= 0 { + if let index = self.itemNodes[i].index { + lastIndex = (i, index) + break + } + i -= 1 + } + if let firstIndex = firstIndex, let lastIndex = lastIndex { + var firstVisibleIndex: (Int, Bool)? + for i in firstIndex.nodeIndex ... lastIndex.nodeIndex { + if let index = self.itemNodes[i].index { + let frame = self.itemNodes[i].apparentFrame + if frame.maxY >= self.insets.top && frame.minY < self.visibleSize.height + self.insets.bottom { + firstVisibleIndex = (index, frame.minY >= self.insets.top - 10.0) + break + } + } + } + + if let firstVisibleIndex = firstVisibleIndex { + var lastVisibleIndex: Int? + for i in (firstIndex.nodeIndex ... lastIndex.nodeIndex).reversed() { + if let index = self.itemNodes[i].index { + let frame = self.itemNodes[i].apparentFrame + if frame.maxY >= self.insets.top && frame.minY < self.visibleSize.height - self.insets.bottom { + lastVisibleIndex = index + break + } + } + } + + if let lastVisibleIndex = lastVisibleIndex { + visibleRange = ListViewVisibleItemRange(firstIndex: firstVisibleIndex.0, firstIndexFullyVisible: firstVisibleIndex.1, lastIndex: lastVisibleIndex) + } + } + + loadedRange = ListViewItemRange(firstIndex: firstIndex.index, lastIndex: lastIndex.index) + } + } + + return ListViewDisplayedItemRange(loadedRange: loadedRange, visibleRange: visibleRange) + } + + private func updateAnimations() { + self.inVSync = true + let actionsForVSync = self.actionsForVSync + self.actionsForVSync.removeAll() + for action in actionsForVSync { + action() + } + self.inVSync = false + + let timestamp: Double = CACurrentMediaTime() + + var continueAnimations = false + + if !self.actionsForVSync.isEmpty { + continueAnimations = true + } + + var i = 0 + var animationCount = self.animations.count + while i < animationCount { + let animation = self.animations[i] + animation.applyAt(timestamp) + + if animation.completeAt(timestamp) { + animations.remove(at: i) + animationCount -= 1 + i -= 1 + } else { + continueAnimations = true + } + + i += 1 + } + + var offsetRanges = OffsetRanges() + + if let reorderOffset = self.reorderNode?.currentOffset(), !self.itemNodes.isEmpty { + if reorderOffset < self.insets.top + 10.0 { + if self.itemNodes[0].apparentFrame.minY < self.insets.top { + continueAnimations = true + offsetRanges.offset(IndexRange(first: 0, last: Int.max), offset: 6.0) + } + } else if reorderOffset > self.visibleSize.height - self.insets.bottom - 10.0 { + if self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY > self.visibleSize.height - self.insets.bottom { + continueAnimations = true + offsetRanges.offset(IndexRange(first: 0, last: Int.max), offset: -6.0) + } + } + } + + var requestUpdateVisibleItems = false + var index = 0 + while index < self.itemNodes.count { + let itemNode = self.itemNodes[index] + + let previousApparentHeight = itemNode.apparentHeight + if itemNode.animate(timestamp) { + continueAnimations = true + } + let updatedApparentHeight = itemNode.apparentHeight + let apparentHeightDelta = updatedApparentHeight - previousApparentHeight + if abs(apparentHeightDelta) > CGFloat.ulpOfOne { + let visualInsets = self.visualInsets ?? self.insets + + if itemNode.apparentFrame.maxY <= visualInsets.top { + offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) + } else { + var offsetDelta = apparentHeightDelta + if offsetDelta < 0.0 { + let maxDelta = visualInsets.top - itemNode.apparentFrame.maxY + if maxDelta > offsetDelta { + let remainingOffset = maxDelta - offsetDelta + offsetRanges.offset(IndexRange(first: 0, last: index), offset: remainingOffset) + offsetDelta = maxDelta + } + } + + offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: offsetDelta) + } + + if let accessoryItemNode = itemNode.accessoryItemNode { + itemNode.layoutAccessoryItemNode(accessoryItemNode, leftInset: self.insets.left, rightInset: self.insets.right) + } + } + + if itemNode.index == nil && updatedApparentHeight <= CGFloat.ulpOfOne { + requestUpdateVisibleItems = true + } + + index += 1 + } + + for (_, headerNode) in self.itemHeaderNodes { + if headerNode.animate(timestamp) { + continueAnimations = true + } + } + + if !offsetRanges.offsets.isEmpty { + requestUpdateVisibleItems = true + var index = 0 + for itemNode in self.itemNodes { + let offset = offsetRanges.offsetForIndex(index) + if offset != 0.0 { + var position = itemNode.position + position.y += offset + itemNode.position = position + } + + index += 1 + } + + if !self.snapToBounds(snapTopItem: false, stackFromBottom: self.stackFromBottom).offset.isZero { + self.updateVisibleContentOffset() + } + } + + self.debugCheckMonotonity() + + if !continueAnimations { + self.pauseAnimations() + } + + if requestUpdateVisibleItems { + self.enqueueUpdateVisibleItems(synchronous: false) + } + + self.checkItemReordering() + } + + override open func touchesBegan(_ touches: Set, with event: UIEvent?) { + let touchesPosition = touches.first!.location(in: self.view) + + if let index = self.itemIndexAtPoint(touchesPosition) { + for i in 0 ..< self.itemNodes.count { + if self.itemNodes[i].preventsTouchesToOtherItems { + if index != self.itemNodes[i].index { + self.itemNodes[i].touchesToOtherItemsPrevented() + return + } + break + } + } + } + + let offset = self.visibleContentOffset() + switch offset { + case let .known(value) where value <= 10.0: + self.beganTrackingAtTopOrigin = true + default: + self.beganTrackingAtTopOrigin = false + } + + self.touchesPosition = touchesPosition + self.selectionTouchLocation = touches.first!.location(in: self.view) + + self.selectionTouchDelayTimer?.invalidate() + self.selectionLongTapDelayTimer?.invalidate() + self.selectionLongTapDelayTimer = nil + let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in + if let strongSelf = self, strongSelf.selectionTouchLocation != nil { + strongSelf.clearHighlightAnimated(false) + + if let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition) { + var canBeSelectedOrLongTapped = false + for itemNode in strongSelf.itemNodes { + if itemNode.index == index && (strongSelf.items[index].selectable && itemNode.canBeSelected) || itemNode.canBeLongTapped { + canBeSelectedOrLongTapped = true + } + } + + if canBeSelectedOrLongTapped { + strongSelf.highlightedItemIndex = index + for itemNode in strongSelf.itemNodes { + if itemNode.index == index && itemNode.canBeSelected { + if true { + if !itemNode.isLayerBacked { + strongSelf.reorderItemNodeToFront(itemNode) + for (_, headerNode) in strongSelf.itemHeaderNodes { + strongSelf.reorderHeaderNodeToFront(headerNode) + } + } + let itemNodeFrame = itemNode.frame + let itemNodeBounds = itemNode.bounds + if strongSelf.items[index].selectable { + itemNode.setHighlighted(true, at: strongSelf.touchesPosition.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY), animated: false) + } + + if itemNode.canBeLongTapped { + let timer = Timer(timeInterval: 0.3, target: ListViewTimerProxy { + if let strongSelf = self, strongSelf.highlightedItemIndex == index { + for itemNode in strongSelf.itemNodes { + if itemNode.index == index && itemNode.canBeLongTapped { + itemNode.longTapped() + strongSelf.clearHighlightAnimated(true) + strongSelf.selectionTouchLocation = nil + break + } + } + } + }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) + strongSelf.selectionLongTapDelayTimer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + } + } + break + } + } + } + } + } + }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) + self.selectionTouchDelayTimer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + + super.touchesBegan(touches, with: event) + + self.updateScroller(transition: .immediate) + } + + public func clearHighlightAnimated(_ animated: Bool) { + if let highlightedItemIndex = self.highlightedItemIndex { + for itemNode in self.itemNodes { + if itemNode.index == highlightedItemIndex { + itemNode.setHighlighted(false, at: CGPoint(), animated: animated) + break + } + } + } + self.highlightedItemIndex = nil + } + + public func updateNodeHighlightsAnimated(_ animated: Bool) { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate + self.updateOverlayHighlight(transition: transition) + } + + private func itemIndexAtPoint(_ point: CGPoint) -> Int? { + for itemNode in self.itemNodes { + if itemNode.apparentContentFrame.contains(point) { + return itemNode.index + } + } + return nil + } + + public func itemNodeAtIndex(_ index: Int) -> ListViewItemNode? { + for itemNode in self.itemNodes { + if itemNode.index == index { + return itemNode + } + } + return nil + } + + public func forEachItemNode(_ f: (ASDisplayNode) -> Void) { + for itemNode in self.itemNodes { + if itemNode.index != nil { + f(itemNode) + } + } + } + + public func forEachVisibleItemNode(_ f: (ASDisplayNode) -> Void) { + for itemNode in self.itemNodes { + if itemNode.index != nil && itemNode.frame.maxY > self.insets.top && itemNode.frame.minY < self.visibleSize.height - self.insets.bottom { + f(itemNode) + } + } + } + + public func forEachItemHeaderNode(_ f: (ListViewItemHeaderNode) -> Void) { + for (_, itemNode) in self.itemHeaderNodes { + f(itemNode) + } + } + + public func forEachAccessoryItemNode(_ f: (ListViewAccessoryItemNode) -> Void) { + for itemNode in self.itemNodes { + if let accessoryItemNode = itemNode.accessoryItemNode { + f(accessoryItemNode) + } + } + } + + public func ensureItemNodeVisible(_ node: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0) { + if let index = node.index { + if node.apparentHeight > self.visibleSize.height - self.insets.top - self.insets.bottom { + if node.frame.maxY > self.visibleSize.height - self.insets.bottom { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.bottom(-overflow), animated: animated, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Down), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + }/* else if node.frame.minY < self.insets.top { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(0.0), animated: true, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + }*/ + } else { + if self.experimentalSnapScrollToItem { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.visible, animated: animated, curve: ListViewAnimationCurve.Default(duration: nil), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } else { + if node.frame.minY < self.insets.top { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(overflow), animated: animated, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } else if node.frame.maxY > self.visibleSize.height - self.insets.bottom { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.bottom(-overflow), animated: animated, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Down), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + } + } + } + + public func ensureItemNodeVisibleAtTopInset(_ node: ListViewItemNode) { + if let index = node.index { + if node.frame.minY != self.insets.top { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(0.0), animated: true, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + } + + public func itemNodeRelativeOffset(_ node: ListViewItemNode) -> CGFloat? { + if let _ = node.index { + return node.frame.minY - self.insets.top + } + return nil + } + + public func itemNodeVisibleInsideInsets(_ node: ListViewItemNode) -> Bool { + if let _ = node.index { + if node.frame.maxY > self.insets.top && node.frame.minY < self.visibleSize.height - self.insets.bottom { + return true + } + } + return false + } + + override open func touchesMoved(_ touches: Set, with event: UIEvent?) { + if let selectionTouchLocation = self.selectionTouchLocation { + let location = touches.first!.location(in: self.view) + let distance = CGPoint(x: selectionTouchLocation.x - location.x, y: selectionTouchLocation.y - location.y) + let maxMovementDistance: CGFloat = 4.0 + if distance.x * distance.x + distance.y * distance.y > maxMovementDistance * maxMovementDistance { + self.selectionTouchLocation = nil + self.selectionTouchDelayTimer?.invalidate() + self.selectionLongTapDelayTimer?.invalidate() + self.selectionTouchDelayTimer = nil + self.selectionLongTapDelayTimer = nil + self.clearHighlightAnimated(false) + } + } + + super.touchesMoved(touches, with: event) + } + + override open func touchesEnded(_ touches: Set, with event: UIEvent?) { + if let selectionTouchLocation = self.selectionTouchLocation { + let index = self.itemIndexAtPoint(selectionTouchLocation) + if index != self.highlightedItemIndex { + self.clearHighlightAnimated(false) + } + + if let index = index { + if self.items[index].selectable { + self.highlightedItemIndex = index + for itemNode in self.itemNodes { + if itemNode.index == index { + if itemNode.canBeSelected { + if !itemNode.isLayerBacked { + self.reorderItemNodeToFront(itemNode) + for (_, headerNode) in self.itemHeaderNodes { + self.reorderHeaderNodeToFront(headerNode) + } + } + let itemNodeFrame = itemNode.frame + itemNode.setHighlighted(true, at: selectionTouchLocation.offsetBy(dx: -itemNodeFrame.minX, dy: -itemNodeFrame.minY), animated: false) + } else { + self.highlightedItemIndex = nil + itemNode.tapped() + } + break + } + } + } + } + } + + if let highlightedItemIndex = self.highlightedItemIndex { + self.items[highlightedItemIndex].selected(listView: self) + } + self.selectionTouchLocation = nil + + super.touchesEnded(touches, with: event) + } + + override open func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + self.selectionTouchLocation = nil + self.selectionTouchDelayTimer?.invalidate() + self.selectionTouchDelayTimer = nil + self.selectionLongTapDelayTimer?.invalidate() + self.selectionLongTapDelayTimer = nil + self.clearHighlightAnimated(false) + + super.touchesCancelled(touches, with: event) + } + + @objc func trackingGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.isTracking = true + self.trackingOffset = 0.0 + case .changed: + self.touchesPosition = recognizer.location(in: self.view) + case .ended, .cancelled: + self.isTracking = false + default: + break + } + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func withTransaction(_ f: @escaping () -> Void) { + self.transactionQueue.addTransaction { completion in + f() + completion() + } + } + + fileprivate func internalHitTest(_ point: CGPoint, with event: UIEvent?) -> Bool { + if self.limitHitTestToNodes { + var foundHit = false + for itemNode in self.itemNodes { + if itemNode.frame.contains(point) { + foundHit = true + break + } + } + if !foundHit { + return false + } + } + return true + } + + fileprivate func headerHitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for (_, headerNode) in self.itemHeaderNodes { + let headerNodeFrame = headerNode.frame + if headerNodeFrame.contains(point) { + return headerNode.hitTest(point.offsetBy(dx: -headerNodeFrame.minX, dy: -headerNodeFrame.minY), with: event) + } + } + return nil + } + + private func reorderItemNodeToFront(_ itemNode: ListViewItemNode) { + itemNode.view.superview?.bringSubview(toFront: itemNode.view) + if let itemHighlightOverlayBackground = self.itemHighlightOverlayBackground { + itemHighlightOverlayBackground.view.superview?.bringSubview(toFront: itemHighlightOverlayBackground.view) + } + if let verticalScrollIndicator = self.verticalScrollIndicator { + verticalScrollIndicator.view.superview?.bringSubview(toFront: verticalScrollIndicator.view) + } + } + + private func reorderHeaderNodeToFront(_ headerNode: ListViewItemHeaderNode) { + headerNode.view.superview?.bringSubview(toFront: headerNode.view) + if let itemHighlightOverlayBackground = self.itemHighlightOverlayBackground { + itemHighlightOverlayBackground.view.superview?.bringSubview(toFront: itemHighlightOverlayBackground.view) + } + if let verticalScrollIndicator = self.verticalScrollIndicator { + verticalScrollIndicator.view.superview?.bringSubview(toFront: verticalScrollIndicator.view) + } + } + + public func scrollWithDirection(_ direction: ListViewScrollDirection, distance: CGFloat) -> Bool { + var accessibilityFocusedNode: (ASDisplayNode, CGRect)? + for itemNode in self.itemNodes { + if findAccessibilityFocus(itemNode) { + accessibilityFocusedNode = (itemNode, itemNode.frame) + break + } + } + let initialOffset = self.scroller.contentOffset + switch direction { + case .up: + var contentOffset = initialOffset + contentOffset.y -= distance + contentOffset.y = max(self.scroller.contentInset.top, contentOffset.y) + if contentOffset.y < initialOffset.y { + self.ignoreScrollingEvents = true + self.scroller.setContentOffset(contentOffset, animated: false) + self.ignoreScrollingEvents = false + self.updateScrollViewDidScroll(self.scroller, synchronous: true) + } else { + return false + } + case .down: + var contentOffset = initialOffset + contentOffset.y += distance + contentOffset.y = max(self.scroller.contentInset.top, min(contentOffset.y, self.scroller.contentSize.height - self.visibleSize.height - self.insets.bottom - self.insets.top)) + if contentOffset.y > initialOffset.y { + self.ignoreScrollingEvents = true + self.scroller.setContentOffset(contentOffset, animated: false) + self.ignoreScrollingEvents = false + self.updateScrollViewDidScroll(self.scroller, synchronous: true) + } else { + return false + } + } + if let (_, frame) = accessibilityFocusedNode { + for itemNode in self.itemNodes { + if frame.intersects(itemNode.frame) { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, itemNode.view) + if let index = itemNode.index { + let scrollStatus = "Row \(index + 1) of \(self.items.count)" + UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, scrollStatus) + } + break + } + } + } + return true + } + + override open func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool { + let distance = floor((self.visibleSize.height - self.insets.top - self.insets.bottom) / 2.0) + let scrollDirection: ListViewScrollDirection + switch direction { + case .down: + scrollDirection = .down + default: + scrollDirection = .up + } + return self.scrollWithDirection(scrollDirection, distance: distance) + } +} + +private func findAccessibilityFocus(_ node: ASDisplayNode) -> Bool { + if node.view.accessibilityElementIsFocused() { + return true + } + return false +} diff --git a/submodules/Display/Display/ListViewAccessoryItem.swift b/submodules/Display/Display/ListViewAccessoryItem.swift new file mode 100644 index 0000000000..37675f9d54 --- /dev/null +++ b/submodules/Display/Display/ListViewAccessoryItem.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol ListViewAccessoryItem { + func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool + func node(synchronous: Bool) -> ListViewAccessoryItemNode +} diff --git a/submodules/Display/Display/ListViewAccessoryItemNode.swift b/submodules/Display/Display/ListViewAccessoryItemNode.swift new file mode 100644 index 0000000000..c36795e903 --- /dev/null +++ b/submodules/Display/Display/ListViewAccessoryItemNode.swift @@ -0,0 +1,50 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +open class ListViewAccessoryItemNode: ASDisplayNode { + var transitionOffset: CGPoint = CGPoint() { + didSet { + self.bounds = CGRect(origin: self.transitionOffset, size: self.bounds.size) + } + } + + private var transitionOffsetAnimation: ListViewAnimation? + + final func animateTransitionOffset(_ from: CGPoint, beginAt: Double, duration: Double, curve: @escaping (CGFloat) -> CGFloat) { + self.transitionOffset = from + self.transitionOffsetAnimation = ListViewAnimation(from: from, to: CGPoint(), duration: duration, curve: curve, beginAt: beginAt, update: { [weak self] _, currentValue in + if let strongSelf = self { + strongSelf.transitionOffset = currentValue + } + }) + } + + final func removeAllAnimations() { + self.transitionOffsetAnimation = nil + self.transitionOffset = CGPoint() + } + + final func animate(_ timestamp: Double) -> Bool { + if let animation = self.transitionOffsetAnimation { + animation.applyAt(timestamp) + + if animation.completeAt(timestamp) { + self.transitionOffsetAnimation = nil + } else { + return true + } + } + + return false + } + + override open func layout() { + super.layout() + + self.updateLayout(size: self.bounds.size, leftInset: 0.0, rightInset: 0.0) + } + + open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + } +} diff --git a/submodules/Display/Display/ListViewAnimation.swift b/submodules/Display/Display/ListViewAnimation.swift new file mode 100644 index 0000000000..d546703e23 --- /dev/null +++ b/submodules/Display/Display/ListViewAnimation.swift @@ -0,0 +1,182 @@ +import Foundation +import UIKit + +#if BUCK +import DisplayPrivate +#endif + +public protocol Interpolatable { + static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> (Interpolatable) +} + +private func floorToPixels(_ value: CGFloat) -> CGFloat { + return round(value * 10.0) / 10.0 +} + +private func floorToPixels(_ value: CGPoint) -> CGPoint { + return CGPoint(x: round(value.x * 10.0) / 10.0, y: round(value.y * 10.0) / 10.0) +} + +private func floorToPixels(_ value: CGSize) -> CGSize { + return CGSize(width: round(value.width * 10.0) / 10.0, height: round(value.height * 10.0) / 10.0) +} + +private func floorToPixels(_ value: CGRect) -> CGRect { + return CGRect(origin: floorToPixels(value.origin), size: floorToPixels(value.size)) +} + +private func floorToPixels(_ value: UIEdgeInsets) -> UIEdgeInsets { + return UIEdgeInsets(top: round(value.top * 10.0) / 10.0, left: round(value.left * 10.0) / 10.0, bottom: round(value.bottom * 10.0) / 10.0, right: round(value.right * 10.0) / 10.0) +} + +extension CGFloat: Interpolatable { + public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { + return { from, to, t -> Interpolatable in + let fromValue: CGFloat = from as! CGFloat + let toValue: CGFloat = to as! CGFloat + let invT: CGFloat = 1.0 - t + let term: CGFloat = toValue * t + fromValue * invT + return floorToPixels(term) + } + } +} + +extension UIEdgeInsets: Interpolatable { + public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { + return { from, to, t -> Interpolatable in + let fromValue = from as! UIEdgeInsets + let toValue = to as! UIEdgeInsets + return floorToPixels(UIEdgeInsets(top: toValue.top * t + fromValue.top * (1.0 - t), left: toValue.left * t + fromValue.left * (1.0 - t), bottom: toValue.bottom * t + fromValue.bottom * (1.0 - t), right: toValue.right * t + fromValue.right * (1.0 - t))) + } + } +} + +extension CGRect: Interpolatable { + public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { + return { from, to, t -> Interpolatable in + let fromValue = from as! CGRect + let toValue = to as! CGRect + return floorToPixels(CGRect(x: toValue.origin.x * t + fromValue.origin.x * (1.0 - t), y: toValue.origin.y * t + fromValue.origin.y * (1.0 - t), width: toValue.size.width * t + fromValue.size.width * (1.0 - t), height: toValue.size.height * t + fromValue.size.height * (1.0 - t))) + } + } +} + +extension CGPoint: Interpolatable { + public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { + return { from, to, t -> Interpolatable in + let fromValue = from as! CGPoint + let toValue = to as! CGPoint + return floorToPixels(CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t))) + } + } +} + +private let springAnimationIn: CABasicAnimation = { + let animation = makeSpringAnimation("") + return animation +}() + +private let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in + if #available(iOS 9.0, *) { + return { t in + return springAnimationValueAt(springAnimationIn, t) + } + } else { + return { t in + return bezierPoint(0.23, 1.0, 0.32, 1.0, t) + } + } +}() + +public let listViewAnimationCurveSystem: (CGFloat) -> CGFloat = { t in + return springAnimationSolver(t) +} + +public let listViewAnimationCurveLinear: (CGFloat) -> CGFloat = { t in + return t +} + +#if os(iOS) +public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIViewAnimationOptions) -> (CGFloat) -> CGFloat { + if animationOptions.rawValue == UInt(7 << 16) { + return listViewAnimationCurveSystem + } else { + return listViewAnimationCurveLinear + } +} +#endif + +public final class ListViewAnimation { + let from: Interpolatable + let to: Interpolatable + let duration: Double + let startTime: Double + private let curve: (CGFloat) -> CGFloat + private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable + private let update: (CGFloat, Interpolatable) -> Void + private let completed: (Bool) -> Void + + public init(from: T, to: T, duration: Double, curve: @escaping (CGFloat) -> CGFloat, beginAt: Double, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) { + self.from = from + self.to = to + self.duration = duration + self.curve = curve + self.startTime = beginAt + self.interpolator = T.interpolator() + self.update = { progress, value in + update(progress, value as! T) + } + self.completed = completed + } + + init(copying: ListViewAnimation, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) { + self.from = copying.from + self.to = copying.to + self.duration = copying.duration + self.curve = copying.curve + self.startTime = copying.startTime + self.interpolator = copying.interpolator + self.update = { progress, value in + update(progress, value as! T) + } + self.completed = completed + } + + public func completeAt(_ timestamp: Double) -> Bool { + if timestamp >= self.startTime + self.duration { + self.completed(true) + return true + } else { + return false + } + } + + public func cancel() { + self.completed(false) + } + + private func valueAt(_ t: CGFloat) -> Interpolatable { + if t <= 0.0 { + return self.from + } else if t >= 1.0 { + return self.to + } else { + return self.interpolator(self.from, self.to, t) + } + } + + public func applyAt(_ timestamp: Double) { + var t = CGFloat((timestamp - self.startTime) / self.duration) + let ct: CGFloat + if t <= 0.0 + CGFloat.ulpOfOne { + t = 0.0 + ct = 0.0 + } else if t >= 1.0 - CGFloat.ulpOfOne { + t = 1.0 + ct = 1.0 + } else { + ct = self.curve(t) + } + self.update(ct, self.valueAt(ct)) + } +} diff --git a/submodules/Display/Display/ListViewFloatingHeaderNode.swift b/submodules/Display/Display/ListViewFloatingHeaderNode.swift new file mode 100644 index 0000000000..cb77020b30 --- /dev/null +++ b/submodules/Display/Display/ListViewFloatingHeaderNode.swift @@ -0,0 +1,9 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +open class ListViewFloatingHeaderNode: ASDisplayNode { + open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + return 0.0 + } +} diff --git a/submodules/Display/Display/ListViewIntermediateState.swift b/submodules/Display/Display/ListViewIntermediateState.swift new file mode 100644 index 0000000000..30733d3ebf --- /dev/null +++ b/submodules/Display/Display/ListViewIntermediateState.swift @@ -0,0 +1,868 @@ +import Foundation +import UIKit +import SwiftSignalKit + +public enum ListViewCenterScrollPositionOverflow { + case top + case bottom +} + +public enum ListViewScrollPosition: Equatable { + case top(CGFloat) + case bottom(CGFloat) + case center(ListViewCenterScrollPositionOverflow) + case visible +} + +public enum ListViewScrollToItemDirectionHint { + case Up + case Down +} + +public enum ListViewAnimationCurve { + case Spring(duration: Double) + case Default(duration: Double?) +} + +public struct ListViewScrollToItem { + public let index: Int + public let position: ListViewScrollPosition + public let animated: Bool + public let curve: ListViewAnimationCurve + public let directionHint: ListViewScrollToItemDirectionHint + + public init(index: Int, position: ListViewScrollPosition, animated: Bool, curve: ListViewAnimationCurve, directionHint: ListViewScrollToItemDirectionHint) { + self.index = index + self.position = position + self.animated = animated + self.curve = curve + self.directionHint = directionHint + } +} + +public enum ListViewItemOperationDirectionHint { + case Up + case Down +} + +public struct ListViewDeleteItem { + public let index: Int + public let directionHint: ListViewItemOperationDirectionHint? + + public init(index: Int, directionHint: ListViewItemOperationDirectionHint?) { + self.index = index + self.directionHint = directionHint + } +} + +public struct ListViewInsertItem { + public let index: Int + public let previousIndex: Int? + public let item: ListViewItem + public let directionHint: ListViewItemOperationDirectionHint? + public let forceAnimateInsertion: Bool + + public init(index: Int, previousIndex: Int?, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?, forceAnimateInsertion: Bool = false) { + self.index = index + self.previousIndex = previousIndex + self.item = item + self.directionHint = directionHint + self.forceAnimateInsertion = forceAnimateInsertion + } +} + +public struct ListViewUpdateItem { + public let index: Int + public let previousIndex: Int + public let item: ListViewItem + public let directionHint: ListViewItemOperationDirectionHint? + + public init(index: Int, previousIndex: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) { + self.index = index + self.previousIndex = previousIndex + self.item = item + self.directionHint = directionHint + } +} + +public struct ListViewDeleteAndInsertOptions: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let AnimateInsertion = ListViewDeleteAndInsertOptions(rawValue: 1) + public static let AnimateAlpha = ListViewDeleteAndInsertOptions(rawValue: 2) + public static let LowLatency = ListViewDeleteAndInsertOptions(rawValue: 4) + public static let Synchronous = ListViewDeleteAndInsertOptions(rawValue: 8) + public static let RequestItemInsertionAnimations = ListViewDeleteAndInsertOptions(rawValue: 16) + public static let AnimateTopItemPosition = ListViewDeleteAndInsertOptions(rawValue: 32) + public static let PreferSynchronousDrawing = ListViewDeleteAndInsertOptions(rawValue: 64) + public static let PreferSynchronousResourceLoading = ListViewDeleteAndInsertOptions(rawValue: 128) + public static let AnimateCrossfade = ListViewDeleteAndInsertOptions(rawValue: 256) +} + +public struct ListViewUpdateSizeAndInsets { + public let size: CGSize + public let insets: UIEdgeInsets + public let headerInsets: UIEdgeInsets? + public let scrollIndicatorInsets: UIEdgeInsets? + public let duration: Double + public let curve: ListViewAnimationCurve + public let ensureTopInsetForOverlayHighlightedItems: CGFloat? + + public init(size: CGSize, insets: UIEdgeInsets, headerInsets: UIEdgeInsets? = nil, scrollIndicatorInsets: UIEdgeInsets? = nil, duration: Double, curve: ListViewAnimationCurve, ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil) { + self.size = size + self.insets = insets + self.headerInsets = headerInsets + self.scrollIndicatorInsets = scrollIndicatorInsets + self.duration = duration + self.curve = curve + self.ensureTopInsetForOverlayHighlightedItems = ensureTopInsetForOverlayHighlightedItems + } +} + +public struct ListViewItemRange: Equatable { + public let firstIndex: Int + public let lastIndex: Int +} + +public struct ListViewVisibleItemRange: Equatable { + public let firstIndex: Int + public let firstIndexFullyVisible: Bool + public let lastIndex: Int +} + +public struct ListViewDisplayedItemRange: Equatable { + public let loadedRange: ListViewItemRange? + public let visibleRange: ListViewVisibleItemRange? +} + +struct IndexRange { + let first: Int + let last: Int + + func contains(_ index: Int) -> Bool { + return index >= first && index <= last + } + + var empty: Bool { + return first > last + } +} + +struct OffsetRanges { + var offsets: [(IndexRange, CGFloat)] = [] + + mutating func append(_ other: OffsetRanges) { + self.offsets.append(contentsOf: other.offsets) + } + + mutating func offset(_ indexRange: IndexRange, offset: CGFloat) { + self.offsets.append((indexRange, offset)) + } + + func offsetForIndex(_ index: Int) -> CGFloat { + var result: CGFloat = 0.0 + for offset in self.offsets { + if offset.0.contains(index) { + result += offset.1 + } + } + return result + } +} + +func binarySearch(_ inputArr: [Int], searchItem: Int) -> Int? { + var lowerIndex = 0; + var upperIndex = inputArr.count - 1 + + if lowerIndex > upperIndex { + return nil + } + + while (true) { + let currentIndex = (lowerIndex + upperIndex) / 2 + if (inputArr[currentIndex] == searchItem) { + return currentIndex + } else if (lowerIndex > upperIndex) { + return nil + } else { + if (inputArr[currentIndex] > searchItem) { + upperIndex = currentIndex - 1 + } else { + lowerIndex = currentIndex + 1 + } + } + } +} + +struct TransactionState { + let visibleSize: CGSize + let items: [ListViewItem] +} + +struct PendingNode { + let index: Int + let node: QueueLocalObject + let apply: () -> (Signal?, () -> Void) + let frame: CGRect + let apparentHeight: CGFloat +} + +enum ListViewStateNode { + case Node(index: Int, frame: CGRect, referenceNode: QueueLocalObject?) + case Placeholder(frame: CGRect) + + var index: Int? { + switch self { + case .Node(let index, _, _): + return index + case .Placeholder(_): + return nil + } + } + + var frame: CGRect { + get { + switch self { + case .Node(_, let frame, _): + return frame + case .Placeholder(let frame): + return frame + } + } set(value) { + switch self { + case let .Node(index, _, referenceNode): + self = .Node(index: index, frame: value, referenceNode: referenceNode) + case .Placeholder(_): + self = .Placeholder(frame: value) + } + } + } +} + +enum ListViewInsertionOffsetDirection { + case Up + case Down + + init(_ hint: ListViewItemOperationDirectionHint) { + switch hint { + case .Up: + self = .Up + case .Down: + self = .Down + } + } + + func inverted() -> ListViewInsertionOffsetDirection { + switch self { + case .Up: + return .Down + case .Down: + return .Up + } + } +} + +struct ListViewInsertionPoint { + let index: Int + let point: CGPoint + let direction: ListViewInsertionOffsetDirection +} + +struct ListViewState { + var insets: UIEdgeInsets + var visibleSize: CGSize + let invisibleInset: CGFloat + var nodes: [ListViewStateNode] + var scrollPosition: (Int, ListViewScrollPosition)? + var stationaryOffset: (Int, CGFloat)? + let stackFromBottom: Bool + + mutating func fixScrollPosition(_ itemCount: Int) { + if let (fixedIndex, fixedPosition) = self.scrollPosition { + for node in self.nodes { + if let index = node.index, index == fixedIndex { + let offset: CGFloat + switch fixedPosition { + case let .bottom(additionalOffset): + offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + additionalOffset + case let .top(additionalOffset): + offset = self.insets.top - node.frame.minY + additionalOffset + case let .center(overflow): + let contentAreaHeight = self.visibleSize.height - self.insets.bottom - self.insets.top + if node.frame.size.height <= contentAreaHeight + CGFloat.ulpOfOne { + offset = self.insets.top + floor((contentAreaHeight - node.frame.size.height) / 2.0) - node.frame.minY + } else { + switch overflow { + case .top: + offset = self.insets.top - node.frame.minY + case .bottom: + offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + } + } + case .visible: + if node.frame.maxY > self.visibleSize.height - self.insets.bottom { + offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + } else if node.frame.minY < self.insets.top { + offset = self.insets.top - node.frame.minY + } else { + offset = 0.0 + } + } + + var minY: CGFloat = CGFloat.greatestFiniteMagnitude + var maxY: CGFloat = 0.0 + for i in 0 ..< self.nodes.count { + var frame = self.nodes[i].frame + frame = frame.offsetBy(dx: 0.0, dy: offset) + self.nodes[i].frame = frame + + minY = min(minY, frame.minY) + maxY = max(maxY, frame.maxY) + } + + var additionalOffset: CGFloat = 0.0 + if minY > self.insets.top { + additionalOffset = self.insets.top - minY + } + + if abs(additionalOffset) > CGFloat.ulpOfOne { + for i in 0 ..< self.nodes.count { + var frame = self.nodes[i].frame + frame = frame.offsetBy(dx: 0.0, dy: additionalOffset) + self.nodes[i].frame = frame + } + } + + self.snapToBounds(itemCount, snapTopItem: true, stackFromBottom: self.stackFromBottom) + + break + } + } + } else if let (stationaryIndex, stationaryOffset) = self.stationaryOffset { + for node in self.nodes { + if node.index == stationaryIndex { + let offset = stationaryOffset - node.frame.minY + + if abs(offset) > CGFloat.ulpOfOne { + for i in 0 ..< self.nodes.count { + var frame = self.nodes[i].frame + frame = frame.offsetBy(dx: 0.0, dy: offset) + self.nodes[i].frame = frame + } + } + + break + } + } + } + } + + mutating func setupStationaryOffset(_ index: Int, boundary: Int, frames: [Int: CGRect]) { + if index < boundary { + for node in self.nodes { + if let nodeIndex = node.index , nodeIndex >= index { + if let frame = frames[nodeIndex] { + self.stationaryOffset = (nodeIndex, frame.minY) + break + } + } + } + } else { + for node in self.nodes.reversed() { + if let nodeIndex = node.index , nodeIndex <= index { + if let frame = frames[nodeIndex] { + self.stationaryOffset = (nodeIndex, frame.minY) + break + } + } + } + } + } + + mutating func snapToBounds(_ itemCount: Int, snapTopItem: Bool, stackFromBottom: Bool) { + var completeHeight: CGFloat = 0.0 + var topItemFound = false + var bottomItemFound = false + var topItemEdge: CGFloat = 0.0 + var bottomItemEdge: CGFloat = 0.0 + + for node in self.nodes { + if let index = node.index { + if index == 0 { + topItemFound = true + topItemEdge = node.frame.minY + } + break + } + } + + for node in self.nodes.reversed() { + if let index = node.index { + if index == itemCount - 1 { + bottomItemFound = true + bottomItemEdge = node.frame.maxY + } + break + } + } + + if topItemFound && bottomItemFound { + for node in self.nodes { + completeHeight += node.frame.size.height + } + } + + let overscroll: CGFloat = 0.0 + + var offset: CGFloat = 0.0 + if topItemFound && bottomItemFound { + let areaHeight = min(completeHeight, self.visibleSize.height - self.insets.bottom - self.insets.top) + if bottomItemEdge < self.insets.top + areaHeight - overscroll { + offset = self.insets.top + areaHeight - overscroll - bottomItemEdge + } else if topItemEdge > self.insets.top - overscroll && snapTopItem { + offset = (self.insets.top - overscroll) - topItemEdge + } + } else if topItemFound { + if topItemEdge > self.insets.top - overscroll && snapTopItem { + offset = (self.insets.top - overscroll) - topItemEdge + } + } else if bottomItemFound { + if bottomItemEdge < self.visibleSize.height - self.insets.bottom - overscroll { + offset = self.visibleSize.height - self.insets.bottom - overscroll - bottomItemEdge + } + } + + if abs(offset) > CGFloat.ulpOfOne { + for i in 0 ..< self.nodes.count { + var frame = self.nodes[i].frame + frame.origin.y += offset + self.nodes[i].frame = frame + } + } + } + + func insertionPoint(_ insertDirectionHints: [Int: ListViewItemOperationDirectionHint], itemCount: Int) -> ListViewInsertionPoint? { + var fixedNode: (nodeIndex: Int, index: Int, frame: CGRect)? + + if let (fixedIndex, _) = self.scrollPosition { + for i in 0 ..< self.nodes.count { + let node = self.nodes[i] + if let index = node.index , index == fixedIndex { + fixedNode = (i, index, node.frame) + break + } + } + + if fixedNode == nil { + return ListViewInsertionPoint(index: fixedIndex, point: CGPoint(), direction: .Down) + } + } + + var fixedNodeIsStationary = false + if fixedNode == nil { + if let (fixedIndex, _) = self.stationaryOffset { + for i in 0 ..< self.nodes.count { + let node = self.nodes[i] + if let index = node.index , index == fixedIndex { + fixedNode = (i, index, node.frame) + fixedNodeIsStationary = true + break + } + } + } + } + + if fixedNode == nil { + for i in 0 ..< self.nodes.count { + let node = self.nodes[i] + if let index = node.index , node.frame.maxY >= self.insets.top { + fixedNode = (i, index, node.frame) + break + } + } + } + + if fixedNode == nil && self.nodes.count != 0 { + for i in (0 ..< self.nodes.count).reversed() { + let node = self.nodes[i] + if let index = node.index { + fixedNode = (i, index, node.frame) + break + } + } + } + + if let fixedNode = fixedNode { + var currentUpperNode = fixedNode + for i in (0 ..< fixedNode.nodeIndex).reversed() { + let node = self.nodes[i] + if let index = node.index { + if index != currentUpperNode.index - 1 { + if currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne { + var directionHint: ListViewInsertionOffsetDirection? + if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.insets.top - CGFloat.ulpOfOne { + directionHint = ListViewInsertionOffsetDirection(hint) + } + return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up) + } else { + break + } + } + currentUpperNode = (i, index, node.frame) + } + } + + if currentUpperNode.index != 0 && currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne { + var directionHint: ListViewInsertionOffsetDirection? + if let hint = insertDirectionHints[currentUpperNode.index - 1] { + if currentUpperNode.frame.minY >= self.insets.top - CGFloat.ulpOfOne { + directionHint = ListViewInsertionOffsetDirection(hint) + } + } else if currentUpperNode.frame.minY >= self.insets.top - CGFloat.ulpOfOne && !fixedNodeIsStationary { + directionHint = .Down + } + + return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up) + } + + var currentLowerNode = fixedNode + if fixedNode.nodeIndex + 1 < self.nodes.count { + for i in (fixedNode.nodeIndex + 1) ..< self.nodes.count { + let node = self.nodes[i] + if let index = node.index { + if index != currentLowerNode.index + 1 { + if currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne { + var directionHint: ListViewInsertionOffsetDirection? + if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne { + directionHint = ListViewInsertionOffsetDirection(hint) + } + return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down) + } else { + break + } + } + currentLowerNode = (i, index, node.frame) + } + } + } + + if currentLowerNode.index != itemCount - 1 && currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne { + var directionHint: ListViewInsertionOffsetDirection? + if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne { + directionHint = ListViewInsertionOffsetDirection(hint) + } + return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down) + } + } else if itemCount != 0 { + return ListViewInsertionPoint(index: 0, point: CGPoint(x: 0.0, y: self.insets.top), direction: .Down) + } + + return nil + } + + mutating func removeInvisibleNodes(_ operations: inout [ListViewStateOperation]) { + var i = 0 + var visibleItemNodeHeight: CGFloat = 0.0 + while i < self.nodes.count { + visibleItemNodeHeight += self.nodes[i].frame.height + i += 1 + } + + if visibleItemNodeHeight > (self.visibleSize.height + self.invisibleInset + self.invisibleInset) { + i = self.nodes.count - 1 + while i >= 0 { + let itemNode = self.nodes[i] + let frame = itemNode.frame + //print("node \(i) frame \(frame)") + if frame.maxY < -self.invisibleInset || frame.origin.y > self.visibleSize.height + self.invisibleInset { + //print("remove invisible 1 \(i) frame \(frame)") + operations.append(.Remove(index: i, offsetDirection: frame.maxY < -self.invisibleInset ? .Down : .Up)) + self.nodes.remove(at: i) + } + + i -= 1 + } + } + + let upperBound = -self.invisibleInset + CGFloat.ulpOfOne + for i in 0 ..< self.nodes.count { + let node = self.nodes[i] + if let index = node.index , node.frame.maxY > upperBound { + if i != 0 { + var previousIndex = index + for j in (0 ..< i).reversed() { + if self.nodes[j].frame.maxY < upperBound { + if let index = self.nodes[j].index { + if index != previousIndex - 1 { + //print("remove monotonity \(j) (\(index))") + operations.append(.Remove(index: j, offsetDirection: .Down)) + self.nodes.remove(at: j) + } else { + previousIndex = index + } + } + } + } + } + break + } + } + + let lowerBound = self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne + for i in (0 ..< self.nodes.count).reversed() { + let node = self.nodes[i] + if let index = node.index , node.frame.minY < lowerBound { + if i != self.nodes.count - 1 { + var previousIndex = index + var removeIndices: [Int] = [] + for j in (i + 1) ..< self.nodes.count { + if self.nodes[j].frame.minY > lowerBound { + if let index = self.nodes[j].index { + if index != previousIndex + 1 { + removeIndices.append(j) + } else { + previousIndex = index + } + } + } + } + if !removeIndices.isEmpty { + for i in removeIndices.reversed() { + //print("remove monotonity \(i) (\(self.nodes[i].index!))") + operations.append(.Remove(index: i, offsetDirection: .Up)) + self.nodes.remove(at: i) + } + } + } + break + } + } + } + + func nodeInsertionPointAndIndex(_ itemIndex: Int) -> (CGPoint, Int) { + if self.nodes.count == 0 { + return (CGPoint(x: 0.0, y: self.insets.top), 0) + } else { + var index = 0 + var lastNodeWithIndex = -1 + for node in self.nodes { + if let nodeItemIndex = node.index { + if nodeItemIndex > itemIndex { + break + } + lastNodeWithIndex = index + } + index += 1 + } + lastNodeWithIndex += 1 + return (CGPoint(x: 0.0, y: lastNodeWithIndex == 0 ? self.nodes[0].frame.minY : self.nodes[lastNodeWithIndex - 1].frame.maxY), lastNodeWithIndex) + } + } + + func continuousHeightRelativeToNodeIndex(_ fixedNodeIndex: Int) -> CGFloat { + let fixedIndex = self.nodes[fixedNodeIndex].index! + + var height: CGFloat = 0.0 + + if fixedNodeIndex != 0 { + var upperIndex = fixedIndex + for i in (0 ..< fixedNodeIndex).reversed() { + if let index = self.nodes[i].index { + if index == upperIndex - 1 { + height += self.nodes[i].frame.size.height + upperIndex = index + } else { + break + } + } + } + } + + if fixedNodeIndex != self.nodes.count - 1 { + var lowerIndex = fixedIndex + for i in (fixedNodeIndex + 1) ..< self.nodes.count { + if let index = self.nodes[i].index { + if index == lowerIndex + 1 { + height += self.nodes[i].frame.size.height + lowerIndex = index + } else { + break + } + } + } + } + + return height + } + + mutating func insertNode(_ itemIndex: Int, node: QueueLocalObject, layout: ListViewItemNodeLayout, apply: @escaping () -> (Signal?, (ListViewItemApply) -> Void), offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, operations: inout [ListViewStateOperation], itemCount: Int) { + let (insertionOrigin, insertionIndex) = self.nodeInsertionPointAndIndex(itemIndex) + + let nodeOrigin: CGPoint + switch offsetDirection { + case .Up: + nodeOrigin = CGPoint(x: insertionOrigin.x, y: insertionOrigin.y - (animated ? 0.0 : layout.size.height)) + case .Down: + nodeOrigin = insertionOrigin + } + + let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: animated ? 0.0 : layout.size.height)) + + operations.append(.InsertNode(index: insertionIndex, offsetDirection: offsetDirection, animated: animated, node: node, layout: layout, apply: apply)) + self.nodes.insert(.Node(index: itemIndex, frame: nodeFrame, referenceNode: nil), at: insertionIndex) + + if !animated { + switch offsetDirection { + case .Up: + var i = insertionIndex - 1 + while i >= 0 { + var frame = self.nodes[i].frame + frame.origin.y -= nodeFrame.size.height + self.nodes[i].frame = frame + i -= 1 + } + case .Down: + var i = insertionIndex + 1 + while i < self.nodes.count { + var frame = self.nodes[i].frame + frame.origin.y += nodeFrame.size.height + self.nodes[i].frame = frame + i += 1 + } + } + } + + var previousIndex: Int? + for node in self.nodes { + if let index = node.index { + if let currentPreviousIndex = previousIndex { + if index <= currentPreviousIndex { + print("index <= previousIndex + 1") + break + } + previousIndex = index + } else { + previousIndex = index + } + } + } + + if let _ = self.scrollPosition { + self.fixScrollPosition(itemCount) + } + } + + mutating func removeNodeAtIndex(_ index: Int, direction: ListViewItemOperationDirectionHint?, animated: Bool, operations: inout [ListViewStateOperation]) { + let node = self.nodes[index] + if case let .Node(_, _, referenceNode) = node { + let nodeFrame = node.frame + self.nodes.remove(at: index) + let offsetDirection: ListViewInsertionOffsetDirection + if let direction = direction { + offsetDirection = ListViewInsertionOffsetDirection(direction) + } else { + if nodeFrame.maxY < self.insets.top + CGFloat.ulpOfOne { + offsetDirection = .Down + } else { + offsetDirection = .Up + } + } + operations.append(.Remove(index: index, offsetDirection: offsetDirection)) + + if let referenceNode = referenceNode , animated { + self.nodes.insert(.Placeholder(frame: nodeFrame), at: index) + operations.append(.InsertDisappearingPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted())) + } else { + if nodeFrame.maxY > self.insets.top - CGFloat.ulpOfOne { + if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne { + for i in (0 ..< index).reversed() { + var frame = self.nodes[i].frame + frame.origin.y += nodeFrame.size.height + self.nodes[i].frame = frame + } + } else { + for i in index ..< self.nodes.count { + var frame = self.nodes[i].frame + frame.origin.y -= nodeFrame.size.height + self.nodes[i].frame = frame + } + } + } else if index != 0 { + for i in (0 ..< index).reversed() { + var frame = self.nodes[i].frame + frame.origin.y += nodeFrame.size.height + self.nodes[i].frame = frame + } + } + } + } else { + assertionFailure() + } + } + + mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, animation: ListViewItemUpdateAnimation, apply: @escaping () -> (Signal?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) { + var i = -1 + for node in self.nodes { + i += 1 + if node.index == itemIndex { + switch animation { + case .None: + let offsetDirection: ListViewInsertionOffsetDirection + if let direction = direction { + offsetDirection = ListViewInsertionOffsetDirection(direction) + } else { + if node.frame.maxY < self.insets.top + CGFloat.ulpOfOne { + offsetDirection = .Down + } else { + offsetDirection = .Up + } + } + + switch offsetDirection { + case .Up: + let offsetDelta = -(layout.size.height - node.frame.size.height) + var updatedFrame = node.frame + updatedFrame.origin.y += offsetDelta + updatedFrame.size.height = layout.size.height + self.nodes[i].frame = updatedFrame + + for j in 0 ..< i { + var frame = self.nodes[j].frame + frame.origin.y += offsetDelta + self.nodes[j].frame = frame + } + case .Down: + let offsetDelta = layout.size.height - node.frame.size.height + var updatedFrame = node.frame + updatedFrame.size.height = layout.size.height + self.nodes[i].frame = updatedFrame + + for j in i + 1 ..< self.nodes.count { + var frame = self.nodes[j].frame + frame.origin.y += offsetDelta + self.nodes[j].frame = frame + } + } + + operations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) + case .System: + operations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) + } + + break + } + } + } +} + +enum ListViewStateOperation { + case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, node: QueueLocalObject, layout: ListViewItemNodeLayout, apply: () -> (Signal?, (ListViewItemApply) -> Void)) + case InsertDisappearingPlaceholder(index: Int, referenceNode: QueueLocalObject, offsetDirection: ListViewInsertionOffsetDirection) + case Remove(index: Int, offsetDirection: ListViewInsertionOffsetDirection) + case Remap([Int: Int]) + case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> (Signal?, (ListViewItemApply) -> Void)) +} diff --git a/submodules/Display/Display/ListViewItem.swift b/submodules/Display/Display/ListViewItem.swift new file mode 100644 index 0000000000..9ca2c287c6 --- /dev/null +++ b/submodules/Display/Display/ListViewItem.swift @@ -0,0 +1,71 @@ +import Foundation +import UIKit +import SwiftSignalKit + +public enum ListViewItemUpdateAnimation { + case None + case System(duration: Double) + + public var isAnimated: Bool { + if case .None = self { + return false + } else { + return true + } + } +} + +public struct ListViewItemConfigureNodeFlags: OptionSet { + public var rawValue: Int32 + + public init() { + self.rawValue = 0 + } + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let preferSynchronousResourceLoading = ListViewItemConfigureNodeFlags(rawValue: 1 << 0) +} + +public struct ListViewItemApply { + public let isOnScreen: Bool + + public init(isOnScreen: Bool) { + self.isOnScreen = isOnScreen + } +} + +public protocol ListViewItem { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) + 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) + + var accessoryItem: ListViewAccessoryItem? { get } + var headerAccessoryItem: ListViewAccessoryItem? { get } + var selectable: Bool { get } + var approximateHeight: CGFloat { get } + + func selected(listView: ListView) +} + +public extension ListViewItem { + var accessoryItem: ListViewAccessoryItem? { + return nil + } + + var headerAccessoryItem: ListViewAccessoryItem? { + return nil + } + + var selectable: Bool { + return false + } + + var approximateHeight: CGFloat { + return 44.0 + } + + func selected(listView: ListView) { + } +} diff --git a/submodules/Display/Display/ListViewItemHeader.swift b/submodules/Display/Display/ListViewItemHeader.swift new file mode 100644 index 0000000000..17bd083862 --- /dev/null +++ b/submodules/Display/Display/ListViewItemHeader.swift @@ -0,0 +1,152 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +#if BUCK +import DisplayPrivate +#endif + +public enum ListViewItemHeaderStickDirection { + case top + case bottom +} + +public typealias ListViewItemHeaderId = Int64 + +public protocol ListViewItemHeader: class { + var id: ListViewItemHeaderId { get } + var stickDirection: ListViewItemHeaderStickDirection { get } + var height: CGFloat { get } + + func node() -> ListViewItemHeaderNode +} + +open class ListViewItemHeaderNode: ASDisplayNode { + private final var spring: ListViewItemSpring? + let wantsScrollDynamics: Bool + let isRotated: Bool + final private(set) var internalStickLocationDistanceFactor: CGFloat = 0.0 + final var internalStickLocationDistance: CGFloat = 0.0 + private var isFlashingOnScrolling = false + + func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) { + self.internalStickLocationDistanceFactor = factor + } + + final func updateFlashingOnScrollingInternal(_ isFlashingOnScrolling: Bool, animated: Bool) { + if self.isFlashingOnScrolling != isFlashingOnScrolling { + self.isFlashingOnScrolling = isFlashingOnScrolling + self.updateFlashingOnScrolling(isFlashingOnScrolling, animated: animated) + } + } + + open func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { + } + + public init(layerBacked: Bool = false, dynamicBounce: Bool = false, isRotated: Bool = false, seeThrough: Bool = false) { + self.wantsScrollDynamics = dynamicBounce + self.isRotated = isRotated + if dynamicBounce { + self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85) + } + + if seeThrough { + if (layerBacked) { + super.init() + self.setLayerBlock({ + return CASeeThroughTracingLayer() + }) + } else { + super.init() + + self.setViewBlock({ + return CASeeThroughTracingView() + }) + } + } else { + super.init() + + self.isLayerBacked = layerBacked + } + } + + open func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + } + + final func addScrollingOffset(_ scrollingOffset: CGFloat) { + if self.spring != nil && internalStickLocationDistanceFactor.isZero { + let bounds = self.bounds + self.bounds = CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + scrollingOffset), size: bounds.size) + } + } + + public func animate(_ timestamp: Double) -> Bool { + var continueAnimations = false + + if let _ = self.spring { + let bounds = self.bounds + var offset = bounds.origin.y + let currentOffset = offset + + let frictionConstant: CGFloat = testSpringFriction + let springConstant: CGFloat = testSpringConstant + let time: CGFloat = 1.0 / 60.0 + + // friction force = velocity * friction constant + let frictionForce = self.spring!.velocity * frictionConstant + // spring force = (target point - current position) * spring constant + let springForce = -currentOffset * springConstant + // force = spring force - friction force + let force = springForce - frictionForce + + // velocity = current velocity + force * time / mass + self.spring!.velocity = self.spring!.velocity + force * time + // position = current position + velocity * time + offset = currentOffset + self.spring!.velocity * time + + offset = offset.isNaN ? 0.0 : offset + + let epsilon: CGFloat = 0.1 + if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon { + offset = 0.0 + self.spring!.velocity = 0.0 + } else { + continueAnimations = true + } + + if abs(offset) > 250.0 { + offset = offset < 0.0 ? -250.0 : 250.0 + } + + self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: bounds.size) + } + + return continueAnimations + } + + open func animateRemoved(duration: Double) { + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false) + } + + private var cachedLayout: (CGSize, CGFloat, CGFloat)? + + func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + var update = false + if let cachedLayout = self.cachedLayout { + if cachedLayout.0 != size || cachedLayout.1 != leftInset || cachedLayout.2 != rightInset { + update = true + } + } else { + update = true + } + if update { + self.cachedLayout = (size, leftInset, rightInset) + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) + } + } + + open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + } +} diff --git a/submodules/Display/Display/ListViewItemNode.swift b/submodules/Display/Display/ListViewItemNode.swift new file mode 100644 index 0000000000..64171bc291 --- /dev/null +++ b/submodules/Display/Display/ListViewItemNode.swift @@ -0,0 +1,549 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +#if BUCK +import DisplayPrivate +#endif + +var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0) +var testSpringFriction: CGFloat = 31.8211269378662 + +var testSpringConstantLimits: (CGFloat, CGFloat) = (3.0, 450.0) +var testSpringConstant: CGFloat = 443.704223632812 + +var testSpringResistanceFreeLimits: (CGFloat, CGFloat) = (0.05, 1.0) +var testSpringFreeResistance: CGFloat = 0.676197171211243 + +var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0) +var testSpringScrollingResistance: CGFloat = 0.6721 + +struct ListViewItemSpring { + let stiffness: CGFloat + let damping: CGFloat + let mass: CGFloat + var velocity: CGFloat = 0.0 + + init(stiffness: CGFloat, damping: CGFloat, mass: CGFloat) { + self.stiffness = stiffness + self.damping = damping + self.mass = mass + } +} + +public struct ListViewItemNodeLayout { + public let contentSize: CGSize + public let insets: UIEdgeInsets + + public init() { + self.contentSize = CGSize() + self.insets = UIEdgeInsets() + } + + public init(contentSize: CGSize, insets: UIEdgeInsets) { + self.contentSize = contentSize + self.insets = insets + } + + public var size: CGSize { + return CGSize(width: self.contentSize.width + self.insets.left + self.insets.right, height: self.contentSize.height + self.insets.top + self.insets.bottom) + } +} + +public enum ListViewItemNodeVisibility: Equatable { + case none + case visible(CGFloat) + + public static func ==(lhs: ListViewItemNodeVisibility, rhs: ListViewItemNodeVisibility) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .visible(fraction): + if case .visible(fraction) = rhs { + return true + } else { + return false + } + } + } +} + +public struct ListViewItemLayoutParams { + public let width: CGFloat + public let leftInset: CGFloat + public let rightInset: CGFloat + + public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat) { + self.width = width + self.leftInset = leftInset + self.rightInset = rightInset + } +} + +open class ListViewItemNode: ASDisplayNode { + let rotated: Bool + final var index: Int? + + public var isHighlightedInOverlay: Bool = false + + public private(set) var accessoryItemNode: ListViewAccessoryItemNode? + + func setAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode?, leftInset: CGFloat, rightInset: CGFloat) { + self.accessoryItemNode = accessoryItemNode + if let accessoryItemNode = accessoryItemNode { + self.layoutAccessoryItemNode(accessoryItemNode, leftInset: leftInset, rightInset: rightInset) + } + } + + final var headerAccessoryItemNode: ListViewAccessoryItemNode? { + didSet { + if let headerAccessoryItemNode = self.headerAccessoryItemNode { + self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode) + } + } + } + + private final var spring: ListViewItemSpring? + private final var animations: [(String, ListViewAnimation)] = [] + + final let wantsScrollDynamics: Bool + + public final var wantsTrailingItemSpaceUpdates: Bool = false + + public final var scrollPositioningInsets: UIEdgeInsets = UIEdgeInsets() + + public final var canBeUsedAsScrollToItemAnchor: Bool = true + + open var visibility: ListViewItemNodeVisibility = .none + + open var canBeSelected: Bool { + return true + } + + open var canBeLongTapped: Bool { + return false + } + + open var preventsTouchesToOtherItems: Bool { + return false + } + + open func touchesToOtherItemsPrevented() { + + } + + open func tapped() { + } + + open func longTapped() { + } + + public final var insets: UIEdgeInsets = UIEdgeInsets() { + didSet { + let effectiveInsets = self.insets + self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: self.contentSize.width, height: self.contentSize.height + effectiveInsets.top + effectiveInsets.bottom)) + let bounds = self.bounds + self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size) + } + } + + private final var _contentSize: CGSize = CGSize() + public final var contentSize: CGSize { + get { + return self._contentSize + } set(value) { + let effectiveInsets = self.insets + self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: value.width, height: value.height + effectiveInsets.top + effectiveInsets.bottom)) + } + } + + private var contentOffset: CGFloat = 0.0 { + didSet { + let effectiveInsets = self.insets + let bounds = self.bounds + self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size) + } + } + + public var transitionOffset: CGFloat = 0.0 { + didSet { + let effectiveInsets = self.insets + let bounds = self.bounds + self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size) + } + } + + public var layout: ListViewItemNodeLayout { + var insets = self.insets + var contentSize = self.contentSize + + if let animation = self.animationForKey("insets") { + insets = animation.to as! UIEdgeInsets + } + + if let animation = self.animationForKey("apparentHeight") { + contentSize.height = (animation.to as! CGFloat) - insets.top - insets.bottom + } + + return ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + } + + public var displayResourcesReady: Signal { + return .complete() + } + + public init(layerBacked: Bool, dynamicBounce: Bool = true, rotated: Bool = false, seeThrough: Bool = false) { + if dynamicBounce { + self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85) + } + self.wantsScrollDynamics = dynamicBounce + + self.rotated = rotated + + if seeThrough { + if (layerBacked) { + super.init() + self.setLayerBlock({ + return CASeeThroughTracingLayer() + }) + } else { + super.init() + + self.setViewBlock({ + return CASeeThroughTracingView() + }) + } + } else { + super.init() + + self.isLayerBacked = layerBacked + } + } + + var apparentHeight: CGFloat = 0.0 + private var _bounds: CGRect = CGRect() + private var _position: CGPoint = CGPoint() + + open override var frame: CGRect { + get { + return CGRect(origin: CGPoint(x: self._position.x - self._bounds.width / 2.0, y: self._position.y - self._bounds.height / 2.0), size: self._bounds.size) + } set(value) { + let previousSize = self._bounds.size + + super.frame = value + self._bounds.size = value.size + self._position = CGPoint(x: value.midX, y: value.midY) + let effectiveInsets = self.insets + self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom) + + if previousSize != value.size { + if let headerAccessoryItemNode = self.headerAccessoryItemNode { + self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode) + } + } + } + } + + open override var bounds: CGRect { + get { + return self._bounds + } set(value) { + let previousSize = self._bounds.size + + super.bounds = value + self._bounds = value + let effectiveInsets = self.insets + self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom) + + if previousSize != value.size { + if let headerAccessoryItemNode = self.headerAccessoryItemNode { + self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode) + } + } + } + } + + public var contentBounds: CGRect { + let bounds = self.bounds + let effectiveInsets = self.insets + return CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + effectiveInsets.top), size: CGSize(width: bounds.size.width, height: bounds.size.height - effectiveInsets.top - effectiveInsets.bottom)) + } + + open override var position: CGPoint { + get { + return self._position + } set(value) { + super.position = value + self._position = value + } + } + + public final var apparentFrame: CGRect { + var frame = self.frame + frame.size.height = self.apparentHeight + return frame + } + + public final var apparentContentFrame: CGRect { + var frame = self.frame + let insets = self.insets + frame.origin.y += insets.top + frame.size.height = self.apparentHeight - insets.top - insets.bottom + return frame + } + + public final var apparentBounds: CGRect { + var bounds = self.bounds + bounds.size.height = self.apparentHeight + return bounds + } + + open func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) { + } + + open func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + } + + open func reuse() { + } + + final func addScrollingOffset(_ scrollingOffset: CGFloat) { + if self.spring != nil { + self.contentOffset += scrollingOffset + } + } + + func initializeDynamicsFromSibling(_ itemView: ListViewItemNode, additionalOffset: CGFloat) { + if let itemViewSpring = itemView.spring { + self.contentOffset = itemView.contentOffset + additionalOffset + self.spring?.velocity = itemViewSpring.velocity + } + } + + public func animate(_ timestamp: Double) -> Bool { + var continueAnimations = false + + if let _ = self.spring { + var offset = self.contentOffset + + let frictionConstant: CGFloat = testSpringFriction + let springConstant: CGFloat = testSpringConstant + let time: CGFloat = 1.0 / 60.0 + + // friction force = velocity * friction constant + let frictionForce = self.spring!.velocity * frictionConstant + // spring force = (target point - current position) * spring constant + let springForce = -self.contentOffset * springConstant + // force = spring force - friction force + let force = springForce - frictionForce + + // velocity = current velocity + force * time / mass + self.spring!.velocity = self.spring!.velocity + force * time + // position = current position + velocity * time + offset = self.contentOffset + self.spring!.velocity * time + + offset = offset.isNaN ? 0.0 : offset + + let epsilon: CGFloat = 0.1 + if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon { + offset = 0.0 + self.spring!.velocity = 0.0 + } else { + continueAnimations = true + } + + if abs(offset) > 250.0 { + offset = offset < 0.0 ? -250.0 : 250.0 + } + self.contentOffset = offset + } + + var i = 0 + var animationCount = self.animations.count + while i < animationCount { + let (_, animation) = self.animations[i] + animation.applyAt(timestamp) + + if animation.completeAt(timestamp) { + animations.remove(at: i) + animationCount -= 1 + i -= 1 + } else { + continueAnimations = true + } + + i += 1 + } + + if let accessoryItemNode = self.accessoryItemNode { + if (accessoryItemNode.animate(timestamp)) { + continueAnimations = true + } + } + + return continueAnimations + } + + open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + } + + public func animationForKey(_ key: String) -> ListViewAnimation? { + for (animationKey, animation) in self.animations { + if animationKey == key { + return animation + } + } + return nil + } + + public final func setAnimationForKey(_ key: String, animation: ListViewAnimation?) { + for i in 0 ..< self.animations.count { + let (currentKey, currentAnimation) = self.animations[i] + if currentKey == key { + self.animations.remove(at: i) + currentAnimation.cancel() + break + } + } + if let animation = animation { + self.animations.append((key, animation)) + } + } + + public final func removeAllAnimations() { + let previousAnimations = self.animations + self.animations.removeAll() + + for (_, animation) in previousAnimations { + animation.cancel() + } + + self.accessoryItemNode?.removeAllAnimations() + } + + public func addInsetsAnimationToValue(_ value: UIEdgeInsets, duration: Double, beginAt: Double) { + let animation = ListViewAnimation(from: self.insets, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in + if let strongSelf = self { + strongSelf.insets = currentValue + } + }) + self.setAnimationForKey("insets", animation: animation) + } + + public func addHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) { + let animation = ListViewAnimation(from: self.bounds.height, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in + if let strongSelf = self { + let frame = strongSelf.frame + strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue)) + if let update = update { + update(progress, currentValue) + } + } + }) + self.setAnimationForKey("height", animation: animation) + } + + func copyHeightAndApparentHeightAnimations(to otherNode: ListViewItemNode) { + if let animation = self.animationForKey("apparentHeight") { + let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in + if let strongSelf = otherNode { + let frame = strongSelf.frame + strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue)) + } + }) + otherNode.setAnimationForKey("height", animation: updatedAnimation) + } + + if let animation = self.animationForKey("apparentHeight") { + let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in + if let strongSelf = otherNode { + strongSelf.apparentHeight = currentValue + } + }) + otherNode.setAnimationForKey("apparentHeight", animation: updatedAnimation) + } + } + + public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) { + let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in + if let strongSelf = self { + strongSelf.apparentHeight = currentValue + if let update = update { + update(progress, currentValue) + } + } + }) + self.setAnimationForKey("apparentHeight", animation: animation) + } + + public func modifyApparentHeightAnimation(_ value: CGFloat, beginAt: Double) { + if let previousAnimation = self.animationForKey("apparentHeight") { + var duration = previousAnimation.startTime + previousAnimation.duration - beginAt + if abs(self.apparentHeight - value) < CGFloat.ulpOfOne { + duration = 0.0 + } + + let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in + if let strongSelf = self { + strongSelf.apparentHeight = currentValue + } + }) + + self.setAnimationForKey("apparentHeight", animation: animation) + } + } + + public func removeApparentHeightAnimation() { + self.setAnimationForKey("apparentHeight", animation: nil) + } + + public func addTransitionOffsetAnimation(_ value: CGFloat, duration: Double, beginAt: Double) { + let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in + if let strongSelf = self { + strongSelf.transitionOffset = currentValue + } + }) + self.setAnimationForKey("transitionOffset", animation: animation) + } + + open func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + } + + open func animateAdded(_ currentTimestamp: Double, duration: Double) { + } + + open func animateRemoved(_ currentTimestamp: Double, duration: Double) { + } + + open func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + } + + open func isReorderable(at point: CGPoint) -> Bool { + return false + } + + open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { + + } + + open func shouldAnimateHorizontalFrameTransition() -> Bool { + return false + } + + open func header() -> ListViewItemHeader? { + return nil + } + + open func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { + + } + + override open func accessibilityElementDidBecomeFocused() { + (self.supernode as? ListView)?.ensureItemNodeVisible(self, animated: false, overflow: 22.0) + } +} diff --git a/submodules/Display/Display/ListViewOverscrollBackgroundNode.swift b/submodules/Display/Display/ListViewOverscrollBackgroundNode.swift new file mode 100644 index 0000000000..8de76ef4a6 --- /dev/null +++ b/submodules/Display/Display/ListViewOverscrollBackgroundNode.swift @@ -0,0 +1,29 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class ListViewOverscrollBackgroundNode: ASDisplayNode { + private let backgroundNode: ASDisplayNode + + var color: UIColor { + didSet { + self.backgroundNode.backgroundColor = color + } + } + + init(color: UIColor) { + self.color = color + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = color + self.backgroundNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.backgroundNode) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + } +} diff --git a/submodules/Display/Display/ListViewReorderingGestureRecognizer.swift b/submodules/Display/Display/ListViewReorderingGestureRecognizer.swift new file mode 100644 index 0000000000..fa631b4d6f --- /dev/null +++ b/submodules/Display/Display/ListViewReorderingGestureRecognizer.swift @@ -0,0 +1,65 @@ +import Foundation +import UIKit + +final class ListViewReorderingGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> Bool + private let ended: () -> Void + private let moved: (CGFloat) -> Void + + private var initialLocation: CGPoint? + + init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) { + self.shouldBegin = shouldBegin + self.ended = ended + self.moved = moved + + super.init(target: nil, action: nil) + } + + override func reset() { + super.reset() + + self.initialLocation = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.state == .possible { + if let location = touches.first?.location(in: self.view), self.shouldBegin(location) { + self.initialLocation = location + self.state = .began + } else { + self.state = .failed + } + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if self.state == .began || self.state == .changed { + self.ended() + self.state = .failed + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + if self.state == .began || self.state == .changed { + self.ended() + self.state = .failed + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = location.y - initialLocation.y + self.moved(offset) + } + } +} diff --git a/submodules/Display/Display/ListViewReorderingItemNode.swift b/submodules/Display/Display/ListViewReorderingItemNode.swift new file mode 100644 index 0000000000..0fc0dc6b3f --- /dev/null +++ b/submodules/Display/Display/ListViewReorderingItemNode.swift @@ -0,0 +1,50 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class ListViewReorderingItemNode: ASDisplayNode { + weak var itemNode: ListViewItemNode? + + var currentState: (Int, Int)? + + private let copyView: UIView? + private let initialLocation: CGPoint + + init(itemNode: ListViewItemNode, initialLocation: CGPoint) { + self.itemNode = itemNode + self.copyView = itemNode.view.snapshotView(afterScreenUpdates: false) + self.initialLocation = initialLocation + + super.init() + + if let copyView = self.copyView { + self.view.addSubview(copyView) + copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: copyView.bounds.size) + copyView.bounds = itemNode.bounds + } + } + + func updateOffset(offset: CGFloat) { + if let copyView = self.copyView { + copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size) + } + } + + func currentOffset() -> CGFloat? { + if let copyView = self.copyView { + return copyView.center.y + } + return nil + } + + func animateCompletion(completion: @escaping () -> Void) { + if let copyView = self.copyView, let itemNode = self.itemNode { + itemNode.isHidden = false + itemNode.transitionOffset = itemNode.apparentFrame.midY - copyView.frame.midY + itemNode.addTransitionOffsetAnimation(0.0, duration: 0.2, beginAt: CACurrentMediaTime()) + completion() + } else { + completion() + } + } +} diff --git a/submodules/Display/Display/ListViewScroller.swift b/submodules/Display/Display/ListViewScroller.swift new file mode 100644 index 0000000000..fe133f920e --- /dev/null +++ b/submodules/Display/Display/ListViewScroller.swift @@ -0,0 +1,31 @@ +import UIKit + +class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { + override init(frame: CGRect) { + super.init(frame: frame) + + #if os(iOS) + self.scrollsToTop = false + if #available(iOSApplicationExtension 11.0, *) { + self.contentInsetAdjustmentBehavior = .never + } + #endif + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is ListViewTapGestureRecognizer { + return true + } + return false + } + + #if os(iOS) + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + #endif +} diff --git a/submodules/Display/Display/ListViewTapGestureRecognizer.swift b/submodules/Display/Display/ListViewTapGestureRecognizer.swift new file mode 100644 index 0000000000..f5090f99d6 --- /dev/null +++ b/submodules/Display/Display/ListViewTapGestureRecognizer.swift @@ -0,0 +1,6 @@ +import Foundation +import UIKit + +public final class ListViewTapGestureRecognizer: UITapGestureRecognizer { + +} diff --git a/submodules/Display/Display/ListViewTempItemNode.swift b/submodules/Display/Display/ListViewTempItemNode.swift new file mode 100644 index 0000000000..f407a488f8 --- /dev/null +++ b/submodules/Display/Display/ListViewTempItemNode.swift @@ -0,0 +1,4 @@ +import Foundation + +final class ListViewTempItemNode: ListViewItemNode { +} diff --git a/submodules/Display/Display/ListViewTransactionQueue.swift b/submodules/Display/Display/ListViewTransactionQueue.swift new file mode 100644 index 0000000000..a790dc1e82 --- /dev/null +++ b/submodules/Display/Display/ListViewTransactionQueue.swift @@ -0,0 +1,65 @@ +import Foundation +import UIKit +import SwiftSignalKit + +public typealias ListViewTransaction = (@escaping () -> Void) -> Void + +public final class ListViewTransactionQueue { + private var transactions: [ListViewTransaction] = [] + public final var transactionCompleted: () -> Void = { } + + public init() { + } + + public func addTransaction(_ transaction: @escaping ListViewTransaction) { + precondition(Thread.isMainThread) + let beginTransaction = self.transactions.count == 0 + self.transactions.append(transaction) + + if beginTransaction { + transaction({ [weak self] in + precondition(Thread.isMainThread) + + if Thread.isMainThread { + if let strongSelf = self { + strongSelf.endTransaction() + } + } else { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.endTransaction() + } + } + } + }) + } + } + + private func endTransaction() { + precondition(Thread.isMainThread) + Queue.mainQueue().async { + self.transactionCompleted() + if !self.transactions.isEmpty { + let _ = self.transactions.removeFirst() + } + + if let nextTransaction = self.transactions.first { + nextTransaction({ [weak self] in + precondition(Thread.isMainThread) + + if Thread.isMainThread { + if let strongSelf = self { + strongSelf.endTransaction() + } + } else { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.endTransaction() + } + } + } + }) + } + } + } +} diff --git a/submodules/Display/Display/MinimizeKeyboardGestureRecognizer.swift b/submodules/Display/Display/MinimizeKeyboardGestureRecognizer.swift new file mode 100644 index 0000000000..b8c2ed10c5 --- /dev/null +++ b/submodules/Display/Display/MinimizeKeyboardGestureRecognizer.swift @@ -0,0 +1,20 @@ +import Foundation +import UIKit + +final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate { + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.cancelsTouchesInView = false + self.delaysTouchesBegan = false + self.delaysTouchesEnded = false + self.delegate = self + + self.direction = [.left, .right] + self.numberOfTouchesRequired = 2 + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} diff --git a/submodules/Display/Display/NSBag.h b/submodules/Display/Display/NSBag.h new file mode 100644 index 0000000000..739fe92552 --- /dev/null +++ b/submodules/Display/Display/NSBag.h @@ -0,0 +1,9 @@ +#import + +@interface NSBag : NSObject + +- (NSInteger)addItem:(id)item; +- (void)enumerateItems:(void (^)(id))block; +- (void)removeItem:(NSInteger)key; + +@end diff --git a/submodules/Display/Display/NSBag.m b/submodules/Display/Display/NSBag.m new file mode 100644 index 0000000000..4da04f5737 --- /dev/null +++ b/submodules/Display/Display/NSBag.m @@ -0,0 +1,64 @@ +#import "NSBag.h" + +@interface NSBag () +{ + NSInteger _nextKey; + NSMutableArray *_items; + NSMutableArray *_itemKeys; +} + +@end + +@implementation NSBag + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _items = [[NSMutableArray alloc] init]; + _itemKeys = [[NSMutableArray alloc] init]; + } + return self; +} + +- (NSInteger)addItem:(id)item +{ + if (item == nil) + return -1; + + NSInteger key = _nextKey; + [_items addObject:item]; + [_itemKeys addObject:@(key)]; + _nextKey++; + + return key; +} + +- (void)enumerateItems:(void (^)(id))block +{ + if (block) + { + for (id item in _items) + { + block(item); + } + } +} + +- (void)removeItem:(NSInteger)key +{ + NSUInteger index = 0; + for (NSNumber *itemKey in _itemKeys) + { + if ([itemKey integerValue] == key) + { + [_items removeObjectAtIndex:index]; + [_itemKeys removeObjectAtIndex:index]; + break; + } + index++; + } +} + +@end diff --git a/submodules/Display/Display/NSWeakReference.h b/submodules/Display/Display/NSWeakReference.h new file mode 100644 index 0000000000..8c762f1256 --- /dev/null +++ b/submodules/Display/Display/NSWeakReference.h @@ -0,0 +1,9 @@ +#import + +@interface NSWeakReference : NSObject + +@property (nonatomic, weak) id value; + +- (instancetype)initWithValue:(id)value; + +@end diff --git a/submodules/Display/Display/NSWeakReference.m b/submodules/Display/Display/NSWeakReference.m new file mode 100644 index 0000000000..f9b714f3d7 --- /dev/null +++ b/submodules/Display/Display/NSWeakReference.m @@ -0,0 +1,13 @@ +#import "NSWeakReference.h" + +@implementation NSWeakReference + +- (instancetype)initWithValue:(id)value { + self = [super init]; + if (self != nil) { + self.value = value; + } + return self; +} + +@end diff --git a/submodules/Display/Display/NativeWindowHostView.swift b/submodules/Display/Display/NativeWindowHostView.swift new file mode 100644 index 0000000000..7361f152fa --- /dev/null +++ b/submodules/Display/Display/NativeWindowHostView.swift @@ -0,0 +1,417 @@ +import Foundation +import UIKit +import SwiftSignalKit + +private let orientationChangeDuration: Double = UIDevice.current.userInterfaceIdiom == .pad ? 0.4 : 0.3 + +private let defaultOrientations: UIInterfaceOrientationMask = { + if UIDevice.current.userInterfaceIdiom == .pad { + return .all + } else { + return .allButUpsideDown + } +}() + +public final class PreviewingHostViewDelegate { + public let controllerForLocation: (UIView, CGPoint) -> (UIViewController, CGRect)? + public let commitController: (UIViewController) -> Void + + public init(controllerForLocation: @escaping (UIView, CGPoint) -> (UIViewController, CGRect)?, commitController: @escaping (UIViewController) -> Void) { + self.controllerForLocation = controllerForLocation + self.commitController = commitController + } +} + +public protocol PreviewingHostView { + @available(iOSApplicationExtension 9.0, iOS 9.0, *) + var previewingDelegate: PreviewingHostViewDelegate? { get } +} + +private func tracePreviewingHostView(view: UIView, point: CGPoint) -> (UIView & PreviewingHostView, CGPoint)? { + if let view = view as? UIView & PreviewingHostView { + return (view, point) + } + if let superview = view.superview { + if let result = tracePreviewingHostView(view: superview, point: superview.convert(point, from: view)) { + return result + } + } + return nil +} + +private final class WindowRootViewControllerView: UIView { + override var frame: CGRect { + get { + return super.frame + } set(value) { + var value = value + value.size.height += value.minY + value.origin.y = 0.0 + super.frame = value + } + } +} + +private final class WindowRootViewController: UIViewController, UIViewControllerPreviewingDelegate { + private var voiceOverStatusObserver: AnyObject? + private var registeredForPreviewing = false + + var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)? + var transitionToSize: ((CGSize, Double) -> Void)? + + var orientations: UIInterfaceOrientationMask = defaultOrientations { + didSet { + if oldValue != self.orientations { + if self.orientations == .portrait { + if UIDevice.current.orientation != .portrait { + let value = UIInterfaceOrientation.portrait.rawValue + UIDevice.current.setValue(value, forKey: "orientation") + } + } else { + UIViewController.attemptRotationToDeviceOrientation() + } + } + } + } + + var gestureEdges: UIRectEdge = [] { + didSet { + if oldValue != self.gestureEdges { + if #available(iOSApplicationExtension 11.0, *) { + self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures() + } + } + } + } + + var preferNavigationUIHidden: Bool = false { + didSet { + if oldValue != self.preferNavigationUIHidden { + if #available(iOSApplicationExtension 11.0, *) { + self.setNeedsUpdateOfHomeIndicatorAutoHidden() + } + } + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .default + } + + override var prefersStatusBarHidden: Bool { + return false + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return orientations + } + + init() { + super.init(nibName: nil, bundle: nil) + + self.extendedLayoutIncludesOpaqueBars = true + + if #available(iOSApplicationExtension 11.0, *) { + self.voiceOverStatusObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIAccessibilityVoiceOverStatusDidChange, object: nil, queue: OperationQueue.main, using: { [weak self] _ in + if let strongSelf = self { + strongSelf.updatePreviewingRegistration() + } + }) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let voiceOverStatusObserver = self.voiceOverStatusObserver { + NotificationCenter.default.removeObserver(voiceOverStatusObserver) + } + } + + override func preferredScreenEdgesDeferringSystemGestures() -> UIRectEdge { + return self.gestureEdges + } + + override func prefersHomeIndicatorAutoHidden() -> Bool { + return self.preferNavigationUIHidden + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + UIView.performWithoutAnimation { + self.transitionToSize?(size, coordinator.transitionDuration) + } + } + + override func loadView() { + self.view = WindowRootViewControllerView() + self.view.isOpaque = false + self.view.backgroundColor = nil + + self.updatePreviewingRegistration() + } + + private var previewingContext: AnyObject? + + private func updatePreviewingRegistration() { + var shouldRegister = false + + var isVoiceOverRunning = false + if #available(iOSApplicationExtension 10.0, *) { + isVoiceOverRunning = UIAccessibility.isVoiceOverRunning + } + if !isVoiceOverRunning { + shouldRegister = true + } + + if shouldRegister != self.registeredForPreviewing { + self.registeredForPreviewing = shouldRegister + if shouldRegister { + if #available(iOSApplicationExtension 9.0, *) { + self.previewingContext = self.registerForPreviewing(with: self, sourceView: self.view) + } + } else if let previewingContext = self.previewingContext { + self.previewingContext = nil + if let previewingContext = previewingContext as? UIViewControllerPreviewing { + if #available(iOSApplicationExtension 9.0, *) { + self.unregisterForPreviewing(withContext: previewingContext) + } + } + } + } + } + + private weak var previousPreviewingHostView: (UIView & PreviewingHostView)? + + public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { + if UIAccessibility.isVoiceOverRunning { + return nil + } + if #available(iOSApplicationExtension 9.0, *) { + guard let result = self.view.hitTest(location, with: nil) else { + return nil + } + if let (result, resultPoint) = tracePreviewingHostView(view: result, point: self.view.convert(location, to: result)), let delegate = result.previewingDelegate { + self.previousPreviewingHostView = result + if let (controller, rect) = delegate.controllerForLocation(previewingContext.sourceView, resultPoint) { + previewingContext.sourceRect = rect + return controller + } + } + } + return nil + } + + public func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { + if #available(iOSApplicationExtension 9.0, *) { + if let previousPreviewingHostView = self.previousPreviewingHostView, let delegate = previousPreviewingHostView.previewingDelegate { + delegate.commitController(viewControllerToCommit) + } + self.previousPreviewingHostView = nil + } + } +} + +private final class NativeWindow: UIWindow, WindowHost { + var updateSize: ((CGSize) -> Void)? + var layoutSubviewsEvent: (() -> Void)? + var updateIsUpdatingOrientationLayout: ((Bool) -> Void)? + var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)? + var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)? + var presentControllerInGlobalOverlay: ((_ controller: ContainableController) -> Void)? + var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? + var presentNativeImpl: ((UIViewController) -> Void)? + var invalidateDeferScreenEdgeGestureImpl: (() -> Void)? + var invalidatePreferNavigationUIHiddenImpl: (() -> Void)? + var cancelInteractiveKeyboardGesturesImpl: (() -> Void)? + var forEachControllerImpl: (((ContainableController) -> Void) -> Void)? + var getAccessibilityElementsImpl: (() -> [Any]?)? + + override var frame: CGRect { + get { + return super.frame + } set(value) { + let sizeUpdated = super.frame.size != value.size + + var frameTransition: ContainedViewLayoutTransition = .immediate + if #available(iOSApplicationExtension 9.0, *) { + let duration = UIView.inheritedAnimationDuration + if !duration.isZero { + frameTransition = .animated(duration: duration, curve: .easeInOut) + } + } + if sizeUpdated, case let .animated(duration, curve) = frameTransition { + let previousFrame = super.frame + super.frame = value + self.layer.animateFrame(from: previousFrame, to: value, duration: duration, timingFunction: curve.timingFunction) + } else { + super.frame = value + } + + if sizeUpdated { + self.updateSize?(value.size) + } + } + } + + override var bounds: CGRect { + get { + return super.bounds + } + set(value) { + let sizeUpdated = super.bounds.size != value.size + super.bounds = value + + if sizeUpdated { + self.updateSize?(value.size) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + if let gestureRecognizers = self.gestureRecognizers { + for recognizer in gestureRecognizers { + recognizer.delaysTouchesBegan = false + } + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.layoutSubviewsEvent?() + } + + override func _update(toInterfaceOrientation arg1: Int32, duration arg2: Double, force arg3: Bool) { + self.updateIsUpdatingOrientationLayout?(true) + super._update(toInterfaceOrientation: arg1, duration: arg2, force: arg3) + self.updateIsUpdatingOrientationLayout?(false) + + let orientation = UIInterfaceOrientation(rawValue: Int(arg1)) ?? .unknown + self.updateToInterfaceOrientation?(orientation) + } + + func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) { + self.presentController?(controller, level, blockInteraction, completion) + } + + func presentInGlobalOverlay(_ controller: ContainableController) { + self.presentControllerInGlobalOverlay?(controller) + } + + func presentNative(_ controller: UIViewController) { + self.presentNativeImpl?(controller) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return self.hitTestImpl?(point, event) + } + + func invalidateDeferScreenEdgeGestures() { + self.invalidateDeferScreenEdgeGestureImpl?() + } + + func invalidatePreferNavigationUIHidden() { + self.invalidatePreferNavigationUIHiddenImpl?() + } + + func cancelInteractiveKeyboardGestures() { + self.cancelInteractiveKeyboardGesturesImpl?() + } + + func forEachController(_ f: (ContainableController) -> Void) { + self.forEachControllerImpl?(f) + } +} + +public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) { + let window = NativeWindow(frame: UIScreen.main.bounds) + + let rootViewController = WindowRootViewController() + window.rootViewController = rootViewController + rootViewController.viewWillAppear(false) + rootViewController.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size) + rootViewController.viewDidAppear(false) + + let hostView = WindowHostView(containerView: rootViewController.view, eventView: window, isRotating: { + return window.isRotating() + }, updateSupportedInterfaceOrientations: { orientations in + rootViewController.orientations = orientations + }, updateDeferScreenEdgeGestures: { edges in + rootViewController.gestureEdges = edges + }, updatePreferNavigationUIHidden: { value in + rootViewController.preferNavigationUIHidden = value + }) + + rootViewController.transitionToSize = { [weak hostView] size, duration in + hostView?.updateSize?(size, duration) + } + + window.updateSize = { _ in + } + + window.layoutSubviewsEvent = { [weak hostView] in + hostView?.layoutSubviews?() + } + + window.updateIsUpdatingOrientationLayout = { [weak hostView] value in + hostView?.isUpdatingOrientationLayout = value + } + + window.updateToInterfaceOrientation = { [weak hostView] orientation in + hostView?.updateToInterfaceOrientation?(orientation) + } + + window.presentController = { [weak hostView] controller, level, blockInteraction, completion in + hostView?.present?(controller, level, blockInteraction, completion) + } + + window.presentControllerInGlobalOverlay = { [weak hostView] controller in + hostView?.presentInGlobalOverlay?(controller) + } + + window.presentNativeImpl = { [weak hostView] controller in + hostView?.presentNative?(controller) + } + + window.hitTestImpl = { [weak hostView] point, event in + return hostView?.hitTest?(point, event) + } + + window.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in + return hostView?.invalidateDeferScreenEdgeGesture?() + } + + window.invalidatePreferNavigationUIHiddenImpl = { [weak hostView] in + return hostView?.invalidatePreferNavigationUIHidden?() + } + + window.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in + hostView?.cancelInteractiveKeyboardGestures?() + } + + window.forEachControllerImpl = { [weak hostView] f in + hostView?.forEachController?(f) + } + + window.getAccessibilityElementsImpl = { [weak hostView] in + return hostView?.getAccessibilityElements?() + } + + rootViewController.presentController = { [weak hostView] controller, level, animated, completion in + if let hostView = hostView { + hostView.present?(LegacyPresentedController(legacyController: controller, presentation: .custom), level, false, completion ?? {}) + completion?() + } + } + + return (window, hostView) +} diff --git a/submodules/Display/Display/NavigationBackArrowLight@2x.png b/submodules/Display/Display/NavigationBackArrowLight@2x.png new file mode 100644 index 0000000000..ca8cabf210 Binary files /dev/null and b/submodules/Display/Display/NavigationBackArrowLight@2x.png differ diff --git a/submodules/Display/Display/NavigationBackButtonNode.swift b/submodules/Display/Display/NavigationBackButtonNode.swift new file mode 100644 index 0000000000..393ba98db8 --- /dev/null +++ b/submodules/Display/Display/NavigationBackButtonNode.swift @@ -0,0 +1,149 @@ +import UIKit +import AsyncDisplayKit + +public class NavigationBackButtonNode: ASControlNode { + private func fontForCurrentState() -> UIFont { + return UIFont.systemFont(ofSize: 17.0) + } + + private func attributesForCurrentState() -> [NSAttributedStringKey : AnyObject] { + return [ + NSAttributedStringKey.font: self.fontForCurrentState(), + NSAttributedStringKey.foregroundColor: self.isEnabled ? self.color : self.disabledColor + ] + } + + let arrow: ASDisplayNode + let label: ASTextNode + + private let arrowSpacing: CGFloat = 4.0 + + private var _text: String = "" + public var text: String { + get { + return self._text + } + set(value) { + self._text = value + self.label.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + self.invalidateCalculatedLayout() + } + } + + public var color: UIColor = UIColor(rgb: 0x007ee5) { + didSet { + self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState()) + } + } + + public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) { + didSet { + self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState()) + } + } + + private var touchCount = 0 + var pressed: () -> () = {} + + override public init() { + self.arrow = ASDisplayNode() + self.label = ASTextNode() + + super.init() + + self.isUserInteractionEnabled = true + self.isExclusiveTouch = true + self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0) + self.displaysAsynchronously = false + + self.arrow.displaysAsynchronously = false + self.label.displaysAsynchronously = false + + self.addSubnode(self.arrow) + let arrowImage = UIImage(named: "NavigationBackArrowLight", in: Bundle(for: NavigationBackButtonNode.self), compatibleWith: nil)?.precomposed() + self.arrow.contents = arrowImage?.cgImage + self.arrow.frame = CGRect(origin: CGPoint(), size: arrowImage?.size ?? CGSize()) + + self.addSubnode(self.label) + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + self.label.measure(CGSize(width: max(0.0, constrainedSize.width - self.arrow.frame.size.width - self.arrowSpacing), height: constrainedSize.height)) + + return CGSize(width: self.arrow.frame.size.width + self.arrowSpacing + self.label.calculatedSize.width, height: max(self.arrow.frame.size.height, self.label.calculatedSize.height)) + } + + var labelFrame: CGRect { + get { + return CGRect(x: self.arrow.frame.size.width + self.arrowSpacing, y: floor((self.frame.size.height - self.label.calculatedSize.height) / 2.0), width: self.label.calculatedSize.width, height: self.label.calculatedSize.height) + } + } + + public override func layout() { + super.layout() + + self.arrow.frame = CGRect(x: 0.0, y: floor((self.frame.size.height - arrow.frame.size.height) / 2.0), width: self.arrow.frame.size.width, height: self.arrow.frame.size.height) + + self.label.frame = self.labelFrame + } + + private func touchInsideApparentBounds(_ touch: UITouch) -> Bool { + var apparentBounds = self.bounds + let hitTestSlop = self.hitTestSlop + apparentBounds.origin.x += hitTestSlop.left + apparentBounds.size.width -= hitTestSlop.left + hitTestSlop.right + apparentBounds.origin.y += hitTestSlop.top + apparentBounds.size.height -= hitTestSlop.top + hitTestSlop.bottom + + return apparentBounds.contains(touch.location(in: self.view)) + } + + public override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + self.touchCount += touches.count + self.updateHighlightedState(true, animated: false) + } + + public override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) + + self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true) + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + self.updateHighlightedState(false, animated: false) + + let previousTouchCount = self.touchCount + self.touchCount = max(0, self.touchCount - touches.count) + + if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && self.touchInsideApparentBounds(touches.first!) { + self.pressed() + } + } + + public override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + + self.touchCount = max(0, self.touchCount - (touches?.count ?? 0)) + self.updateHighlightedState(false, animated: false) + } + + private var _highlighted = false + private func updateHighlightedState(_ highlighted: Bool, animated: Bool) { + if _highlighted != highlighted { + _highlighted = highlighted + + let alpha: CGFloat = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0) + + if animated { + UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions.beginFromCurrentState, animations: { () -> Void in + self.alpha = alpha + }, completion: nil) + } + else { + self.alpha = alpha + } + } + } +} diff --git a/submodules/Display/Display/NavigationBar.swift b/submodules/Display/Display/NavigationBar.swift new file mode 100644 index 0000000000..c8d2420b84 --- /dev/null +++ b/submodules/Display/Display/NavigationBar.swift @@ -0,0 +1,1150 @@ +import UIKit +import AsyncDisplayKit + +private var backArrowImageCache: [Int32: UIImage] = [:] + +public final class NavigationBarTheme { + public static func generateBackArrowImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 13.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + + context.translateBy(x: 0.0, y: -UIScreenPixel) + + let _ = try? drawSvgPath(context, path: "M3.60751322,11.5 L11.5468531,3.56066017 C12.1326395,2.97487373 12.1326395,2.02512627 11.5468531,1.43933983 C10.9610666,0.853553391 10.0113191,0.853553391 9.42553271,1.43933983 L0.449102936,10.4157696 C-0.149700979,11.0145735 -0.149700979,11.9854265 0.449102936,12.5842304 L9.42553271,21.5606602 C10.0113191,22.1464466 10.9610666,22.1464466 11.5468531,21.5606602 C12.1326395,20.9748737 12.1326395,20.0251263 11.5468531,19.4393398 L3.60751322,11.5 Z ") + }) + } + + public let buttonColor: UIColor + public let disabledButtonColor: UIColor + public let primaryTextColor: UIColor + public let backgroundColor: UIColor + public let separatorColor: UIColor + public let badgeBackgroundColor: UIColor + public let badgeStrokeColor: UIColor + public let badgeTextColor: UIColor + + public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { + self.buttonColor = buttonColor + self.disabledButtonColor = disabledButtonColor + self.primaryTextColor = primaryTextColor + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + self.badgeBackgroundColor = badgeBackgroundColor + self.badgeStrokeColor = badgeStrokeColor + self.badgeTextColor = badgeTextColor + } + + public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme { + return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) + } +} + +public final class NavigationBarStrings { + public let back: String + public let close: String + + public init(back: String, close: String) { + self.back = back + self.close = close + } +} + +public final class NavigationBarPresentationData { + public let theme: NavigationBarTheme + public let strings: NavigationBarStrings + + public init(theme: NavigationBarTheme, strings: NavigationBarStrings) { + self.theme = theme + self.strings = strings + } +} + +private func backArrowImage(color: UIColor) -> UIImage? { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let key = (Int32(alpha * 255.0) << 24) | (Int32(red * 255.0) << 16) | (Int32(green * 255.0) << 8) | Int32(blue * 255.0) + if let image = backArrowImageCache[key] { + return image + } else { + if let image = NavigationBarTheme.generateBackArrowImage(color: color) { + backArrowImageCache[key] = image + return image + } else { + return nil + } + } +} + +enum NavigationPreviousAction: Equatable { + case item(UINavigationItem) + case close + + static func ==(lhs: NavigationPreviousAction, rhs: NavigationPreviousAction) -> Bool { + switch lhs { + case let .item(lhsItem): + if case let .item(rhsItem) = rhs, lhsItem === rhsItem { + return true + } else { + return false + } + case .close: + if case .close = rhs { + return true + } else { + return false + } + } + } +} + +open class NavigationBar: ASDisplayNode { + private var presentationData: NavigationBarPresentationData + + private var validLayout: (CGSize, CGFloat, CGFloat)? + private var requestedLayout: Bool = false + var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in } + + public var backPressed: () -> () = { } + + private var collapsed: Bool { + get { + return self.frame.size.height.isLess(than: 44.0) + } + } + + private let stripeNode: ASDisplayNode + private let clippingNode: ASDisplayNode + + public private(set) var contentNode: NavigationBarContentNode? + + private var itemTitleListenerKey: Int? + private var itemTitleViewListenerKey: Int? + + private var itemLeftButtonListenerKey: Int? + private var itemLeftButtonSetEnabledListenerKey: Int? + + private var itemRightButtonListenerKey: Int? + private var itemRightButtonsListenerKey: Int? + + private var itemBadgeListenerKey: Int? + + private var hintAnimateTitleNodeOnNextLayout: Bool = false + + private var _item: UINavigationItem? + public var item: UINavigationItem? { + get { + return self._item + } set(value) { + if let previousValue = self._item { + if let itemTitleListenerKey = self.itemTitleListenerKey { + previousValue.removeSetTitleListener(itemTitleListenerKey) + self.itemTitleListenerKey = nil + } + + if let itemLeftButtonListenerKey = self.itemLeftButtonListenerKey { + previousValue.removeSetLeftBarButtonItemListener(itemLeftButtonListenerKey) + self.itemLeftButtonListenerKey = nil + } + + if let itemLeftButtonSetEnabledListenerKey = self.itemLeftButtonSetEnabledListenerKey { + previousValue.leftBarButtonItem?.removeSetEnabledListener(itemLeftButtonSetEnabledListenerKey) + self.itemLeftButtonSetEnabledListenerKey = nil + } + + if let itemRightButtonListenerKey = self.itemRightButtonListenerKey { + previousValue.removeSetRightBarButtonItemListener(itemRightButtonListenerKey) + self.itemRightButtonListenerKey = nil + } + + if let itemRightButtonsListenerKey = self.itemRightButtonsListenerKey { + previousValue.removeSetMultipleRightBarButtonItemsListener(itemRightButtonsListenerKey) + self.itemRightButtonsListenerKey = nil + } + + if let itemBadgeListenerKey = self.itemBadgeListenerKey { + previousValue.removeSetBadgeListener(itemBadgeListenerKey) + self.itemBadgeListenerKey = nil + } + } + self._item = value + + self.leftButtonNode.removeFromSupernode() + self.rightButtonNode.removeFromSupernode() + + if let item = value { + self.title = item.title + self.itemTitleListenerKey = item.addSetTitleListener { [weak self] text, animated in + if let strongSelf = self { + let animateIn = animated && (strongSelf.title?.isEmpty ?? true) + strongSelf.title = text + if animateIn { + strongSelf.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + } + + self.titleView = item.titleView + self.itemTitleViewListenerKey = item.addSetTitleViewListener { [weak self] titleView in + if let strongSelf = self { + strongSelf.titleView = titleView + } + } + + self.itemLeftButtonListenerKey = item.addSetLeftBarButtonItemListener { [weak self] previousItem, _, animated in + if let strongSelf = self { + if let itemLeftButtonSetEnabledListenerKey = strongSelf.itemLeftButtonSetEnabledListenerKey { + previousItem?.removeSetEnabledListener(itemLeftButtonSetEnabledListenerKey) + strongSelf.itemLeftButtonSetEnabledListenerKey = nil + } + + strongSelf.updateLeftButton(animated: animated) + strongSelf.invalidateCalculatedLayout() + strongSelf.requestLayout() + } + } + + self.itemRightButtonListenerKey = item.addSetRightBarButtonItemListener { [weak self] previousItem, currentItem, animated in + if let strongSelf = self { + strongSelf.updateRightButton(animated: animated) + strongSelf.invalidateCalculatedLayout() + strongSelf.requestLayout() + } + } + + self.itemRightButtonsListenerKey = item.addSetMultipleRightBarButtonItemsListener { [weak self] items, animated in + if let strongSelf = self { + strongSelf.updateRightButton(animated: animated) + strongSelf.invalidateCalculatedLayout() + strongSelf.requestLayout() + } + } + + self.itemBadgeListenerKey = item.addSetBadgeListener { [weak self] text in + if let strongSelf = self { + strongSelf.updateBadgeText(text: text) + } + } + self.updateBadgeText(text: item.badge) + + self.updateLeftButton(animated: false) + self.updateRightButton(animated: false) + } else { + self.title = nil + self.updateLeftButton(animated: false) + self.updateRightButton(animated: false) + } + self.invalidateCalculatedLayout() + self.requestLayout() + } + } + + private var title: String? { + didSet { + if let title = self.title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.presentationData.theme.primaryTextColor) + self.titleNode.accessibilityLabel = title + if self.titleNode.supernode == nil { + self.clippingNode.addSubnode(self.titleNode) + } + } else { + self.titleNode.removeFromSupernode() + } + + self.updateAccessibilityElements() + self.invalidateCalculatedLayout() + self.requestLayout() + } + } + + private var titleView: UIView? { + didSet { + if let oldValue = oldValue { + oldValue.removeFromSuperview() + } + + if let titleView = self.titleView { + self.clippingNode.view.addSubview(titleView) + } + + self.invalidateCalculatedLayout() + self.requestLayout() + } + } + + public var layoutSuspended: Bool = false + + private let titleNode: ASTextNode + + var previousItemListenerKey: Int? + var previousItemBackListenerKey: Int? + + private func updateAccessibilityElements() { + /*if !self.isNodeLoaded { + return + } + var accessibilityElements: [AnyObject] = [] + + if self.leftButtonNode.supernode != nil { + accessibilityElements.append(self.leftButtonNode) + } + if self.titleNode.supernode != nil { + accessibilityElements.append(self.titleNode) + } + if let titleView = self.titleView, titleView.superview != nil { + accessibilityElements.append(titleView) + } + if self.rightButtonNode.supernode != nil { + accessibilityElements.append(self.rightButtonNode) + } + + var updated = false + if let currentAccessibilityElements = self.accessibilityElements { + if currentAccessibilityElements.count != accessibilityElements.count { + updated = true + } else { + for i in 0 ..< accessibilityElements.count { + let element = currentAccessibilityElements[i] as AnyObject + if element !== accessibilityElements[i] { + updated = true + } + } + } + } + if updated { + self.accessibilityElements = accessibilityElements + }*/ + } + + override open var accessibilityElements: [Any]? { + get { + var accessibilityElements: [Any] = [] + if self.backButtonNode.supernode != nil { + addAccessibilityChildren(of: self.backButtonNode, container: self, to: &accessibilityElements) + } + if self.leftButtonNode.supernode != nil { + addAccessibilityChildren(of: self.leftButtonNode, container: self, to: &accessibilityElements) + } + if self.titleNode.supernode != nil { + addAccessibilityChildren(of: self.titleNode, container: self, to: &accessibilityElements) + accessibilityElements.append(self.titleNode) + } + if let titleView = self.titleView, titleView.superview != nil { + titleView.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(titleView.bounds, titleView) + accessibilityElements.append(titleView) + } + if self.rightButtonNode.supernode != nil { + addAccessibilityChildren(of: self.rightButtonNode, container: self, to: &accessibilityElements) + } + if let contentNode = self.contentNode { + addAccessibilityChildren(of: contentNode, container: self, to: &accessibilityElements) + } + return accessibilityElements + } set(value) { + } + } + + override open func didLoad() { + super.didLoad() + + self.updateAccessibilityElements() + } + + var _previousItem: NavigationPreviousAction? + var previousItem: NavigationPreviousAction? { + get { + return self._previousItem + } set(value) { + if self._previousItem != value { + if let previousValue = self._previousItem, case let .item(itemValue) = previousValue { + if let previousItemListenerKey = self.previousItemListenerKey { + itemValue.removeSetTitleListener(previousItemListenerKey) + self.previousItemListenerKey = nil + } + if let previousItemBackListenerKey = self.previousItemBackListenerKey { + itemValue.removeSetBackBarButtonItemListener(previousItemBackListenerKey) + self.previousItemBackListenerKey = nil + } + } + self._previousItem = value + + if let previousItem = value { + switch previousItem { + case let .item(itemValue): + self.previousItemListenerKey = itemValue.addSetTitleListener { [weak self] _, _ in + if let strongSelf = self, let previousItem = strongSelf.previousItem, case let .item(itemValue) = previousItem { + if let backBarButtonItem = itemValue.backBarButtonItem { + strongSelf.backButtonNode.updateManualText(backBarButtonItem.title ?? "") + } else { + strongSelf.backButtonNode.updateManualText(itemValue.title ?? "") + } + strongSelf.invalidateCalculatedLayout() + strongSelf.requestLayout() + } + } + + self.previousItemBackListenerKey = itemValue.addSetBackBarButtonItemListener { [weak self] _, _, _ in + if let strongSelf = self, let previousItem = strongSelf.previousItem, case let .item(itemValue) = previousItem { + if let backBarButtonItem = itemValue.backBarButtonItem { + strongSelf.backButtonNode.updateManualText(backBarButtonItem.title ?? "") + } else { + strongSelf.backButtonNode.updateManualText(itemValue.title ?? "") + } + strongSelf.invalidateCalculatedLayout() + strongSelf.requestLayout() + } + } + case .close: + break + } + } + self.updateLeftButton(animated: false) + + self.invalidateCalculatedLayout() + self.requestLayout() + } + } + } + + private func updateBadgeText(text: String?) { + let actualText = text ?? "" + if self.badgeNode.text != actualText { + self.badgeNode.text = actualText + self.badgeNode.isHidden = actualText.isEmpty + + self.invalidateCalculatedLayout() + self.requestLayout() + } + } + + private func updateLeftButton(animated: Bool) { + if let item = self.item { + var needsLeftButton = false + if let leftBarButtonItem = item.leftBarButtonItem, !leftBarButtonItem.backButtonAppearance { + needsLeftButton = true + } else if let previousItem = self.previousItem, case .close = previousItem { + needsLeftButton = true + } + + if needsLeftButton { + if animated { + if self.leftButtonNode.view.superview != nil { + if let snapshotView = self.leftButtonNode.view.snapshotContentTree() { + snapshotView.frame = self.leftButtonNode.frame + self.leftButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.leftButtonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + if self.backButtonNode.view.superview != nil { + if let snapshotView = self.backButtonNode.view.snapshotContentTree() { + snapshotView.frame = self.backButtonNode.frame + self.backButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.backButtonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + if self.backButtonArrow.view.superview != nil { + if let snapshotView = self.backButtonArrow.view.snapshotContentTree() { + snapshotView.frame = self.backButtonArrow.frame + self.backButtonArrow.view.superview?.insertSubview(snapshotView, aboveSubview: self.backButtonArrow.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + if self.badgeNode.view.superview != nil { + if let snapshotView = self.badgeNode.view.snapshotContentTree() { + snapshotView.frame = self.badgeNode.frame + self.badgeNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.badgeNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + } + + self.backButtonNode.removeFromSupernode() + self.backButtonArrow.removeFromSupernode() + self.badgeNode.removeFromSupernode() + + if let leftBarButtonItem = item.leftBarButtonItem { + self.leftButtonNode.updateItems([leftBarButtonItem]) + } else { + self.leftButtonNode.updateItems([UIBarButtonItem(title: self.presentationData.strings.close, style: .plain, target: nil, action: nil)]) + } + + if self.leftButtonNode.supernode == nil { + self.clippingNode.addSubnode(self.leftButtonNode) + } + + if animated { + self.leftButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } else { + if animated { + if self.leftButtonNode.view.superview != nil { + if let snapshotView = self.leftButtonNode.view.snapshotContentTree() { + snapshotView.frame = self.leftButtonNode.frame + self.leftButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.leftButtonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + } + self.leftButtonNode.removeFromSupernode() + + var backTitle: String? + if let leftBarButtonItem = item.leftBarButtonItem, leftBarButtonItem.backButtonAppearance { + backTitle = leftBarButtonItem.title + } else if let previousItem = self.previousItem { + switch previousItem { + case let .item(itemValue): + if let backBarButtonItem = itemValue.backBarButtonItem { + backTitle = backBarButtonItem.title ?? self.presentationData.strings.back + } else { + backTitle = itemValue.title ?? self.presentationData.strings.back + } + case .close: + backTitle = nil + } + } + + if let backTitle = backTitle { + self.backButtonNode.updateManualText(backTitle) + if self.backButtonNode.supernode == nil { + self.clippingNode.addSubnode(self.backButtonNode) + self.clippingNode.addSubnode(self.backButtonArrow) + self.clippingNode.addSubnode(self.badgeNode) + } + + if animated { + self.backButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.backButtonArrow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.badgeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } else { + self.backButtonNode.removeFromSupernode() + } + } + } else { + self.leftButtonNode.removeFromSupernode() + self.backButtonNode.removeFromSupernode() + self.backButtonArrow.removeFromSupernode() + self.badgeNode.removeFromSupernode() + } + + self.updateAccessibilityElements() + if animated { + self.hintAnimateTitleNodeOnNextLayout = true + } + } + + private func updateRightButton(animated: Bool) { + if let item = self.item { + var items: [UIBarButtonItem] = [] + if let rightBarButtonItems = item.rightBarButtonItems, !rightBarButtonItems.isEmpty { + items = rightBarButtonItems + } else if let rightBarButtonItem = item.rightBarButtonItem { + items = [rightBarButtonItem] + } + + if !items.isEmpty { + if animated, self.rightButtonNode.view.superview != nil { + if let snapshotView = self.rightButtonNode.view.snapshotContentTree() { + snapshotView.frame = self.rightButtonNode.frame + self.rightButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.rightButtonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + self.rightButtonNode.updateItems(items) + if self.rightButtonNode.supernode == nil { + self.clippingNode.addSubnode(self.rightButtonNode) + } + if animated { + self.rightButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } else { + self.rightButtonNode.removeFromSupernode() + } + } else { + self.rightButtonNode.removeFromSupernode() + } + + if animated { + self.hintAnimateTitleNodeOnNextLayout = true + } + self.updateAccessibilityElements() + } + + private let backButtonNode: NavigationButtonNode + private let badgeNode: NavigationBarBadgeNode + private let backButtonArrow: ASImageNode + private let leftButtonNode: NavigationButtonNode + private let rightButtonNode: NavigationButtonNode + + + private var _transitionState: NavigationBarTransitionState? + var transitionState: NavigationBarTransitionState? { + get { + return self._transitionState + } set(value) { + let updateNodes = self._transitionState?.navigationBar !== value?.navigationBar + + self._transitionState = value + + if updateNodes { + if let transitionTitleNode = self.transitionTitleNode { + transitionTitleNode.removeFromSupernode() + self.transitionTitleNode = nil + } + + if let transitionBackButtonNode = self.transitionBackButtonNode { + transitionBackButtonNode.removeFromSupernode() + self.transitionBackButtonNode = nil + } + + if let transitionBackArrowNode = self.transitionBackArrowNode { + transitionBackArrowNode.removeFromSupernode() + self.transitionBackArrowNode = nil + } + + if let transitionBadgeNode = self.transitionBadgeNode { + transitionBadgeNode.removeFromSupernode() + self.transitionBadgeNode = nil + } + + if let value = value { + switch value.role { + case .top: + if let transitionTitleNode = value.navigationBar?.makeTransitionTitleNode(foregroundColor: self.presentationData.theme.primaryTextColor) { + self.transitionTitleNode = transitionTitleNode + if self.leftButtonNode.supernode != nil { + self.clippingNode.insertSubnode(transitionTitleNode, belowSubnode: self.leftButtonNode) + } else if self.backButtonNode.supernode != nil { + self.clippingNode.insertSubnode(transitionTitleNode, belowSubnode: self.backButtonNode) + } else { + self.clippingNode.addSubnode(transitionTitleNode) + } + } + case .bottom: + if let transitionBackButtonNode = value.navigationBar?.makeTransitionBackButtonNode(accentColor: self.presentationData.theme.buttonColor) { + self.transitionBackButtonNode = transitionBackButtonNode + self.clippingNode.addSubnode(transitionBackButtonNode) + } + if let transitionBackArrowNode = value.navigationBar?.makeTransitionBackArrowNode(accentColor: self.presentationData.theme.buttonColor) { + self.transitionBackArrowNode = transitionBackArrowNode + self.clippingNode.addSubnode(transitionBackArrowNode) + } + if let transitionBadgeNode = value.navigationBar?.makeTransitionBadgeNode() { + self.transitionBadgeNode = transitionBadgeNode + self.clippingNode.addSubnode(transitionBadgeNode) + } + } + } + } + + self.requestedLayout = true + self.layout() + } + } + + private var transitionTitleNode: ASDisplayNode? + private var transitionBackButtonNode: NavigationButtonNode? + private var transitionBackArrowNode: ASDisplayNode? + private var transitionBadgeNode: ASDisplayNode? + + public init(presentationData: NavigationBarPresentationData) { + self.presentationData = presentationData + self.stripeNode = ASDisplayNode() + + self.titleNode = ASTextNode() + self.titleNode.isAccessibilityElement = true + self.titleNode.accessibilityTraits = UIAccessibilityTraitHeader + + self.backButtonNode = NavigationButtonNode() + self.badgeNode = NavigationBarBadgeNode(fillColor: self.presentationData.theme.badgeBackgroundColor, strokeColor: self.presentationData.theme.badgeStrokeColor, textColor: self.presentationData.theme.badgeTextColor) + self.badgeNode.isUserInteractionEnabled = false + self.badgeNode.isHidden = true + self.backButtonArrow = ASImageNode() + self.backButtonArrow.displayWithoutProcessing = true + self.backButtonArrow.displaysAsynchronously = false + self.leftButtonNode = NavigationButtonNode() + self.rightButtonNode = NavigationButtonNode() + self.rightButtonNode.hitTestSlop = UIEdgeInsets(top: -4.0, left: -4.0, bottom: -4.0, right: -10.0) + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.backButtonNode.color = self.presentationData.theme.buttonColor + self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.leftButtonNode.color = self.presentationData.theme.buttonColor + self.leftButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.rightButtonNode.color = self.presentationData.theme.buttonColor + self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) + if let title = self.title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) + self.titleNode.accessibilityLabel = title + } + self.stripeNode.backgroundColor = self.presentationData.theme.separatorColor + + super.init() + + self.addSubnode(self.clippingNode) + + self.backgroundColor = self.presentationData.theme.backgroundColor + + self.stripeNode.isLayerBacked = true + self.stripeNode.displaysAsynchronously = false + self.addSubnode(self.stripeNode) + + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.isOpaque = false + + self.backButtonNode.highlightChanged = { [weak self] index, highlighted in + if let strongSelf = self, index == 0 { + strongSelf.backButtonArrow.alpha = (highlighted ? 0.4 : 1.0) + } + } + self.backButtonNode.pressed = { [weak self] index in + if let strongSelf = self, index == 0 { + if let leftBarButtonItem = strongSelf.item?.leftBarButtonItem, leftBarButtonItem.backButtonAppearance { + leftBarButtonItem.performActionOnTarget() + } else { + strongSelf.backPressed() + } + } + } + + self.leftButtonNode.pressed = { [weak self] index in + if let item = self?.item { + if index == 0 { + if let leftBarButtonItem = item.leftBarButtonItem { + leftBarButtonItem.performActionOnTarget() + } else if let previousItem = self?.previousItem, case .close = previousItem { + self?.backPressed() + } + } + } + } + + self.rightButtonNode.pressed = { [weak self] index in + if let item = self?.item { + if let rightBarButtonItems = item.rightBarButtonItems, !rightBarButtonItems.isEmpty { + if index < rightBarButtonItems.count { + rightBarButtonItems[index].performActionOnTarget() + } + } else if let rightBarButtonItem = item.rightBarButtonItem { + rightBarButtonItem.performActionOnTarget() + } + } + } + } + + public func updatePresentationData(_ presentationData: NavigationBarPresentationData) { + if presentationData.theme !== self.presentationData.theme || presentationData.strings !== self.presentationData.strings { + self.presentationData = presentationData + + self.backgroundColor = self.presentationData.theme.backgroundColor + + self.backButtonNode.color = self.presentationData.theme.buttonColor + self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.leftButtonNode.color = self.presentationData.theme.buttonColor + self.leftButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.rightButtonNode.color = self.presentationData.theme.buttonColor + self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) + if let title = self.title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) + self.titleNode.accessibilityLabel = title + } + self.stripeNode.backgroundColor = self.presentationData.theme.separatorColor + + self.badgeNode.updateTheme(fillColor: self.presentationData.theme.badgeBackgroundColor, strokeColor: self.presentationData.theme.badgeStrokeColor, textColor: self.presentationData.theme.badgeTextColor) + } + } + + private func requestLayout() { + self.requestedLayout = true + self.setNeedsLayout() + } + + override open func layout() { + super.layout() + + if let validLayout = self.validLayout, self.requestedLayout { + self.requestedLayout = false + self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: .immediate) + } + } + + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + if self.layoutSuspended { + return + } + + self.validLayout = (size, leftInset, rightInset) + + let leftButtonInset: CGFloat = leftInset + 16.0 + let backButtonInset: CGFloat = leftInset + 27.0 + + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: size)) + var expansionHeight: CGFloat = 0.0 + if let contentNode = self.contentNode { + let contentNodeFrame: CGRect + switch contentNode.mode { + case .replacement: + expansionHeight = contentNode.height - 44.0 + contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + case .expansion: + expansionHeight = contentNode.height + contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - expansionHeight), size: CGSize(width: size.width, height: expansionHeight)) + } + transition.updateFrame(node: contentNode, frame: contentNodeFrame) + contentNode.updateLayout(size: contentNodeFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) + } + + transition.updateFrame(node: self.stripeNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)) + + let nominalHeight: CGFloat = self.collapsed ? 32.0 : 44.0 + let contentVerticalOrigin = size.height - nominalHeight - expansionHeight + + var leftTitleInset: CGFloat = leftInset + 1.0 + var rightTitleInset: CGFloat = rightInset + 1.0 + if self.backButtonNode.supernode != nil { + let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight)) + leftTitleInset += backButtonSize.width + backButtonInset + 1.0 + + let topHitTestSlop = (nominalHeight - backButtonSize.height) * 0.5 + self.backButtonNode.hitTestSlop = UIEdgeInsetsMake(-topHitTestSlop, -27.0, -topHitTestSlop, -8.0) + + if let transitionState = self.transitionState { + let progress = transitionState.progress + + switch transitionState.role { + case .top: + let initialX: CGFloat = backButtonInset + let finalX: CGFloat = floor((size.width - backButtonSize.width) / 2.0) - size.width + + self.backButtonNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize) + self.backButtonNode.alpha = (1.0 - progress) * (1.0 - progress) + + if let transitionTitleNode = self.transitionTitleNode { + let transitionTitleSize = transitionTitleNode.measure(CGSize(width: size.width, height: nominalHeight)) + + let initialX: CGFloat = backButtonInset + floor((backButtonSize.width - transitionTitleSize.width) / 2.0) + let finalX: CGFloat = floor((size.width - transitionTitleSize.width) / 2.0) - size.width + + transitionTitleNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - transitionTitleSize.height) / 2.0)), size: transitionTitleSize) + transitionTitleNode.alpha = progress * progress + } + + self.backButtonArrow.frame = CGRect(origin: CGPoint(x: leftInset + 8.0 - progress * size.width, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0)) + self.backButtonArrow.alpha = max(0.0, 1.0 - progress * 1.3) + self.badgeNode.alpha = max(0.0, 1.0 - progress * 1.3) + case .bottom: + self.backButtonNode.alpha = 1.0 + self.backButtonNode.frame = CGRect(origin: CGPoint(x: backButtonInset, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize) + self.backButtonArrow.alpha = 1.0 + self.backButtonArrow.frame = CGRect(origin: CGPoint(x: leftInset + 8.0, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0)) + self.badgeNode.alpha = 1.0 + } + } else { + self.backButtonNode.alpha = 1.0 + self.backButtonNode.frame = CGRect(origin: CGPoint(x: backButtonInset, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize) + self.backButtonArrow.alpha = 1.0 + self.backButtonArrow.frame = CGRect(origin: CGPoint(x: leftInset + 8.0, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0)) + self.badgeNode.alpha = 1.0 + } + } else if self.leftButtonNode.supernode != nil { + let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight)) + leftTitleInset += leftButtonSize.width + leftButtonInset + 1.0 + + self.leftButtonNode.alpha = 1.0 + self.leftButtonNode.frame = CGRect(origin: CGPoint(x: leftButtonInset, y: contentVerticalOrigin + floor((nominalHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize) + } + + let badgeSize = self.badgeNode.measure(CGSize(width: 200.0, height: 100.0)) + let backButtonArrowFrame = self.backButtonArrow.frame + self.badgeNode.frame = CGRect(origin: backButtonArrowFrame.origin.offsetBy(dx: 7.0, dy: -9.0), size: badgeSize) + + if self.rightButtonNode.supernode != nil { + let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight))) + rightTitleInset += rightButtonSize.width + leftButtonInset + 1.0 + self.rightButtonNode.alpha = 1.0 + self.rightButtonNode.frame = CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize) + } + + if let transitionState = self.transitionState { + let progress = transitionState.progress + + switch transitionState.role { + case .top: + break + case .bottom: + if let transitionBackButtonNode = self.transitionBackButtonNode { + let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight)) + let initialX: CGFloat = backButtonInset + size.width * 0.3 + let finalX: CGFloat = floor((size.width - transitionBackButtonSize.width) / 2.0) + + transitionBackButtonNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - transitionBackButtonSize.height) / 2.0)), size: transitionBackButtonSize) + transitionBackButtonNode.alpha = (1.0 - progress) * (1.0 - progress) + } + + if let transitionBackArrowNode = self.transitionBackArrowNode { + let initialX: CGFloat = leftInset + 8.0 + size.width * 0.3 + let finalX: CGFloat = leftInset + 8.0 + + transitionBackArrowNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0)) + transitionBackArrowNode.alpha = max(0.0, 1.0 - progress * 1.3) + + if let transitionBadgeNode = self.transitionBadgeNode { + transitionBadgeNode.frame = CGRect(origin: transitionBackArrowNode.frame.origin.offsetBy(dx: 7.0, dy: -9.0), size: transitionBadgeNode.bounds.size) + transitionBadgeNode.alpha = transitionBackArrowNode.alpha + } + } + } + } + + leftTitleInset = floor(leftTitleInset) + if Int(leftTitleInset) % 2 != 0 { + leftTitleInset -= 1.0 + } + + if self.titleNode.supernode != nil { + let titleSize = self.titleNode.measure(CGSize(width: max(1.0, size.width - max(leftTitleInset, rightTitleInset) * 2.0), height: nominalHeight)) + + if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar { + let progress = transitionState.progress + + switch transitionState.role { + case .top: + let initialX = floor((size.width - titleSize.width) / 2.0) + let finalX: CGFloat = leftButtonInset + + self.titleNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + self.titleNode.alpha = (1.0 - progress) * (1.0 - progress) + case .bottom: + var initialX: CGFloat = backButtonInset + if otherNavigationBar.backButtonNode.supernode != nil { + initialX += floor((otherNavigationBar.backButtonNode.frame.size.width - titleSize.width) / 2.0) + } + initialX += size.width * 0.3 + let finalX: CGFloat = floor((size.width - titleSize.width) / 2.0) + + self.titleNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + self.titleNode.alpha = progress * progress + } + } else { + self.titleNode.alpha = 1.0 + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + } + } + + if let titleView = self.titleView { + let titleSize = CGSize(width: max(1.0, size.width - max(leftTitleInset, rightTitleInset) * 2.0), height: nominalHeight) + let titleFrame = CGRect(origin: CGPoint(x: leftTitleInset, y: contentVerticalOrigin), size: titleSize) + titleView.frame = titleFrame + + if let titleView = titleView as? NavigationBarTitleView { + let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset) + + titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: transition) + } + + if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar { + let progress = transitionState.progress + + switch transitionState.role { + case .top: + let initialX = floor((size.width - titleSize.width) / 2.0) + let finalX: CGFloat = leftButtonInset + + titleView.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + titleView.alpha = (1.0 - progress) * (1.0 - progress) + case .bottom: + var initialX: CGFloat = backButtonInset + if otherNavigationBar.backButtonNode.supernode != nil { + initialX += floor((otherNavigationBar.backButtonNode.frame.size.width - titleSize.width) / 2.0) + } + initialX += size.width * 0.3 + let finalX: CGFloat = floor((size.width - titleSize.width) / 2.0) + + titleView.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + titleView.alpha = progress * progress + } + } else { + if self.hintAnimateTitleNodeOnNextLayout { + self.hintAnimateTitleNodeOnNextLayout = false + if let titleView = titleView as? NavigationBarTitleView { + titleView.animateLayoutTransition() + } + } + titleView.alpha = 1.0 + titleView.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + } + } + } + + public func makeTransitionTitleNode(foregroundColor: UIColor) -> ASDisplayNode? { + if let titleView = self.titleView { + if let transitionView = titleView as? NavigationBarTitleTransitionNode { + return transitionView.makeTransitionMirrorNode() + } else { + return nil + } + } else if let title = self.title { + let node = ASTextNode() + node.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: foregroundColor) + return node + } else { + return nil + } + } + + private func makeTransitionBackButtonNode(accentColor: UIColor) -> NavigationButtonNode? { + if self.backButtonNode.supernode != nil { + let node = NavigationButtonNode() + node.updateManualText(self.backButtonNode.manualText) + node.color = accentColor + return node + } else { + return nil + } + } + + private func makeTransitionBackArrowNode(accentColor: UIColor) -> ASDisplayNode? { + if self.backButtonArrow.supernode != nil { + let node = ASImageNode() + node.image = backArrowImage(color: accentColor) + node.frame = self.backButtonArrow.frame + node.displayWithoutProcessing = true + node.displaysAsynchronously = false + return node + } else { + return nil + } + } + + private func makeTransitionBadgeNode() -> ASDisplayNode? { + if self.badgeNode.supernode != nil && !self.badgeNode.isHidden { + let node = NavigationBarBadgeNode(fillColor: self.presentationData.theme.badgeBackgroundColor, strokeColor: self.presentationData.theme.badgeStrokeColor, textColor: self.presentationData.theme.badgeTextColor) + node.text = self.badgeNode.text + let nodeSize = node.measure(CGSize(width: 200.0, height: 100.0)) + node.frame = CGRect(origin: CGPoint(), size: nodeSize) + return node + } else { + return nil + } + } + + public var canTransitionInline: Bool { + if let contentNode = self.contentNode, case .replacement = contentNode.mode { + return false + } else { + return true + } + } + + public var contentHeight: CGFloat { + if let contentNode = self.contentNode { + switch contentNode.mode { + case .expansion: + return 44.0 + contentNode.height + case .replacement: + return contentNode.height + } + } else { + return 44.0 + } + } + + public func setContentNode(_ contentNode: NavigationBarContentNode?, animated: Bool) { + if self.contentNode !== contentNode { + if let previous = self.contentNode { + if animated { + previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak previous] _ in + if let strongSelf = self, let previous = previous { + if previous !== strongSelf.contentNode { + previous.removeFromSupernode() + } + } + }) + } else { + previous.removeFromSupernode() + } + } + self.contentNode = contentNode + self.contentNode?.requestContainerLayout = { [weak self] transition in + self?.requestContainerLayout(transition) + } + if let contentNode = contentNode { + contentNode.clipsToBounds = true + contentNode.layer.removeAnimation(forKey: "opacity") + self.insertSubnode(contentNode, belowSubnode: self.stripeNode) + if animated { + contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + if case .replacement = contentNode.mode, !self.clippingNode.alpha.isZero { + self.clippingNode.alpha = 0.0 + if animated { + self.clippingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + + if !self.bounds.size.width.isZero { + self.requestedLayout = true + self.layout() + } else { + self.requestLayout() + } + } else if self.clippingNode.alpha.isZero { + self.clippingNode.alpha = 1.0 + if animated { + self.clippingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + } + + public func executeBack() -> Bool { + if self.backButtonNode.isInHierarchy { + self.backButtonNode.pressed(0) + } else if self.leftButtonNode.isInHierarchy { + self.leftButtonNode.pressed(0) + } else { + self.backButtonNode.pressed(0) + } + return true + } + + public func setHidden(_ hidden: Bool, animated: Bool) { + if let contentNode = self.contentNode, case .replacement = contentNode.mode { + } else { + let targetAlpha: CGFloat = hidden ? 0.0 : 1.0 + let previousAlpha = self.clippingNode.alpha + if previousAlpha != targetAlpha { + self.clippingNode.alpha = targetAlpha + if animated { + self.clippingNode.layer.animateAlpha(from: previousAlpha, to: targetAlpha, duration: 0.2) + } + } + } + } +} diff --git a/submodules/Display/Display/NavigationBarBadge.swift b/submodules/Display/Display/NavigationBarBadge.swift new file mode 100644 index 0000000000..089b88b0e3 --- /dev/null +++ b/submodules/Display/Display/NavigationBarBadge.swift @@ -0,0 +1,60 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class NavigationBarBadgeNode: ASDisplayNode { + private var fillColor: UIColor + private var strokeColor: UIColor + private var textColor: UIColor + + private let textNode: ASTextNode + private let backgroundNode: ASImageNode + + private let font: UIFont = Font.regular(13.0) + + var text: String = "" { + didSet { + self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) + self.invalidateCalculatedLayout() + } + } + + init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { + self.fillColor = fillColor + self.strokeColor = strokeColor + self.textColor = textColor + + self.textNode = ASTextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + } + + func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { + self.fillColor = fillColor + self.strokeColor = strokeColor + self.textColor = textColor + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) + self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + let badgeSize = self.textNode.measure(constrainedSize) + let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) + let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) + self.backgroundNode.frame = backgroundFrame + self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0)), size: badgeSize) + + return backgroundSize + } +} diff --git a/submodules/Display/Display/NavigationBarContentNode.swift b/submodules/Display/Display/NavigationBarContentNode.swift new file mode 100644 index 0000000000..768628733b --- /dev/null +++ b/submodules/Display/Display/NavigationBarContentNode.swift @@ -0,0 +1,31 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum NavigationBarContentMode { + case replacement + case expansion +} + +open class NavigationBarContentNode: ASDisplayNode { + open var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in } + + open var height: CGFloat { + return self.nominalHeight + } + + open var clippedHeight: CGFloat { + return self.nominalHeight + } + + open var nominalHeight: CGFloat { + return 44.0 + } + + open var mode: NavigationBarContentMode { + return .replacement + } + + open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + } +} diff --git a/submodules/Display/Display/NavigationBarProxy.h b/submodules/Display/Display/NavigationBarProxy.h new file mode 100644 index 0000000000..9936b4726a --- /dev/null +++ b/submodules/Display/Display/NavigationBarProxy.h @@ -0,0 +1,7 @@ +#import + +@interface NavigationBarProxy : UINavigationBar + +@property (nonatomic, copy) void (^setItemsProxy)(NSArray *, NSArray *, bool); + +@end diff --git a/submodules/Display/Display/NavigationBarProxy.m b/submodules/Display/Display/NavigationBarProxy.m new file mode 100644 index 0000000000..9f5700c13b --- /dev/null +++ b/submodules/Display/Display/NavigationBarProxy.m @@ -0,0 +1,67 @@ +#import "NavigationBarProxy.h" + +@interface NavigationBarProxy () +{ + NSArray *_items; +} + +@end + +@implementation NavigationBarProxy + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + } + return self; +} + +- (void)pushNavigationItem:(UINavigationItem *)item animated:(BOOL)animated +{ + [self setItems:[[self items] arrayByAddingObject:item] animated:animated]; +} + +- (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated +{ + NSMutableArray *items = [[NSMutableArray alloc] initWithArray:[self items]]; + UINavigationItem *lastItem = [items lastObject]; + [items removeLastObject]; + [self setItems:items animated:animated]; + return lastItem; +} + +- (UINavigationItem *)topItem +{ + return [[self items] lastObject]; +} + +- (UINavigationItem *)backItem +{ + NSLog(@"backItem"); + return nil; +} + +- (NSArray *)items +{ + if (_items == nil) + return @[]; + return _items; +} + +- (void)setItems:(NSArray *)items +{ + [self setItems:items animated:false]; +} + +- (void)setItems:(NSArray *)items animated:(BOOL)animated +{ + NSArray *previousItems = _items; + _items = items; + + if (_setItemsProxy) + _setItemsProxy(previousItems, items, animated); +} + +@end diff --git a/submodules/Display/Display/NavigationBarTitleTransitionNode.swift b/submodules/Display/Display/NavigationBarTitleTransitionNode.swift new file mode 100644 index 0000000000..1121280999 --- /dev/null +++ b/submodules/Display/Display/NavigationBarTitleTransitionNode.swift @@ -0,0 +1,6 @@ +import Foundation +import AsyncDisplayKit + +public protocol NavigationBarTitleTransitionNode { + func makeTransitionMirrorNode() -> ASDisplayNode +} diff --git a/submodules/Display/Display/NavigationBarTitleView.swift b/submodules/Display/Display/NavigationBarTitleView.swift new file mode 100644 index 0000000000..ee8fa822e5 --- /dev/null +++ b/submodules/Display/Display/NavigationBarTitleView.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit + +public protocol NavigationBarTitleView { + func animateLayoutTransition() + + func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) +} diff --git a/submodules/Display/Display/NavigationBarTransitionContainer.swift b/submodules/Display/Display/NavigationBarTransitionContainer.swift new file mode 100644 index 0000000000..2333a0e7a0 --- /dev/null +++ b/submodules/Display/Display/NavigationBarTransitionContainer.swift @@ -0,0 +1,69 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +class NavigationBarTransitionContainer: ASDisplayNode { + var progress: CGFloat = 0.0 { + didSet { + self.layout() + } + } + + let transition: NavigationTransition + let topNavigationBar: NavigationBar + let bottomNavigationBar: NavigationBar + + let topClippingNode: ASDisplayNode + let bottomClippingNode: ASDisplayNode + + let topNavigationBarSupernode: ASDisplayNode? + let bottomNavigationBarSupernode: ASDisplayNode? + + init(transition: NavigationTransition, topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) { + self.transition = transition + + self.topNavigationBar = topNavigationBar + self.topNavigationBarSupernode = topNavigationBar.supernode + + self.bottomNavigationBar = bottomNavigationBar + self.bottomNavigationBarSupernode = bottomNavigationBar.supernode + + self.topClippingNode = ASDisplayNode() + self.topClippingNode.clipsToBounds = true + self.bottomClippingNode = ASDisplayNode() + self.bottomClippingNode.clipsToBounds = true + + super.init() + + self.topClippingNode.addSubnode(self.topNavigationBar) + self.bottomClippingNode.addSubnode(self.bottomNavigationBar) + + self.addSubnode(self.bottomClippingNode) + self.addSubnode(self.topClippingNode) + } + + func complete() { + self.topNavigationBarSupernode?.addSubnode(self.topNavigationBar) + self.bottomNavigationBarSupernode?.addSubnode(self.bottomNavigationBar) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + let position: CGFloat + switch self.transition { + case .Push: + position = 1.0 - progress + case .Pop: + position = progress + } + + let offset = floorToScreenPixels(size.width * position) + + self.topClippingNode.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: size) + + self.bottomClippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: offset, height: size.height)) + } +} diff --git a/submodules/Display/Display/NavigationBarTransitionState.swift b/submodules/Display/Display/NavigationBarTransitionState.swift new file mode 100644 index 0000000000..dce45512df --- /dev/null +++ b/submodules/Display/Display/NavigationBarTransitionState.swift @@ -0,0 +1,21 @@ +import Foundation +import UIKit + +enum NavigationBarTransitionRole { + case top + case bottom +} + +final class NavigationBarTransitionState { + weak var navigationBar: NavigationBar? + let transition: NavigationTransition + let role: NavigationBarTransitionRole + let progress: CGFloat + + init(navigationBar: NavigationBar, transition: NavigationTransition, role: NavigationBarTransitionRole, progress: CGFloat) { + self.navigationBar = navigationBar + self.transition = transition + self.role = role + self.progress = progress + } +} diff --git a/submodules/Display/Display/NavigationButtonNode.swift b/submodules/Display/Display/NavigationButtonNode.swift new file mode 100644 index 0000000000..f69a5b3f30 --- /dev/null +++ b/submodules/Display/Display/NavigationButtonNode.swift @@ -0,0 +1,390 @@ +import UIKit +import AsyncDisplayKit + +public protocol NavigationButtonCustomDisplayNode { + var isHighlightable: Bool { get } +} + +private final class NavigationButtonItemNode: ASTextNode { + private func fontForCurrentState() -> UIFont { + return self.bold ? UIFont.boldSystemFont(ofSize: 17.0) : UIFont.systemFont(ofSize: 17.0) + } + + private func attributesForCurrentState() -> [NSAttributedStringKey : AnyObject] { + return [ + NSAttributedStringKey.font: self.fontForCurrentState(), + NSAttributedStringKey.foregroundColor: self.isEnabled ? self.color : self.disabledColor + ] + } + + private var setEnabledListener: Int? + + var item: UIBarButtonItem? { + didSet { + if self.item !== oldValue { + if let oldValue = oldValue, let setEnabledListener = self.setEnabledListener { + oldValue.removeSetEnabledListener(setEnabledListener) + self.setEnabledListener = nil + } + + if let item = self.item { + self.setEnabledListener = item.addSetEnabledListener { [weak self] value in + self?.isEnabled = value + } + self.accessibilityHint = item.accessibilityHint + } + } + } + } + + private var _text: String? + public var text: String { + get { + return _text ?? "" + } + set(value) { + _text = value + + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + self.item?.accessibilityLabel = value + } + } + + private var imageNode: ASImageNode? + + private var _image: UIImage? + public var image: UIImage? { + get { + return _image + } set(value) { + _image = value + + if let _ = value { + if self.imageNode == nil { + let imageNode = ASImageNode() + imageNode.displayWithoutProcessing = true + imageNode.displaysAsynchronously = false + self.imageNode = imageNode + self.addSubnode(imageNode) + } + self.imageNode?.image = image + } else if let imageNode = self.imageNode { + imageNode.removeFromSupernode() + self.imageNode = nil + } + + self.invalidateCalculatedLayout() + self.setNeedsLayout() + } + } + + public var node: ASDisplayNode? { + didSet { + if self.node !== oldValue { + oldValue?.removeFromSupernode() + if let node = self.node { + self.addSubnode(node) + self.invalidateCalculatedLayout() + self.setNeedsLayout() + } + } + } + } + + public var color: UIColor = UIColor(rgb: 0x007ee5) { + didSet { + if let text = self._text { + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + } + } + } + + public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) { + didSet { + if let text = self._text { + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + } + } + } + + private var _bold: Bool = false + public var bold: Bool { + get { + return _bold + } + set(value) { + if _bold != value { + _bold = value + + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + } + } + } + + private var touchCount = 0 + public var pressed: () -> () = { } + public var highlightChanged: (Bool) -> () = { _ in } + + override public var isAccessibilityElement: Bool { + get { + return true + } set(value) { + super.isAccessibilityElement = true + } + } + + + override public init() { + super.init() + + self.isAccessibilityElement = true + + self.isUserInteractionEnabled = true + self.isExclusiveTouch = true + self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0) + self.displaysAsynchronously = false + + self.accessibilityTraits = UIAccessibilityTraitButton + } + + func updateLayout(_ constrainedSize: CGSize) -> CGSize { + let superSize = super.calculateSizeThatFits(constrainedSize) + + if let node = self.node { + let nodeSize = node.measure(constrainedSize) + let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height)) + node.frame = CGRect(origin: CGPoint(), size: nodeSize) + return size + } else if let imageNode = self.imageNode { + let nodeSize = imageNode.image?.size ?? CGSize() + let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height)) + imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize) + return size + } + return superSize + } + + private func touchInsideApparentBounds(_ touch: UITouch) -> Bool { + var apparentBounds = self.bounds + let hitTestSlop = self.hitTestSlop + apparentBounds.origin.x += hitTestSlop.left + apparentBounds.size.width += -hitTestSlop.left - hitTestSlop.right + apparentBounds.origin.y += hitTestSlop.top + apparentBounds.size.height += -hitTestSlop.top - hitTestSlop.bottom + + return apparentBounds.contains(touch.location(in: self.view)) + } + + public override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + self.touchCount += touches.count + self.updateHighlightedState(true, animated: false) + } + + public override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) + + self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true) + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + self.updateHighlightedState(false, animated: false) + + let previousTouchCount = self.touchCount + self.touchCount = max(0, self.touchCount - touches.count) + + if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && self.touchInsideApparentBounds(touches.first!) { + self.pressed() + } + } + + public override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + + self.touchCount = max(0, self.touchCount - (touches?.count ?? 0)) + self.updateHighlightedState(false, animated: false) + } + + private var _highlighted = false + private func updateHighlightedState(_ highlighted: Bool, animated: Bool) { + if _highlighted != highlighted { + _highlighted = highlighted + + var shouldChangeHighlight = true + if let node = self.node as? NavigationButtonCustomDisplayNode { + shouldChangeHighlight = node.isHighlightable + } + + if shouldChangeHighlight { + self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0) + self.highlightChanged(highlighted) + } + } + } + + public override var isEnabled: Bool { + get { + return super.isEnabled + } + set(value) { + if self.isEnabled != value { + super.isEnabled = value + + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + } + } + } +} + + +final class NavigationButtonNode: ASDisplayNode { + private var nodes: [NavigationButtonItemNode] = [] + + public var pressed: (Int) -> () = { _ in } + public var highlightChanged: (Int, Bool) -> () = { _, _ in } + + public var color: UIColor = UIColor(rgb: 0x007ee5) { + didSet { + if !self.color.isEqual(oldValue) { + for node in self.nodes { + node.color = self.color + } + } + } + } + + public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) { + didSet { + if !self.disabledColor.isEqual(oldValue) { + for node in self.nodes { + node.disabledColor = self.disabledColor + } + } + } + } + + override public var accessibilityElements: [Any]? { + get { + return self.nodes + } set(value) { + } + } + + override init() { + super.init() + + self.isAccessibilityElement = false + } + + var manualText: String { + return self.nodes.first?.text ?? "" + } + + func updateManualText(_ text: String, isBack: Bool = true) { + let node: NavigationButtonItemNode + if self.nodes.count > 0 { + node = self.nodes[0] + } else { + node = NavigationButtonItemNode() + node.color = self.color + node.highlightChanged = { [weak node, weak self] value in + if let strongSelf = self, let node = node { + if let index = strongSelf.nodes.index(where: { $0 === node }) { + strongSelf.highlightChanged(index, value) + } + } + } + node.pressed = { [weak self, weak node] in + if let strongSelf = self, let node = node { + if let index = strongSelf.nodes.index(where: { $0 === node }) { + strongSelf.pressed(index) + } + } + } + self.nodes.append(node) + self.addSubnode(node) + } + node.item = nil + node.text = text + + /*if isBack { + node.accessibilityHint = "Back button" + node.accessibilityTraits = 0 + } else { + node.accessibilityHint = nil + node.accessibilityTraits = UIAccessibilityTraitButton + }*/ + + node.image = nil + node.bold = false + node.isEnabled = true + node.node = nil + + if 1 < self.nodes.count { + for i in 1 ..< self.nodes.count { + self.nodes[i].removeFromSupernode() + } + self.nodes.removeSubrange(1...) + } + } + + func updateItems(_ items: [UIBarButtonItem]) { + for i in 0 ..< items.count { + let node: NavigationButtonItemNode + if self.nodes.count > i { + node = self.nodes[i] + } else { + node = NavigationButtonItemNode() + node.color = self.color + node.highlightChanged = { [weak node, weak self] value in + if let strongSelf = self, let node = node { + if let index = strongSelf.nodes.index(where: { $0 === node }) { + strongSelf.highlightChanged(index, value) + } + } + } + node.pressed = { [weak self, weak node] in + if let strongSelf = self, let node = node { + if let index = strongSelf.nodes.index(where: { $0 === node }) { + strongSelf.pressed(index) + } + } + } + self.nodes.append(node) + self.addSubnode(node) + } + node.item = items[i] + node.text = items[i].title ?? "" + node.image = items[i].image + node.bold = items[i].style == .done + node.isEnabled = items[i].isEnabled + node.node = items[i].customDisplayNode + } + if items.count < self.nodes.count { + for i in items.count ..< self.nodes.count { + self.nodes[i].removeFromSupernode() + } + self.nodes.removeSubrange(items.count...) + } + } + + func updateLayout(constrainedSize: CGSize) -> CGSize { + var nodeOrigin = CGPoint() + var totalSize = CGSize() + for node in self.nodes { + if !totalSize.width.isZero { + totalSize.width += 16.0 + nodeOrigin.x += 16.0 + } + var nodeSize = node.updateLayout(constrainedSize) + nodeSize.width = ceil(nodeSize.width) + nodeSize.height = ceil(nodeSize.height) + totalSize.width += nodeSize.width + totalSize.height = max(totalSize.height, nodeSize.height) + node.frame = CGRect(origin: nodeOrigin, size: nodeSize) + nodeOrigin.x += node.bounds.width + } + return totalSize + } +} diff --git a/submodules/Display/Display/NavigationController.swift b/submodules/Display/Display/NavigationController.swift new file mode 100644 index 0000000000..e0e2965134 --- /dev/null +++ b/submodules/Display/Display/NavigationController.swift @@ -0,0 +1,1120 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +#if BUCK +import DisplayPrivate +#endif + +public final class NavigationControllerTheme { + public let navigationBar: NavigationBarTheme + public let emptyAreaColor: UIColor + public let emptyDetailIcon: UIImage? + + public init(navigationBar: NavigationBarTheme, emptyAreaColor: UIColor, emptyDetailIcon: UIImage?) { + self.navigationBar = navigationBar + self.emptyAreaColor = emptyAreaColor + self.emptyDetailIcon = emptyDetailIcon + } +} + +private final class NavigationControllerContainerView: UIView { + override class var layerClass: AnyClass { + return CATracingLayer.self + } +} + +private final class NavigationControllerView: UITracingLayerView { + var inTransition = false + + let sharedStatusBar: StatusBar + let containerView: NavigationControllerContainerView + let separatorView: UIView + var navigationBackgroundView: UIView? + var navigationSeparatorView: UIView? + var emptyDetailView: UIImageView? + + var topControllerNode: ASDisplayNode? + + /*override var accessibilityElements: [Any]? { + get { + var accessibilityElements: [Any] = [] + if let topControllerNode = self.topControllerNode { + addAccessibilityChildren(of: topControllerNode, container: self, to: &accessibilityElements) + } + return accessibilityElements + } set(value) { + } + }*/ + + override init(frame: CGRect) { + self.containerView = NavigationControllerContainerView() + self.separatorView = UIView() + self.sharedStatusBar = StatusBar() + + super.init(frame: frame) + + self.addSubview(self.containerView) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override class var layerClass: AnyClass { + return CATracingLayer.self + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) && self.inTransition { + return self + } + return super.hitTest(point, with: event) + } +} + +private enum ControllerTransition { + case none + case appearance +} + +private final class ControllerRecord { + let controller: UIViewController + var transition: ControllerTransition = .none + + init(controller: UIViewController) { + self.controller = controller + } +} + +private enum ControllerLayoutConfiguration { + case single + case masterDetail +} + +public enum NavigationControllerMode { + case single + case automaticMasterDetail +} + +open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate { + public var isOpaqueWhenInOverlay: Bool = true + public var blocksBackgroundWhenInOverlay: Bool = true + + public var ready: Promise = Promise(true) + + public var lockOrientation: Bool = false + + public var deferScreenEdgeGestures: UIRectEdge = UIRectEdge() + + private let mode: NavigationControllerMode + private var theme: NavigationControllerTheme + + public private(set) weak var overlayPresentingController: ViewController? + + private var controllerView: NavigationControllerView { + return self.view as! NavigationControllerView + } + + private var validLayout: ContainerViewLayout? + + private var scheduledLayoutTransitionRequestId: Int = 0 + private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + + private var navigationTransitionCoordinator: NavigationTransitionCoordinator? + + private var currentPushDisposable = MetaDisposable() + private var currentPresentDisposable = MetaDisposable() + + private var _presentedViewController: UIViewController? + open override var presentedViewController: UIViewController? { + return self._presentedViewController + } + + private var _viewControllers: [ControllerRecord] = [] + override open var viewControllers: [UIViewController] { + get { + return self._viewControllers.map { $0.controller } + } set(value) { + self.setViewControllers(value, animated: false) + } + } + + override open var topViewController: UIViewController? { + return self._viewControllers.last?.controller + } + + private var _displayNode: ASDisplayNode? + public var displayNode: ASDisplayNode { + return self._displayNode! + } + + public init(mode: NavigationControllerMode, theme: NavigationControllerTheme) { + self.mode = mode + self.theme = theme + + super.init(nibName: nil, bundle: nil) + } + + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + preconditionFailure() + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.currentPushDisposable.dispose() + self.currentPresentDisposable.dispose() + } + + public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations { + var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) + if let controller = self.viewControllers.last { + if let controller = controller as? ViewController { + if controller.lockOrientation { + if let lockedOrientation = controller.lockedOrientation { + supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: lockedOrientation, compactSize: lockedOrientation)) + } else { + supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: currentOrientationToLock, compactSize: currentOrientationToLock)) + } + } else { + supportedOrientations = supportedOrientations.intersection(controller.supportedOrientations) + } + } + } + return supportedOrientations + } + + public func updateTheme(_ theme: NavigationControllerTheme) { + self.theme = theme + if self.isViewLoaded { + self.controllerView.backgroundColor = theme.emptyAreaColor + self.controllerView.separatorView.backgroundColor = theme.navigationBar.separatorColor + self.controllerView.navigationBackgroundView?.backgroundColor = theme.navigationBar.backgroundColor + self.controllerView.navigationSeparatorView?.backgroundColor = theme.navigationBar.separatorColor + if let emptyDetailView = self.controllerView.emptyDetailView { + emptyDetailView.image = theme.emptyDetailIcon + if let image = theme.emptyDetailIcon { + emptyDetailView.frame = CGRect(origin: CGPoint(x: floor(emptyDetailView.center.x - image.size.width / 2.0), y: floor(emptyDetailView.center.y - image.size.height / 2.0)), size: image.size) + } + } + } + } + + private var previouslyLaidOutMasterController: UIViewController? + private var previouslyLaidOutTopController: UIViewController? + + private func layoutConfiguration(for layout: ContainerViewLayout) -> ControllerLayoutConfiguration { + switch self.mode { + case .single: + return .single + case .automaticMasterDetail: + if case .regular = layout.metrics.widthClass, case .regular = layout.metrics.heightClass { + if layout.size.width > 690.0 { + return .masterDetail + } + } + return .single + } + } + + private func layoutDataForConfiguration(_ layoutConfiguration: ControllerLayoutConfiguration, layout: ContainerViewLayout, index: Int) -> (CGRect, ContainerViewLayout) { + switch layoutConfiguration { + case .masterDetail: + let masterWidth: CGFloat = max(320.0, floor(layout.size.width / 3.0)) + let detailWidth: CGFloat = layout.size.width - masterWidth + if index == 0 { + return (CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: masterWidth, height: layout.size.height)), ContainerViewLayout(size: CGSize(width: masterWidth, height: layout.size.height), metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)) + } else { + let detailFrame = CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)) + return (CGRect(origin: CGPoint(), size: detailFrame.size), ContainerViewLayout(size: CGSize(width: detailWidth, height: layout.size.height), metrics: LayoutMetrics(widthClass: .regular, heightClass: .regular), intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)) + } + case .single: + return (CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)), ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height), metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)) + } + } + + private func updateControllerLayouts(previousControllers: [ControllerRecord], layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var firstControllerFrameAndLayout: (CGRect, ContainerViewLayout)? + let lastControllerFrameAndLayout: (CGRect, ContainerViewLayout) + + let layoutConfiguration = self.layoutConfiguration(for: layout) + + switch layoutConfiguration { + case .masterDetail: + self.viewControllers.first?.view.clipsToBounds = true + self.controllerView.containerView.clipsToBounds = true + let masterData = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 0) + firstControllerFrameAndLayout = masterData + lastControllerFrameAndLayout = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 1) + if self.controllerView.separatorView.superview == nil { + self.controllerView.addSubview(self.controllerView.separatorView) + } + + let navigationBackgroundFrame = CGRect(origin: CGPoint(x: masterData.0.maxX, y: 0.0), size: CGSize(width: lastControllerFrameAndLayout.0.width, height: (layout.statusBarHeight ?? 0.0) + 44.0)) + + if let navigationBackgroundView = self.controllerView.navigationBackgroundView, let navigationSeparatorView = self.controllerView.navigationSeparatorView, let emptyDetailView = self.controllerView.emptyDetailView { + transition.updateFrame(view: navigationBackgroundView, frame: navigationBackgroundFrame) + transition.updateFrame(view: navigationSeparatorView, frame: CGRect(origin: CGPoint(x: navigationBackgroundFrame.minX, y: navigationBackgroundFrame.maxY), size: CGSize(width: navigationBackgroundFrame.width, height: UIScreenPixel))) + if let image = emptyDetailView.image { + transition.updateFrame(view: emptyDetailView, frame: CGRect(origin: CGPoint(x: masterData.0.maxX + floor((lastControllerFrameAndLayout.0.size.width - image.size.width) / 2.0), y: floor((lastControllerFrameAndLayout.0.size.height - image.size.height) / 2.0)), size: image.size)) + } + } else { + let navigationBackgroundView = UIView() + navigationBackgroundView.backgroundColor = self.theme.navigationBar.backgroundColor + let navigationSeparatorView = UIView() + navigationSeparatorView.backgroundColor = self.theme.navigationBar.separatorColor + let emptyDetailView = UIImageView() + emptyDetailView.image = self.theme.emptyDetailIcon + emptyDetailView.alpha = 0.0 + + self.controllerView.navigationBackgroundView = navigationBackgroundView + self.controllerView.navigationSeparatorView = navigationSeparatorView + self.controllerView.emptyDetailView = emptyDetailView + + self.controllerView.insertSubview(navigationBackgroundView, at: 0) + self.controllerView.insertSubview(navigationSeparatorView, at: 1) + self.controllerView.insertSubview(emptyDetailView, at: 2) + + navigationBackgroundView.frame = navigationBackgroundFrame + navigationSeparatorView.frame = CGRect(origin: CGPoint(x: navigationBackgroundFrame.minX, y: navigationBackgroundFrame.maxY), size: CGSize(width: navigationBackgroundFrame.width, height: UIScreenPixel)) + + transition.animatePositionAdditive(layer: navigationBackgroundView.layer, offset: CGPoint(x: navigationBackgroundFrame.width, y: 0.0)) + transition.animatePositionAdditive(layer: navigationSeparatorView.layer, offset: CGPoint(x: navigationBackgroundFrame.width, y: 0.0)) + + if let image = emptyDetailView.image { + emptyDetailView.frame = CGRect(origin: CGPoint(x: masterData.0.maxX + floor((lastControllerFrameAndLayout.0.size.width - image.size.width) / 2.0), y: floor((lastControllerFrameAndLayout.0.size.height - image.size.height) / 2.0)), size: image.size) + } + + transition.updateAlpha(layer: emptyDetailView.layer, alpha: 1.0) + } + transition.updateFrame(view: self.controllerView.separatorView, frame: CGRect(origin: CGPoint(x: masterData.0.maxX, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height))) + case .single: + self.viewControllers.first?.view.clipsToBounds = false + if let navigationBackgroundView = self.controllerView.navigationBackgroundView, let navigationSeparatorView = self.controllerView.navigationSeparatorView { + self.controllerView.navigationBackgroundView = nil + self.controllerView.navigationSeparatorView = nil + + transition.updatePosition(layer: navigationBackgroundView.layer, position: CGPoint(x: layout.size.width + navigationBackgroundView.bounds.size.width / 2.0, y: navigationBackgroundView.center.y), completion: { [weak navigationBackgroundView] _ in + navigationBackgroundView?.removeFromSuperview() + }) + transition.updatePosition(layer: navigationSeparatorView.layer, position: CGPoint(x: layout.size.width + navigationSeparatorView.bounds.size.width / 2.0, y: navigationSeparatorView.center.y), completion: { [weak navigationSeparatorView] _ in + navigationSeparatorView?.removeFromSuperview() + }) + if let emptyDetailView = self.controllerView.emptyDetailView { + self.controllerView.emptyDetailView = nil + transition.updateAlpha(layer: emptyDetailView.layer, alpha: 0.0, completion: { [weak emptyDetailView] _ in + emptyDetailView?.removeFromSuperview() + }) + } + } + self.controllerView.containerView.clipsToBounds = false + lastControllerFrameAndLayout = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 1) + transition.updateFrame(view: self.controllerView.separatorView, frame: CGRect(origin: CGPoint(x: -UIScreenPixel, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)), completion: { [weak self] completed in + if let strongSelf = self, completed { + strongSelf.controllerView.separatorView.removeFromSuperview() + } + }) + } + transition.updateFrame(view: self.controllerView.containerView, frame: CGRect(origin: CGPoint(x: firstControllerFrameAndLayout?.0.maxX ?? 0.0, y: 0.0), size: lastControllerFrameAndLayout.0.size)) + + switch layoutConfiguration { + case .single: + if self.controllerView.sharedStatusBar.view.superview != nil { + self.controllerView.sharedStatusBar.removeFromSupernode() + self.controllerView.containerView.layer.setTraceableInfo(nil) + } + case .masterDetail: + if self.controllerView.sharedStatusBar.view.superview == nil { + self.controllerView.addSubnode(self.controllerView.sharedStatusBar) + self.controllerView.containerView.layer.setTraceableInfo(CATracingLayerInfo(shouldBeAdjustedToInverseTransform: true, userData: self, tracingTag: 0, disableChildrenTracingTags: WindowTracingTags.statusBar | WindowTracingTags.keyboard)) + } + } + + if let _ = layout.statusBarHeight { + self.controllerView.sharedStatusBar.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 40.0)) + } + + var controllersAndFrames: [(Bool, ControllerRecord, ContainerViewLayout)] = [] + for i in 0 ..< self._viewControllers.count { + if let controller = self._viewControllers[i].controller as? ViewController { + if i == 0 { + controller.navigationBar?.previousItem = nil + } else if case .masterDetail = layoutConfiguration, i == 1 { + controller.navigationBar?.previousItem = .close + } else { + controller.navigationBar?.previousItem = .item(viewControllers[i - 1].navigationItem) + } + } + viewControllers[i].navigation_setNavigationController(self) + + if i == 0, let (_, layout) = firstControllerFrameAndLayout { + controllersAndFrames.append((true, self._viewControllers[i], layout)) + } else if i == self._viewControllers.count - 1 { + controllersAndFrames.append((false, self._viewControllers[i], lastControllerFrameAndLayout.1)) + } + } + + var masterController: UIViewController? + var appearingMasterController: ControllerRecord? + var appearingDetailController: ControllerRecord? + + for (isMaster, record, layout) in controllersAndFrames { + let frame: CGRect + if isMaster, let firstControllerFrameAndLayout = firstControllerFrameAndLayout { + masterController = record.controller + frame = firstControllerFrameAndLayout.0 + if let controller = masterController as? ViewController { + self.controllerView.sharedStatusBar.statusBarStyle = controller.statusBar.statusBarStyle + } + } else { + frame = lastControllerFrameAndLayout.0 + } + let isAppearing = record.controller.view.superview == nil + (record.controller as? ViewController)?.containerLayoutUpdated(layout, transition: isAppearing ? .immediate : transition) + if isAppearing { + if isMaster { + appearingMasterController = record + } else { + appearingDetailController = record + } + } else if record.controller.view.superview !== (isMaster ? self.controllerView : self.controllerView.containerView) { + record.controller.setIgnoreAppearanceMethodInvocations(true) + if isMaster { + self.controllerView.insertSubview(record.controller.view, at: 0) + } else { + self.controllerView.containerView.addSubview(record.controller.view) + } + record.controller.setIgnoreAppearanceMethodInvocations(false) + } + var applyFrame = false + if !isAppearing { + applyFrame = true + } else if case .immediate = transition { + applyFrame = true + } + if applyFrame { + var isPartOfTransition = false + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator { + if navigationTransitionCoordinator.topView == record.controller.view || navigationTransitionCoordinator.bottomView == record.controller.view { + isPartOfTransition = true + } + } + if !isPartOfTransition { + transition.updateFrame(view: record.controller.view, frame: frame) + } + } + } + + var animatedAppearingDetailController = false + + if let previousController = self.previouslyLaidOutTopController, !controllersAndFrames.contains(where: { $0.1.controller === previousController }), previousController.view.superview != nil { + if transition.isAnimated, let record = appearingDetailController { + animatedAppearingDetailController = true + + previousController.viewWillDisappear(true) + record.controller.viewWillAppear(true) + record.controller.setIgnoreAppearanceMethodInvocations(true) + + if let controller = record.controller as? ViewController, !controller.hasActiveInput { + let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: 1) + + let appliedLayout = controllerLayout.withUpdatedInputHeight(controller.hasActiveInput ? controllerLayout.inputHeight : nil) + controller.containerLayoutUpdated(appliedLayout, transition: .immediate) + } + self.controllerView.containerView.addSubview(record.controller.view) + record.controller.setIgnoreAppearanceMethodInvocations(false) + + if let _ = previousControllers.index(where: { $0.controller === record.controller }) { + //previousControllers[index].transition = .appearance + let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: previousController.view, topNavigationBar: (previousController as? ViewController)?.navigationBar, bottomView: record.controller.view, bottomNavigationBar: (record.controller as? ViewController)?.navigationBar) + self.navigationTransitionCoordinator = navigationTransitionCoordinator + + self.controllerView.inTransition = true + navigationTransitionCoordinator.animateCompletion(0.0, completion: { [weak self] in + if let strongSelf = self { + strongSelf.navigationTransitionCoordinator = nil + strongSelf.controllerView.inTransition = false + + record.controller.viewDidAppear(true) + + previousController.setIgnoreAppearanceMethodInvocations(true) + previousController.view.removeFromSuperview() + previousController.setIgnoreAppearanceMethodInvocations(false) + previousController.viewDidDisappear(true) + } + }) + } else { + if let index = self._viewControllers.index(where: { $0.controller === previousController }) { + self._viewControllers[index].transition = .appearance + } + let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Push, container: self.controllerView.containerView, topView: record.controller.view, topNavigationBar: (record.controller as? ViewController)?.navigationBar, bottomView: previousController.view, bottomNavigationBar: (previousController as? ViewController)?.navigationBar) + self.navigationTransitionCoordinator = navigationTransitionCoordinator + + self.controllerView.inTransition = true + navigationTransitionCoordinator.animateCompletion(0.0, completion: { [weak self] in + if let strongSelf = self { + if let index = strongSelf._viewControllers.index(where: { $0.controller === previousController }) { + strongSelf._viewControllers[index].transition = .none + } + strongSelf.navigationTransitionCoordinator = nil + strongSelf.controllerView.inTransition = false + + record.controller.viewDidAppear(true) + + previousController.setIgnoreAppearanceMethodInvocations(true) + previousController.view.removeFromSuperview() + previousController.setIgnoreAppearanceMethodInvocations(false) + previousController.viewDidDisappear(true) + } + }) + } + } else { + previousController.viewWillDisappear(false) + previousController.view.removeFromSuperview() + previousController.viewDidDisappear(false) + } + } + + if !animatedAppearingDetailController, let record = appearingDetailController { + record.controller.viewWillAppear(false) + record.controller.setIgnoreAppearanceMethodInvocations(true) + self.controllerView.containerView.addSubview(record.controller.view) + record.controller.setIgnoreAppearanceMethodInvocations(false) + record.controller.viewDidAppear(false) + if let controller = record.controller as? ViewController { + controller.displayNode.recursivelyEnsureDisplaySynchronously(true) + } + } + + if let record = appearingMasterController, let firstControllerFrameAndLayout = firstControllerFrameAndLayout { + record.controller.viewWillAppear(false) + record.controller.setIgnoreAppearanceMethodInvocations(true) + self.controllerView.insertSubview(record.controller.view, belowSubview: self.controllerView.containerView) + record.controller.setIgnoreAppearanceMethodInvocations(false) + record.controller.viewDidAppear(false) + if let controller = record.controller as? ViewController { + controller.displayNode.recursivelyEnsureDisplaySynchronously(true) + } + + record.controller.view.frame = firstControllerFrameAndLayout.0 + record.controller.viewDidAppear(transition.isAnimated) + transition.animatePositionAdditive(layer: record.controller.view.layer, offset: CGPoint(x: -firstControllerFrameAndLayout.0.width, y: 0.0)) + } + + for record in self._viewControllers { + let controller = record.controller + if case .none = record.transition, !controllersAndFrames.contains(where: { $0.1.controller === controller }) { + if controller === self.previouslyLaidOutMasterController { + controller.viewWillDisappear(true) + record.transition = .appearance + transition.animatePositionAdditive(layer: controller.view.layer, offset: CGPoint(), to: CGPoint(x: -controller.view.bounds.size.width, y: 0.0), removeOnCompletion: false, completion: { [weak self] in + if let strongSelf = self { + controller.setIgnoreAppearanceMethodInvocations(true) + controller.view.removeFromSuperview() + controller.setIgnoreAppearanceMethodInvocations(false) + controller.viewDidDisappear(true) + controller.view.layer.removeAllAnimations() + for r in strongSelf._viewControllers { + if r.controller === controller { + r.transition = .none + } + } + } + }) + } else { + if controller.isViewLoaded && controller.view.superview != nil { + var isPartOfTransition = false + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator { + if navigationTransitionCoordinator.topView == controller.view || navigationTransitionCoordinator.bottomView == controller.view { + isPartOfTransition = true + } + } + + if !isPartOfTransition { + controller.viewWillDisappear(false) + controller.setIgnoreAppearanceMethodInvocations(true) + controller.view.removeFromSuperview() + controller.setIgnoreAppearanceMethodInvocations(false) + controller.viewDidDisappear(false) + } + } + } + } + } + + for previous in previousControllers { + var isFound = false + inner: for current in self._viewControllers { + if previous.controller === current.controller { + isFound = true + break inner + } + } + if !isFound { + (previous.controller as? ViewController)?.navigationStackConfigurationUpdated(next: []) + } + } + + (self.view as! NavigationControllerView).topControllerNode = (self._viewControllers.last?.controller as? ViewController)?.displayNode + + for i in 0 ..< self._viewControllers.count { + var currentNext: UIViewController? = (i == (self._viewControllers.count - 1)) ? nil : self._viewControllers[i + 1].controller + if case .single = layoutConfiguration { + currentNext = nil + } + + var previousNext: UIViewController? + inner: for j in 0 ..< previousControllers.count { + if previousControllers[j].controller === self._viewControllers[i].controller { + previousNext = (j == (previousControllers.count - 1)) ? nil : previousControllers[j + 1].controller + break inner + } + } + + if currentNext !== previousNext { + let next = currentNext as? ViewController + (self._viewControllers[i].controller as? ViewController)?.navigationStackConfigurationUpdated(next: next == nil ? [] : [next!]) + } + } + + self.previouslyLaidOutMasterController = masterController + self.previouslyLaidOutTopController = self._viewControllers.last?.controller + } + + public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if !self.isViewLoaded { + self.loadView() + } + self.validLayout = layout + transition.updateFrame(view: self.view, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) + + self.updateControllerLayouts(previousControllers: self._viewControllers, layout: layout, transition: transition) + + if let presentedViewController = self.presentedViewController { + let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver) + + if let presentedViewController = presentedViewController as? ContainableController { + presentedViewController.containerLayoutUpdated(containedLayout, transition: transition) + } else { + transition.updateFrame(view: presentedViewController.view, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)) + } + } + + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator { + navigationTransitionCoordinator.updateProgress() + } + } + + public func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { + for record in self._viewControllers { + if let controller = record.controller as? ContainableController { + controller.updateToInterfaceOrientation(orientation) + } + } + } + + open override func loadView() { + self._displayNode = ASDisplayNode(viewBlock: { + return NavigationControllerView() + }, didLoad: nil) + + self.view = self.displayNode.view + self.view.clipsToBounds = true + self.view.autoresizingMask = [] + + self.controllerView.backgroundColor = self.theme.emptyAreaColor + self.controllerView.separatorView.backgroundColor = theme.navigationBar.separatorColor + + if #available(iOSApplicationExtension 11.0, *) { + self.navigationBar.prefersLargeTitles = false + } + self.navigationBar.removeFromSuperview() + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.view.addGestureRecognizer(panRecognizer) + + if self.topViewController != nil { + self.topViewController?.view.frame = CGRect(origin: CGPoint(), size: self.view.frame.size) + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case UIGestureRecognizerState.began: + guard let layout = self.validLayout else { + return + } + guard self.navigationTransitionCoordinator == nil else { + return + } + let beginGesture: Bool + switch self.layoutConfiguration(for: layout) { + case .masterDetail: + let location = recognizer.location(in: self.controllerView.containerView) + if self.controllerView.containerView.bounds.contains(location) { + beginGesture = self._viewControllers.count >= 3 + } else { + beginGesture = false + } + case .single: + beginGesture = self._viewControllers.count >= 2 + } + + if beginGesture { + let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController + let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + + if let topController = topController as? ViewController { + if !topController.attemptNavigation({ [weak self] in + let _ = self?.popViewController(animated: true) + }) { + return + } + } + + topController.viewWillDisappear(true) + let topView = topController.view! + if let bottomController = bottomController as? ViewController { + let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: self.viewControllers.count - 2) + + let appliedLayout = controllerLayout.withUpdatedInputHeight(bottomController.hasActiveInput ? controllerLayout.inputHeight : nil) + bottomController.containerLayoutUpdated(appliedLayout, transition: .immediate) + } + bottomController.viewWillAppear(true) + let bottomView = bottomController.view! + + if let bottomController = bottomController as? ViewController { + bottomController.displayNode.recursivelyEnsureDisplaySynchronously(true) + } + + let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: topView, topNavigationBar: (topController as? ViewController)?.navigationBar, bottomView: bottomView, bottomNavigationBar: (bottomController as? ViewController)?.navigationBar) + self.navigationTransitionCoordinator = navigationTransitionCoordinator + } + case UIGestureRecognizerState.changed: + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { + let translation = recognizer.translation(in: self.view).x + navigationTransitionCoordinator.progress = max(0.0, min(1.0, translation / self.view.frame.width)) + } + case UIGestureRecognizerState.ended: + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { + let velocity = recognizer.velocity(in: self.view).x + + if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 { + (self.view as! NavigationControllerView).inTransition = true + navigationTransitionCoordinator.animateCompletion(velocity, completion: { + (self.view as! NavigationControllerView).inTransition = false + + self.navigationTransitionCoordinator = nil + + if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController + let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + + topController.setIgnoreAppearanceMethodInvocations(true) + bottomController.setIgnoreAppearanceMethodInvocations(true) + let _ = self.popViewController(animated: false) + topController.setIgnoreAppearanceMethodInvocations(false) + bottomController.setIgnoreAppearanceMethodInvocations(false) + + topController.viewDidDisappear(true) + bottomController.viewDidAppear(true) + } + }) + } else { + if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController + let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + + topController.viewWillAppear(true) + bottomController.viewWillDisappear(true) + } + + (self.view as! NavigationControllerView).inTransition = true + navigationTransitionCoordinator.animateCancel({ + (self.view as! NavigationControllerView).inTransition = false + self.navigationTransitionCoordinator = nil + + if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController + let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + + topController.viewDidAppear(true) + bottomController.viewDidDisappear(true) + } + }) + } + } + case .cancelled: + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { + if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController + let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + + topController.viewWillAppear(true) + bottomController.viewWillDisappear(true) + } + + (self.view as! NavigationControllerView).inTransition = true + navigationTransitionCoordinator.animateCancel({ + (self.view as! NavigationControllerView).inTransition = false + self.navigationTransitionCoordinator = nil + + if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController + let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + + topController.viewDidAppear(true) + bottomController.viewDidDisappear(true) + } + }) + } + default: + break + } + } + + public func pushViewController(_ controller: ViewController) { + self.pushViewController(controller, completion: {}) + } + + public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void) { + let navigateAction: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + if !controller.hasActiveInput { + strongSelf.view.endEditing(true) + } + strongSelf.scheduleAfterLayout({ + guard let strongSelf = self else { + return + } + + if let validLayout = strongSelf.validLayout { + let (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count) + + let appliedLayout = controllerLayout.withUpdatedInputHeight(controller.hasActiveInput ? controllerLayout.inputHeight : nil) + controller.containerLayoutUpdated(appliedLayout, transition: .immediate) + strongSelf.currentPushDisposable.set((controller.ready.get() + |> deliverOnMainQueue + |> take(1)).start(next: { _ in + guard let strongSelf = self else { + return + } + + if let validLayout = strongSelf.validLayout { + let (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count) + + let containerLayout = controllerLayout.withUpdatedInputHeight(controller.hasActiveInput ? controllerLayout.inputHeight : nil) + if containerLayout != appliedLayout { + controller.containerLayoutUpdated(containerLayout, transition: .immediate) + } + strongSelf.pushViewController(controller, animated: animated) + completion() + } + })) + } else { + strongSelf.pushViewController(controller, animated: false) + completion() + } + }) + } + + if let lastController = self.viewControllers.last as? ViewController, !lastController.attemptNavigation(navigateAction) { + } else { + navigateAction() + } + } + + open override func pushViewController(_ viewController: UIViewController, animated: Bool) { + self.currentPushDisposable.set(nil) + + var controllers = self.viewControllers + controllers.append(viewController) + self.setViewControllers(controllers, animated: animated) + } + + public func replaceTopController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil) { + self.view.endEditing(true) + if !controller.hasActiveInput { + self.view.endEditing(true) + } + if let validLayout = self.validLayout { + var (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: validLayout), layout: validLayout, index: self.viewControllers.count) + controllerLayout.inputHeight = nil + controller.containerLayoutUpdated(controllerLayout, transition: .immediate) + } + self.currentPushDisposable.set((controller.ready.get() + |> deliverOnMainQueue + |> take(1)).start(next: { [weak self] _ in + if let strongSelf = self { + ready?.set(true) + var controllers = strongSelf.viewControllers + controllers.removeLast() + controllers.append(controller) + strongSelf.setViewControllers(controllers, animated: animated) + } + })) + } + + public func filterController(_ controller: ViewController, animated: Bool) { + let controllers = self.viewControllers.filter({ $0 !== controller }) + if controllers.count != self.viewControllers.count { + self.setViewControllers(controllers, animated: animated) + } + } + + public func replaceControllersAndPush(controllers: [UIViewController], controller: ViewController, animated: Bool, ready: ValuePromise? = nil, completion: @escaping () -> Void = {}) { + self.view.endEditing(true) + self.scheduleAfterLayout { [weak self] in + guard let strongSelf = self else { + return + } + if let validLayout = strongSelf.validLayout { + var (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count) + controllerLayout.inputHeight = nil + controller.containerLayoutUpdated(controllerLayout, transition: .immediate) + } + strongSelf.currentPushDisposable.set((controller.ready.get() + |> deliverOnMainQueue + |> take(1)).start(next: { _ in + guard let strongSelf = self else { + return + } + ready?.set(true) + var controllers = controllers + controllers.append(controller) + strongSelf.setViewControllers(controllers, animated: animated) + completion() + })) + } + } + + public func replaceAllButRootController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil, completion: @escaping () -> Void = {}) { + self.view.endEditing(true) + self.scheduleAfterLayout { [weak self] in + guard let strongSelf = self else { + return + } + if let validLayout = strongSelf.validLayout { + var (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count) + controllerLayout.inputHeight = nil + controller.containerLayoutUpdated(controllerLayout, transition: .immediate) + } + strongSelf.currentPushDisposable.set((controller.ready.get() + |> deliverOnMainQueue + |> take(1)).start(next: { _ in + guard let strongSelf = self else { + return + } + ready?.set(true) + var controllers = strongSelf.viewControllers + while controllers.count > 1 { + controllers.removeLast() + } + controllers.append(controller) + strongSelf.setViewControllers(controllers, animated: animated) + completion() + })) + } + } + + public func popToRoot(animated: Bool) { + var controllers = self.viewControllers + while controllers.count > 1 { + controllers.removeLast() + } + self.setViewControllers(controllers, animated: animated) + } + + override open func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? { + var poppedControllers: [UIViewController] = [] + var found = false + var controllers = self.viewControllers + if !controllers.contains(where: { $0 === viewController }) { + return nil + } + while !controllers.isEmpty { + if controllers[controllers.count - 1] === viewController { + found = true + break + } + poppedControllers.insert(controllers[controllers.count - 1], at: 0) + controllers.removeLast() + } + if found { + self.setViewControllers(controllers, animated: animated) + return poppedControllers + } else { + return nil + } + } + + open override func popViewController(animated: Bool) -> UIViewController? { + var controller: UIViewController? + var controllers = self.viewControllers + if controllers.count != 0 { + controller = controllers[controllers.count - 1] as UIViewController + controllers.remove(at: controllers.count - 1) + self.setViewControllers(controllers, animated: animated) + } + return controller + } + + open override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { + var resultControllers: [ControllerRecord] = [] + for controller in viewControllers { + var found = false + inner: for current in self._viewControllers { + if current.controller === controller { + resultControllers.append(current) + found = true + break inner + } + } + if !found { + resultControllers.append(ControllerRecord(controller: controller)) + } + } + let previousControllers = self._viewControllers + self._viewControllers = resultControllers + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator { + navigationTransitionCoordinator.complete() + } + if let layout = self.validLayout { + self.updateControllerLayouts(previousControllers: previousControllers, layout: layout, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) + } + } + + override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { + if let controller = viewControllerToPresent as? NavigationController { + controller.navigation_setDismiss({ [weak self] in + if let strongSelf = self { + strongSelf.dismiss(animated: false, completion: nil) + } + }, rootController: self.view!.window!.rootViewController) + self._presentedViewController = controller + + self.view.endEditing(true) + if let validLayout = self.validLayout { + controller.containerLayoutUpdated(validLayout, transition: .immediate) + } + + var ready: Signal = .single(true) + + if let controller = controller.topViewController as? ViewController { + ready = controller.ready.get() + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue + } + + self.currentPresentDisposable.set(ready.start(next: { [weak self] _ in + if let strongSelf = self { + if flag { + controller.view.frame = strongSelf.view.bounds.offsetBy(dx: 0.0, dy: strongSelf.view.bounds.height) + strongSelf.view.addSubview(controller.view) + UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: { + controller.view.frame = strongSelf.view.bounds + }, completion: { _ in + if let completion = completion { + completion() + } + }) + } else { + controller.view.frame = strongSelf.view.bounds + strongSelf.view.addSubview(controller.view) + + if let completion = completion { + completion() + } + } + } + })) + } else { + preconditionFailure("NavigationController can't present \(viewControllerToPresent). Only subclasses of NavigationController are allowed.") + } + } + + override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + if let controller = self.presentedViewController { + if flag { + UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: { + controller.view.frame = self.view.bounds.offsetBy(dx: 0.0, dy: self.view.bounds.height) + }, completion: { _ in + controller.view.removeFromSuperview() + self._presentedViewController = nil + if let completion = completion { + completion() + } + }) + } else { + controller.view.removeFromSuperview() + self._presentedViewController = nil + if let completion = completion { + completion() + } + } + } + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + public final var currentWindow: WindowHost? { + if let window = self.view.window as? WindowHost { + return window + } else if let superwindow = self.view.window { + for subview in superwindow.subviews { + if let subview = subview as? WindowHost { + return subview + } + } + } + return nil + } + + private func scheduleAfterLayout(_ f: @escaping () -> Void) { + (self.view as? UITracingLayerView)?.schedule(layout: { + f() + }) + self.view.setNeedsLayout() + } + + private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { + let requestId = self.scheduledLayoutTransitionRequestId + self.scheduledLayoutTransitionRequestId += 1 + self.scheduledLayoutTransitionRequest = (requestId, transition) + (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in + if let strongSelf = self { + if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId { + strongSelf.scheduledLayoutTransitionRequest = nil + strongSelf.requestLayout(transition: currentRequestTransition) + } + } + }) + self.view.setNeedsLayout() + } + + private func requestLayout(transition: ContainedViewLayoutTransition) { + if self.isViewLoaded, let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: transition) + } + } +} diff --git a/submodules/Display/Display/NavigationControllerProxy.h b/submodules/Display/Display/NavigationControllerProxy.h new file mode 100644 index 0000000000..02e4fb6208 --- /dev/null +++ b/submodules/Display/Display/NavigationControllerProxy.h @@ -0,0 +1,7 @@ +#import + +@interface NavigationControllerProxy : UINavigationController + +- (instancetype)init; + +@end diff --git a/submodules/Display/Display/NavigationControllerProxy.m b/submodules/Display/Display/NavigationControllerProxy.m new file mode 100644 index 0000000000..6a86a5062f --- /dev/null +++ b/submodules/Display/Display/NavigationControllerProxy.m @@ -0,0 +1,15 @@ +#import "NavigationControllerProxy.h" + +#import "NavigationBarProxy.h" + +@implementation NavigationControllerProxy + +- (instancetype)init +{ + self = [super initWithNavigationBarClass:[NavigationBarProxy class] toolbarClass:[UIToolbar class]]; + if (self != nil) { + } + return self; +} + +@end diff --git a/submodules/Display/Display/NavigationShadow@2x.png b/submodules/Display/Display/NavigationShadow@2x.png new file mode 100644 index 0000000000..3eecc232e5 Binary files /dev/null and b/submodules/Display/Display/NavigationShadow@2x.png differ diff --git a/submodules/Display/Display/NavigationTitleNode.swift b/submodules/Display/Display/NavigationTitleNode.swift new file mode 100644 index 0000000000..5ef72daed8 --- /dev/null +++ b/submodules/Display/Display/NavigationTitleNode.swift @@ -0,0 +1,58 @@ +import UIKit +import AsyncDisplayKit + +public class NavigationTitleNode: ASDisplayNode { + private let label: ASTextNode + + private var _text: NSString = "" + public var text: NSString { + get { + return self._text + } + set(value) { + self._text = value + self.setText(value) + } + } + + public var color: UIColor = UIColor.black { + didSet { + self.setText(self._text) + } + } + + public init(text: NSString) { + self.label = ASTextNode() + self.label.maximumNumberOfLines = 1 + self.label.truncationMode = .byTruncatingTail + self.label.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.label) + + self.setText(text) + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setText(_ text: NSString) { + var titleAttributes = [NSAttributedStringKey : AnyObject]() + titleAttributes[NSAttributedStringKey.font] = UIFont.boldSystemFont(ofSize: 17.0) + titleAttributes[NSAttributedStringKey.foregroundColor] = self.color + let titleString = NSAttributedString(string: text as String, attributes: titleAttributes) + self.label.attributedText = titleString + self.invalidateCalculatedLayout() + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + self.label.measure(constrainedSize) + return self.label.calculatedSize + } + + public override func layout() { + self.label.frame = self.bounds + } +} diff --git a/submodules/Display/Display/NavigationTransitionCoordinator.swift b/submodules/Display/Display/NavigationTransitionCoordinator.swift new file mode 100644 index 0000000000..809f669aec --- /dev/null +++ b/submodules/Display/Display/NavigationTransitionCoordinator.swift @@ -0,0 +1,247 @@ +import UIKit + +enum NavigationTransition { + case Push + case Pop +} + +private let shadowWidth: CGFloat = 16.0 + +private func generateShadow() -> UIImage? { + return UIImage(named: "NavigationShadow", in: Bundle(for: NavigationBackButtonNode.self), compatibleWith: nil)?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(), resizingMode: .tile) +} + +private let shadowImage = generateShadow() + +class NavigationTransitionCoordinator { + private var _progress: CGFloat = 0.0 + var progress: CGFloat { + get { + return self._progress + } + set(value) { + self._progress = value + self.updateProgress() + } + } + + private let container: UIView + private let transition: NavigationTransition + let topView: UIView + private let viewSuperview: UIView? + let bottomView: UIView + private let topNavigationBar: NavigationBar? + private let bottomNavigationBar: NavigationBar? + private let dimView: UIView + private let shadowView: UIImageView + + private let inlineNavigationBarTransition: Bool + + private(set) var animatingCompletion = false + private var currentCompletion: (() -> Void)? + + init(transition: NavigationTransition, container: UIView, topView: UIView, topNavigationBar: NavigationBar?, bottomView: UIView, bottomNavigationBar: NavigationBar?) { + self.transition = transition + self.container = container + self.topView = topView + switch transition { + case .Push: + self.viewSuperview = bottomView.superview + case .Pop: + self.viewSuperview = topView.superview + } + self.bottomView = bottomView + self.topNavigationBar = topNavigationBar + self.bottomNavigationBar = bottomNavigationBar + self.dimView = UIView() + self.dimView.backgroundColor = UIColor.black + self.shadowView = UIImageView(image: shadowImage) + + if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar, !topNavigationBar.isHidden, !bottomNavigationBar.isHidden, topNavigationBar.canTransitionInline, bottomNavigationBar.canTransitionInline, topNavigationBar.item?.leftBarButtonItem == nil { + var topFrame = topNavigationBar.view.convert(topNavigationBar.bounds, to: container) + var bottomFrame = bottomNavigationBar.view.convert(bottomNavigationBar.bounds, to: container) + topFrame.origin.x = 0.0 + bottomFrame.origin.x = 0.0 + self.inlineNavigationBarTransition = true// topFrame.equalTo(bottomFrame) + } else { + self.inlineNavigationBarTransition = false + } + + switch transition { + case .Push: + self.viewSuperview?.insertSubview(topView, belowSubview: topView) + case .Pop: + self.viewSuperview?.insertSubview(bottomView, belowSubview: topView) + } + + self.viewSuperview?.insertSubview(self.dimView, belowSubview: topView) + self.viewSuperview?.insertSubview(self.shadowView, belowSubview: dimView) + + self.maybeCreateNavigationBarTransition() + self.updateProgress() + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateProgress() { + let position: CGFloat + switch self.transition { + case .Push: + position = 1.0 - progress + case .Pop: + position = progress + } + + var dimInset: CGFloat = 0.0 + if let bottomNavigationBar = self.bottomNavigationBar , self.inlineNavigationBarTransition { + dimInset = bottomNavigationBar.frame.maxY + } + + let containerSize = self.container.bounds.size + + self.topView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(position * containerSize.width), y: 0.0), size: containerSize) + self.dimView.frame = CGRect(origin: CGPoint(x: 0.0, y: dimInset), size: CGSize(width: max(0.0, self.topView.frame.minX), height: self.container.bounds.size.height - dimInset)) + self.shadowView.frame = CGRect(origin: CGPoint(x: self.dimView.frame.maxX - shadowWidth, y: dimInset), size: CGSize(width: shadowWidth, height: containerSize.height - dimInset)) + self.dimView.alpha = (1.0 - position) * 0.15 + self.shadowView.alpha = (1.0 - position) * 0.9 + self.bottomView.frame = CGRect(origin: CGPoint(x: ((position - 1.0) * containerSize.width * 0.3), y: 0.0), size: containerSize) + + self.updateNavigationBarTransition() + } + + func updateNavigationBarTransition() { + if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { + let position: CGFloat + switch self.transition { + case .Push: + position = 1.0 - progress + case .Pop: + position = progress + } + + topNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: bottomNavigationBar, transition: self.transition, role: .top, progress: position) + bottomNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: topNavigationBar, transition: self.transition, role: .bottom, progress: position) + } + } + + func maybeCreateNavigationBarTransition() { + if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { + let position: CGFloat + switch self.transition { + case .Push: + position = 1.0 - progress + case .Pop: + position = progress + } + + topNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: bottomNavigationBar, transition: self.transition, role: .top, progress: position) + bottomNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: topNavigationBar, transition: self.transition, role: .bottom, progress: position) + } + } + + func endNavigationBarTransition() { + if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { + topNavigationBar.transitionState = nil + bottomNavigationBar.transitionState = nil + } + } + + func animateCancel(_ completion: @escaping () -> ()) { + self.currentCompletion = completion + + UIView.animate(withDuration: 0.1, delay: 0.0, options: UIViewAnimationOptions(), animations: { () -> Void in + self.progress = 0.0 + }) { (completed) -> Void in + switch self.transition { + case .Push: + if let viewSuperview = self.viewSuperview { + viewSuperview.addSubview(self.bottomView) + } else { + self.bottomView.removeFromSuperview() + } + self.topView.removeFromSuperview() + case .Pop: + if let viewSuperview = self.viewSuperview { + viewSuperview.addSubview(self.topView) + } else { + self.topView.removeFromSuperview() + } + self.bottomView.removeFromSuperview() + } + + self.dimView.removeFromSuperview() + self.shadowView.removeFromSuperview() + + self.endNavigationBarTransition() + + if let currentCompletion = self.currentCompletion { + self.currentCompletion = nil + currentCompletion() + } + } + } + + func complete() { + self.animatingCompletion = true + + self.progress = 1.0 + + self.dimView.removeFromSuperview() + self.shadowView.removeFromSuperview() + + self.endNavigationBarTransition() + + if let currentCompletion = self.currentCompletion { + self.currentCompletion = nil + currentCompletion() + } + } + + func animateCompletion(_ velocity: CGFloat, completion: @escaping () -> ()) { + self.animatingCompletion = true + let distance = (1.0 - self.progress) * self.container.bounds.size.width + self.currentCompletion = completion + let f = { + /*switch self.transition { + case .Push: + if let viewSuperview = self.viewSuperview { + viewSuperview.addSubview(self.bottomView) + } else { + self.bottomView.removeFromSuperview() + } + case .Pop: + if let viewSuperview = self.viewSuperview { + viewSuperview.addSubview(self.topView) + } else { + self.topView.removeFromSuperview() + } + }*/ + + self.dimView.removeFromSuperview() + self.shadowView.removeFromSuperview() + + self.endNavigationBarTransition() + + if let currentCompletion = self.currentCompletion { + self.currentCompletion = nil + currentCompletion() + } + } + + if abs(velocity) < CGFloat.ulpOfOne && abs(self.progress) < CGFloat.ulpOfOne { + UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: { + self.progress = 1.0 + }, completion: { _ in + f() + }) + } else { + UIView.animate(withDuration: Double(max(0.05, min(0.2, abs(distance / velocity)))), delay: 0.0, options: UIViewAnimationOptions(), animations: { () -> Void in + self.progress = 1.0 + }) { (completed) -> Void in + f() + } + } + } +} diff --git a/submodules/Display/Display/NotificationCenterUtils.h b/submodules/Display/Display/NotificationCenterUtils.h new file mode 100644 index 0000000000..6918561d91 --- /dev/null +++ b/submodules/Display/Display/NotificationCenterUtils.h @@ -0,0 +1,9 @@ +#import + +typedef bool (^NotificationHandlerBlock)(NSString *, id, NSDictionary *, void (^)()); + +@interface NotificationCenterUtils : NSObject + ++ (void)addNotificationHandler:(NotificationHandlerBlock)handler; + +@end diff --git a/submodules/Display/Display/NotificationCenterUtils.m b/submodules/Display/Display/NotificationCenterUtils.m new file mode 100644 index 0000000000..1c26229623 --- /dev/null +++ b/submodules/Display/Display/NotificationCenterUtils.m @@ -0,0 +1,70 @@ +#import "NotificationCenterUtils.h" + +#import "RuntimeUtils.h" +#import + +static NSMutableArray *notificationHandlers() { + static NSMutableArray *array = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + array = [[NSMutableArray alloc] init]; + }); + return array; +} + +@interface NSNotificationCenter (_a65afc19) + +@end + +@implementation NSNotificationCenter (_a65afc19) + +- (void)_a65afc19_postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo +{ + if ([NSThread isMainThread]) { + for (NotificationHandlerBlock handler in notificationHandlers()) + { + if (handler(aName, anObject, aUserInfo, ^{ + [self _a65afc19_postNotificationName:aName object:anObject userInfo:aUserInfo]; + })) { + return; + } + } + } + + [self _a65afc19_postNotificationName:aName object:anObject userInfo:aUserInfo]; +} + +@end + +@interface CATransaction (Swizzle) + ++ (void)swizzle_flush; + +@end + +@implementation CATransaction (Swizzle) + ++ (void)swizzle_flush { + //printf("===flush\n"); + + [self swizzle_flush]; +} + +@end + +@implementation NotificationCenterUtils + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [RuntimeUtils swizzleInstanceMethodOfClass:[NSNotificationCenter class] currentSelector:@selector(postNotificationName:object:userInfo:) newSelector:@selector(_a65afc19_postNotificationName:object:userInfo:)]; + + //[RuntimeUtils swizzleClassMethodOfClass:[CATransaction class] currentSelector:@selector(flush) newSelector:@selector(swizzle_flush)]; + }); +} + ++ (void)addNotificationHandler:(NotificationHandlerBlock)handler { + [notificationHandlers() addObject:[handler copy]]; +} + +@end diff --git a/submodules/Display/Display/PageControlNode.swift b/submodules/Display/Display/PageControlNode.swift new file mode 100644 index 0000000000..1353ef6dd4 --- /dev/null +++ b/submodules/Display/Display/PageControlNode.swift @@ -0,0 +1,97 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public final class PageControlNode: ASDisplayNode { + private let dotSize: CGFloat + private let dotSpacing: CGFloat + public var dotColor: UIColor { + didSet { + if !oldValue.isEqual(self.dotColor) { + self.normalDotImage = generateFilledCircleImage(diameter: dotSize, color: self.dotColor)! + for dotNode in self.dotNodes { + if dotNode === oldValue { + dotNode.image = self.normalDotImage + } + } + } + } + } + public var inactiveDotColor: UIColor { + didSet { + if !oldValue.isEqual(self.inactiveDotColor) { + self.inactiveDotImage = generateFilledCircleImage(diameter: dotSize, color: self.inactiveDotColor)! + for dotNode in self.dotNodes { + if dotNode === oldValue { + dotNode.image = self.inactiveDotImage + } + } + } + } + } + private var dotNodes: [ASImageNode] = [] + + private var normalDotImage: UIImage + private var inactiveDotImage: UIImage + + public init(dotSize: CGFloat = 7.0, dotSpacing: CGFloat = 9.0, dotColor: UIColor, inactiveDotColor: UIColor) { + self.dotSize = dotSize + self.dotSpacing = dotSpacing + self.dotColor = dotColor + self.inactiveDotColor = inactiveDotColor + self.normalDotImage = generateFilledCircleImage(diameter: dotSize, color: dotColor)! + self.inactiveDotImage = generateFilledCircleImage(diameter: dotSize, color: inactiveDotColor)! + + super.init() + } + + public var pagesCount: Int = 0 { + didSet { + if self.pagesCount != oldValue { + while self.dotNodes.count > self.pagesCount { + self.dotNodes[self.dotNodes.count - 1].removeFromSupernode() + self.dotNodes.removeLast() + } + while self.dotNodes.count < self.pagesCount { + let dotNode = ASImageNode() + dotNode.image = self.normalDotImage + dotNode.displaysAsynchronously = false + dotNode.displayWithoutProcessing = true + dotNode.isUserInteractionEnabled = false + self.dotNodes.append(dotNode) + self.addSubnode(dotNode) + } + } + } + } + + public func setPage(_ pageValue: CGFloat) { + let page = Int(round(pageValue)) + + for i in 0 ..< self.dotNodes.count { + if i != page { + self.dotNodes[i].image = self.inactiveDotImage + } else { + self.dotNodes[i].image = self.normalDotImage + } + } + } + + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: self.dotSize * CGFloat(self.pagesCount) + self.dotSpacing * max(CGFloat(self.pagesCount - 1), 0.0), height: self.dotSize) + } + + override public func layout() { + super.layout() + + let dotSize = CGSize(width: self.dotSize, height: self.dotSize) + + let nominalWidth = self.dotSize * CGFloat(self.pagesCount) + self.dotSpacing * max(CGFloat(self.pagesCount - 1), 0.0) + + let startX = floor((self.bounds.size.width - nominalWidth) / 2) + + for i in 0 ..< self.dotNodes.count { + self.dotNodes[i].frame = CGRect(origin: CGPoint(x: startX + CGFloat(i) * (dotSize.width + self.dotSpacing), y: 0.0), size: dotSize) + } + } +} diff --git a/submodules/Display/Display/PeekController.swift b/submodules/Display/Display/PeekController.swift new file mode 100644 index 0000000000..456c3ceb29 --- /dev/null +++ b/submodules/Display/Display/PeekController.swift @@ -0,0 +1,82 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public final class PeekControllerTheme { + public let isDark: Bool + public let menuBackgroundColor: UIColor + public let menuItemHighligtedColor: UIColor + public let menuItemSeparatorColor: UIColor + public let accentColor: UIColor + public let destructiveColor: UIColor + + public init(isDark: Bool, menuBackgroundColor: UIColor, menuItemHighligtedColor: UIColor, menuItemSeparatorColor: UIColor, accentColor: UIColor, destructiveColor: UIColor) { + self.isDark = isDark + self.menuBackgroundColor = menuBackgroundColor + self.menuItemHighligtedColor = menuItemHighligtedColor + self.menuItemSeparatorColor = menuItemSeparatorColor + self.accentColor = accentColor + self.destructiveColor = destructiveColor + } +} + +public final class PeekController: ViewController { + private var controllerNode: PeekControllerNode { + return self.displayNode as! PeekControllerNode + } + + private let theme: PeekControllerTheme + private let content: PeekControllerContent + var sourceNode: () -> ASDisplayNode? + + private var animatedIn = false + + public init(theme: PeekControllerTheme, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) { + self.theme = theme + self.content = content + self.sourceNode = sourceNode + + super.init(navigationBarPresentationData: nil) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = PeekControllerNode(theme: self.theme, content: self.content, requestDismiss: { [weak self] in + self?.dismiss() + }) + self.displayNodeDidLoad() + } + + private func getSourceRect() -> CGRect { + if let sourceNode = self.sourceNode() { + return sourceNode.view.convert(sourceNode.bounds, to: self.view) + } else { + let size = self.displayNode.bounds.size + return CGRect(origin: CGPoint(x: floor((size.width - 10.0) / 2.0), y: floor((size.height - 10.0) / 2.0)), size: CGSize(width: 10.0, height: 10.0)) + } + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn(from: self.getSourceRect()) + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + } +} diff --git a/submodules/Display/Display/PeekControllerContent.swift b/submodules/Display/Display/PeekControllerContent.swift new file mode 100644 index 0000000000..c2767e87c6 --- /dev/null +++ b/submodules/Display/Display/PeekControllerContent.swift @@ -0,0 +1,28 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum PeekControllerContentPresentation { + case contained + case freeform +} + +public enum PeerkControllerMenuActivation { + case drag + case press +} + +public protocol PeekControllerContent { + func presentation() -> PeekControllerContentPresentation + func menuActivation() -> PeerkControllerMenuActivation + func menuItems() -> [PeekControllerMenuItem] + func node() -> PeekControllerContentNode & ASDisplayNode + + func topAccessoryNode() -> ASDisplayNode? + + func isEqual(to: PeekControllerContent) -> Bool +} + +public protocol PeekControllerContentNode { + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize +} diff --git a/submodules/Display/Display/PeekControllerGestureRecognizer.swift b/submodules/Display/Display/PeekControllerGestureRecognizer.swift new file mode 100644 index 0000000000..505c039ee7 --- /dev/null +++ b/submodules/Display/Display/PeekControllerGestureRecognizer.swift @@ -0,0 +1,301 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit + +private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool { + if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating { + return true + } + for subview in view.subviews { + let subviewPoint = view.convert(point, to: subview) + if traceDeceleratingScrollView(subview, at: subviewPoint) { + return true + } + } + return false +} + +public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { + private let contentAtPoint: (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? + private let present: (PeekControllerContent, ASDisplayNode) -> PeekController? + private let updateContent: (PeekControllerContent?) -> Void + private let activateBySingleTap: Bool + + private var tapLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var pressTimer: SwiftSignalKit.Timer? + + private let candidateContentDisposable = MetaDisposable() + private var candidateContent: (ASDisplayNode, PeekControllerContent)? { + didSet { + self.updateContent(self.candidateContent?.1) + } + } + + private var menuActivation: PeerkControllerMenuActivation? + private weak var presentedController: PeekController? + + public init(contentAtPoint: @escaping (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, ASDisplayNode) -> PeekController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) { + self.contentAtPoint = contentAtPoint + self.present = present + self.updateContent = updateContent + self.activateBySingleTap = activateBySingleTap + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.pressTimer?.invalidate() + self.candidateContentDisposable.dispose() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func startPressTimer() { + self.pressTimer?.invalidate() + let pressTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + self?.pressTimerFired() + }, queue: Queue.mainQueue()) + self.pressTimer = pressTimer + pressTimer.start() + } + + private func stopLongTapTimer() { + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func stopPressTimer() { + self.pressTimer?.invalidate() + self.pressTimer = nil + } + + override public func reset() { + super.reset() + + self.stopLongTapTimer() + self.stopPressTimer() + self.tapLocation = nil + self.candidateContent = nil + self.menuActivation = nil + self.presentedController = nil + } + + private func longTapTimerFired() { + guard let tapLocation = self.tapLocation else { + return + } + + self.checkCandidateContent(at: tapLocation) + } + + private func pressTimerFired() { + if let _ = self.tapLocation, let menuActivation = self.menuActivation, case .press = menuActivation { + if let presentedController = self.presentedController { + if presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + } + self.menuActivation = nil + self.presentedController = nil + self.state = .ended + } + } + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let view = self.view, let tapLocation = touches.first?.location(in: view) { + if traceDeceleratingScrollView(view, at: tapLocation) { + self.candidateContent = nil + self.state = .failed + } else { + self.tapLocation = tapLocation + self.startLongTapTimer() + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if self.activateBySingleTap, self.presentedController == nil { + self.longTapTimer?.invalidate() + self.pressTimer?.invalidate() + if let tapLocation = self.tapLocation { + self.checkCandidateContent(at: tapLocation, forceActivate: true) + } + self.state = .ended + } else { + let velocity = self.velocity(in: self.view) + + if let presentedController = self.presentedController, presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.endDraggingWithVelocity(velocity.y) + self.presentedController = nil + self.menuActivation = nil + } + + self.tapLocation = nil + self.candidateContent = nil + self.longTapTimer?.invalidate() + self.pressTimer?.invalidate() + self.candidateContentDisposable.set(nil) + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.tapLocation = nil + self.candidateContent = nil + self.state = .failed + + if let presentedController = self.presentedController { + self.menuActivation = nil + self.presentedController = nil + presentedController.dismiss() + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if let touch = touches.first, let initialTapLocation = self.tapLocation { + let touchLocation = touch.location(in: self.view) + if let menuActivation = self.menuActivation, let presentedController = self.presentedController { + switch menuActivation { + case .drag: + var offset = touchLocation.y - initialTapLocation.y + let delta = abs(offset) + let factor: CGFloat = 60.0 + offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) + + if presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset) + } + case .press: + if #available(iOSApplicationExtension 9.0, *) { + if touch.force >= 2.5 { + if presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + self.menuActivation = nil + self.presentedController = nil + self.candidateContent = nil + self.state = .ended + self.candidateContentDisposable.set(nil) + return + } + } + } + + if self.pressTimer != nil { + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.startPressTimer() + } + } + + if self.presentedController != nil { + self.checkCandidateContent(at: touchLocation) + } + } + } else { + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.tapLocation = nil + self.candidateContent = nil + self.state = .failed + } + } + } + } + + private func checkCandidateContent(at touchLocation: CGPoint, forceActivate: Bool = false) { + //print("check begin") + if let contentSignal = self.contentAtPoint(touchLocation) { + self.candidateContentDisposable.set((contentSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + let processResult: Bool + if forceActivate { + processResult = true + } else { + switch strongSelf.state { + case .possible, .changed: + processResult = true + default: + processResult = false + } + } + //print("check received, will process: \(processResult), force: \(forceActivate), state: \(strongSelf.state)") + if processResult { + if let (sourceNode, content) = result { + if let currentContent = strongSelf.candidateContent { + if !currentContent.1.isEqual(to: content) { + strongSelf.tapLocation = touchLocation + strongSelf.candidateContent = (sourceNode, content) + strongSelf.menuActivation = content.menuActivation() + if let presentedController = strongSelf.presentedController, presentedController.isNodeLoaded { + presentedController.sourceNode = { + return sourceNode + } + (presentedController.displayNode as? PeekControllerNode)?.updateContent(content: content) + } + } + } else { + if let presentedController = strongSelf.present(content, sourceNode) { + if forceActivate { + strongSelf.candidateContent = nil + if case .press = content.menuActivation() { + (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + } + } else { + strongSelf.candidateContent = (sourceNode, content) + strongSelf.menuActivation = content.menuActivation() + strongSelf.presentedController = presentedController + + strongSelf.state = .began + + switch content.menuActivation() { + case .drag: + break + case .press: + if #available(iOSApplicationExtension 9.0, *) { + if presentedController.traitCollection.forceTouchCapability != .available { + strongSelf.startPressTimer() + } + } else { + strongSelf.startPressTimer() + } + } + } + } + } + } else if strongSelf.presentedController == nil { + if strongSelf.state != .possible && strongSelf.state != .ended { + strongSelf.state = .failed + } + } + } + } + })) + } else if self.presentedController == nil { + self.state = .failed + } + } +} diff --git a/submodules/Display/Display/PeekControllerMenuItemNode.swift b/submodules/Display/Display/PeekControllerMenuItemNode.swift new file mode 100644 index 0000000000..c1dba53c6d --- /dev/null +++ b/submodules/Display/Display/PeekControllerMenuItemNode.swift @@ -0,0 +1,106 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum PeekControllerMenuItemColor { + case accent + case destructive +} + +public enum PeekControllerMenuItemFont { + case `default` + case bold +} + +public struct PeekControllerMenuItem { + public let title: String + public let color: PeekControllerMenuItemColor + public let font: PeekControllerMenuItemFont + public let action: () -> Void + + public init(title: String, color: PeekControllerMenuItemColor, font: PeekControllerMenuItemFont = .default, action: @escaping () -> Void) { + self.title = title + self.color = color + self.font = font + self.action = action + } +} + +final class PeekControllerMenuItemNode: HighlightTrackingButtonNode { + private let item: PeekControllerMenuItem + private let activatedAction: () -> Void + + private let separatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let textNode: ASTextNode + + init(theme: PeekControllerTheme, item: PeekControllerMenuItem, activatedAction: @escaping () -> Void) { + self.item = item + self.activatedAction = activatedAction + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.menuItemSeparatorColor + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + self.highlightedBackgroundNode.backgroundColor = theme.menuItemHighligtedColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.textNode = ASTextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + let textColor: UIColor + let textFont: UIFont + switch item.color { + case .accent: + textColor = theme.accentColor + case .destructive: + textColor = theme.destructiveColor + } + switch item.font { + case .default: + textFont = Font.regular(20.0) + case .bold: + textFont = Font.medium(20.0) + } + self.textNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) + + super.init() + + self.addSubnode(self.separatorNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.view.superview?.bringSubview(toFront: strongSelf.view) + 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.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let height: CGFloat = 57.0 + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: width, height: UIScreenPixel))) + + let textSize = self.textNode.measure(CGSize(width: width - 10.0, height: height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor((height - textSize.height) / 2.0)), size: textSize)) + + return height + } + + @objc func buttonPressed() { + self.activatedAction() + self.item.action() + } +} diff --git a/submodules/Display/Display/PeekControllerMenuNode.swift b/submodules/Display/Display/PeekControllerMenuNode.swift new file mode 100644 index 0000000000..6b7e5c4fdb --- /dev/null +++ b/submodules/Display/Display/PeekControllerMenuNode.swift @@ -0,0 +1,31 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class PeekControllerMenuNode: ASDisplayNode { + private let itemNodes: [PeekControllerMenuItemNode] + + init(theme: PeekControllerTheme, items: [PeekControllerMenuItem], activatedAction: @escaping () -> Void) { + self.itemNodes = items.map { PeekControllerMenuItemNode(theme: theme, item: $0, activatedAction: activatedAction) } + + super.init() + + self.backgroundColor = theme.menuBackgroundColor + self.cornerRadius = 16.0 + self.clipsToBounds = true + + for itemNode in self.itemNodes { + self.addSubnode(itemNode) + } + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + var verticalOffset: CGFloat = 0.0 + for itemNode in self.itemNodes { + let itemHeight = itemNode.updateLayout(width: width, transition: transition) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: width, height: itemHeight))) + verticalOffset += itemHeight + } + return verticalOffset - UIScreenPixel + } +} diff --git a/submodules/Display/Display/PeekControllerNode.swift b/submodules/Display/Display/PeekControllerNode.swift new file mode 100644 index 0000000000..89c65cbaeb --- /dev/null +++ b/submodules/Display/Display/PeekControllerNode.swift @@ -0,0 +1,358 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class PeekControllerNode: ViewControllerTracingNode { + private let requestDismiss: () -> Void + + private let theme: PeekControllerTheme + + private let blurView: UIView + private let dimNode: ASDisplayNode + private let containerBackgroundNode: ASImageNode + private let containerNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + private var containerOffset: CGFloat = 0.0 + private var panInitialContainerOffset: CGFloat? + + private var content: PeekControllerContent + private var contentNode: PeekControllerContentNode & ASDisplayNode + private var contentNodeHasValidLayout = false + + private var topAccessoryNode: ASDisplayNode? + + private var menuNode: PeekControllerMenuNode? + private var displayingMenu = false + + private var hapticFeedback: HapticFeedback? + + init(theme: PeekControllerTheme, content: PeekControllerContent, requestDismiss: @escaping () -> Void) { + self.theme = theme + self.requestDismiss = requestDismiss + + self.dimNode = ASDisplayNode() + self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: theme.isDark ? .dark : .light)) + self.blurView.isUserInteractionEnabled = false + + switch content.menuActivation() { + case .drag: + self.dimNode.backgroundColor = nil + self.blurView.alpha = 1.0 + case .press: + self.dimNode.backgroundColor = UIColor(white: theme.isDark ? 0.0 : 1.0, alpha: 0.5) + self.blurView.alpha = 0.0 + } + + self.containerBackgroundNode = ASImageNode() + self.containerBackgroundNode.isLayerBacked = true + self.containerBackgroundNode.displayWithoutProcessing = true + self.containerBackgroundNode.displaysAsynchronously = false + + self.containerNode = ASDisplayNode() + + self.content = content + self.contentNode = content.node() + self.topAccessoryNode = content.topAccessoryNode() + + var activatedActionImpl: (() -> Void)? + let menuItems = content.menuItems() + if menuItems.isEmpty { + self.menuNode = nil + } else { + self.menuNode = PeekControllerMenuNode(theme: theme, items: menuItems, activatedAction: { + activatedActionImpl?() + }) + } + + super.init() + + if content.presentation() == .freeform { + self.containerNode.isUserInteractionEnabled = false + } else { + self.containerNode.clipsToBounds = true + self.containerNode.cornerRadius = 16.0 + } + + self.addSubnode(self.dimNode) + self.view.addSubview(self.blurView) + self.containerNode.addSubnode(self.contentNode) + self.addSubnode(self.containerNode) + + if let topAccessoryNode = self.topAccessoryNode { + self.addSubnode(topAccessoryNode) + } + + if let menuNode = self.menuNode { + self.addSubnode(menuNode) + } + + activatedActionImpl = { [weak self] in + self?.requestDismiss() + } + + self.hapticFeedback = HapticFeedback() + self.hapticFeedback?.prepareTap() + } + + deinit { + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var layoutInsets = layout.insets(options: []) + let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) + + layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0) + layoutInsets.right = layoutInsets.left + if !layoutInsets.bottom.isZero { + layoutInsets.bottom -= 12.0 + } + + let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0) + + var menuSize: CGSize? + + let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate) + if self.contentNodeHasValidLayout { + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) + } else { + self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) + } + + var containerFrame: CGRect + switch self.content.presentation() { + case .contained: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + case .freeform: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 4.0)), size: contentSize) + } + + if let menuNode = self.menuNode { + let menuWidth = layout.size.width - layoutInsets.left - layoutInsets.right - 14.0 * 2.0 + let menuHeight = menuNode.updateLayout(width: menuWidth, transition: transition) + menuSize = CGSize(width: menuWidth, height: menuHeight) + + if self.displayingMenu { + let upperBound = layout.size.height - layoutInsets.bottom - menuHeight - 14.0 * 2.0 - containerFrame.height + if containerFrame.origin.y > upperBound { + containerFrame.origin.y = upperBound + } + + transition.updateAlpha(layer: self.blurView.layer, alpha: 1.0) + } + } + + if self.displayingMenu { + var offset = self.containerOffset + let delta = abs(offset) + let factor: CGFloat = 60.0 + offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) + containerFrame = containerFrame.offsetBy(dx: 0.0, dy: offset) + } else { + containerFrame = containerFrame.offsetBy(dx: 0.0, dy: self.containerOffset) + } + + transition.updateFrame(node: self.containerNode, frame: containerFrame) + + if let topAccessoryNode = self.topAccessoryNode { + let accessorySize = topAccessoryNode.frame.size + let accessoryFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(containerFrame.midX - accessorySize.width / 2.0), y: containerFrame.minY - accessorySize.height - 16.0), size: accessorySize) + transition.updateFrame(node: topAccessoryNode, frame: accessoryFrame) + transition.updateAlpha(node: topAccessoryNode, alpha: self.displayingMenu ? 0.0 : 1.0) + } + + if let menuNode = self.menuNode, let menuSize = menuSize { + let menuY: CGFloat + if self.displayingMenu { + menuY = max(containerFrame.maxY + 14.0, layout.size.height - layoutInsets.bottom - 14.0 - menuSize.height) + } else { + menuY = layout.size.height + 14.0 + } + + let menuFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - menuSize.width) / 2.0), y: menuY), size: menuSize) + + if self.contentNodeHasValidLayout { + transition.updateFrame(node: menuNode, frame: menuFrame) + } else { + menuNode.frame = menuFrame + } + } + + self.contentNodeHasValidLayout = true + } + + func animateIn(from rect: CGRect) { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3) + + let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) + self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + + if let topAccessoryNode = self.topAccessoryNode { + topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) + topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) + topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + if case .press = self.content.menuActivation() { + self.hapticFeedback?.tap() + } else { + self.hapticFeedback?.impact() + } + } + + func animateOut(to rect: CGRect, completion: @escaping () -> Void) { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) + + let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) + self.containerNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, force: true, completion: { _ in + completion() + }) + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) + + if let topAccessoryNode = self.topAccessoryNode { + topAccessoryNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, force: true, completion: { _ in + completion() + }) + topAccessoryNode.layer.animateAlpha(from: topAccessoryNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + topAccessoryNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) + } + + if let menuNode = self.menuNode { + menuNode.layer.animatePosition(from: menuNode.position, to: CGPoint(x: menuNode.position.x, y: self.bounds.size.height + menuNode.bounds.size.height / 2.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false) + } + } + + @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.requestDismiss() + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard case .drag = self.content.menuActivation() else { + return + } + + switch recognizer.state { + case .began: + self.panInitialContainerOffset = self.containerOffset + case .changed: + if let panInitialContainerOffset = self.panInitialContainerOffset { + let translation = recognizer.translation(in: self.view) + var offset = panInitialContainerOffset + translation.y + if offset < 0.0 { + let delta = abs(offset) + let factor: CGFloat = 60.0 + offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) + } + self.applyDraggingOffset(offset) + } + case .cancelled, .ended: + if let _ = self.panInitialContainerOffset { + self.panInitialContainerOffset = nil + if self.containerOffset < 0.0 { + self.activateMenu() + } else { + self.requestDismiss() + } + } + default: + break + } + } + + func applyDraggingOffset(_ offset: CGFloat) { + self.containerOffset = offset + if self.containerOffset < -25.0 { + //self.displayingMenu = true + } else { + //self.displayingMenu = false + } + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .immediate) + } + } + + func activateMenu() { + if case .press = self.content.menuActivation() { + self.hapticFeedback?.impact() + } + if let layout = self.validLayout { + self.displayingMenu = true + self.containerOffset = 0.0 + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) + } + } + + func endDraggingWithVelocity(_ velocity: CGFloat) { + if let _ = self.menuNode, velocity < -600.0 || self.containerOffset < -38.0 { + if let layout = self.validLayout { + self.displayingMenu = true + self.containerOffset = 0.0 + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) + } + } else { + self.requestDismiss() + } + } + + func updateContent(content: PeekControllerContent) { + let contentNode = self.contentNode + contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in + contentNode?.removeFromSupernode() + }) + contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false) + + self.menuNode?.removeFromSupernode() + self.menuNode = nil + + self.content = content + self.contentNode = content.node() + self.containerNode.addSubnode(self.contentNode) + self.contentNodeHasValidLayout = false + + var activatedActionImpl: (() -> Void)? + let menuItems = content.menuItems() + if menuItems.isEmpty { + self.menuNode = nil + } else { + self.menuNode = PeekControllerMenuNode(theme: self.theme, items: menuItems, activatedAction: { + activatedActionImpl?() + }) + } + + if let menuNode = self.menuNode { + self.addSubnode(menuNode) + } + + activatedActionImpl = { [weak self] in + self?.requestDismiss() + } + + self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut)) + } + + self.hapticFeedback?.tap() + } +} diff --git a/submodules/Display/Display/PresentationContext.swift b/submodules/Display/Display/PresentationContext.swift new file mode 100644 index 0000000000..b61fe57e63 --- /dev/null +++ b/submodules/Display/Display/PresentationContext.swift @@ -0,0 +1,349 @@ +import UIKit +import SwiftSignalKit + +public struct PresentationSurfaceLevel: RawRepresentable { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } +} + +public enum PresentationContextType { + case current + case window(PresentationSurfaceLevel) +} + +final class PresentationContext { + private var _view: UIView? + var view: UIView? { + get { + return self._view + } set(value) { + let wasReady = self.ready + self._view = value + if wasReady != self.ready { + if !wasReady { + self.addViews() + } else { + self.removeViews() + } + } + } + } + + weak var volumeControlStatusBarNodeView: UIView? + + var updateIsInteractionBlocked: ((Bool) -> Void)? + + var updateHasOpaqueOverlay: ((Bool) -> Void)? + private(set) var hasOpaqueOverlay: Bool = false { + didSet { + if self.hasOpaqueOverlay != oldValue { + self.updateHasOpaqueOverlay?(self.hasOpaqueOverlay) + } + } + } + + private var layout: ContainerViewLayout? + + private var ready: Bool { + return self.view != nil && self.layout != nil + } + + private(set) var controllers: [(ContainableController, PresentationSurfaceLevel)] = [] + + private var presentationDisposables = DisposableSet() + + var topLevelSubview: UIView? + + var isCurrentlyOpaque: Bool { + for (controller, _) in self.controllers { + if controller.isOpaqueWhenInOverlay && controller.isViewLoaded { + if traceIsOpaque(layer: controller.view.layer, rect: controller.view.bounds) { + return true + } + } + } + return false + } + + var currentlyBlocksBackgroundWhenInOverlay: Bool { + for (controller, _) in self.controllers { + if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay { + return true + } + } + return false + } + + private func topLevelSubview(for level: PresentationSurfaceLevel) -> UIView? { + var topController: ContainableController? + for (controller, controllerLevel) in self.controllers.reversed() { + if !controller.isViewLoaded || controller.view.superview == nil { + continue + } + if controllerLevel.rawValue > level.rawValue { + topController = controller + } else { + break + } + } + if let topController = topController { + return topController.view + } else { + return self.topLevelSubview + } + } + + private var nextBlockInteractionToken = 0 + private var blockInteractionTokens = Set() + + private func addBlockInteraction() -> Int { + let token = self.nextBlockInteractionToken + self.nextBlockInteractionToken += 1 + let wasEmpty = self.blockInteractionTokens.isEmpty + self.blockInteractionTokens.insert(token) + if wasEmpty { + self.updateIsInteractionBlocked?(true) + } + return token + } + + private func removeBlockInteraction(_ token: Int) { + let wasEmpty = self.blockInteractionTokens.isEmpty + self.blockInteractionTokens.remove(token) + if !wasEmpty && self.blockInteractionTokens.isEmpty { + self.updateIsInteractionBlocked?(false) + } + } + + public func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void) { + let controllerReady = controller.ready.get() + |> filter({ $0 }) + |> take(1) + |> deliverOnMainQueue + |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true)) + + if let _ = self.view, let initialLayout = self.layout { + if let controller = controller as? ViewController { + if controller.lockOrientation { + let orientations: UIInterfaceOrientationMask + if initialLayout.size.width < initialLayout.size.height { + orientations = .portrait + } else { + orientations = .landscape + } + + controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: orientations, compactSize: orientations) + } + } + controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size) + controller.containerLayoutUpdated(initialLayout, transition: .immediate) + var blockInteractionToken: Int? + if blockInteraction { + blockInteractionToken = self.addBlockInteraction() + } + self.presentationDisposables.add((controllerReady |> afterDisposed { [weak self] in + Queue.mainQueue().async { + if let blockInteractionToken = blockInteractionToken { + self?.removeBlockInteraction(blockInteractionToken) + } + completion() + } + }).start(next: { [weak self] _ in + if let strongSelf = self { + if let blockInteractionToken = blockInteractionToken { + strongSelf.removeBlockInteraction(blockInteractionToken) + } + if strongSelf.controllers.contains(where: { $0.0 === controller }) { + return + } + + var insertIndex: Int? + for i in (0 ..< strongSelf.controllers.count).reversed() { + if strongSelf.controllers[i].1.rawValue > level.rawValue { + insertIndex = i + } + } + strongSelf.controllers.insert((controller, level), at: insertIndex ?? strongSelf.controllers.count) + if let view = strongSelf.view, let layout = strongSelf.layout { + (controller as? UIViewController)?.navigation_setDismiss({ [weak controller] in + if let strongSelf = self, let controller = controller { + strongSelf.dismiss(controller) + } + }, rootController: nil) + (controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(true) + if layout != initialLayout { + controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + if let topLevelSubview = strongSelf.topLevelSubview(for: level) { + view.insertSubview(controller.view, belowSubview: topLevelSubview) + } else { + if let volumeControlStatusBarNodeView = strongSelf.volumeControlStatusBarNodeView { + view.insertSubview(controller.view, belowSubview: volumeControlStatusBarNodeView) + } else { + view.addSubview(controller.view) + } + } + controller.containerLayoutUpdated(layout, transition: .immediate) + } else { + if let topLevelSubview = strongSelf.topLevelSubview(for: level) { + view.insertSubview(controller.view, belowSubview: topLevelSubview) + } else { + if let volumeControlStatusBarNodeView = strongSelf.volumeControlStatusBarNodeView { + view.insertSubview(controller.view, belowSubview: volumeControlStatusBarNodeView) + } else { + view.addSubview(controller.view) + } + } + } + (controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(false) + view.layer.invalidateUpTheTree() + controller.viewWillAppear(false) + if let controller = controller as? PresentableController { + controller.viewDidAppear(completion: { [weak self] in + self?.notifyAccessibilityScreenChanged() + }) + } else { + controller.viewDidAppear(false) + strongSelf.notifyAccessibilityScreenChanged() + } + } + strongSelf.updateViews() + } + })) + } else { + self.controllers.append((controller, level)) + self.updateViews() + } + } + + deinit { + self.presentationDisposables.dispose() + } + + private func dismiss(_ controller: ContainableController) { + if let index = self.controllers.index(where: { $0.0 === controller }) { + self.controllers.remove(at: index) + controller.viewWillDisappear(false) + controller.view.removeFromSuperview() + controller.viewDidDisappear(false) + self.updateViews() + } + } + + public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let wasReady = self.ready + self.layout = layout + + if wasReady != self.ready { + self.readyChanged(wasReady: wasReady) + } else if self.ready { + for (controller, _) in self.controllers { + controller.containerLayoutUpdated(layout, transition: transition) + } + } + } + + private func readyChanged(wasReady: Bool) { + if !wasReady { + self.addViews() + } else { + self.removeViews() + } + } + + private func addViews() { + if let view = self.view, let layout = self.layout { + for (controller, _) in self.controllers { + controller.viewWillAppear(false) + if let topLevelSubview = self.topLevelSubview { + view.insertSubview(controller.view, belowSubview: topLevelSubview) + } else { + if let volumeControlStatusBarNodeView = self.volumeControlStatusBarNodeView { + view.insertSubview(controller.view, belowSubview: volumeControlStatusBarNodeView) + } else { + view.addSubview(controller.view) + } + } + controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + controller.containerLayoutUpdated(layout, transition: .immediate) + if let controller = controller as? PresentableController { + controller.viewDidAppear(completion: { [weak self] in + self?.notifyAccessibilityScreenChanged() + }) + } else { + controller.viewDidAppear(false) + self.notifyAccessibilityScreenChanged() + } + } + self.updateViews() + } + } + + private func removeViews() { + for (controller, _) in self.controllers { + controller.viewWillDisappear(false) + controller.view.removeFromSuperview() + controller.viewDidDisappear(false) + } + } + + private func updateViews() { + self.hasOpaqueOverlay = self.currentlyBlocksBackgroundWhenInOverlay + var topHasOpaque = false + for (controller, _) in self.controllers.reversed() { + if topHasOpaque { + controller.displayNode.accessibilityElementsHidden = true + } else { + if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay { + topHasOpaque = true + } + controller.displayNode.accessibilityElementsHidden = false + } + } + } + + private func notifyAccessibilityScreenChanged() { + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil) + } + + func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for (controller, _) in self.controllers.reversed() { + if controller.isViewLoaded { + if let result = controller.view.hitTest(point, with: event) { + return result + } + } + } + return nil + } + + func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { + if self.ready { + for (controller, _) in self.controllers { + controller.updateToInterfaceOrientation(orientation) + } + } + } + + func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations { + var mask = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) + + for (controller, _) in self.controllers { + mask = mask.intersection(controller.combinedSupportedOrientations(currentOrientationToLock: currentOrientationToLock)) + } + + return mask + } + + func combinedDeferScreenEdgeGestures() -> UIRectEdge { + var edges: UIRectEdge = [] + + for (controller, _) in self.controllers { + edges = edges.union(controller.deferScreenEdgeGestures) + } + + return edges + } +} diff --git a/submodules/Display/Display/RuntimeUtils.h b/submodules/Display/Display/RuntimeUtils.h new file mode 100644 index 0000000000..654d3504d4 --- /dev/null +++ b/submodules/Display/Display/RuntimeUtils.h @@ -0,0 +1,24 @@ +#import + +typedef enum { + NSObjectAssociationPolicyRetain = 0, + NSObjectAssociationPolicyCopy = 1 +} NSObjectAssociationPolicy; + +@interface RuntimeUtils : NSObject + ++ (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector; ++ (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector withAnotherClass:(Class)anotherClass newSelector:(SEL)newSelector; ++ (void)swizzleClassMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector; + +@end + +@interface NSObject (AssociatedObject) + +- (void)setAssociatedObject:(id)object forKey:(void const *)key; +- (void)setAssociatedObject:(id)object forKey:(void const *)key associationPolicy:(NSObjectAssociationPolicy)associationPolicy; +- (id)associatedObjectForKey:(void const *)key; +- (bool)checkObjectIsKindOfClass:(Class)targetClass; +- (void)setClass:(Class)newClass; + +@end diff --git a/submodules/Display/Display/RuntimeUtils.m b/submodules/Display/Display/RuntimeUtils.m new file mode 100644 index 0000000000..639e5848eb --- /dev/null +++ b/submodules/Display/Display/RuntimeUtils.m @@ -0,0 +1,111 @@ +#import "RuntimeUtils.h" + +#import + +@implementation RuntimeUtils + ++ (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector { + Method origMethod = nil, newMethod = nil; + + origMethod = class_getInstanceMethod(targetClass, currentSelector); + newMethod = class_getInstanceMethod(targetClass, newSelector); + if ((origMethod != nil) && (newMethod != nil)) { + if(class_addMethod(targetClass, currentSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { + class_replaceMethod(targetClass, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); + } else { + method_exchangeImplementations(origMethod, newMethod); + } + } +} + ++ (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector withAnotherClass:(Class)anotherClass newSelector:(SEL)newSelector { + Method origMethod = nil, newMethod = nil; + + origMethod = class_getInstanceMethod(targetClass, currentSelector); + newMethod = class_getInstanceMethod(anotherClass, newSelector); + if ((origMethod != nil) && (newMethod != nil)) { + method_exchangeImplementations(origMethod, newMethod); + } +} + ++ (void)swizzleClassMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector { + Method origMethod = nil, newMethod = nil; + + origMethod = class_getClassMethod(targetClass, currentSelector); + newMethod = class_getClassMethod(targetClass, newSelector); + + targetClass = object_getClass((id)targetClass); + + if ((origMethod != nil) && (newMethod != nil)) { + if(class_addMethod(targetClass, currentSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { + class_replaceMethod(targetClass, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); + } else { + method_exchangeImplementations(origMethod, newMethod); + } + } +} + +@end + +@implementation NSObject (AssociatedObject) + +- (void)setAssociatedObject:(id)object forKey:(void const *)key +{ + [self setAssociatedObject:object forKey:key associationPolicy:NSObjectAssociationPolicyRetain]; +} + +- (void)setAssociatedObject:(id)object forKey:(void const *)key associationPolicy:(NSObjectAssociationPolicy)associationPolicy +{ + int policy = 0; + switch (associationPolicy) + { + case NSObjectAssociationPolicyRetain: + policy = OBJC_ASSOCIATION_RETAIN_NONATOMIC; + break; + case NSObjectAssociationPolicyCopy: + policy = OBJC_ASSOCIATION_COPY_NONATOMIC; + break; + default: + policy = OBJC_ASSOCIATION_RETAIN_NONATOMIC; + break; + } + objc_setAssociatedObject(self, key, object, policy); +} + +- (id)associatedObjectForKey:(void const *)key +{ + return objc_getAssociatedObject(self, key); +} + +- (bool)checkObjectIsKindOfClass:(Class)targetClass { + return [self isKindOfClass:targetClass]; +} + +- (void)setClass:(Class)newClass { + object_setClass(self, newClass); +} + +static Class freedomMakeClass(Class superclass, Class subclass, SEL *copySelectors, int copySelectorsCount) +{ + if (superclass == Nil || subclass == Nil) + return nil; + + Class decoratedClass = objc_allocateClassPair(superclass, [[NSString alloc] initWithFormat:@"%@_%@", NSStringFromClass(superclass), NSStringFromClass(subclass)].UTF8String, 0); + + unsigned int count = 0; + Method *methodList = class_copyMethodList(subclass, &count); + if (methodList != NULL) { + for (unsigned int i = 0; i < count; i++) { + SEL methodName = method_getName(methodList[i]); + class_addMethod(decoratedClass, methodName, method_getImplementation(methodList[i]), method_getTypeEncoding(methodList[i])); + } + + free(methodList); + } + + objc_registerClassPair(decoratedClass); + + return decoratedClass; +} + +@end diff --git a/submodules/Display/Display/RuntimeUtils.swift b/submodules/Display/Display/RuntimeUtils.swift new file mode 100644 index 0000000000..7ae936e860 --- /dev/null +++ b/submodules/Display/Display/RuntimeUtils.swift @@ -0,0 +1,23 @@ +import Foundation +import UIKit + +private let systemVersion = { () -> (Int, Int) in + let string = UIDevice.current.systemVersion as NSString + var minor = 0 + let range = string.range(of: ".") + if range.location != NSNotFound { + minor = Int((string.substring(from: range.location + 1) as NSString).intValue) + } + return (Int(string.intValue), minor) +}() + +public func matchMinimumSystemVersion(major: Int, minor: Int = 0) -> Bool { + let version = systemVersion + if version.0 == major { + return version.1 >= minor + } else if version.0 < major { + return false + } else { + return true + } +} diff --git a/submodules/Display/Display/ScrollToTopProxyView.swift b/submodules/Display/Display/ScrollToTopProxyView.swift new file mode 100644 index 0000000000..3433b8b88f --- /dev/null +++ b/submodules/Display/Display/ScrollToTopProxyView.swift @@ -0,0 +1,35 @@ +import UIKit + +class ScrollToTopView: UIScrollView, UIScrollViewDelegate { + var action: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.delegate = self + self.scrollsToTop = true + if #available(iOSApplicationExtension 11.0, *) { + self.contentInsetAdjustmentBehavior = .never + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var frame: CGRect { + didSet { + let frame = self.frame + self.contentSize = CGSize(width: frame.width, height: frame.height + 1.0) + self.contentOffset = CGPoint(x: 0.0, y: 1.0) + } + } + + @objc func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + if let action = self.action { + action() + } + + return false + } +} diff --git a/submodules/Display/Display/Spring.swift b/submodules/Display/Display/Spring.swift new file mode 100644 index 0000000000..169ed107db --- /dev/null +++ b/submodules/Display/Display/Spring.swift @@ -0,0 +1,67 @@ +import Foundation +import UIKit + +struct ViewportItemSpring { + let stiffness: CGFloat + let damping: CGFloat + let mass: CGFloat + var velocity: CGFloat = 0.0 + + init(stiffness: CGFloat, damping: CGFloat, mass: CGFloat) { + self.stiffness = stiffness + self.damping = damping + self.mass = mass + } +} + +private func a(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return 1.0 - 3.0 * a2 + 3.0 * a1 +} + +private func b(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return 3.0 * a2 - 6.0 * a1 +} + +private func c(_ a1: CGFloat) -> CGFloat +{ + return 3.0 * a1 +} + +private func calcBezier(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return ((a(a1, a2)*t + b(a1, a2))*t + c(a1)) * t +} + +private func calcSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat +{ + return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1) +} + +private func getTForX(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat { + var t = x + var i = 0 + while i < 4 { + let currentSlope = calcSlope(t, x1, x2) + if currentSlope == 0.0 { + return t + } else { + let currentX = calcBezier(t, x1, x2) - x + t -= currentX / currentSlope + } + + i += 1 + } + + return t +} + +func bezierPoint(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat) -> CGFloat +{ + var value = calcBezier(getTForX(x, x1, x2), y1, y2) + if value >= 0.997 { + value = 1.0 + } + return value +} diff --git a/submodules/Display/Display/StatusBar.swift b/submodules/Display/Display/StatusBar.swift new file mode 100644 index 0000000000..31da12342b --- /dev/null +++ b/submodules/Display/Display/StatusBar.swift @@ -0,0 +1,252 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +#if BUCK +import DisplayPrivate +#endif + +public class StatusBarSurface { + var statusBars: [StatusBar] = [] + + func addStatusBar(_ statusBar: StatusBar) { + self.removeStatusBar(statusBar) + self.statusBars.append(statusBar) + } + + func insertStatusBar(_ statusBar: StatusBar, atIndex index: Int) { + self.removeStatusBar(statusBar) + self.statusBars.insert(statusBar, at: index) + } + + func removeStatusBar(_ statusBar: StatusBar) { + for i in 0 ..< self.statusBars.count { + if self.statusBars[i] === statusBar { + self.statusBars.remove(at: i) + break + } + } + } +} + +private let inCallBackgroundColor = UIColor(rgb: 0x43d551) + +private func addInCallAnimation(_ layer: CALayer) { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.keyTimes = [0.0 as NSNumber, 0.1 as NSNumber, 0.5 as NSNumber, 0.9 as NSNumber, 1.0 as NSNumber] + animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber, 1.0 as NSNumber, 1.0 as NSNumber] + animation.duration = 1.8 + animation.autoreverses = true + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + layer.add(animation, forKey: "blink") +} + +private final class StatusBarLabelNode: ASTextNode { + override func willEnterHierarchy() { + super.willEnterHierarchy() + + addInCallAnimation(self.layer) + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.layer.removeAnimation(forKey: "blink") + } +} + +private final class StatusBarView: UITracingLayerView { + weak var node: StatusBar? + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let node = self.node { + return node.hitTest(point, with: event) + } + return nil + } +} + +public final class StatusBar: ASDisplayNode { + public var statusBarStyle: StatusBarStyle = .Black { + didSet { + if self.statusBarStyle != oldValue { + self.layer.invalidateUpTheTree() + } + } + } + + public var ignoreInCall: Bool = false + + var inCallNavigate: (() -> Void)? + + private var proxyNode: StatusBarProxyNode? + private var removeProxyNodeScheduled = false + + let offsetNode = ASDisplayNode() + private let inCallBackgroundNode = ASDisplayNode() + private let inCallLabel: StatusBarLabelNode + + private var inCallText: String? = nil + + public var verticalOffset: CGFloat = 0.0 { + didSet { + if !self.verticalOffset.isEqual(to: oldValue) { + self.offsetNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -self.verticalOffset), size: CGSize()) + self.layer.invalidateUpTheTree() + } + } + } + + public override init() { + self.inCallLabel = StatusBarLabelNode() + self.inCallLabel.isUserInteractionEnabled = false + + self.offsetNode.isUserInteractionEnabled = false + + let labelSize = self.inCallLabel.measure(CGSize(width: 300.0, height: 300.0)) + self.inCallLabel.frame = CGRect(origin: CGPoint(x: 10.0, y: 20.0 + 4.0), size: labelSize) + + super.init() + + self.setViewBlock({ + return StatusBarView() + }) + + (self.view as! StatusBarView).node = self + + self.addSubnode(self.offsetNode) + self.addSubnode(self.inCallBackgroundNode) + + self.layer.setTraceableInfo(CATracingLayerInfo(shouldBeAdjustedToInverseTransform: true, userData: self, tracingTag: WindowTracingTags.statusBar, disableChildrenTracingTags: 0)) + + self.clipsToBounds = true + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func updateState(statusBar: UIView?, withSafeInsets: Bool, inCallText: String?, animated: Bool) { + if let statusBar = statusBar { + self.removeProxyNodeScheduled = false + let resolvedStyle: StatusBarStyle + if inCallText != nil && !self.ignoreInCall { + resolvedStyle = .White + } else { + resolvedStyle = self.statusBarStyle + } + if let proxyNode = self.proxyNode { + proxyNode.statusBarStyle = resolvedStyle + } else { + self.proxyNode = StatusBarProxyNode(statusBarStyle: resolvedStyle, statusBar: statusBar) + self.proxyNode!.isHidden = false + self.addSubnode(self.proxyNode!) + } + } else { + self.removeProxyNodeScheduled = true + + DispatchQueue.main.async(execute: { [weak self] in + if let strongSelf = self { + if strongSelf.removeProxyNodeScheduled { + strongSelf.removeProxyNodeScheduled = false + strongSelf.proxyNode?.isHidden = true + strongSelf.proxyNode?.removeFromSupernode() + strongSelf.proxyNode = nil + } + } + }) + } + + var ignoreInCall = self.ignoreInCall + switch self.statusBarStyle { + case .Black, .White: + break + default: + ignoreInCall = true + } + + var resolvedInCallText: String? = inCallText + if ignoreInCall { + resolvedInCallText = nil + } + + if (resolvedInCallText != nil) != (self.inCallText != nil) { + if let _ = resolvedInCallText { + if !withSafeInsets { + self.addSubnode(self.inCallLabel) + } + addInCallAnimation(self.inCallLabel.layer) + + self.inCallBackgroundNode.layer.backgroundColor = inCallBackgroundColor.cgColor + if animated { + self.inCallBackgroundNode.layer.animate(from: UIColor.clear.cgColor, to: inCallBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.3) + } + } else { + self.inCallLabel.removeFromSupernode() + + self.inCallBackgroundNode.layer.backgroundColor = UIColor.clear.cgColor + if animated { + self.inCallBackgroundNode.layer.animate(from: inCallBackgroundColor.cgColor, to: UIColor.clear.cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.3) + } + } + } + + + if let resolvedInCallText = resolvedInCallText { + if self.inCallText != resolvedInCallText { + self.inCallLabel.attributedText = NSAttributedString(string: resolvedInCallText, font: Font.regular(14.0), textColor: .white) + } + + self.layoutInCallLabel() + } + + self.inCallText = resolvedInCallText + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) && self.inCallText != nil { + return self.view + } else { + return nil + } + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, self.inCallText != nil { + self.inCallNavigate?() + } + } + + override public func layout() { + super.layout() + + self.layoutInCallLabel() + } + + override public var frame: CGRect { + didSet { + if oldValue.size != self.frame.size { + let bounds = self.bounds + self.inCallBackgroundNode.frame = CGRect(origin: CGPoint(), size: bounds.size) + } + } + } + + override public var bounds: CGRect { + didSet { + if oldValue.size != self.bounds.size { + let bounds = self.bounds + self.inCallBackgroundNode.frame = CGRect(origin: CGPoint(), size: bounds.size) + } + } + } + + private func layoutInCallLabel() { + if self.inCallLabel.supernode != nil { + let size = self.bounds.size + if !size.width.isZero && !size.height.isZero { + let labelSize = self.inCallLabel.measure(size) + self.inCallLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: 20.0 + floor((20.0 - labelSize.height) / 2.0)), size: labelSize) + } + } + } +} diff --git a/submodules/Display/Display/StatusBarHost.swift b/submodules/Display/Display/StatusBarHost.swift new file mode 100644 index 0000000000..35a7299d4e --- /dev/null +++ b/submodules/Display/Display/StatusBarHost.swift @@ -0,0 +1,14 @@ +import UIKit +import SwiftSignalKit + +public protocol StatusBarHost { + var statusBarFrame: CGRect { get } + var statusBarStyle: UIStatusBarStyle { get set } + var statusBarWindow: UIView? { get } + var statusBarView: UIView? { get } + + var keyboardWindow: UIWindow? { get } + var keyboardView: UIView? { get } + + var handleVolumeControl: Signal { get } +} diff --git a/submodules/Display/Display/StatusBarManager.swift b/submodules/Display/Display/StatusBarManager.swift new file mode 100644 index 0000000000..3aee140873 --- /dev/null +++ b/submodules/Display/Display/StatusBarManager.swift @@ -0,0 +1,303 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +private struct MappedStatusBar { + let style: StatusBarStyle + let frame: CGRect + let statusBar: StatusBar? +} + +private struct MappedStatusBarSurface { + let statusBars: [MappedStatusBar] + let surface: StatusBarSurface +} + +private func mapStatusBar(_ statusBar: StatusBar, forceInCall: Bool) -> MappedStatusBar { + let frame = CGRect(origin: statusBar.view.convert(CGPoint(), to: nil), size: statusBar.frame.size) + let resolvedStyle: StatusBarStyle + switch statusBar.statusBarStyle { + case .Black, .White: + if forceInCall { + resolvedStyle = .White + } else { + resolvedStyle = statusBar.statusBarStyle + } + default: + resolvedStyle = statusBar.statusBarStyle + } + return MappedStatusBar(style: resolvedStyle, frame: frame, statusBar: statusBar) +} + +private func mappedSurface(_ surface: StatusBarSurface, forceInCall: Bool) -> MappedStatusBarSurface { + var statusBars: [MappedStatusBar] = [] + for statusBar in surface.statusBars { + if statusBar.statusBarStyle != .Ignore { + statusBars.append(mapStatusBar(statusBar, forceInCall: forceInCall)) + } + } + return MappedStatusBarSurface(statusBars: statusBars, surface: surface) +} + +private func optimizeMappedSurface(statusBarSize: CGSize, surface: MappedStatusBarSurface, forceInCall: Bool) -> MappedStatusBarSurface { + if surface.statusBars.count > 1 { + for i in 1 ..< surface.statusBars.count { + if (!forceInCall && surface.statusBars[i].style != surface.statusBars[i - 1].style) || abs(surface.statusBars[i].frame.origin.y - surface.statusBars[i - 1].frame.origin.y) > CGFloat.ulpOfOne { + return surface + } + if let lhsStatusBar = surface.statusBars[i - 1].statusBar, let rhsStatusBar = surface.statusBars[i].statusBar , !lhsStatusBar.alpha.isEqual(to: rhsStatusBar.alpha) { + return surface + } + } + let size = statusBarSize + return MappedStatusBarSurface(statusBars: [MappedStatusBar(style: forceInCall ? .White : surface.statusBars[0].style, frame: CGRect(origin: CGPoint(x: 0.0, y: surface.statusBars[0].frame.origin.y), size: size), statusBar: nil)], surface: surface.surface) + } else { + return surface + } +} + +private func displayHiddenAnimation() -> CAAnimation { + let animation = CABasicAnimation(keyPath: "transform.translation.y") + animation.fromValue = NSNumber(value: Float(-40.0)) + animation.toValue = NSNumber(value: Float(-40.0)) + animation.fillMode = kCAFillModeBoth + animation.duration = 1.0 + animation.speed = 0.0 + animation.isAdditive = true + animation.isRemovedOnCompletion = false + + return animation +} + +class StatusBarManager { + private var host: StatusBarHost + private let volumeControlStatusBar: VolumeControlStatusBar + private let volumeControlStatusBarNode: VolumeControlStatusBarNode + + private var surfaces: [StatusBarSurface] = [] + private var validParams: (withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool)? + + var inCallNavigate: (() -> Void)? + + private var volumeTimer: SwiftSignalKit.Timer? + + init(host: StatusBarHost, volumeControlStatusBar: VolumeControlStatusBar, volumeControlStatusBarNode: VolumeControlStatusBarNode) { + self.host = host + self.volumeControlStatusBar = volumeControlStatusBar + self.volumeControlStatusBarNode = volumeControlStatusBarNode + self.volumeControlStatusBarNode.isHidden = true + + self.volumeControlStatusBar.valueChanged = { [weak self] previous, updated in + if let strongSelf = self { + strongSelf.startVolumeTimer() + strongSelf.volumeControlStatusBarNode.updateValue(from: CGFloat(previous), to: CGFloat(updated)) + } + } + } + + private func startVolumeTimer() { + self.volumeTimer?.invalidate() + let timer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in + self?.endVolumeTimer() + }, queue: Queue.mainQueue()) + self.volumeTimer = timer + timer.start() + if self.volumeControlStatusBarNode.isHidden { + self.volumeControlStatusBarNode.isHidden = false + self.volumeControlStatusBarNode.alpha = 1.0 + self.volumeControlStatusBarNode.allowsGroupOpacity = true + self.volumeControlStatusBarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.18, completion: { [weak self] _ in + self?.volumeControlStatusBarNode.allowsGroupOpacity = false + }) + } + if let (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows) = self.validParams { + self.updateSurfaces(self.surfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: false, alphaTransition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + private func endVolumeTimer() { + self.volumeControlStatusBarNode.alpha = 0.0 + self.volumeControlStatusBarNode.allowsGroupOpacity = true + self.volumeControlStatusBarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { [weak self] completed in + if let strongSelf = self, completed { + strongSelf.volumeControlStatusBarNode.isHidden = true + strongSelf.volumeControlStatusBarNode.allowsGroupOpacity = false + } + }) + self.volumeTimer = nil + if let (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows) = self.validParams { + self.updateSurfaces(self.surfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: false, alphaTransition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + func updateState(surfaces: [StatusBarSurface], withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, animated: Bool) { + let previousSurfaces = self.surfaces + self.surfaces = surfaces + self.updateSurfaces(previousSurfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: animated, alphaTransition: .immediate) + } + + private func updateSurfaces(_ previousSurfaces: [StatusBarSurface], withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, animated: Bool, alphaTransition: ContainedViewLayoutTransition) { + let statusBarFrame = self.host.statusBarFrame + guard let statusBarView = self.host.statusBarView else { + return + } + + self.validParams = (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows) + + if self.host.statusBarWindow?.isUserInteractionEnabled != (forceInCallStatusBarText == nil) { + self.host.statusBarWindow?.isUserInteractionEnabled = (forceInCallStatusBarText == nil) + } + + var mappedSurfaces: [MappedStatusBarSurface] = [] + var mapIndex = 0 + var doNotOptimize = false + for surface in self.surfaces { + inner: for statusBar in surface.statusBars { + if statusBar.statusBarStyle == .Hide { + doNotOptimize = true + break inner + } + } + + let mapped = mappedSurface(surface, forceInCall: forceInCallStatusBarText != nil) + + if doNotOptimize { + mappedSurfaces.append(mapped) + } else { + mappedSurfaces.append(optimizeMappedSurface(statusBarSize: statusBarFrame.size, surface: mapped, forceInCall: forceInCallStatusBarText != nil)) + } + mapIndex += 1 + } + + var reduceSurfaces = true + var reduceSurfacesStatusBarStyleAndAlpha: (StatusBarStyle, CGFloat)? + var reduceIndex = 0 + outer: for surface in mappedSurfaces { + for mappedStatusBar in surface.statusBars { + if reduceIndex == 0 && mappedStatusBar.style == .Hide { + reduceSurfaces = false + break outer + } + if mappedStatusBar.frame.origin.equalTo(CGPoint()) { + let statusBarAlpha = mappedStatusBar.statusBar?.alpha ?? 1.0 + if let reduceSurfacesStatusBarStyleAndAlpha = reduceSurfacesStatusBarStyleAndAlpha { + if mappedStatusBar.style != reduceSurfacesStatusBarStyleAndAlpha.0 { + reduceSurfaces = false + break outer + } + if !statusBarAlpha.isEqual(to: reduceSurfacesStatusBarStyleAndAlpha.1) { + reduceSurfaces = false + break outer + } + } else { + reduceSurfacesStatusBarStyleAndAlpha = (mappedStatusBar.style, statusBarAlpha) + } + } + } + reduceIndex += 1 + } + + if reduceSurfaces { + outer: for surface in mappedSurfaces { + for mappedStatusBar in surface.statusBars { + if mappedStatusBar.frame.origin.equalTo(CGPoint()) { + if let statusBar = mappedStatusBar.statusBar , !statusBar.layer.hasPositionOrOpacityAnimations() { + mappedSurfaces = [MappedStatusBarSurface(statusBars: [mappedStatusBar], surface: surface.surface)] + break outer + } + } + } + } + } + + var visibleStatusBars: [StatusBar] = [] + + var globalStatusBar: (StatusBarStyle, CGFloat, CGFloat)? + + var coveredIdentity = false + var statusBarIndex = 0 + for i in 0 ..< mappedSurfaces.count { + for mappedStatusBar in mappedSurfaces[i].statusBars { + if let statusBar = mappedStatusBar.statusBar { + if mappedStatusBar.frame.origin.equalTo(CGPoint()) && !statusBar.layer.hasPositionOrOpacityAnimations() && !statusBar.offsetNode.layer.hasPositionAnimations() { + if !coveredIdentity { + if statusBar.statusBarStyle != .Hide { + if statusBar.offsetNode.frame.origin.equalTo(CGPoint()) { + coveredIdentity = CGFloat(1.0).isLessThanOrEqualTo(statusBar.alpha) + } + if statusBarIndex == 0 && globalStatusBar == nil { + globalStatusBar = (mappedStatusBar.style, statusBar.alpha, statusBar.offsetNode.frame.origin.y) + } else { + visibleStatusBars.append(statusBar) + } + } + } + } else { + visibleStatusBars.append(statusBar) + } + } else { + if !coveredIdentity { + coveredIdentity = true + if statusBarIndex == 0 && globalStatusBar == nil { + globalStatusBar = (mappedStatusBar.style, 1.0, 0.0) + } + } + } + statusBarIndex += 1 + } + } + + for surface in previousSurfaces { + for statusBar in surface.statusBars { + if !visibleStatusBars.contains(where: {$0 === statusBar}) { + statusBar.updateState(statusBar: nil, withSafeInsets: withSafeInsets, inCallText: forceInCallStatusBarText, animated: animated) + } + } + } + + for surface in self.surfaces { + for statusBar in surface.statusBars { + statusBar.inCallNavigate = self.inCallNavigate + if !visibleStatusBars.contains(where: {$0 === statusBar}) { + statusBar.updateState(statusBar: nil, withSafeInsets: withSafeInsets, inCallText: forceInCallStatusBarText, animated: animated) + } + } + } + + for statusBar in visibleStatusBars { + statusBar.updateState(statusBar: statusBarView, withSafeInsets: withSafeInsets, inCallText: forceInCallStatusBarText, animated: animated) + } + + if self.volumeTimer != nil { + globalStatusBar?.1 = 0.0 + } + var isDark = true + if let globalStatusBar = globalStatusBar { + isDark = globalStatusBar.0.systemStyle == UIStatusBarStyle.lightContent + } + self.volumeControlStatusBarNode.isDark = isDark + + if let globalStatusBar = globalStatusBar, !forceHiddenBySystemWindows { + let statusBarStyle: UIStatusBarStyle + if forceInCallStatusBarText != nil { + statusBarStyle = .lightContent + } else { + statusBarStyle = globalStatusBar.0 == .Black ? .default : .lightContent + } + if self.host.statusBarStyle != statusBarStyle { + self.host.statusBarStyle = statusBarStyle + } + if let statusBarWindow = self.host.statusBarWindow { + alphaTransition.updateAlpha(layer: statusBarView.layer, alpha: globalStatusBar.1) + var statusBarBounds = statusBarWindow.bounds + if !statusBarBounds.origin.y.isEqual(to: globalStatusBar.2) { + statusBarBounds.origin.y = globalStatusBar.2 + statusBarWindow.bounds = statusBarBounds + } + } + } else { + statusBarView.alpha = 0.0 + } + } +} diff --git a/submodules/Display/Display/StatusBarProxyNode.swift b/submodules/Display/Display/StatusBarProxyNode.swift new file mode 100644 index 0000000000..be2f0f3795 --- /dev/null +++ b/submodules/Display/Display/StatusBarProxyNode.swift @@ -0,0 +1,513 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum StatusBarStyle { + case Black + case White + case Ignore + case Hide + + public init(systemStyle: UIStatusBarStyle) { + switch systemStyle { + case .default: + self = .Black + case .lightContent: + self = .White + case .blackOpaque: + self = .Black + } + } + + public var systemStyle: UIStatusBarStyle { + switch self { + case .Black: + return .default + case .White: + return .lightContent + default: + return .default + } + } +} + +private enum StatusBarItemType { + case Generic + case Battery + case Activity +} + +func makeStatusBarProxy(_ statusBarStyle: StatusBarStyle, statusBar: UIView) -> StatusBarProxyNode { + return StatusBarProxyNode(statusBarStyle: statusBarStyle, statusBar: statusBar) +} + +private func maxSubviewBounds(_ view: UIView) -> CGRect { + var bounds = view.bounds + for subview in view.subviews { + let subviewFrame = subview.frame + let subviewBounds = maxSubviewBounds(subview).offsetBy(dx: subviewFrame.minX, dy: subviewFrame.minY) + bounds = bounds.union(subviewBounds) + } + return bounds +} + +private class StatusBarItemNode: ASDisplayNode { + var statusBarStyle: StatusBarStyle + var targetView: UIView + var rootView: UIView + private let contentNode: ASDisplayNode + + init(statusBarStyle: StatusBarStyle, targetView: UIView, rootView: UIView) { + self.statusBarStyle = statusBarStyle + self.targetView = targetView + self.rootView = rootView + self.contentNode = ASDisplayNode() + self.contentNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.contentNode) + } + + func update() { + let containingBounds = maxSubviewBounds(self.targetView) + let context = DrawingContext(size: containingBounds.size, clear: true) + + if let contents = self.targetView.layer.contents, (self.targetView.layer.sublayers?.count ?? 0) == 0 && CFGetTypeID(contents as CFTypeRef) == CGImage.typeID && false { + let image = contents as! CGImage + context.withFlippedContext { c in + c.setAlpha(CGFloat(self.targetView.layer.opacity)) + c.draw(image, in: CGRect(origin: CGPoint(), size: context.size)) + c.setAlpha(1.0) + } + + if let sublayers = self.targetView.layer.sublayers { + for sublayer in sublayers { + let origin = sublayer.frame.origin + if let contents = sublayer.contents , CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { + let image = contents as! CGImage + context.withFlippedContext { c in + c.translateBy(x: origin.x, y: origin.y) + c.draw(image, in: CGRect(origin: CGPoint(), size: context.size)) + c.translateBy(x: -origin.x, y: -origin.y) + } + } else { + context.withContext { c in + UIGraphicsPushContext(c) + c.translateBy(x: origin.x, y: origin.y) + sublayer.render(in: c) + c.translateBy(x: -origin.x, y: -origin.y) + UIGraphicsPopContext() + } + } + } + } + } else { + context.withContext { c in + c.translateBy(x: containingBounds.minX, y: -containingBounds.minY) + UIGraphicsPushContext(c) + self.targetView.layer.render(in: c) + UIGraphicsPopContext() + } + } + //dumpViews(self.targetView) + var type: StatusBarItemType = .Generic + if let batteryItemClass = batteryItemClass { + if self.targetView.checkIsKind(of: batteryItemClass) { + type = .Battery + } + } + if let batteryViewClass = batteryViewClass { + if self.targetView.checkIsKind(of: batteryViewClass) { + type = .Battery + } + } + if case .Generic = type { + var hasActivityBackground = false + var hasText = false + for subview in self.targetView.subviews { + if let stringClass = stringClass, subview.checkIsKind(of: stringClass) { + hasText = true + } else if let activityClass = activityClass, subview.checkIsKind(of: activityClass) { + hasActivityBackground = true + } + } + if hasActivityBackground && hasText { + type = .Activity + } + } + tintStatusBarItem(context, type: type, style: statusBarStyle) + self.contentNode.contents = context.generateImage()?.cgImage + + let mappedFrame = self.targetView.convert(self.targetView.bounds, to: self.rootView) + self.frame = mappedFrame + self.contentNode.frame = containingBounds + } +} + +private func tintStatusBarItem(_ context: DrawingContext, type: StatusBarItemType, style: StatusBarStyle) { + switch type { + case .Battery: + let minY = 0 + let minX = 0 + let maxY = Int(context.size.height * context.scale) + let maxX = Int(context.size.width * context.scale) + if minY < maxY && minX < maxX { + let basePixel = context.bytes.assumingMemoryBound(to: UInt32.self) + let pixelsPerRow = context.bytesPerRow / 4 + + let midX = (maxX + minX) / 2 + let midY = (maxY + minY) / 2 + let baseMidRow = basePixel + pixelsPerRow * midY + var baseX = minX + while baseX < maxX { + let pixel = baseMidRow + baseX + let alpha = pixel.pointee & 0xff000000 + if alpha != 0 { + break + } + baseX += 1 + } + + while baseX < maxX { + let pixel = baseMidRow + baseX + let alpha = pixel.pointee & 0xff000000 + if alpha == 0 { + break + } + baseX += 1 + } + + while baseX < maxX { + let pixel = baseMidRow + baseX + let alpha = pixel.pointee & 0xff000000 + if alpha != 0 { + break + } + baseX += 1 + } + + var targetX = baseX + while targetX < maxX { + let pixel = baseMidRow + targetX + let alpha = pixel.pointee & 0xff000000 + if alpha == 0 { + break + } + + targetX += 1 + } + + let batteryColor = (baseMidRow + baseX + 2).pointee + let batteryR = (batteryColor >> 16) & 0xff + let batteryG = (batteryColor >> 8) & 0xff + let batteryB = batteryColor & 0xff + + var baseY = minY + while baseY < maxY { + let baseRow = basePixel + pixelsPerRow * baseY + let pixel = baseRow + midX + let alpha = pixel.pointee & 0xff000000 + if alpha != 0 { + break + } + baseY += 1 + } + + var targetY = maxY - 1 + while targetY >= baseY { + let baseRow = basePixel + pixelsPerRow * targetY + let pixel = baseRow + midX + let alpha = pixel.pointee & 0xff000000 + if alpha != 0 { + break + } + targetY -= 1 + } + + targetY -= 1 + + let baseColor: UInt32 + switch style { + case .Black, .Ignore, .Hide: + baseColor = 0x000000 + case .White: + baseColor = 0xffffff + } + + let baseR = (baseColor >> 16) & 0xff + let baseG = (baseColor >> 8) & 0xff + let baseB = baseColor & 0xff + + var pixel = context.bytes.assumingMemoryBound(to: UInt32.self) + let end = context.bytes.advanced(by: context.length).assumingMemoryBound(to: UInt32.self) + while pixel != end { + let alpha = (pixel.pointee & 0xff000000) >> 24 + + let r = (baseR * alpha) / 255 + let g = (baseG * alpha) / 255 + let b = (baseB * alpha) / 255 + + pixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + pixel += 1 + } + + let whiteColor: UInt32 = 0xffffffff as UInt32 + let blackColor: UInt32 = 0xff000000 as UInt32 + if batteryColor != whiteColor && batteryColor != blackColor { + var y = baseY + 2 + while y < targetY { + let baseRow = basePixel + pixelsPerRow * y + var x = baseX + while x < targetX { + let pixel = baseRow + x + let alpha = (pixel.pointee >> 24) & 0xff + + let r = (batteryR * alpha) / 255 + let g = (batteryG * alpha) / 255 + let b = (batteryB * alpha) / 255 + + pixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + x += 1 + } + y += 1 + } + } + } + case .Activity: + break + case .Generic: + var pixel = context.bytes.assumingMemoryBound(to: UInt32.self) + let end = context.bytes.advanced(by: context.length).assumingMemoryBound(to: UInt32.self) + + let baseColor: UInt32 + switch style { + case .Black, .Ignore, .Hide: + baseColor = 0x000000 + case .White: + baseColor = 0xffffff + } + + let baseR = (baseColor >> 16) & 0xff + let baseG = (baseColor >> 8) & 0xff + let baseB = baseColor & 0xff + + while pixel != end { + let alpha = (pixel.pointee & 0xff000000) >> 24 + + let r = (baseR * alpha) / 255 + let g = (baseG * alpha) / 255 + let b = (baseB * alpha) / 255 + + pixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + pixel += 1 + } + } +} + +private let foregroundClass: AnyClass? = { + var nameString = "StatusBar" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "ForegroundView" + } + return NSClassFromString("_UI" + nameString) +}() + +private let foregroundClass2: AnyClass? = { + var nameString = "StatusBar" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "ForegroundView" + } + return NSClassFromString("UI" + nameString) +}() + +private let batteryItemClass: AnyClass? = { + var nameString = "StatusBarBattery" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "ItemView" + } + return NSClassFromString("UI" + nameString) +}() + +private let batteryViewClass: AnyClass? = { + var nameString = "Battery" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "View" + } + return NSClassFromString("_UI" + nameString) +}() + +private let activityClass: AnyClass? = { + var nameString = "StatusBarBackground" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "ActivityView" + } + return NSClassFromString("_UI" + nameString) +}() + +private let stringClass: AnyClass? = { + var nameString = "StatusBar" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "StringView" + } + return NSClassFromString("_UI" + nameString) +}() + +private func containsSubviewOfClass(view: UIView, of subviewClass: AnyClass?) -> Bool { + guard let subviewClass = subviewClass else { + return false + } + for subview in view.subviews { + if subview.checkIsKind(of: subviewClass) { + return true + } + } + return false +} + +private class StatusBarProxyNodeTimerTarget: NSObject { + let action: () -> Void + + init(action: @escaping () -> Void) { + self.action = action + } + + @objc func tick() { + action() + } +} + +private func forEachSubview(statusBar: UIView, _ f: (UIView, UIView) -> Bool) { + var rootView: UIView = statusBar + for subview in statusBar.subviews { + if let foregroundClass = foregroundClass, subview.checkIsKind(of: foregroundClass) { + rootView = subview + break + } else if let foregroundClass2 = foregroundClass2, subview.checkIsKind(of: foregroundClass2) { + rootView = subview + break + } + } + for subview in rootView.subviews { + if true || subview.subviews.isEmpty { + if !f(rootView, subview) { + break + } + } else { + for subSubview in subview.subviews { + if !f(rootView, subSubview) { + break + } + } + } + } +} + +class StatusBarProxyNode: ASDisplayNode { + private let statusBar: UIView + + var timer: Timer? + var statusBarStyle: StatusBarStyle { + didSet { + if oldValue != self.statusBarStyle { + if !self.isHidden { + self.updateItems() + } + } + } + } + + private var itemNodes: [StatusBarItemNode] = [] + + override var isHidden: Bool { + get { + return super.isHidden + } set(value) { + if super.isHidden != value { + super.isHidden = value + + if !value { + self.updateItems() + self.timer = Timer(timeInterval: 5.0, target: StatusBarProxyNodeTimerTarget { [weak self] in + self?.updateItems() + }, selector: #selector(StatusBarProxyNodeTimerTarget.tick), userInfo: nil, repeats: true) + RunLoop.main.add(self.timer!, forMode: .commonModes) + } else { + self.timer?.invalidate() + self.timer = nil + } + } + } + } + + init(statusBarStyle: StatusBarStyle, statusBar: UIView) { + self.statusBarStyle = statusBarStyle + self.statusBar = statusBar + + super.init() + + self.isHidden = true + + self.clipsToBounds = true + //self.backgroundColor = UIColor.blueColor().colorWithAlphaComponent(0.2) + + //dumpViews(statusBar) + forEachSubview(statusBar: statusBar, { rootView, subview in + let itemNode = StatusBarItemNode(statusBarStyle: statusBarStyle, targetView: subview, rootView: rootView) + self.itemNodes.append(itemNode) + self.addSubnode(itemNode) + return true + }) + + self.frame = statusBar.bounds + } + + deinit { + self.timer?.invalidate() + } + + private func updateItems() { + let statusBar = self.statusBar + + var i = 0 + while i < self.itemNodes.count { + var found = false + forEachSubview(statusBar: statusBar, { rootView, subview in + if self.itemNodes[i].rootView === rootView && self.itemNodes[i].targetView === subview { + found = true + return false + } else { + return true + } + }) + if !found { + self.itemNodes[i].removeFromSupernode() + self.itemNodes.remove(at: i) + } else { + self.itemNodes[i].statusBarStyle = self.statusBarStyle + self.itemNodes[i].update() + i += 1 + } + } + + forEachSubview(statusBar: statusBar, { rootView, subview in + var found = false + for itemNode in self.itemNodes { + if itemNode.targetView == subview { + found = true + break + } + } + if !found { + let itemNode = StatusBarItemNode(statusBarStyle: self.statusBarStyle, targetView: subview, rootView: rootView) + itemNode.update() + self.itemNodes.append(itemNode) + self.addSubnode(itemNode) + } + return true + }) + } +} diff --git a/submodules/Display/Display/SwitchNode.swift b/submodules/Display/Display/SwitchNode.swift new file mode 100644 index 0000000000..e8790656af --- /dev/null +++ b/submodules/Display/Display/SwitchNode.swift @@ -0,0 +1,93 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +private final class SwitchNodeViewLayer: CALayer { + override func setNeedsDisplay() { + } +} + +private final class SwitchNodeView: UISwitch { + override class var layerClass: AnyClass { + return SwitchNodeViewLayer.self + } +} + +open class SwitchNode: ASDisplayNode { + public var valueUpdated: ((Bool) -> Void)? + + public var frameColor = UIColor(rgb: 0xe0e0e0) { + didSet { + if self.isNodeLoaded { + (self.view as! UISwitch).tintColor = self.frameColor + } + } + } + public var handleColor = UIColor(rgb: 0xffffff) { + didSet { + if self.isNodeLoaded { + //(self.view as! UISwitch).thumbTintColor = self.handleColor + } + } + } + public var contentColor = UIColor(rgb: 0x42d451) { + didSet { + if self.isNodeLoaded { + (self.view as! UISwitch).onTintColor = self.contentColor + } + } + } + + private var _isOn: Bool = false + public var isOn: Bool { + get { + return self._isOn + } set(value) { + if (value != self._isOn) { + self._isOn = value + if self.isNodeLoaded { + (self.view as! UISwitch).setOn(value, animated: false) + } + } + } + } + + override public init() { + super.init() + + self.setViewBlock({ + return SwitchNodeView() + }) + } + + override open func didLoad() { + super.didLoad() + + self.view.isAccessibilityElement = false + + (self.view as! UISwitch).backgroundColor = self.backgroundColor + (self.view as! UISwitch).tintColor = self.frameColor + //(self.view as! UISwitch).thumbTintColor = self.handleColor + (self.view as! UISwitch).onTintColor = self.contentColor + + (self.view as! UISwitch).setOn(self._isOn, animated: false) + + (self.view as! UISwitch).addTarget(self, action: #selector(switchValueChanged(_:)), for: .valueChanged) + } + + public func setOn(_ value: Bool, animated: Bool) { + self._isOn = value + if self.isNodeLoaded { + (self.view as! UISwitch).setOn(value, animated: animated) + } + } + + override open func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: 51.0, height: 31.0) + } + + @objc func switchValueChanged(_ view: UISwitch) { + self._isOn = view.isOn + self.valueUpdated?(view.isOn) + } +} diff --git a/submodules/Display/Display/TabBarContollerNode.swift b/submodules/Display/Display/TabBarContollerNode.swift new file mode 100644 index 0000000000..69cff6073f --- /dev/null +++ b/submodules/Display/Display/TabBarContollerNode.swift @@ -0,0 +1,98 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public enum ToolbarActionOption { + case left + case right + case middle +} + +final class TabBarControllerNode: ASDisplayNode { + private var theme: TabBarControllerTheme + let tabBarNode: TabBarNode + private let navigationBar: NavigationBar? + private var toolbarNode: ToolbarNode? + private let toolbarActionSelected: (ToolbarActionOption) -> Void + + var currentControllerNode: ASDisplayNode? { + didSet { + oldValue?.removeFromSupernode() + + if let currentControllerNode = self.currentControllerNode { + self.insertSubnode(currentControllerNode, at: 0) + } + } + } + + init(theme: TabBarControllerTheme, navigationBar: NavigationBar?, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void) { + self.theme = theme + self.navigationBar = navigationBar + self.tabBarNode = TabBarNode(theme: theme, itemSelected: itemSelected) + self.toolbarActionSelected = toolbarActionSelected + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.backgroundColor = theme.backgroundColor + + self.addSubnode(self.tabBarNode) + } + + func updateTheme(_ theme: TabBarControllerTheme) { + self.theme = theme + self.backgroundColor = theme.backgroundColor + + self.tabBarNode.updateTheme(theme) + self.toolbarNode?.updateTheme(theme) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) { + var tabBarHeight: CGFloat + var options: ContainerViewLayoutInsetOptions = [] + if layout.metrics.widthClass == .regular { + options.insert(.input) + } + let bottomInset: CGFloat = layout.insets(options: options).bottom + if !layout.safeInsets.left.isZero { + tabBarHeight = 34.0 + bottomInset + } else { + tabBarHeight = 49.0 + bottomInset + } + + let tabBarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) + + transition.updateFrame(node: self.tabBarNode, frame: tabBarFrame) + self.tabBarNode.updateLayout(size: layout.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: bottomInset, transition: transition) + + if let toolbar = toolbar { + if let toolbarNode = self.toolbarNode { + transition.updateFrame(node: toolbarNode, frame: tabBarFrame) + toolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: bottomInset, toolbar: toolbar, transition: transition) + } else { + let toolbarNode = ToolbarNode(theme: self.theme, left: { [weak self] in + self?.toolbarActionSelected(.left) + }, right: { [weak self] in + self?.toolbarActionSelected(.right) + }, middle: { [weak self] in + self?.toolbarActionSelected(.middle) + }) + toolbarNode.frame = tabBarFrame + toolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate) + self.addSubnode(toolbarNode) + self.toolbarNode = toolbarNode + if transition.isAnimated { + toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } else if let toolbarNode = self.toolbarNode { + self.toolbarNode = nil + transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in + toolbarNode?.removeFromSupernode() + }) + } + } +} diff --git a/submodules/Display/Display/TabBarController.swift b/submodules/Display/Display/TabBarController.swift new file mode 100644 index 0000000000..900e286a55 --- /dev/null +++ b/submodules/Display/Display/TabBarController.swift @@ -0,0 +1,354 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +public final class TabBarControllerTheme { + public let backgroundColor: UIColor + public let tabBarBackgroundColor: UIColor + public let tabBarSeparatorColor: UIColor + public let tabBarTextColor: UIColor + public let tabBarSelectedTextColor: UIColor + public let tabBarBadgeBackgroundColor: UIColor + public let tabBarBadgeStrokeColor: UIColor + public let tabBarBadgeTextColor: UIColor + + public init(backgroundColor: UIColor, tabBarBackgroundColor: UIColor, tabBarSeparatorColor: UIColor, tabBarTextColor: UIColor, tabBarSelectedTextColor: UIColor, tabBarBadgeBackgroundColor: UIColor, tabBarBadgeStrokeColor: UIColor, tabBarBadgeTextColor: UIColor) { + self.backgroundColor = backgroundColor + self.tabBarBackgroundColor = tabBarBackgroundColor + self.tabBarSeparatorColor = tabBarSeparatorColor + self.tabBarTextColor = tabBarTextColor + self.tabBarSelectedTextColor = tabBarSelectedTextColor + self.tabBarBadgeBackgroundColor = tabBarBadgeBackgroundColor + self.tabBarBadgeStrokeColor = tabBarBadgeStrokeColor + self.tabBarBadgeTextColor = tabBarBadgeTextColor + } +} + +public final class TabBarItemInfo: NSObject { + public let previewing: Bool + + public init(previewing: Bool) { + self.previewing = previewing + + super.init() + } + + override public func isEqual(_ object: Any?) -> Bool { + if let object = object as? TabBarItemInfo { + if self.previewing != object.previewing { + return false + } + return true + } else { + return false + } + } + + public static func ==(lhs: TabBarItemInfo, rhs: TabBarItemInfo) -> Bool { + if lhs.previewing != rhs.previewing { + return false + } + return true + } +} + +public enum TabBarContainedControllerPresentationUpdate { + case dismiss + case present + case progress(CGFloat) +} + +public protocol TabBarContainedController { + func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode]) + func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) +} + +open class TabBarController: ViewController { + private var validLayout: ContainerViewLayout? + + private var tabBarControllerNode: TabBarControllerNode { + get { + return super.displayNode as! TabBarControllerNode + } + } + + public private(set) var controllers: [ViewController] = [] + + private let _ready = Promise() + override open var ready: Promise { + return self._ready + } + + private var _selectedIndex: Int? + public var selectedIndex: Int { + get { + if let _selectedIndex = self._selectedIndex { + return _selectedIndex + } else { + return 0 + } + } set(value) { + let index = max(0, min(self.controllers.count - 1, value)) + if _selectedIndex != index { + _selectedIndex = index + + self.updateSelectedIndex() + } + } + } + + var currentController: ViewController? + + private let pendingControllerDisposable = MetaDisposable() + + private var theme: TabBarControllerTheme + + public init(navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) { + self.theme = theme + + super.init(navigationBarPresentationData: navigationBarPresentationData) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.pendingControllerDisposable.dispose() + } + + public func updateTheme(navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) { + self.navigationBar?.updatePresentationData(navigationBarPresentationData) + if self.theme !== theme { + self.theme = theme + if self.isNodeLoaded { + self.tabBarControllerNode.updateTheme(theme) + } + } + } + + private var debugTapCounter: (Double, Int) = (0.0, 0) + + public func sourceNodesForController(at index: Int) -> [ASDisplayNode]? { + return self.tabBarControllerNode.tabBarNode.sourceNodesForController(at: index) + } + + override open func loadDisplayNode() { + self.displayNode = TabBarControllerNode(theme: self.theme, navigationBar: self.navigationBar, itemSelected: { [weak self] index, longTap, itemNodes in + if let strongSelf = self { + if longTap, let controller = strongSelf.controllers[index] as? TabBarContainedController { + controller.presentTabBarPreviewingController(sourceNodes: itemNodes) + return + } + + if strongSelf.selectedIndex == index { + let timestamp = CACurrentMediaTime() + if strongSelf.debugTapCounter.0 < timestamp - 0.4 { + strongSelf.debugTapCounter.0 = timestamp + strongSelf.debugTapCounter.1 = 0 + } + + if strongSelf.debugTapCounter.0 >= timestamp - 0.4 { + strongSelf.debugTapCounter.0 = timestamp + strongSelf.debugTapCounter.1 += 1 + } + + if strongSelf.debugTapCounter.1 >= 10 { + strongSelf.debugTapCounter.1 = 0 + + strongSelf.controllers[index].tabBarItemDebugTapAction?() + } + } + if let validLayout = strongSelf.validLayout { + strongSelf.controllers[index].containerLayoutUpdated(validLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate) + } + let startTime = CFAbsoluteTimeGetCurrent() + strongSelf.pendingControllerDisposable.set((strongSelf.controllers[index].ready.get() + |> deliverOnMainQueue).start(next: { _ in + if let strongSelf = self { + let readyTime = CFAbsoluteTimeGetCurrent() - startTime + if readyTime > 0.5 { + print("TabBarController: controller took \(readyTime) to become ready") + } + + if strongSelf.selectedIndex == index { + if let controller = strongSelf.currentController { + if longTap { + controller.longTapWithTabBar?() + } else { + controller.scrollToTopWithTabBar?() + } + } + } else { + strongSelf.selectedIndex = index + } + } + })) + } + }, toolbarActionSelected: { [weak self] action in + self?.currentController?.toolbarActionSelected(action: action) + }) + + self.updateSelectedIndex() + self.displayNodeDidLoad() + } + + private func updateSelectedIndex() { + if !self.isNodeLoaded { + return + } + + self.tabBarControllerNode.tabBarNode.selectedIndex = self.selectedIndex + + if let currentController = self.currentController { + currentController.willMove(toParentViewController: nil) + self.tabBarControllerNode.currentControllerNode = nil + currentController.removeFromParentViewController() + currentController.didMove(toParentViewController: nil) + + self.currentController = nil + } + + if let _selectedIndex = self._selectedIndex, _selectedIndex < self.controllers.count { + self.currentController = self.controllers[_selectedIndex] + } + + var displayNavigationBar = false + if let currentController = self.currentController { + currentController.willMove(toParentViewController: self) + self.tabBarControllerNode.currentControllerNode = currentController.displayNode + currentController.navigationBar?.isHidden = true + self.addChildViewController(currentController) + currentController.didMove(toParentViewController: self) + + currentController.navigationBar?.layoutSuspended = true + currentController.navigationItem.setTarget(self.navigationItem) + displayNavigationBar = currentController.displayNavigationBar + self.navigationBar?.setContentNode(currentController.navigationBar?.contentNode, animated: false) + currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) + self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle + } else { + self.navigationItem.title = nil + self.navigationItem.leftBarButtonItem = nil + self.navigationItem.rightBarButtonItem = nil + self.navigationItem.titleView = nil + self.navigationItem.backBarButtonItem = nil + self.navigationBar?.setContentNode(nil, animated: false) + displayNavigationBar = false + } + if self.displayNavigationBar != displayNavigationBar { + self.setDisplayNavigationBar(displayNavigationBar) + } + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: .immediate) + } + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.validLayout = layout + + self.tabBarControllerNode.containerLayoutUpdated(layout, toolbar: self.currentController?.toolbar, transition: transition) + + if let currentController = self.currentController { + currentController.view.frame = CGRect(origin: CGPoint(), size: layout.size) + + currentController.containerLayoutUpdated(layout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: transition) + } + } + + override open func navigationStackConfigurationUpdated(next: [ViewController]) { + super.navigationStackConfigurationUpdated(next: next) + for controller in self.controllers { + controller.navigationStackConfigurationUpdated(next: next) + } + } + + override open func viewWillDisappear(_ animated: Bool) { + if let currentController = self.currentController { + currentController.viewWillDisappear(animated) + } + } + + override open func viewWillAppear(_ animated: Bool) { + if let currentController = self.currentController { + currentController.viewWillAppear(animated) + } + } + + override open func viewDidAppear(_ animated: Bool) { + if let currentController = self.currentController { + currentController.viewDidAppear(animated) + } + } + + override open func viewDidDisappear(_ animated: Bool) { + if let currentController = self.currentController { + currentController.viewDidDisappear(animated) + } + } + + public func setControllers(_ controllers: [ViewController], selectedIndex: Int?) { + var updatedSelectedIndex: Int? = selectedIndex + if updatedSelectedIndex == nil, let selectedIndex = self._selectedIndex, selectedIndex < self.controllers.count { + if let index = controllers.index(where: { $0 === self.controllers[selectedIndex] }) { + updatedSelectedIndex = index + } else { + updatedSelectedIndex = 0 + } + } + self.controllers = controllers + self.tabBarControllerNode.tabBarNode.tabBarItems = self.controllers.map({ $0.tabBarItem }) + + let signals = combineLatest(self.controllers.map({ $0.tabBarItem }).map { tabBarItem -> Signal in + if let tabBarItem = tabBarItem, tabBarItem.image == nil { + return Signal { [weak tabBarItem] subscriber in + let index = tabBarItem?.addSetImageListener({ image in + if image != nil { + subscriber.putNext(true) + subscriber.putCompletion() + } + }) + return ActionDisposable { + Queue.mainQueue().async { + if let index = index { + tabBarItem?.removeSetImageListener(index) + } + } + } + } + |> runOn(.mainQueue()) + } else { + return .single(true) + } + }) + |> map { items -> Bool in + for item in items { + if !item { + return false + } + } + return true + } + |> filter { $0 } + |> take(1) + + let allReady = signals + |> deliverOnMainQueue + |> mapToSignal { _ -> Signal in + // wait for tab bar items to be applied + return .single(true) + |> delay(0.0, queue: Queue.mainQueue()) + } + + self._ready.set(allReady) + + if let updatedSelectedIndex = updatedSelectedIndex { + self.selectedIndex = updatedSelectedIndex + self.updateSelectedIndex() + } + } +} diff --git a/submodules/Display/Display/TabBarNode.swift b/submodules/Display/Display/TabBarNode.swift new file mode 100644 index 0000000000..1e112e5baf --- /dev/null +++ b/submodules/Display/Display/TabBarNode.swift @@ -0,0 +1,478 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +#if BUCK +import DisplayPrivate +#endif + +private let separatorHeight: CGFloat = 1.0 / UIScreen.main.scale +private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: UIColor, tintColor: UIColor, horizontal: Bool, imageMode: Bool) -> (UIImage, CGFloat) { + let font = horizontal ? Font.regular(13.0) : Font.medium(10.0) + let titleSize = (title as NSString).boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], attributes: [NSAttributedStringKey.font: font], context: nil).size + + let imageSize: CGSize + if let image = image { + if horizontal { + let factor: CGFloat = 0.8 + imageSize = CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)) + } else { + imageSize = image.size + } + } else { + imageSize = CGSize() + } + + let horizontalSpacing: CGFloat = 4.0 + + let size: CGSize + let contentWidth: CGFloat + if horizontal { + size = CGSize(width: max(1.0, ceil(titleSize.width) + horizontalSpacing + imageSize.width), height: 34.0) + contentWidth = size.width + } else { + size = CGSize(width: max(1.0, max(ceil(titleSize.width), imageSize.width), 1.0), height: 45.0) + contentWidth = imageSize.width + } + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + if let context = UIGraphicsGetCurrentContext() { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + if let image = image, imageMode { + if horizontal { + let imageRect = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - imageSize.height) / 2.0)), size: imageSize) + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + if image.renderingMode == .alwaysOriginal { + context.draw(image.cgImage!, in: imageRect) + } else { + context.clip(to: imageRect, mask: image.cgImage!) + context.setFillColor(tintColor.cgColor) + context.fill(imageRect) + } + context.restoreGState() + } else { + let imageRect = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - imageSize.width) / 2.0), y: 1.0), size: imageSize) + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + if image.renderingMode == .alwaysOriginal { + context.draw(image.cgImage!, in: imageRect) + } else { + context.clip(to: imageRect, mask: image.cgImage!) + context.setFillColor(tintColor.cgColor) + context.fill(imageRect) + } + context.restoreGState() + } + } + } + + if !imageMode { + if horizontal { + (title as NSString).draw(at: CGPoint(x: imageSize.width + horizontalSpacing, y: floor((size.height - titleSize.height) / 2.0) - 2.0), withAttributes: [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: tintColor]) + } else { + (title as NSString).draw(at: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 2.0), withAttributes: [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: tintColor]) + } + } + + let resultImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return (resultImage!, contentWidth) +} + +private let badgeFont = Font.regular(13.0) + +private final class TabBarItemNode: ASDisplayNode { + let imageNode: ASImageNode + let textImageNode: ASImageNode + var contentWidth: CGFloat? + + override init() { + self.imageNode = ASImageNode() + self.imageNode.displayWithoutProcessing = true + self.imageNode.displaysAsynchronously = false + self.imageNode.isAccessibilityElement = false + self.textImageNode = ASImageNode() + self.textImageNode.displayWithoutProcessing = true + self.textImageNode.displaysAsynchronously = false + self.textImageNode.isAccessibilityElement = false + + super.init() + + self.isAccessibilityElement = true + + self.addSubnode(self.textImageNode) + self.addSubnode(self.imageNode) + } +} + +private final class TabBarNodeContainer { + let item: UITabBarItem + let updateBadgeListenerIndex: Int + let updateTitleListenerIndex: Int + let updateImageListenerIndex: Int + let updateSelectedImageListenerIndex: Int + + let imageNode: TabBarItemNode + let badgeContainerNode: ASDisplayNode + let badgeBackgroundNode: ASImageNode + let badgeTextNode: ImmediateTextNode + + var badgeValue: String? + var appliedBadgeValue: String? + + var titleValue: String? + var appliedTitleValue: String? + + var imageValue: UIImage? + var appliedImageValue: UIImage? + + var selectedImageValue: UIImage? + var appliedSelectedImageValue: UIImage? + + init(item: UITabBarItem, imageNode: TabBarItemNode, updateBadge: @escaping (String) -> Void, updateTitle: @escaping (String, Bool) -> Void, updateImage: @escaping (UIImage?) -> Void, updateSelectedImage: @escaping (UIImage?) -> Void) { + self.item = item + + self.imageNode = imageNode + self.imageNode.isAccessibilityElement = true + self.imageNode.accessibilityTraits = UIAccessibilityTraitButton + + self.badgeContainerNode = ASDisplayNode() + self.badgeContainerNode.isUserInteractionEnabled = false + self.badgeContainerNode.isAccessibilityElement = false + + self.badgeBackgroundNode = ASImageNode() + self.badgeBackgroundNode.isUserInteractionEnabled = false + self.badgeBackgroundNode.displayWithoutProcessing = true + self.badgeBackgroundNode.displaysAsynchronously = false + self.badgeBackgroundNode.isAccessibilityElement = false + + self.badgeTextNode = ImmediateTextNode() + self.badgeTextNode.maximumNumberOfLines = 1 + self.badgeTextNode.isUserInteractionEnabled = false + self.badgeTextNode.displaysAsynchronously = false + self.badgeTextNode.isAccessibilityElement = false + + self.badgeContainerNode.addSubnode(self.badgeBackgroundNode) + self.badgeContainerNode.addSubnode(self.badgeTextNode) + + self.badgeValue = item.badgeValue ?? "" + self.updateBadgeListenerIndex = UITabBarItem_addSetBadgeListener(item, { value in + updateBadge(value ?? "") + }) + + self.titleValue = item.title + self.updateTitleListenerIndex = item.addSetTitleListener { value, animated in + updateTitle(value ?? "", animated) + } + + self.imageValue = item.image + self.updateImageListenerIndex = item.addSetImageListener { value in + updateImage(value) + } + + self.selectedImageValue = item.selectedImage + self.updateSelectedImageListenerIndex = item.addSetSelectedImageListener { value in + updateSelectedImage(value) + } + } + + deinit { + item.removeSetBadgeListener(self.updateBadgeListenerIndex) + item.removeSetTitleListener(self.updateTitleListenerIndex) + item.removeSetImageListener(self.updateImageListenerIndex) + item.removeSetSelectedImageListener(self.updateSelectedImageListenerIndex) + } +} + +class TabBarNode: ASDisplayNode { + var tabBarItems: [UITabBarItem] = [] { + didSet { + self.reloadTabBarItems() + } + } + + var selectedIndex: Int? { + didSet { + if self.selectedIndex != oldValue { + if let oldValue = oldValue { + self.updateNodeImage(oldValue, layout: true) + } + + if let selectedIndex = self.selectedIndex { + self.updateNodeImage(selectedIndex, layout: true) + } + } + } + } + + private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void + + private var theme: TabBarControllerTheme + private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? + private var horizontal: Bool = false + + private var badgeImage: UIImage + + let separatorNode: ASDisplayNode + private var tabBarNodeContainers: [TabBarNodeContainer] = [] + + init(theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void) { + self.itemSelected = itemSelected + self.theme = theme + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.tabBarSeparatorColor + self.separatorNode.isOpaque = true + self.separatorNode.isLayerBacked = true + + self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.tabBarBadgeBackgroundColor, strokeColor: theme.tabBarBadgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil)! + + super.init() + + self.isAccessibilityContainer = false + + self.isOpaque = true + self.backgroundColor = theme.tabBarBackgroundColor + + self.addSubnode(self.separatorNode) + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(TabBarTapRecognizer(tap: { [weak self] point in + self?.tapped(at: point, longTap: false) + }, longTap: { [weak self] point in + self?.tapped(at: point, longTap: true) + })) + } + + func updateTheme(_ theme: TabBarControllerTheme) { + if self.theme !== theme { + self.theme = theme + + self.separatorNode.backgroundColor = theme.tabBarSeparatorColor + self.backgroundColor = theme.tabBarBackgroundColor + + self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.tabBarBadgeBackgroundColor, strokeColor: theme.tabBarBadgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil)! + for container in self.tabBarNodeContainers { + if let attributedText = container.badgeTextNode.attributedText, !attributedText.string.isEmpty { + container.badgeTextNode.attributedText = NSAttributedString(string: attributedText.string, font: badgeFont, textColor: self.theme.tabBarBadgeTextColor) + } + } + + for i in 0 ..< self.tabBarItems.count { + self.updateNodeImage(i, layout: false) + + self.tabBarNodeContainers[i].badgeBackgroundNode.image = self.badgeImage + } + + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, bottomInset: validLayout.3, transition: .immediate) + } + } + } + + func sourceNodesForController(at index: Int) -> [ASDisplayNode]? { + let container = self.tabBarNodeContainers[index] + return [container.imageNode.imageNode, container.imageNode.textImageNode, container.badgeContainerNode] + } + + private func reloadTabBarItems() { + for node in self.tabBarNodeContainers { + node.imageNode.removeFromSupernode() + node.badgeContainerNode.removeFromSupernode() + } + + var tabBarNodeContainers: [TabBarNodeContainer] = [] + for i in 0 ..< self.tabBarItems.count { + let item = self.tabBarItems[i] + let node = TabBarItemNode() + node.isUserInteractionEnabled = false + let container = TabBarNodeContainer(item: item, imageNode: node, updateBadge: { [weak self] value in + self?.updateNodeBadge(i, value: value) + }, updateTitle: { [weak self] _, _ in + self?.updateNodeImage(i, layout: true) + }, updateImage: { [weak self] _ in + self?.updateNodeImage(i, layout: true) + }, updateSelectedImage: { [weak self] _ in + self?.updateNodeImage(i, layout: true) + }) + if let selectedIndex = self.selectedIndex, selectedIndex == i { + let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false) + let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: true) + node.textImageNode.image = textImage + node.imageNode.image = image + node.accessibilityLabel = item.title + node.contentWidth = max(contentWidth, imageContentWidth) + } else { + let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false) + let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: true) + node.textImageNode.image = textImage + node.accessibilityLabel = item.title + node.imageNode.image = image + node.contentWidth = max(contentWidth, imageContentWidth) + } + container.badgeBackgroundNode.image = self.badgeImage + tabBarNodeContainers.append(container) + self.addSubnode(node) + } + + for container in tabBarNodeContainers { + self.addSubnode(container.badgeContainerNode) + } + + self.tabBarNodeContainers = tabBarNodeContainers + + self.setNeedsLayout() + } + + private func updateNodeImage(_ index: Int, layout: Bool) { + if index < self.tabBarNodeContainers.count && index < self.tabBarItems.count { + let node = self.tabBarNodeContainers[index].imageNode + let item = self.tabBarItems[index] + + let previousImageSize = node.imageNode.image?.size ?? CGSize() + let previousTextImageSize = node.textImageNode.image?.size ?? CGSize() + if let selectedIndex = self.selectedIndex, selectedIndex == index { + let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false) + let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: true) + node.textImageNode.image = textImage + node.accessibilityLabel = item.title + node.imageNode.image = image + node.contentWidth = max(contentWidth, imageContentWidth) + } else { + let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false) + let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: true) + node.textImageNode.image = textImage + node.accessibilityLabel = item.title + node.imageNode.image = image + node.contentWidth = max(contentWidth, imageContentWidth) + } + + let updatedImageSize = node.imageNode.image?.size ?? CGSize() + let updatedTextImageSize = node.textImageNode.image?.size ?? CGSize() + + if previousImageSize != updatedImageSize || previousTextImageSize != updatedTextImageSize { + if let validLayout = self.validLayout, layout { + self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, bottomInset: validLayout.3, transition: .immediate) + } + } + } + } + + private func updateNodeBadge(_ index: Int, value: String) { + self.tabBarNodeContainers[index].badgeValue = value + if self.tabBarNodeContainers[index].badgeValue != self.tabBarNodeContainers[index].appliedBadgeValue { + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, bottomInset: validLayout.3, transition: .immediate) + } + } + } + + private func updateNodeTitle(_ index: Int, value: String) { + self.tabBarNodeContainers[index].titleValue = value + if self.tabBarNodeContainers[index].titleValue != self.tabBarNodeContainers[index].appliedTitleValue { + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, bottomInset: validLayout.3, transition: .immediate) + } + } + } + + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, leftInset, rightInset, bottomInset) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight), size: CGSize(width: size.width, height: separatorHeight))) + + let horizontal = !leftInset.isZero + if self.horizontal != horizontal { + self.horizontal = horizontal + for i in 0 ..< self.tabBarItems.count { + self.updateNodeImage(i, layout: false) + } + } + + if self.tabBarNodeContainers.count != 0 { + let distanceBetweenNodes = size.width / CGFloat(self.tabBarNodeContainers.count) + + let internalWidth = distanceBetweenNodes * CGFloat(self.tabBarNodeContainers.count - 1) + let leftNodeOriginX = (size.width - internalWidth) / 2.0 + + for i in 0 ..< self.tabBarNodeContainers.count { + let container = self.tabBarNodeContainers[i] + let node = container.imageNode + let nodeSize = node.textImageNode.image?.size ?? CGSize() + + let originX = floor(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - nodeSize.width / 2.0) + let nodeFrame = CGRect(origin: CGPoint(x: originX, y: 4.0), size: nodeSize) + transition.updateFrame(node: node, frame: nodeFrame) + node.imageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) + node.textImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) + + if container.badgeValue != container.appliedBadgeValue { + container.appliedBadgeValue = container.badgeValue + if let badgeValue = container.badgeValue, !badgeValue.isEmpty { + container.badgeTextNode.attributedText = NSAttributedString(string: badgeValue, font: badgeFont, textColor: self.theme.tabBarBadgeTextColor) + container.badgeContainerNode.isHidden = false + } else { + container.badgeContainerNode.isHidden = true + } + } + + if !container.badgeContainerNode.isHidden { + let hasSingleLetterValue = container.badgeTextNode.attributedText?.string.count == 1 + let badgeSize = container.badgeTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) + let backgroundSize = CGSize(width: hasSingleLetterValue ? 18.0 : max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) + let backgroundFrame: CGRect + if horizontal { + backgroundFrame = CGRect(origin: CGPoint(x: originX + 8.0, y: 2.0), size: backgroundSize) + } else { + let contentWidth = node.contentWidth ?? node.frame.width + backgroundFrame = CGRect(origin: CGPoint(x: floor(originX + node.frame.width / 2.0) - 1.0 + contentWidth - backgroundSize.width - 1.0, y: 2.0), size: backgroundSize) + } + transition.updateFrame(node: container.badgeContainerNode, frame: backgroundFrame) + container.badgeBackgroundNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size) + let scaleFactor: CGFloat = horizontal ? 0.8 : 1.0 + container.badgeContainerNode.subnodeTransform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0) + + container.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.size.width - badgeSize.width) / 2.0), y: 1.0), size: badgeSize) + } + } + } + } + + private func tapped(at location: CGPoint, longTap: Bool) { + if let bottomInset = self.validLayout?.3 { + if location.y > self.bounds.size.height - bottomInset { + return + } + var closestNode: (Int, CGFloat)? + + for i in 0 ..< self.tabBarNodeContainers.count { + let node = self.tabBarNodeContainers[i].imageNode + let distance = abs(location.x - node.position.x) + if let previousClosestNode = closestNode { + if previousClosestNode.1 > distance { + closestNode = (i, distance) + } + } else { + closestNode = (i, distance) + } + } + + if let closestNode = closestNode { + let container = self.tabBarNodeContainers[closestNode.0] + self.itemSelected(closestNode.0, longTap, [container.imageNode.imageNode, container.imageNode.textImageNode, container.badgeContainerNode]) + } + } + } +} diff --git a/submodules/Display/Display/TabBarTapRecognizer.swift b/submodules/Display/Display/TabBarTapRecognizer.swift new file mode 100644 index 0000000000..616ced31e5 --- /dev/null +++ b/submodules/Display/Display/TabBarTapRecognizer.swift @@ -0,0 +1,83 @@ +import Foundation +import UIKit +import SwiftSignalKit + +final class TabBarTapRecognizer: UIGestureRecognizer { + private let tap: (CGPoint) -> Void + private let longTap: (CGPoint) -> Void + + private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + + init(tap: @escaping (CGPoint) -> Void, longTap: @escaping (CGPoint) -> Void) { + self.tap = tap + self.longTap = longTap + + super.init(target: nil, action: nil) + } + + override func reset() { + super.reset() + + self.initialLocation = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.initialLocation == nil { + self.initialLocation = touches.first?.location(in: self.view) + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + if let initialLocation = strongSelf.initialLocation { + strongSelf.initialLocation = nil + strongSelf.longTap(initialLocation) + strongSelf.state = .ended + } + }, queue: Queue.mainQueue()) + self.longTapTimer?.invalidate() + self.longTapTimer = longTapTimer + longTapTimer.start() + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if let initialLocation = self.initialLocation { + self.initialLocation = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + self.tap(initialLocation) + self.state = .ended + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + let deltaX = initialLocation.x - location.x + let deltaY = initialLocation.y - location.y + if deltaX * deltaX + deltaY * deltaY > 4.0 { + self.longTapTimer?.invalidate() + self.longTapTimer = nil + self.initialLocation = nil + self.state = .failed + } + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + self.state = .failed + } +} diff --git a/submodules/Display/Display/TapLongTapOrDoubleTapGestureRecognizer.swift b/submodules/Display/Display/TapLongTapOrDoubleTapGestureRecognizer.swift new file mode 100644 index 0000000000..8f6771b93f --- /dev/null +++ b/submodules/Display/Display/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -0,0 +1,257 @@ +import Foundation +import UIKit +import UIKit.UIGestureRecognizerSubclass + +private class TapLongTapOrDoubleTapGestureRecognizerTimerTarget: NSObject { + weak var target: TapLongTapOrDoubleTapGestureRecognizer? + + init(target: TapLongTapOrDoubleTapGestureRecognizer) { + self.target = target + + super.init() + } + + @objc func longTapEvent() { + self.target?.longTapEvent() + } + + @objc func tapEvent() { + self.target?.tapEvent() + } + + @objc func holdEvent() { + self.target?.holdEvent() + } +} + +enum TapLongTapOrDoubleTapGesture { + case tap + case doubleTap + case longTap + case hold +} + +enum TapLongTapOrDoubleTapGestureRecognizerAction { + case waitForDoubleTap + case waitForSingleTap + case waitForHold(timeout: Double, acceptTap: Bool) + case fail +} + +public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + private var touchLocationAndTimestamp: (CGPoint, Double)? + private var touchCount: Int = 0 + private var tapCount: Int = 0 + + private var timer: Foundation.Timer? + private(set) var lastRecognizedGestureAndLocation: (TapLongTapOrDoubleTapGesture, CGPoint)? + + var tapActionAtPoint: ((CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction)? + var highlight: ((CGPoint?) -> Void)? + + var hapticFeedback: HapticFeedback? + + private var highlightPoint: CGPoint? + + override public init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delegate = self + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UIPanGestureRecognizer { + return false + } + return false + } + + override public func reset() { + self.timer?.invalidate() + self.timer = nil + self.touchLocationAndTimestamp = nil + self.tapCount = 0 + self.touchCount = 0 + self.hapticFeedback = nil + + if self.highlightPoint != nil { + self.highlightPoint = nil + self.highlight?(nil) + } + + super.reset() + } + + fileprivate func longTapEvent() { + self.timer?.invalidate() + self.timer = nil + if let (location, _) = self.touchLocationAndTimestamp { + self.lastRecognizedGestureAndLocation = (.longTap, location) + } else { + self.lastRecognizedGestureAndLocation = nil + } + self.state = .ended + } + + fileprivate func tapEvent() { + self.timer?.invalidate() + self.timer = nil + if let (location, _) = self.touchLocationAndTimestamp { + self.lastRecognizedGestureAndLocation = (.tap, location) + } else { + self.lastRecognizedGestureAndLocation = nil + } + self.state = .ended + } + + fileprivate func holdEvent() { + self.timer?.invalidate() + self.timer = nil + if let (location, _) = self.touchLocationAndTimestamp { + self.hapticFeedback?.tap() + self.lastRecognizedGestureAndLocation = (.hold, location) + } else { + self.lastRecognizedGestureAndLocation = nil + } + self.state = .began + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + self.lastRecognizedGestureAndLocation = nil + + super.touchesBegan(touches, with: event) + + self.touchCount += touches.count + + if let touch = touches.first { + let touchLocation = touch.location(in: self.view) + + if self.highlightPoint != touchLocation { + self.highlightPoint = touchLocation + self.highlight?(touchLocation) + } + + if let hitResult = self.view?.hitTest(touch.location(in: self.view), with: event), let _ = hitResult as? UIButton { + self.state = .failed + return + } + + self.tapCount += 1 + if self.tapCount == 2 && self.touchCount == 1 { + self.timer?.invalidate() + self.timer = nil + self.lastRecognizedGestureAndLocation = (.doubleTap, self.location(in: self.view)) + self.state = .ended + } else { + let touchLocationAndTimestamp = (touch.location(in: self.view), CACurrentMediaTime()) + self.touchLocationAndTimestamp = touchLocationAndTimestamp + + var tapAction: TapLongTapOrDoubleTapGestureRecognizerAction = .waitForDoubleTap + if let tapActionAtPoint = self.tapActionAtPoint { + tapAction = tapActionAtPoint(touchLocationAndTimestamp.0) + } + + switch tapAction { + case .waitForSingleTap, .waitForDoubleTap: + self.timer?.invalidate() + let timer = Timer(timeInterval: 0.3, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.longTapEvent), userInfo: nil, repeats: false) + self.timer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + case let .waitForHold(timeout, _): + self.hapticFeedback = HapticFeedback() + self.hapticFeedback?.prepareTap() + let timer = Timer(timeInterval: timeout, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.holdEvent), userInfo: nil, repeats: false) + self.timer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + case .fail: + self.state = .failed + } + } + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + guard let touch = touches.first else { + return + } + + if let (gesture, _) = self.lastRecognizedGestureAndLocation, case .hold = gesture { + let location = touch.location(in: self.view) + self.lastRecognizedGestureAndLocation = (.hold, location) + self.state = .changed + return + } + + if let touch = touches.first, let (touchLocation, _) = self.touchLocationAndTimestamp { + let location = touch.location(in: self.view) + let distance = CGPoint(x: location.x - touchLocation.x, y: location.y - touchLocation.y) + if distance.x * distance.x + distance.y * distance.y > 4.0 { + self.state = .cancelled + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.touchCount -= touches.count + + if self.highlightPoint != nil { + self.highlightPoint = nil + self.highlight?(nil) + } + + self.timer?.invalidate() + + if let (gesture, location) = self.lastRecognizedGestureAndLocation, case .hold = gesture { + self.lastRecognizedGestureAndLocation = (.hold, location) + self.state = .ended + return + } + + if self.tapCount == 1 { + var tapAction: TapLongTapOrDoubleTapGestureRecognizerAction = .waitForDoubleTap + if let tapActionAtPoint = self.tapActionAtPoint, let (touchLocation, _) = self.touchLocationAndTimestamp { + tapAction = tapActionAtPoint(touchLocation) + } + + switch tapAction { + case .waitForSingleTap: + if let (touchLocation, _) = self.touchLocationAndTimestamp { + self.lastRecognizedGestureAndLocation = (.tap, touchLocation) + } + self.state = .ended + case .waitForDoubleTap: + self.state = .began + let timer = Timer(timeInterval: 0.2, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.tapEvent), userInfo: nil, repeats: false) + self.timer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + case let .waitForHold(_, acceptTap): + if let (touchLocation, _) = self.touchLocationAndTimestamp, acceptTap { + if self.state != .began { + self.lastRecognizedGestureAndLocation = (.tap, touchLocation) + self.state = .began + } + } + self.state = .ended + case .fail: + self.state = .failed + } + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.touchCount -= touches.count + + if self.highlightPoint != nil { + self.highlightPoint = nil + self.highlight?(nil) + } + + self.state = .cancelled + } +} diff --git a/submodules/Display/Display/TextAlertController.swift b/submodules/Display/Display/TextAlertController.swift new file mode 100644 index 0000000000..7b4d6cc62a --- /dev/null +++ b/submodules/Display/Display/TextAlertController.swift @@ -0,0 +1,369 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +private let alertWidth: CGFloat = 270.0 + +public enum TextAlertActionType { + case genericAction + case defaultAction + case destructiveAction +} + +public struct TextAlertAction { + public let type: TextAlertActionType + public let title: String + public let action: () -> Void + + public init(type: TextAlertActionType, title: String, action: @escaping () -> Void) { + self.type = type + self.title = title + self.action = action + } +} + +public final class TextAlertContentActionNode: HighlightableButtonNode { + private var theme: AlertControllerTheme + let action: TextAlertAction + + private let backgroundNode: ASDisplayNode + + public init(theme: AlertControllerTheme, action: TextAlertAction) { + self.theme = theme + self.action = action + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.alpha = 0.0 + + super.init() + + self.titleNode.maximumNumberOfLines = 2 + + self.highligthedChanged = { [weak self] value in + if let strongSelf = self { + if value { + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 1.0 + } else if !strongSelf.backgroundNode.alpha.isZero { + strongSelf.backgroundNode.alpha = 0.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + + self.updateTheme(theme) + } + + public var actionEnabled: Bool = true { + didSet { + self.isUserInteractionEnabled = self.actionEnabled + self.updateTitle() + } + } + + public func updateTheme(_ theme: AlertControllerTheme) { + self.theme = theme + self.backgroundNode.backgroundColor = theme.highlightedItemColor + self.updateTitle() + } + + private func updateTitle() { + var font = Font.regular(17.0) + var color: UIColor + switch self.action.type { + case .defaultAction, .genericAction: + color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor + case .destructiveAction: + color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor + } + switch self.action.type { + case .defaultAction: + font = Font.semibold(17.0) + case .destructiveAction, .genericAction: + break + } + self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: []) + } + + override public func didLoad() { + super.didLoad() + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc func pressed() { + self.action.action() + } + + override public func layout() { + super.layout() + + self.backgroundNode.frame = self.bounds + } +} + +public enum TextAlertContentActionLayout { + case horizontal + case vertical +} + +public final class TextAlertContentNode: AlertContentNode { + private var theme: AlertControllerTheme + private let actionLayout: TextAlertContentActionLayout + + private let titleNode: ASTextNode? + private let textNode: ImmediateTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + public var textAttributeAction: (NSAttributedStringKey, (Any) -> Void)? { + didSet { + if let (attribute, textAttributeAction) = self.textAttributeAction { + self.textNode.highlightAttributeAction = { attributes in + if let _ = attributes[attribute] { + return attribute + } else { + return nil + } + } + self.textNode.tapAttributeAction = { attributes in + if let value = attributes[attribute] { + textAttributeAction(value) + } + } + self.textNode.linkHighlightColor = self.theme.accentColor.withAlphaComponent(0.5) + } else { + self.textNode.highlightAttributeAction = nil + self.textNode.tapAttributeAction = nil + } + } + } + + public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout) { + self.theme = theme + self.actionLayout = actionLayout + if let title = title { + let titleNode = ASTextNode() + titleNode.attributedText = title + titleNode.displaysAsynchronously = false + titleNode.isUserInteractionEnabled = false + titleNode.maximumNumberOfLines = 2 + titleNode.truncationMode = .byTruncatingTail + titleNode.isAccessibilityElement = true + self.titleNode = titleNode + } else { + self.titleNode = nil + } + + self.textNode = ImmediateTextNode() + self.textNode.maximumNumberOfLines = 0 + self.textNode.attributedText = text + self.textNode.displaysAsynchronously = false + self.textNode.isLayerBacked = false + self.textNode.isAccessibilityElement = true + self.textNode.accessibilityLabel = text.string + if text.length != 0 { + if let paragraphStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { + self.textNode.textAlignment = paragraphStyle.alignment + } + } + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + self.actionNodesSeparator.backgroundColor = theme.separatorColor + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + separatorNode.backgroundColor = theme.separatorColor + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + if let titleNode = self.titleNode { + self.addSubnode(titleNode) + } + self.addSubnode(self.textNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + } + + override public func updateTheme(_ theme: AlertControllerTheme) { + self.theme = theme + + if let titleNode = self.titleNode, let attributedText = titleNode.attributedText { + let updatedText = NSMutableAttributedString(attributedString: attributedText) + updatedText.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.primaryColor, range: NSRange(location: 0, length: updatedText.length)) + titleNode.attributedText = updatedText + } + if let attributedText = self.textNode.attributedText { + let updatedText = NSMutableAttributedString(attributedString: attributedText) + updatedText.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.primaryColor, range: NSRange(location: 0, length: updatedText.length)) + self.textNode.attributedText = updatedText + } + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + self.validLayout = size + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var size = size + size.width = min(size.width, alertWidth) + + var titleSize: CGSize? + if let titleNode = self.titleNode { + titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + } + let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + + let actionButtonHeight: CGFloat = 44.0 + + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = self.actionLayout + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let resultSize: CGSize + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let contentWidth = alertWidth - insets.left - insets.right + if let titleNode = self.titleNode, let titleSize = titleSize { + let spacing: CGFloat = 6.0 + let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize) + transition.updateFrame(node: titleNode, frame: titleFrame) + + let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom) + } else { + let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: insets.top), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: textSize.height + actionsHeight + insets.top + insets.bottom) + } + + self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +public func textAlertController(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal) -> AlertController { + return AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions, actionLayout: actionLayout)) +} + +public func standardTextAlertController(theme: AlertControllerTheme, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal) -> AlertController { + var dismissImpl: (() -> Void)? + let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title != nil ? NSAttributedString(string: title!, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) : nil, text: NSAttributedString(string: text, font: title == nil ? Font.semibold(17.0) : Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center), actions: actions.map { action in + return TextAlertAction(type: action.type, title: action.title, action: { + dismissImpl?() + action.action() + }) + }, actionLayout: actionLayout)) + dismissImpl = { [weak controller] in + controller?.dismissAnimated() + } + return controller +} diff --git a/submodules/Display/Display/TextFieldNode.swift b/submodules/Display/Display/TextFieldNode.swift new file mode 100644 index 0000000000..0defa9ce46 --- /dev/null +++ b/submodules/Display/Display/TextFieldNode.swift @@ -0,0 +1,67 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public final class TextFieldNodeView: UITextField { + public var didDeleteBackwardWhileEmpty: (() -> Void)? + + var fixOffset: Bool = true + + override public func editingRect(forBounds bounds: CGRect) -> CGRect { + return bounds.offsetBy(dx: 0.0, dy: 0.0).integral + } + + override public func textRect(forBounds bounds: CGRect) -> CGRect { + return bounds.offsetBy(dx: 0.0, dy: 0.0).integral + } + + override public func placeholderRect(forBounds bounds: CGRect) -> CGRect { + return self.editingRect(forBounds: bounds.offsetBy(dx: 0.0, dy: -1.0)) + } + + override public func deleteBackward() { + if self.text == nil || self.text!.isEmpty { + self.didDeleteBackwardWhileEmpty?() + } + super.deleteBackward() + } + + override public var keyboardAppearance: UIKeyboardAppearance { + get { + return super.keyboardAppearance + } + set { + guard newValue != self.keyboardAppearance else { + return + } + let resigning = self.isFirstResponder + if resigning { + self.resignFirstResponder() + } + super.keyboardAppearance = newValue + if resigning { + self.becomeFirstResponder() + } + } + } +} + +public class TextFieldNode: ASDisplayNode { + public var textField: TextFieldNodeView { + return self.view as! TextFieldNodeView + } + + public var fixOffset: Bool = true { + didSet { + self.textField.fixOffset = self.fixOffset + } + } + + override public init() { + super.init() + + self.setViewBlock({ + return TextFieldNodeView() + }) + } +} diff --git a/submodules/Display/Display/TextNode.swift b/submodules/Display/Display/TextNode.swift new file mode 100644 index 0000000000..b70b5400fa --- /dev/null +++ b/submodules/Display/Display/TextNode.swift @@ -0,0 +1,898 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import CoreText + +private let defaultFont = UIFont.systemFont(ofSize: 15.0) + +private final class TextNodeLine { + let line: CTLine + let frame: CGRect + let range: NSRange + let isRTL: Bool + let strikethroughs: [TextNodeStrikethrough] + + init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough]) { + self.line = line + self.frame = frame + self.range = range + self.isRTL = isRTL + self.strikethroughs = strikethroughs + } +} + +private final class TextNodeStrikethrough { + let frame: CGRect + + init(frame: CGRect) { + self.frame = frame + } +} + +public enum TextNodeCutoutPosition { + case TopLeft + case TopRight + case BottomRight +} + +public struct TextNodeCutout: Equatable { + public var topLeft: CGSize? + public var topRight: CGSize? + public var bottomRight: CGSize? + + public init(topLeft: CGSize? = nil, topRight: CGSize? = nil, bottomRight: CGSize? = nil) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomRight = bottomRight + } +} + +private func displayLineFrame(frame: CGRect, isRTL: Bool, boundingRect: CGRect, cutout: TextNodeCutout?) -> CGRect { + if frame.width.isEqual(to: boundingRect.width) { + return frame + } + var lineFrame = frame + let intersectionFrame = lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.height) + if isRTL { + lineFrame.origin.x = max(0.0, floor(boundingRect.width - lineFrame.size.width)) + if let topRight = cutout?.topRight { + let topRightRect = CGRect(origin: CGPoint(x: boundingRect.width - topRight.width, y: 0.0), size: topRight) + if intersectionFrame.intersects(topRightRect) { + lineFrame.origin.x -= topRight.width + return lineFrame + } + } + if let bottomRight = cutout?.bottomRight { + let bottomRightRect = CGRect(origin: CGPoint(x: boundingRect.width - bottomRight.width, y: boundingRect.height - bottomRight.height), size: bottomRight) + if intersectionFrame.intersects(bottomRightRect) { + lineFrame.origin.x -= bottomRight.width + return lineFrame + } + } + } + return lineFrame +} + +public final class TextNodeLayoutArguments { + public let attributedString: NSAttributedString? + public let backgroundColor: UIColor? + public let maximumNumberOfLines: Int + public let truncationType: CTLineTruncationType + public let constrainedSize: CGSize + public let alignment: NSTextAlignment + public let lineSpacing: CGFloat + public let cutout: TextNodeCutout? + public let insets: UIEdgeInsets + + public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets()) { + self.attributedString = attributedString + self.backgroundColor = backgroundColor + self.maximumNumberOfLines = maximumNumberOfLines + self.truncationType = truncationType + self.constrainedSize = constrainedSize + self.alignment = alignment + self.lineSpacing = lineSpacing + self.cutout = cutout + self.insets = insets + } +} + +public final class TextNodeLayout: NSObject { + fileprivate let attributedString: NSAttributedString? + fileprivate let maximumNumberOfLines: Int + fileprivate let truncationType: CTLineTruncationType + fileprivate let backgroundColor: UIColor? + fileprivate let constrainedSize: CGSize + fileprivate let alignment: NSTextAlignment + fileprivate let lineSpacing: CGFloat + fileprivate let cutout: TextNodeCutout? + fileprivate let insets: UIEdgeInsets + public let size: CGSize + public let truncated: Bool + fileprivate let firstLineOffset: CGFloat + fileprivate let lines: [TextNodeLine] + public let hasRTL: Bool + + fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) { + self.attributedString = attributedString + self.maximumNumberOfLines = maximumNumberOfLines + self.truncationType = truncationType + self.constrainedSize = constrainedSize + self.alignment = alignment + self.lineSpacing = lineSpacing + self.cutout = cutout + self.insets = insets + self.size = size + self.truncated = truncated + self.firstLineOffset = firstLineOffset + self.lines = lines + self.backgroundColor = backgroundColor + var hasRTL = false + for line in lines { + if line.isRTL { + hasRTL = true + } + } + self.hasRTL = hasRTL + } + + public func areLinesEqual(to other: TextNodeLayout) -> Bool { + if self.lines.count != other.lines.count { + return false + } + for i in 0 ..< self.lines.count { + if !self.lines[i].frame.equalTo(other.lines[i].frame) { + return false + } + if self.lines[i].isRTL != other.lines[i].isRTL { + return false + } + if self.lines[i].range != other.lines[i].range { + return false + } + let lhsRuns = CTLineGetGlyphRuns(self.lines[i].line) as NSArray + let rhsRuns = CTLineGetGlyphRuns(other.lines[i].line) as NSArray + + if lhsRuns.count != rhsRuns.count { + return false + } + + for j in 0 ..< lhsRuns.count { + let lhsRun = lhsRuns[j] as! CTRun + let rhsRun = rhsRuns[j] as! CTRun + let lhsGlyphCount = CTRunGetGlyphCount(lhsRun) + let rhsGlyphCount = CTRunGetGlyphCount(rhsRun) + if lhsGlyphCount != rhsGlyphCount { + return false + } + + for k in 0 ..< lhsGlyphCount { + var lhsGlyph = CGGlyph() + var rhsGlyph = CGGlyph() + CTRunGetGlyphs(lhsRun, CFRangeMake(k, 1), &lhsGlyph) + CTRunGetGlyphs(rhsRun, CFRangeMake(k, 1), &rhsGlyph) + if lhsGlyph != rhsGlyph { + return false + } + } + } + } + return true + } + + public var numberOfLines: Int { + return self.lines.count + } + + public var trailingLineWidth: CGFloat { + if let lastLine = self.lines.last { + return lastLine.frame.width + } else { + return 0.0 + } + } + + public func attributesAtPoint(_ point: CGPoint) -> (Int, [NSAttributedStringKey: Any])? { + if let attributedString = self.attributedString { + let transformedPoint = CGPoint(x: point.x - self.insets.left, y: point.y - self.insets.top) + var lineIndex = -1 + for line in self.lines { + lineIndex += 1 + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + switch self.alignment { + case .center: + lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) + case .natural: + if line.isRTL { + lineFrame.origin.x = self.size.width - lineFrame.size.width + } + lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + default: + break + } + if lineFrame.contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } + if index >= 0 && index < attributedString.length { + return (index, attributedString.attributes(at: index, effectiveRange: nil)) + } + break + } + } + lineIndex = -1 + for line in self.lines { + lineIndex += 1 + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + switch self.alignment { + case .center: + lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) + case .natural: + if line.isRTL { + lineFrame.origin.x = floor(self.size.width - lineFrame.size.width) + } + lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + default: + break + } + if lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.size.height).insetBy(dx: -3.0, dy: -3.0).contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } + if index >= 0 && index < attributedString.length { + return (index, attributedString.attributes(at: index, effectiveRange: nil)) + } + break + } + } + } + return nil + } + + public func linesRects() -> [CGRect] { + var rects: [CGRect] = [] + for line in self.lines { + rects.append(line.frame) + } + return rects + } + + public func textRangesRects(text: String) -> [[CGRect]] { + guard let attributedString = self.attributedString else { + return [] + } + var ranges: [Range] = [] + var searchRange = attributedString.string.startIndex ..< attributedString.string.endIndex + while searchRange.lowerBound != attributedString.string.endIndex { + if let range = attributedString.string.range(of: text, options: [.caseInsensitive, .diacriticInsensitive], range: searchRange, locale: nil) { + ranges.append(range) + searchRange = range.upperBound ..< attributedString.string.endIndex + } else { + break + } + } + var result: [[CGRect]] = [] + for stringRange in ranges { + var rects: [CGRect] = [] + let range = NSRange(stringRange, in: attributedString.string) + for line in self.lines { + let lineRange = NSIntersectionRange(range, line.range) + if lineRange.length != 0 { + var leftOffset: CGFloat = 0.0 + if lineRange.location != line.range.location { + leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) + } + var rightOffset: CGFloat = line.frame.width + if lineRange.location + lineRange.length != line.range.length { + var secondaryOffset: CGFloat = 0.0 + let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) + rightOffset = ceil(rawOffset) + if !rawOffset.isEqual(to: secondaryOffset) { + rightOffset = ceil(secondaryOffset) + } + } + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + + lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + + rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height))) + } + } + if !rects.isEmpty { + result.append(rects) + } + } + return result + } + + public func attributeSubstring(name: String, index: Int) -> String? { + if let attributedString = self.attributedString { + var range = NSRange() + let _ = attributedString.attribute(NSAttributedStringKey(rawValue: name), at: index, effectiveRange: &range) + if range.length != 0 { + return (attributedString.string as NSString).substring(with: range) + } + } + return nil + } + + public func allAttributeRects(name: String) -> [(Any, CGRect)] { + guard let attributedString = self.attributedString else { + return [] + } + var result: [(Any, CGRect)] = [] + attributedString.enumerateAttribute(NSAttributedStringKey(rawValue: name), in: NSRange(location: 0, length: attributedString.length), options: []) { (value, range, _) in + if let value = value, range.length != 0 { + var coveringRect = CGRect() + for line in self.lines { + let lineRange = NSIntersectionRange(range, line.range) + if lineRange.length != 0 { + var leftOffset: CGFloat = 0.0 + if lineRange.location != line.range.location { + leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) + } + var rightOffset: CGFloat = line.frame.width + if lineRange.location + lineRange.length != line.range.length { + var secondaryOffset: CGFloat = 0.0 + let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) + rightOffset = ceil(rawOffset) + if !rawOffset.isEqual(to: secondaryOffset) { + rightOffset = ceil(secondaryOffset) + } + } + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + + let rect = CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height)) + if coveringRect.isEmpty { + coveringRect = rect + } else { + coveringRect = coveringRect.union(rect) + } + } + } + if !coveringRect.isEmpty { + result.append((value, coveringRect)) + } + } + } + return result + } + + public func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? { + if let attributedString = self.attributedString { + var range = NSRange() + let _ = attributedString.attribute(NSAttributedStringKey(rawValue: name), at: index, effectiveRange: &range) + if range.length != 0 { + var rects: [(CGRect, CGRect)] = [] + for line in self.lines { + let lineRange = NSIntersectionRange(range, line.range) + if lineRange.length != 0 { + var leftOffset: CGFloat = 0.0 + if lineRange.location != line.range.location { + leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) + } + var rightOffset: CGFloat = line.frame.width + if lineRange.location + lineRange.length != line.range.length { + var secondaryOffset: CGFloat = 0.0 + let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) + rightOffset = ceil(rawOffset) + if !rawOffset.isEqual(to: secondaryOffset) { + rightOffset = ceil(secondaryOffset) + } + } + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + + lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + + rects.append((lineFrame, CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height)))) + } + } + if !rects.isEmpty { + return rects + } + } + } + return nil + } +} + +private final class TextAccessibilityOverlayElement: UIAccessibilityElement { + private let url: String + private let openUrl: (String) -> Void + + init(accessibilityContainer: Any, url: String, openUrl: @escaping (String) -> Void) { + self.url = url + self.openUrl = openUrl + + super.init(accessibilityContainer: accessibilityContainer) + } + + override func accessibilityActivate() -> Bool { + self.openUrl(self.url) + return true + } +} + +private final class TextAccessibilityOverlayNodeView: UIView { + fileprivate var cachedLayout: TextNodeLayout? { + didSet { + self.currentAccessibilityNodes?.forEach({ $0.removeFromSupernode() }) + self.currentAccessibilityNodes = nil + } + } + fileprivate let openUrl: (String) -> Void + + private var currentAccessibilityNodes: [AccessibilityAreaNode]? + + override var accessibilityElements: [Any]? { + get { + if let _ = self.currentAccessibilityNodes { + return nil + } + guard let cachedLayout = self.cachedLayout else { + return nil + } + let urlAttributesAndRects = cachedLayout.allAttributeRects(name: "UrlAttributeT") + + var urlElements: [AccessibilityAreaNode] = [] + for (value, rect) in urlAttributesAndRects { + let element = AccessibilityAreaNode() + element.accessibilityLabel = value as? String ?? "" + element.frame = rect + element.accessibilityTraits = UIAccessibilityTraitLink + element.activate = { [weak self] in + self?.openUrl(value as? String ?? "") + return true + } + self.addSubnode(element) + urlElements.append(element) + } + self.currentAccessibilityNodes = urlElements + return nil + } set(value) { + } + } + + init(openUrl: @escaping (String) -> Void) { + self.openUrl = openUrl + + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +public final class TextAccessibilityOverlayNode: ASDisplayNode { + public var cachedLayout: TextNodeLayout? { + didSet { + if self.isNodeLoaded { + (self.view as? TextAccessibilityOverlayNodeView)?.cachedLayout = self.cachedLayout + } + } + } + + public var openUrl: ((String) -> Void)? + + override public init() { + super.init() + + self.isOpaque = false + self.backgroundColor = nil + + let openUrl: (String) -> Void = { [weak self] url in + self?.openUrl?(url) + } + + self.isAccessibilityElement = false + + self.setViewBlock({ + return TextAccessibilityOverlayNodeView(openUrl: openUrl) + }) + } + + override public func didLoad() { + super.didLoad() + + (self.view as? TextAccessibilityOverlayNodeView)?.cachedLayout = self.cachedLayout + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} + +public class TextNode: ASDisplayNode { + public private(set) var cachedLayout: TextNodeLayout? + + override public init() { + super.init() + + self.backgroundColor = UIColor.clear + self.isOpaque = false + self.clipsToBounds = false + } + + public func attributesAtPoint(_ point: CGPoint) -> (Int, [NSAttributedStringKey: Any])? { + if let cachedLayout = self.cachedLayout { + return cachedLayout.attributesAtPoint(point) + } else { + return nil + } + } + + public func textRangesRects(text: String) -> [[CGRect]] { + return self.cachedLayout?.textRangesRects(text: text) ?? [] + } + + public func attributeSubstring(name: String, index: Int) -> String? { + return self.cachedLayout?.attributeSubstring(name: name, index: index) + } + + public func attributeRects(name: String, at index: Int) -> [CGRect]? { + if let cachedLayout = self.cachedLayout { + return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 } + } else { + return nil + } + } + + public func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? { + if let cachedLayout = self.cachedLayout { + return cachedLayout.lineAndAttributeRects(name: name, at: index) + } else { + return nil + } + } + + private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets) -> TextNodeLayout { + if let attributedString = attributedString { + let stringLength = attributedString.length + + let font: CTFont + if stringLength != 0 { + if let stringFont = attributedString.attribute(NSAttributedStringKey.font, at: 0, effectiveRange: nil) { + font = stringFont as! CTFont + } else { + font = defaultFont + } + } else { + font = defaultFont + } + + let fontAscent = CTFontGetAscent(font) + let fontDescent = CTFontGetDescent(font) + let fontLineHeight = floor(fontAscent + fontDescent) + let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) + + var lines: [TextNodeLine] = [] + + var maybeTypesetter: CTTypesetter? + maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) + if maybeTypesetter == nil { + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) + } + + let typesetter = maybeTypesetter! + + var lastLineCharacterIndex: CFIndex = 0 + var layoutSize = CGSize() + + var cutoutEnabled = false + var cutoutMinY: CGFloat = 0.0 + var cutoutMaxY: CGFloat = 0.0 + var cutoutWidth: CGFloat = 0.0 + var cutoutOffset: CGFloat = 0.0 + + var bottomCutoutEnabled = false + var bottomCutoutSize = CGSize() + + if let topLeft = cutout?.topLeft { + cutoutMinY = -fontLineSpacing + cutoutMaxY = topLeft.height + fontLineSpacing + cutoutWidth = topLeft.width + cutoutOffset = cutoutWidth + cutoutEnabled = true + } else if let topRight = cutout?.topRight { + cutoutMinY = -fontLineSpacing + cutoutMaxY = topRight.height + fontLineSpacing + cutoutWidth = topRight.width + cutoutEnabled = true + } + + if let bottomRight = cutout?.bottomRight { + bottomCutoutSize = bottomRight + bottomCutoutEnabled = true + } + + let firstLineOffset = floorToScreenPixels(fontDescent) + + var truncated = false + var first = true + while true { + var strikethroughs: [TextNodeStrikethrough] = [] + + var lineConstrainedWidth = constrainedSize.width + var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) + if !first { + lineOriginY += fontLineSpacing + } + var lineCutoutOffset: CGFloat = 0.0 + var lineAdditionalWidth: CGFloat = 0.0 + + if cutoutEnabled { + if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { + lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) + lineCutoutOffset = cutoutOffset + lineAdditionalWidth = cutoutWidth + } + } + + let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) + + var isLastLine = false + if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { + isLastLine = true + } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { + isLastLine = true + } + + if isLastLine { + if first { + first = false + } else { + layoutSize.height += fontLineSpacing + } + + let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + if lineRange.length == 0 { + break + } + + let coreTextLine: CTLine + let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) + + if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) { + coreTextLine = originalLine + } else { + var truncationTokenAttributes: [NSAttributedStringKey : AnyObject] = [:] + truncationTokenAttributes[NSAttributedStringKey.font] = font + truncationTokenAttributes[NSAttributedStringKey(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber + let tokenString = "\u{2026}" + let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) + let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) + + coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken + truncated = true + } + + let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight) + layoutSize.height += fontLineHeight + fontLineSpacing + layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) + + var isRTL = false + let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray + if glyphRuns.count != 0 { + let run = glyphRuns[0] as! CTRun + if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { + isRTL = true + } + } + + attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if let _ = attributes[NSAttributedStringKey.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughs.append(TextNodeStrikethrough(frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + } + } + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs)) + + break + } else { + if lineCharacterCount > 0 { + if first { + first = false + } else { + layoutSize.height += fontLineSpacing + } + + let lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) + let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) + lastLineCharacterIndex += lineCharacterCount + + let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) + let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight) + layoutSize.height += fontLineHeight + layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) + + var isRTL = false + let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray + if glyphRuns.count != 0 { + let run = glyphRuns[0] as! CTRun + if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { + isRTL = true + } + } + + attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if let _ = attributes[NSAttributedStringKey.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughs.append(TextNodeStrikethrough(frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + } + } + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs)) + } else { + if !lines.isEmpty { + layoutSize.height += fontLineSpacing + } + break + } + } + } + + if !lines.isEmpty && bottomCutoutEnabled { + let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width + if proposedWidth > layoutSize.width { + if proposedWidth < constrainedSize.width { + layoutSize.width = proposedWidth + } else { + layoutSize.height += bottomCutoutSize.height + } + } + } + + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor) + } else { + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) + } + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return self.cachedLayout + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + if isCancelled() { + return + } + + let context = UIGraphicsGetCurrentContext()! + + context.setAllowsAntialiasing(true) + + context.setAllowsFontSmoothing(false) + context.setShouldSmoothFonts(false) + + context.setAllowsFontSubpixelPositioning(false) + context.setShouldSubpixelPositionFonts(false) + + context.setAllowsFontSubpixelQuantization(true) + context.setShouldSubpixelQuantizeFonts(true) + + if let layout = parameters as? TextNodeLayout { + if !isRasterizing || layout.backgroundColor != nil { + context.setBlendMode(.copy) + context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor) + context.fill(bounds) + } + + let textMatrix = context.textMatrix + let textPosition = context.textPosition + //CGContextSaveGState(context) + + context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) + + //let clipRect = CGContextGetClipBoundingBox(context) + + let alignment = layout.alignment + let offset = CGPoint(x: layout.insets.left, y: layout.insets.top) + + for i in 0 ..< layout.lines.count { + let line = layout.lines[i] + + var lineFrame = line.frame + lineFrame.origin.y += offset.y + + if alignment == .center { + lineFrame.origin.x = offset.x + floor((bounds.size.width - lineFrame.width) / 2.0) + } else if alignment == .natural, line.isRTL { + lineFrame.origin.x = offset.x + floor(bounds.size.width - lineFrame.width) + + lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: bounds.size), cutout: layout.cutout) + } + context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY) + CTLineDraw(line.line, context) + + if !line.strikethroughs.isEmpty { + for strikethrough in line.strikethroughs { + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0)) + } + } + } + + //CGContextRestoreGState(context) + context.textMatrix = textMatrix + context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y) + } + + context.setBlendMode(.normal) + } + + public static func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) { + let existingLayout: TextNodeLayout? = maybeNode?.cachedLayout + + return { arguments in + let layout: TextNodeLayout + + var updated = false + if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.alignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) { + let stringMatch: Bool + + var colorMatch: Bool = true + if let backgroundColor = arguments.backgroundColor, let previousBackgroundColor = existingLayout.backgroundColor { + if !backgroundColor.isEqual(previousBackgroundColor) { + colorMatch = false + } + } else if (arguments.backgroundColor != nil) != (existingLayout.backgroundColor != nil) { + colorMatch = false + } + + if !colorMatch { + stringMatch = false + } else if let existingString = existingLayout.attributedString, let string = arguments.attributedString { + stringMatch = existingString.isEqual(to: string) + } else if existingLayout.attributedString == nil && arguments.attributedString == nil { + stringMatch = true + } else { + stringMatch = false + } + + if stringMatch { + layout = existingLayout + } else { + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets) + updated = true + } + } else { + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets) + updated = true + } + + let node = maybeNode ?? TextNode() + + return (layout, { + node.cachedLayout = layout + if updated { + if layout.size.width.isZero && layout.size.height.isZero { + node.contents = nil + } + node.setNeedsDisplay() + } + + return node + }) + } + } +} diff --git a/submodules/Display/Display/Toolbar.swift b/submodules/Display/Display/Toolbar.swift new file mode 100644 index 0000000000..1d87078945 --- /dev/null +++ b/submodules/Display/Display/Toolbar.swift @@ -0,0 +1,24 @@ +import Foundation +import UIKit + +public struct ToolbarAction: Equatable { + public let title: String + public let isEnabled: Bool + + public init(title: String, isEnabled: Bool) { + self.title = title + self.isEnabled = isEnabled + } +} + +public struct Toolbar: Equatable { + public let leftAction: ToolbarAction? + public let rightAction: ToolbarAction? + public let middleAction: ToolbarAction? + + public init(leftAction: ToolbarAction?, rightAction: ToolbarAction?, middleAction: ToolbarAction?) { + self.leftAction = leftAction + self.rightAction = rightAction + self.middleAction = middleAction + } +} diff --git a/submodules/Display/Display/ToolbarNode.swift b/submodules/Display/Display/ToolbarNode.swift new file mode 100644 index 0000000000..960400a2e7 --- /dev/null +++ b/submodules/Display/Display/ToolbarNode.swift @@ -0,0 +1,151 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public final class ToolbarNode: ASDisplayNode { + private var theme: TabBarControllerTheme + private let displaySeparator: Bool + private let left: () -> Void + private let right: () -> Void + private let middle: () -> Void + + private let separatorNode: ASDisplayNode + private let leftTitle: ImmediateTextNode + private let leftButton: HighlightableButtonNode + private let rightTitle: ImmediateTextNode + private let rightButton: HighlightableButtonNode + private let middleTitle: ImmediateTextNode + private let middleButton: HighlightableButtonNode + + public init(theme: TabBarControllerTheme, displaySeparator: Bool = false, left: @escaping () -> Void, right: @escaping () -> Void, middle: @escaping () -> Void) { + self.theme = theme + self.displaySeparator = displaySeparator + self.left = left + self.right = right + self.middle = middle + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.leftTitle = ImmediateTextNode() + self.leftTitle.displaysAsynchronously = false + self.leftButton = HighlightableButtonNode() + self.rightTitle = ImmediateTextNode() + self.rightTitle.displaysAsynchronously = false + self.rightButton = HighlightableButtonNode() + self.middleTitle = ImmediateTextNode() + self.middleTitle.displaysAsynchronously = false + self.middleButton = HighlightableButtonNode() + + super.init() + + self.addSubnode(self.leftTitle) + self.addSubnode(self.leftButton) + self.addSubnode(self.rightTitle) + self.addSubnode(self.rightButton) + self.addSubnode(self.middleTitle) + self.addSubnode(self.middleButton) + if self.displaySeparator { + self.addSubnode(self.separatorNode) + } + + self.updateTheme(theme) + + self.leftButton.addTarget(self, action: #selector(self.leftPressed), forControlEvents: .touchUpInside) + self.leftButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.leftTitle.layer.removeAnimation(forKey: "opacity") + strongSelf.leftTitle.alpha = 0.4 + } else { + strongSelf.leftTitle.alpha = 1.0 + strongSelf.leftTitle.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.rightButton.addTarget(self, action: #selector(self.rightPressed), forControlEvents: .touchUpInside) + self.rightButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.rightTitle.layer.removeAnimation(forKey: "opacity") + strongSelf.rightTitle.alpha = 0.4 + } else { + strongSelf.rightTitle.alpha = 1.0 + strongSelf.rightTitle.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.middleButton.addTarget(self, action: #selector(self.middlePressed), forControlEvents: .touchUpInside) + self.middleButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.middleTitle.layer.removeAnimation(forKey: "opacity") + strongSelf.middleTitle.alpha = 0.4 + } else { + strongSelf.middleTitle.alpha = 1.0 + strongSelf.middleTitle.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + public func updateTheme(_ theme: TabBarControllerTheme) { + self.separatorNode.backgroundColor = theme.tabBarSeparatorColor + self.backgroundColor = theme.tabBarBackgroundColor + } + + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, toolbar: Toolbar, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) + + let sideInset: CGFloat = 16.0 + + self.leftTitle.attributedText = NSAttributedString(string: toolbar.leftAction?.title ?? "", font: Font.regular(17.0), textColor: (toolbar.leftAction?.isEnabled ?? false) ? self.theme.tabBarSelectedTextColor : self.theme.tabBarTextColor) + self.rightTitle.attributedText = NSAttributedString(string: toolbar.rightAction?.title ?? "", font: Font.regular(17.0), textColor: (toolbar.rightAction?.isEnabled ?? false) ? self.theme.tabBarSelectedTextColor : self.theme.tabBarTextColor) + self.middleTitle.attributedText = NSAttributedString(string: toolbar.middleAction?.title ?? "", font: Font.regular(17.0), textColor: (toolbar.middleAction?.isEnabled ?? false) ? self.theme.tabBarSelectedTextColor : self.theme.tabBarTextColor) + let leftSize = self.leftTitle.updateLayout(size) + let rightSize = self.rightTitle.updateLayout(size) + let middleSize = self.middleTitle.updateLayout(size) + + let leftFrame = CGRect(origin: CGPoint(x: leftInset + sideInset, y: floor((size.height - bottomInset - leftSize.height) / 2.0)), size: leftSize) + let rightFrame = CGRect(origin: CGPoint(x: size.width - rightInset - sideInset - rightSize.width, y: floor((size.height - bottomInset - rightSize.height) / 2.0)), size: rightSize) + let middleFrame = CGRect(origin: CGPoint(x: floor((size.width - middleSize.width) / 2.0), y: floor((size.height - bottomInset - middleSize.height) / 2.0)), size: middleSize) + + if leftFrame.size == self.leftTitle.frame.size { + transition.updateFrame(node: self.leftTitle, frame: leftFrame) + } else { + self.leftTitle.frame = leftFrame + } + + if rightFrame.size == self.rightTitle.frame.size { + transition.updateFrame(node: self.rightTitle, frame: rightFrame) + } else { + self.rightTitle.frame = rightFrame + } + + if middleFrame.size == self.middleTitle.frame.size { + transition.updateFrame(node: self.middleTitle, frame: middleFrame) + } else { + self.middleTitle.frame = middleFrame + } + + self.leftButton.isEnabled = toolbar.leftAction?.isEnabled ?? false + self.rightButton.isEnabled = toolbar.rightAction?.isEnabled ?? false + self.middleButton.isEnabled = toolbar.middleAction?.isEnabled ?? false + + self.leftButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: leftSize.width + sideInset * 2.0, height: size.height - bottomInset)) + self.rightButton.frame = CGRect(origin: CGPoint(x: size.width - rightInset - sideInset * 2.0 - rightSize.width, y: 0.0), size: CGSize(width: rightSize.width + sideInset * 2.0, height: size.height - bottomInset)) + self.middleButton.frame = CGRect(origin: CGPoint(x: floor((size.width - middleSize.width) / 2.0), y: 0.0), size: CGSize(width: middleSize.width + sideInset * 2.0, height: size.height - bottomInset)) + } + + @objc private func leftPressed() { + self.left() + } + + @objc private func rightPressed() { + self.right() + } + + @objc private func middlePressed() { + self.middle() + } +} diff --git a/submodules/Display/Display/TooltipController.swift b/submodules/Display/Display/TooltipController.swift new file mode 100644 index 0000000000..51e0985d3c --- /dev/null +++ b/submodules/Display/Display/TooltipController.swift @@ -0,0 +1,172 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +public enum TooltipControllerContent: Equatable { + case text(String) + case iconAndText(UIImage, String) + + var text: String { + switch self { + case let .text(text), let .iconAndText(_, text): + return text + } + } + + var image: UIImage? { + if case let .iconAndText(image, _) = self { + return image + } + return nil + } +} + +private enum SourceAndRect { + case node(() -> (ASDisplayNode, CGRect)?) + case view(() -> (UIView, CGRect)?) + + func globalRect() -> CGRect? { + switch self { + case let .node(node): + if let (sourceNode, sourceRect) = node() { + return sourceNode.view.convert(sourceRect, to: nil) + } + case let .view(view): + if let (sourceView, sourceRect) = view() { + return sourceView.convert(sourceRect, to: nil) + } + } + return nil + } +} + +public final class TooltipControllerPresentationArguments { + fileprivate let sourceAndRect: SourceAndRect + + public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?) { + self.sourceAndRect = .node(sourceNodeAndRect) + } + + public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?) { + self.sourceAndRect = .view(sourceViewAndRect) + } +} + +public final class TooltipController: ViewController { + private var controllerNode: TooltipControllerNode { + return self.displayNode as! TooltipControllerNode + } + + public var content: TooltipControllerContent { + didSet { + if self.content != oldValue { + if self.isNodeLoaded { + self.controllerNode.updateText(self.content.text, transition: .animated(duration: 0.25, curve: .easeInOut)) + if self.timeoutTimer != nil { + self.timeoutTimer?.invalidate() + self.timeoutTimer = nil + self.beginTimeout() + } + } + } + } + } + + private let timeout: Double + private let dismissByTapOutside: Bool + private let dismissImmediatelyOnLayoutUpdate: Bool + private var timeoutTimer: SwiftSignalKit.Timer? + + private var layout: ContainerViewLayout? + + public var dismissed: (() -> Void)? + + public init(content: TooltipControllerContent, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false) { + self.content = content + self.timeout = timeout + self.dismissByTapOutside = dismissByTapOutside + self.dismissImmediatelyOnLayoutUpdate = dismissImmediatelyOnLayoutUpdate + + super.init(navigationBarPresentationData: nil) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timeoutTimer?.invalidate() + } + + public override func loadDisplayNode() { + self.displayNode = TooltipControllerNode(content: self.content, dismiss: { [weak self] in + self?.dismiss() + }, dismissByTapOutside: self.dismissByTapOutside) + self.displayNodeDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.controllerNode.animateIn() + self.beginTimeout() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if self.layout != nil && self.layout! != layout { + if self.dismissImmediatelyOnLayoutUpdate { + self.dismissImmediately() + } else { + self.dismiss() + } + } else { + self.layout = layout + + if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect() { + self.controllerNode.sourceRect = sourceRect + } else { + self.controllerNode.sourceRect = nil + } + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.controllerNode.animateIn() + self.beginTimeout() + } + + private func beginTimeout() { + if self.timeoutTimer == nil { + let timeoutTimer = SwiftSignalKit.Timer(timeout: self.timeout, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.dismissed?() + strongSelf.controllerNode.animateOut { + self?.presentingViewController?.dismiss(animated: false) + } + } + }, queue: Queue.mainQueue()) + self.timeoutTimer = timeoutTimer + timeoutTimer.start() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.dismissed?() + self.controllerNode.animateOut { [weak self] in + self?.presentingViewController?.dismiss(animated: false) + completion?() + } + } + + public func dismissImmediately() { + self.dismissed?() + self.presentingViewController?.dismiss(animated: false) + } +} diff --git a/submodules/Display/Display/TooltipControllerNode.swift b/submodules/Display/Display/TooltipControllerNode.swift new file mode 100644 index 0000000000..b3a478bbb7 --- /dev/null +++ b/submodules/Display/Display/TooltipControllerNode.swift @@ -0,0 +1,138 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +final class TooltipControllerNode: ASDisplayNode { + private let dismiss: () -> Void + + private var validLayout: ContainerViewLayout? + + private let containerNode: ContextMenuContainerNode + private let imageNode: ASImageNode + private let textNode: ImmediateTextNode + + private let dismissByTapOutside: Bool + + var sourceRect: CGRect? + var arrowOnBottom: Bool = true + + private var dismissedByTouchOutside = false + + init(content: TooltipControllerContent, dismiss: @escaping () -> Void, dismissByTapOutside: Bool) { + self.dismissByTapOutside = dismissByTapOutside + + self.containerNode = ContextMenuContainerNode() + self.containerNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + + self.imageNode = ASImageNode() + self.imageNode.image = content.image + + self.textNode = ImmediateTextNode() + self.textNode.attributedText = NSAttributedString(string: content.text, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center) + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.maximumNumberOfLines = 0 + + self.dismiss = dismiss + + super.init() + + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.textNode) + + self.addSubnode(self.containerNode) + } + + func updateText(_ text: String, transition: ContainedViewLayoutTransition) { + if transition.isAnimated, let copyLayer = self.textNode.layer.snapshotContentTree() { + copyLayer.frame = self.textNode.layer.frame + self.textNode.layer.superlayer?.addSublayer(copyLayer) + transition.updateAlpha(layer: copyLayer, alpha: 0.0, completion: { [weak copyLayer] _ in + copyLayer?.removeFromSuperlayer() + }) + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + } + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center) + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: transition) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + let maxActionsWidth = layout.size.width - 20.0 + + var imageSize = CGSize() + var imageSizeWithInset = CGSize() + if let image = self.imageNode.image { + imageSize = image.size + imageSizeWithInset = CGSize(width: image.size.width + 12.0, height: image.size.height) + } + + var textSize = self.textNode.updateLayout(CGSize(width: maxActionsWidth, height: CGFloat.greatestFiniteMagnitude)) + textSize.width = ceil(textSize.width / 2.0) * 2.0 + textSize.height = ceil(textSize.height / 2.0) * 2.0 + let contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0, height: textSize.height + 34.0) + + let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize()) + + let insets = layout.insets(options: [.statusBar, .input]) + + let verticalOrigin: CGFloat + var arrowOnBottom = true + if sourceRect.minY - 54.0 > insets.top { + verticalOrigin = sourceRect.minY - contentSize.height + } else { + verticalOrigin = min(layout.size.height - insets.bottom - contentSize.height, sourceRect.maxY) + arrowOnBottom = false + } + self.arrowOnBottom = arrowOnBottom + + let horizontalOrigin: CGFloat = floor(min(max(8.0, sourceRect.midX - contentSize.width / 2.0), layout.size.width - contentSize.width - 8.0)) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: contentSize)) + self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom) + + self.containerNode.updateLayout(transition: transition) + + let textFrame = CGRect(origin: CGPoint(x: 6.0 + imageSizeWithInset.width, y: 17.0), size: textSize) + if transition.isAnimated, textFrame.size != self.textNode.frame.size { + transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0)) + } + + let imageFrame = CGRect(origin: CGPoint(x: 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize) + self.imageNode.frame = imageFrame + self.textNode.frame = textFrame + } + + func animateIn() { + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + func animateOut(completion: @escaping () -> Void) { + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let event = event { + var eventIsPresses = false + if #available(iOSApplicationExtension 9.0, *) { + eventIsPresses = event.type == .presses + } + if event.type == .touches || eventIsPresses { + if self.containerNode.frame.contains(point) || self.dismissByTapOutside { + if !self.dismissedByTouchOutside { + self.dismissedByTouchOutside = true + self.dismiss() + } + } + return nil + } + } + return super.hitTest(point, with: event) + } +} + diff --git a/submodules/Display/Display/UIBarButtonItem+Proxy.h b/submodules/Display/Display/UIBarButtonItem+Proxy.h new file mode 100644 index 0000000000..62a3b1137a --- /dev/null +++ b/submodules/Display/Display/UIBarButtonItem+Proxy.h @@ -0,0 +1,22 @@ +#import +#import + +typedef void (^UIBarButtonItemSetTitleListener)(NSString *); +typedef void (^UIBarButtonItemSetEnabledListener)(BOOL); + +@interface UIBarButtonItem (Proxy) + +@property (nonatomic, strong, readonly) ASDisplayNode *customDisplayNode; +@property (nonatomic, readonly) bool backButtonAppearance; + +- (instancetype)initWithCustomDisplayNode:(ASDisplayNode *)customDisplayNode; +- (instancetype)initWithBackButtonAppearanceWithTitle:(NSString *)title target:(id)target action:(SEL)action; + +- (void)performActionOnTarget; + +- (NSInteger)addSetTitleListener:(UIBarButtonItemSetTitleListener)listener; +- (void)removeSetTitleListener:(NSInteger)key; +- (NSInteger)addSetEnabledListener:(UIBarButtonItemSetEnabledListener)listener; +- (void)removeSetEnabledListener:(NSInteger)key; + +@end diff --git a/submodules/Display/Display/UIBarButtonItem+Proxy.m b/submodules/Display/Display/UIBarButtonItem+Proxy.m new file mode 100644 index 0000000000..d9c85ae8f3 --- /dev/null +++ b/submodules/Display/Display/UIBarButtonItem+Proxy.m @@ -0,0 +1,111 @@ +#import "UIBarButtonItem+Proxy.h" + +#import "NSBag.h" +#import "RuntimeUtils.h" + +static const void *setEnabledListenerBagKey = &setEnabledListenerBagKey; +static const void *setTitleListenerBagKey = &setTitleListenerBagKey; +static const void *customDisplayNodeKey = &customDisplayNodeKey; +static const void *backButtonAppearanceKey = &backButtonAppearanceKey; + +@implementation UIBarButtonItem (Proxy) + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + [RuntimeUtils swizzleInstanceMethodOfClass:[UIBarButtonItem class] currentSelector:@selector(setEnabled:) newSelector:@selector(_c1e56039_setEnabled:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIBarButtonItem class] currentSelector:@selector(setTitle:) newSelector:@selector(_c1e56039_setTitle:)]; + }); +} + +- (instancetype)initWithCustomDisplayNode:(ASDisplayNode *)customDisplayNode { + self = [self init]; + if (self != nil) { + [self setAssociatedObject:customDisplayNode forKey:customDisplayNodeKey]; + } + return self; +} + +- (instancetype)initWithBackButtonAppearanceWithTitle:(NSString *)title target:(id)target action:(SEL)action { + self = [self initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action]; + if (self != nil) { + [self setAssociatedObject:@true forKey:backButtonAppearanceKey]; + } + return self; +} + +- (ASDisplayNode *)customDisplayNode { + return [self associatedObjectForKey:customDisplayNodeKey]; +} + +- (bool)backButtonAppearance { + return [[self associatedObjectForKey:backButtonAppearanceKey] boolValue]; +} + +- (void)_c1e56039_setEnabled:(BOOL)enabled +{ + [self _c1e56039_setEnabled:enabled]; + + [(NSBag *)[self associatedObjectForKey:setEnabledListenerBagKey] enumerateItems:^(UIBarButtonItemSetEnabledListener listener) + { + listener(enabled); + }]; +} + +- (void)_c1e56039_setTitle:(NSString *)title +{ + [self _c1e56039_setTitle:title]; + + [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] enumerateItems:^(UIBarButtonItemSetTitleListener listener) + { + listener(title); + }]; +} + +- (void)performActionOnTarget +{ + if (self.target == nil) { + return; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.target performSelector:self.action]; +#pragma clang diagnostic pop +} + +- (NSInteger)addSetTitleListener:(UIBarButtonItemSetTitleListener)listener +{ + NSBag *bag = [self associatedObjectForKey:setTitleListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setTitleListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetTitleListener:(NSInteger)key +{ + [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetEnabledListener:(UIBarButtonItemSetEnabledListener)listener +{ + NSBag *bag = [self associatedObjectForKey:setEnabledListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setEnabledListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetEnabledListener:(NSInteger)key +{ + [(NSBag *)[self associatedObjectForKey:setEnabledListenerBagKey] removeItem:key]; +} + +@end diff --git a/submodules/Display/Display/UIKitUtils.h b/submodules/Display/Display/UIKitUtils.h new file mode 100644 index 0000000000..292a972750 --- /dev/null +++ b/submodules/Display/Display/UIKitUtils.h @@ -0,0 +1,13 @@ +#import +#import + +@interface UIView (AnimationUtils) + ++ (double)animationDurationFactor; + +@end + +CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath); +CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping); +CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t); + diff --git a/submodules/Display/Display/UIKitUtils.m b/submodules/Display/Display/UIKitUtils.m new file mode 100644 index 0000000000..1041ec2a67 --- /dev/null +++ b/submodules/Display/Display/UIKitUtils.m @@ -0,0 +1,89 @@ +#import "UIKitUtils.h" + +#import + +#if TARGET_IPHONE_SIMULATOR +UIKIT_EXTERN float UIAnimationDragCoefficient(); // UIKit private drag coeffient, use judiciously +#endif + +@implementation UIView (AnimationUtils) + ++ (double)animationDurationFactor +{ +#if TARGET_IPHONE_SIMULATOR + return (double)UIAnimationDragCoefficient(); +#endif + + return 1.0f; +} + +@end + +@interface CASpringAnimation () + +@end + +@implementation CASpringAnimation (AnimationUtils) + +- (CGFloat)valueAt:(CGFloat)t { + static dispatch_once_t onceToken; + static float (*impl)(id, float) = NULL; + static double (*dimpl)(id, double) = NULL; + dispatch_once(&onceToken, ^{ + Method method = class_getInstanceMethod([CASpringAnimation class], NSSelectorFromString([@"_" stringByAppendingString:@"solveForInput:"])); + if (method) { + const char *encoding = method_getTypeEncoding(method); + NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:encoding]; + const char *argType = [signature getArgumentTypeAtIndex:2]; + if (strncmp(argType, "f", 1) == 0) { + impl = (float (*)(id, float))method_getImplementation(method); + } else if (strncmp(argType, "d", 1) == 0) { + dimpl = (double (*)(id, double))method_getImplementation(method); + } + } + }); + if (impl) { + float result = impl(self, (float)t); + return (CGFloat)result; + } else if (dimpl) { + double result = dimpl(self, (double)t); + return (CGFloat)result; + } + return t; +} + +@end + +CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath) { + CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath]; + springAnimation.mass = 3.0f; + springAnimation.stiffness = 1000.0f; + springAnimation.damping = 500.0f; + springAnimation.duration = 0.5; + springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + return springAnimation; +} + +CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping) { + CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath]; + springAnimation.mass = 5.0f; + springAnimation.stiffness = 900.0f; + springAnimation.damping = damping; + static bool canSetInitialVelocity = true; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + canSetInitialVelocity = [springAnimation respondsToSelector:@selector(setInitialVelocity:)]; + }); + if (canSetInitialVelocity) { + springAnimation.initialVelocity = initialVelocity; + springAnimation.duration = springAnimation.settlingDuration; + } else { + springAnimation.duration = 0.1; + } + springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + return springAnimation; +} + +CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t) { + return [(CASpringAnimation *)animation valueAt:t]; +} diff --git a/submodules/Display/Display/UIKitUtils.swift b/submodules/Display/Display/UIKitUtils.swift new file mode 100644 index 0000000000..25d1d222a4 --- /dev/null +++ b/submodules/Display/Display/UIKitUtils.swift @@ -0,0 +1,368 @@ +import UIKit + +public func dumpViews(_ view: UIView) { + dumpViews(view, indent: "") +} + +private func dumpViews(_ view: UIView, indent: String = "") { + print("\(indent)\(view)") + let nextIndent = indent + "-" + for subview in view.subviews { + dumpViews(subview as UIView, indent: nextIndent) + } +} + +public func dumpLayers(_ layer: CALayer) { + dumpLayers(layer, indent: "") +} + +private func dumpLayers(_ layer: CALayer, indent: String = "") { + print("\(indent)\(layer)(frame: \(layer.frame), bounds: \(layer.bounds))") + if layer.sublayers != nil { + let nextIndent = indent + ".." + if let sublayers = layer.sublayers { + for sublayer in sublayers { + dumpLayers(sublayer as CALayer, indent: nextIndent) + } + } + } +} + +public let UIScreenScale = UIScreen.main.scale +public func floorToScreenPixels(_ value: CGFloat) -> CGFloat { + return floor(value * UIScreenScale) / UIScreenScale +} +public func ceilToScreenPixels(_ value: CGFloat) -> CGFloat { + return ceil(value * UIScreenScale) / UIScreenScale +} + +public let UIScreenPixel = 1.0 / UIScreenScale + +public extension UIColor { + convenience init(rgb: UInt32) { + self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) + } + + convenience init(rgb: UInt32, alpha: CGFloat) { + self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: alpha) + } + + convenience init(argb: UInt32) { + self.init(red: CGFloat((argb >> 16) & 0xff) / 255.0, green: CGFloat((argb >> 8) & 0xff) / 255.0, blue: CGFloat(argb & 0xff) / 255.0, alpha: CGFloat((argb >> 24) & 0xff) / 255.0) + } + + convenience init?(hexString: String) { + let scanner = Scanner(string: hexString) + if hexString.hasPrefix("#") { + scanner.scanLocation = 1 + } + var num: UInt32 = 0 + if scanner.scanHexInt32(&num) { + self.init(rgb: num) + } else { + return nil + } + } + + var alpha: CGFloat { + var alpha: CGFloat = 0.0 + if self.getRed(nil, green: nil, blue: nil, alpha: &alpha) { + return alpha + } else if self.getWhite(nil, alpha: &alpha) { + return alpha + } else { + return 0.0 + } + } + + var rgb: UInt32 { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + self.getRed(&red, green: &green, blue: &blue, alpha: nil) + + return (UInt32(red * 255.0) << 16) | (UInt32(green * 255.0) << 8) | (UInt32(blue * 255.0)) + } + + var argb: UInt32 { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + return (UInt32(alpha * 255.0) << 24) | (UInt32(red * 255.0) << 16) | (UInt32(green * 255.0) << 8) | (UInt32(blue * 255.0)) + } + + var hsv: (CGFloat, CGFloat, CGFloat) { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var value: CGFloat = 0.0 + if self.getHue(&hue, saturation: &saturation, brightness: &value, alpha: nil) { + return (hue, saturation, value) + } else { + return (0.0, 0.0, 0.0) + } + } + + func withMultipliedBrightnessBy(_ factor: CGFloat) -> UIColor { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + + return UIColor(hue: hue, saturation: saturation, brightness: max(0.0, min(1.0, brightness * factor)), alpha: alpha) + } + + func mixedWith(_ other: UIColor, alpha: CGFloat) -> UIColor { + let alpha = min(1.0, max(0.0, alpha)) + let oneMinusAlpha = 1.0 - alpha + + var r1: CGFloat = 0.0 + var r2: CGFloat = 0.0 + var g1: CGFloat = 0.0 + var g2: CGFloat = 0.0 + var b1: CGFloat = 0.0 + var b2: CGFloat = 0.0 + var a1: CGFloat = 0.0 + var a2: CGFloat = 0.0 + if self.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) && + other.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) + { + let r = r1 * oneMinusAlpha + r2 * alpha + let g = g1 * oneMinusAlpha + g2 * alpha + let b = b1 * oneMinusAlpha + b2 * alpha + let a = a1 * oneMinusAlpha + a2 * alpha + return UIColor(red: r, green: g, blue: b, alpha: a) + } + return self + } +} + +public extension CGSize { + public func fitted(_ size: CGSize) -> CGSize { + var fittedSize = self + if fittedSize.width > size.width { + fittedSize = CGSize(width: size.width, height: floor((fittedSize.height * size.width / max(fittedSize.width, 1.0)))) + } + if fittedSize.height > size.height { + fittedSize = CGSize(width: floor((fittedSize.width * size.height / max(fittedSize.height, 1.0))), height: size.height) + } + return fittedSize + } + + public func cropped(_ size: CGSize) -> CGSize { + return CGSize(width: min(size.width, self.width), height: min(size.height, self.height)) + } + + public func fittedToArea(_ area: CGFloat) -> CGSize { + if self.height < 1.0 || self.width < 1.0 { + return CGSize() + } + let aspect = self.width / self.height + let height = sqrt(area / aspect) + let width = aspect * height + return CGSize(width: floor(width), height: floor(height)) + } + + public func aspectFilled(_ size: CGSize) -> CGSize { + let scale = max(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) + return CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) + } + + public func aspectFitted(_ size: CGSize) -> CGSize { + let scale = min(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) + return CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) + } + + public func aspectFittedOrSmaller(_ size: CGSize) -> CGSize { + let scale = min(1.0, min(size.width / max(1.0, self.width), size.height / max(1.0, self.height))) + return CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) + } + + public func aspectFittedWithOverflow(_ size: CGSize, leeway: CGFloat) -> CGSize { + let scale = min(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) + var result = CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) + if result.width < size.width && result.width > size.width - leeway { + result.height += size.width - result.width + result.width = size.width + } + if result.height < size.height && result.height > size.height - leeway { + result.width += size.height - result.height + result.height = size.height + } + return result + } + + public func fittedToWidthOrSmaller(_ width: CGFloat) -> CGSize { + let scale = min(1.0, width / max(1.0, self.width)) + return CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) + } + + public func multipliedByScreenScale() -> CGSize { + let scale = UIScreenScale + return CGSize(width: self.width * scale, height: self.height * scale) + } + + public func dividedByScreenScale() -> CGSize { + let scale = UIScreenScale + return CGSize(width: self.width / scale, height: self.height / scale) + } + + public var integralFloor: CGSize { + return CGSize(width: floor(self.width), height: floor(self.height)) + } +} + +public func assertNotOnMainThread(_ file: String = #file, line: Int = #line) { + assert(!Thread.isMainThread, "\(file):\(line) running on main thread") +} + +public extension UIImage { + public func precomposed() -> UIImage { + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + self.draw(at: CGPoint()) + let result = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + if !UIEdgeInsetsEqualToEdgeInsets(self.capInsets, UIEdgeInsets()) { + return result.resizableImage(withCapInsets: self.capInsets, resizingMode: self.resizingMode) + } + return result + } +} + +private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) -> UIView? { + let view = UIView() + view.layer.isHidden = layer.isHidden + view.layer.opacity = layer.opacity + view.layer.contents = layer.contents + view.layer.contentsRect = layer.contentsRect + view.layer.contentsScale = layer.contentsScale + view.layer.contentsCenter = layer.contentsCenter + view.layer.contentsGravity = layer.contentsGravity + view.layer.masksToBounds = layer.masksToBounds + if let mask = layer.mask { + let maskLayer = CALayer() + maskLayer.contents = mask.contents + maskLayer.contentsRect = mask.contentsRect + maskLayer.contentsScale = mask.contentsScale + maskLayer.contentsCenter = mask.contentsCenter + maskLayer.contentsGravity = mask.contentsGravity + view.layer.mask = maskLayer + } + view.layer.cornerRadius = layer.cornerRadius + view.layer.backgroundColor = layer.backgroundColor + if let sublayers = layer.sublayers { + for sublayer in sublayers { + let subtree = makeSubtreeSnapshot(layer: sublayer, keepTransform: keepTransform) + if let subtree = subtree { + if keepTransform { + subtree.layer.transform = sublayer.transform + } + subtree.frame = sublayer.frame + subtree.bounds = sublayer.bounds + if let maskLayer = subtree.layer.mask { + maskLayer.frame = sublayer.bounds + } + view.addSubview(subtree) + } else { + return nil + } + } + } + return view +} + +private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? { + let view = CALayer() + view.isHidden = layer.isHidden + view.opacity = layer.opacity + view.contents = layer.contents + view.contentsRect = layer.contentsRect + view.contentsScale = layer.contentsScale + view.contentsCenter = layer.contentsCenter + view.contentsGravity = layer.contentsGravity + view.masksToBounds = layer.masksToBounds + view.cornerRadius = layer.cornerRadius + view.backgroundColor = layer.backgroundColor + if let sublayers = layer.sublayers { + for sublayer in sublayers { + let subtree = makeLayerSubtreeSnapshot(layer: sublayer) + if let subtree = subtree { + subtree.frame = sublayer.frame + subtree.bounds = sublayer.bounds + layer.addSublayer(subtree) + } else { + return nil + } + } + } + return view +} + +public extension UIView { + public func snapshotContentTree(unhide: Bool = false, keepTransform: Bool = false) -> UIView? { + let wasHidden = self.isHidden + if unhide && wasHidden { + self.isHidden = false + } + let snapshot = makeSubtreeSnapshot(layer: self.layer, keepTransform: keepTransform) + if unhide && wasHidden { + self.isHidden = true + } + if let snapshot = snapshot { + snapshot.frame = self.frame + return snapshot + } + + return nil + } +} + +public extension CALayer { + public func snapshotContentTree(unhide: Bool = false) -> CALayer? { + let wasHidden = self.isHidden + if unhide && wasHidden { + self.isHidden = false + } + let snapshot = makeLayerSubtreeSnapshot(layer: self) + if unhide && wasHidden { + self.isHidden = true + } + if let snapshot = snapshot { + snapshot.frame = self.frame + return snapshot + } + + return nil + } +} + +public extension CGRect { + public var topLeft: CGPoint { + return self.origin + } + + public var topRight: CGPoint { + return CGPoint(x: self.maxX, y: self.minY) + } + + public var bottomLeft: CGPoint { + return CGPoint(x: self.minX, y: self.maxY) + } + + public var bottomRight: CGPoint { + return CGPoint(x: self.maxX, y: self.maxY) + } + + public var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} + +public extension CGPoint { + public func offsetBy(dx: CGFloat, dy: CGFloat) -> CGPoint { + return CGPoint(x: self.x + dx, y: self.y + dy) + } +} diff --git a/submodules/Display/Display/UIMenuItem+Icons.h b/submodules/Display/Display/UIMenuItem+Icons.h new file mode 100644 index 0000000000..9db66d10b1 --- /dev/null +++ b/submodules/Display/Display/UIMenuItem+Icons.h @@ -0,0 +1,8 @@ +#import + +@interface UIMenuItem (Icons) + +- (instancetype)initWithTitle:(NSString *)title icon:(UIImage *)icon action:(SEL)action; + +@end + diff --git a/submodules/Display/Display/UIMenuItem+Icons.m b/submodules/Display/Display/UIMenuItem+Icons.m new file mode 100644 index 0000000000..1bd837c10c --- /dev/null +++ b/submodules/Display/Display/UIMenuItem+Icons.m @@ -0,0 +1,139 @@ +#import "UIMenuItem+Icons.h" + +#import "NSBag.h" +#import "RuntimeUtils.h" + +static const void *imageKey = &imageKey; +static const void *imageViewKey = &imageViewKey; +static NSString *const imageItemIdetifier = @"\uFEFF\u200B"; + +@interface UIMenuController (Icons) + +@end + +@implementation UIMenuController (Icons) + +- (UIMenuItem *)findImageItemByTitle:(NSString *)title { + if ([title hasSuffix:imageItemIdetifier]) { + for (UIMenuItem *item in self.menuItems) { + if ([item.title isEqualToString:title]) { + return item; + } + } + } + return nil; +} + +@end + + +@implementation UIMenuItem (Icons) + +- (instancetype)initWithTitle:(NSString *)title icon:(UIImage *)icon action:(SEL)action { + NSString *combinedTitle = title; + if (icon != nil) { + combinedTitle = [NSString stringWithFormat:@"%@%@", title, imageItemIdetifier]; + } + self = [self initWithTitle:combinedTitle action:action]; + if (self != nil) { + if (icon != nil) { + [self _tg_setImage:icon]; + } + } + return self; +} + +- (UIImage *)_tg_image { + return (UIImage *)[self associatedObjectForKey:imageKey]; +} + +- (void)_tg_setImage:(UIImage *)image { + [self setAssociatedObject:image forKey:imageKey associationPolicy:NSObjectAssociationPolicyRetain]; +} + +@end + +@interface NSString (Items) + +@end + +@implementation NSString (Items) + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + [RuntimeUtils swizzleInstanceMethodOfClass:[NSString class] currentSelector:@selector(sizeWithAttributes:) newSelector:@selector(_78724db9_sizeWithAttributes:)]; + }); +} + +- (CGSize)_78724db9_sizeWithAttributes:(NSDictionary *)attrs { + UIMenuItem *item = [[UIMenuController sharedMenuController] findImageItemByTitle:self]; + UIImage *image = item._tg_image; + if (image != nil) { + return image.size; + } else { + return [self _78724db9_sizeWithAttributes:attrs]; + } +} + +@end + + +@interface UILabel (Icons) + +@end + +@implementation UILabel (Icons) + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + [RuntimeUtils swizzleInstanceMethodOfClass:[UILabel class] currentSelector:@selector(drawTextInRect:) newSelector:@selector(_78724db9_drawTextInRect:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UILabel class] currentSelector:@selector(layoutSubviews) newSelector:@selector(_78724db9_layoutSubviews)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UILabel class] currentSelector:@selector(setFrame:) newSelector:@selector(_78724db9_setFrame:)]; + }); +} + +- (void)_78724db9_drawTextInRect:(CGRect)rect { + UIMenuItem *item = [[UIMenuController sharedMenuController] findImageItemByTitle:self.text]; + UIImage *image = item._tg_image; + if (image == nil) { + [self _78724db9_drawTextInRect:rect]; + } +} + +- (void)_78724db9_layoutSubviews { + UIMenuItem *item = [[UIMenuController sharedMenuController] findImageItemByTitle:self.text]; + UIImage *image = item._tg_image; + if (image == nil) { + [self _78724db9_layoutSubviews]; + return; + } + + CGPoint point = CGPointMake(ceil((self.bounds.size.width - image.size.width) / 2.0), ceil((self.bounds.size.height - image.size.height) / 2.0)); + UIImageView *imageView = [self associatedObjectForKey:imageViewKey]; + if (imageView == nil) { + imageView = [[UIImageView alloc] init]; + [self addSubview:imageView]; + [self setAssociatedObject:imageView forKey:imageViewKey associationPolicy:NSObjectAssociationPolicyRetain]; + } + + imageView.image = image; + imageView.frame = CGRectMake(point.x, point.y, image.size.width, image.size.height); +} + +- (void)_78724db9_setFrame:(CGRect)frame +{ + bool hasImage = [[UIMenuController sharedMenuController] findImageItemByTitle:self.text]._tg_image != nil; + CGRect rect = frame; + if (hasImage && self.superview != nil) { + rect = self.superview.bounds; + } + [self _78724db9_setFrame:rect]; +} + +@end diff --git a/submodules/Display/Display/UINavigationItem+Proxy.h b/submodules/Display/Display/UINavigationItem+Proxy.h new file mode 100644 index 0000000000..f378a98aa2 --- /dev/null +++ b/submodules/Display/Display/UINavigationItem+Proxy.h @@ -0,0 +1,54 @@ +#import + +typedef void (^UINavigationItemSetTitleListener)(NSString * _Nullable, bool); +typedef void (^UINavigationItemSetTitleViewListener)(UIView * _Nullable); +typedef void (^UINavigationItemSetImageListener)(UIImage * _Nullable); +typedef void (^UINavigationItemSetBarButtonItemListener)(UIBarButtonItem * _Nullable, UIBarButtonItem * _Nullable, BOOL); +typedef void (^UINavigationItemSetMutipleBarButtonItemsListener)(NSArray * _Nullable, BOOL); +typedef void (^UITabBarItemSetBadgeListener)(NSString * _Nullable); + +@interface UINavigationItem (Proxy) + +- (void)setTargetItem:(UINavigationItem * _Nullable)targetItem; +- (BOOL)hasTargetItem; + +- (void)setTitle:(NSString * _Nullable)title animated:(bool)animated; + +- (NSInteger)addSetTitleListener:(UINavigationItemSetTitleListener _Nonnull)listener; +- (void)removeSetTitleListener:(NSInteger)key; +- (NSInteger)addSetTitleViewListener:(UINavigationItemSetTitleViewListener _Nonnull)listener; +- (void)removeSetTitleViewListener:(NSInteger)key; +- (NSInteger)addSetLeftBarButtonItemListener:(UINavigationItemSetBarButtonItemListener _Nonnull)listener; +- (void)removeSetLeftBarButtonItemListener:(NSInteger)key; +- (NSInteger)addSetRightBarButtonItemListener:(UINavigationItemSetBarButtonItemListener _Nonnull)listener; +- (void)removeSetRightBarButtonItemListener:(NSInteger)key; +- (NSInteger)addSetMultipleRightBarButtonItemsListener:(UINavigationItemSetMutipleBarButtonItemsListener _Nonnull)listener; +- (void)removeSetMultipleRightBarButtonItemsListener:(NSInteger)key; +- (NSInteger)addSetBackBarButtonItemListener:(UINavigationItemSetBarButtonItemListener _Nonnull)listener; +- (void)removeSetBackBarButtonItemListener:(NSInteger)key; +- (NSInteger)addSetBadgeListener:(UITabBarItemSetBadgeListener _Nonnull)listener; +- (void)removeSetBadgeListener:(NSInteger)key; + +@property (nonatomic, strong) NSString * _Nullable badge; + +@end + +NSInteger UITabBarItem_addSetBadgeListener(UITabBarItem * _Nonnull item, UITabBarItemSetBadgeListener _Nonnull listener); + +@interface UITabBarItem (Proxy) + +- (void)removeSetBadgeListener:(NSInteger)key; + +- (NSInteger)addSetTitleListener:(UINavigationItemSetTitleListener _Nonnull)listener; +- (void)removeSetTitleListener:(NSInteger)key; + +- (NSInteger)addSetImageListener:(UINavigationItemSetImageListener _Nonnull)listener; +- (void)removeSetImageListener:(NSInteger)key; + +- (NSInteger)addSetSelectedImageListener:(UINavigationItemSetImageListener _Nonnull)listener; +- (void)removeSetSelectedImageListener:(NSInteger)key; + +- (NSObject * _Nullable)userInfo; +- (void)setUserInfo:(NSObject * _Nullable)userInfo; + +@end diff --git a/submodules/Display/Display/UINavigationItem+Proxy.m b/submodules/Display/Display/UINavigationItem+Proxy.m new file mode 100644 index 0000000000..8b166e5cd4 --- /dev/null +++ b/submodules/Display/Display/UINavigationItem+Proxy.m @@ -0,0 +1,413 @@ +#import "UINavigationItem+Proxy.h" + +#import "NSBag.h" +#import "RuntimeUtils.h" +#import "NSWeakReference.h" + +static const void *sourceItemKey = &sourceItemKey; +static const void *targetItemKey = &targetItemKey; +static const void *setTitleListenerBagKey = &setTitleListenerBagKey; +static const void *setImageListenerBagKey = &setImageListenerBagKey; +static const void *setSelectedImageListenerBagKey = &setSelectedImageListenerBagKey; +static const void *setTitleViewListenerBagKey = &setTitleViewListenerBagKey; +static const void *setLeftBarButtonItemListenerBagKey = &setLeftBarButtonItemListenerBagKey; +static const void *setRightBarButtonItemListenerBagKey = &setRightBarButtonItemListenerBagKey; +static const void *setMultipleRightBarButtonItemsListenerKey = &setMultipleRightBarButtonItemsListenerKey; +static const void *setBackBarButtonItemListenerBagKey = &setBackBarButtonItemListenerBagKey; +static const void *setBadgeListenerBagKey = &setBadgeListenerBagKey; +static const void *badgeKey = &badgeKey; +static const void *userInfoKey = &userInfoKey; + +@implementation UINavigationItem (Proxy) + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setTitle:) newSelector:@selector(_ac91f40f_setTitle:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setTitleView:) newSelector:@selector(_ac91f40f_setTitleView:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setLeftBarButtonItem:) newSelector:@selector(_ac91f40f_setLeftBarButtonItem:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setLeftBarButtonItem:animated:) newSelector:@selector(_ac91f40f_setLeftBarButtonItem:animated:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setRightBarButtonItem:) newSelector:@selector(_ac91f40f_setRightBarButtonItem:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setRightBarButtonItem:animated:) newSelector:@selector(_ac91f40f_setRightBarButtonItem:animated:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setRightBarButtonItems:) newSelector:@selector(_ac91f40f_setRightBarButtonItems:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setRightBarButtonItems:animated:) newSelector:@selector(_ac91f40f_setRightBarButtonItems:animated:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UINavigationItem class] currentSelector:@selector(setBackBarButtonItem:) newSelector:@selector(_ac91f40f_setBackBarButtonItem:)]; + }); +} + +- (void)_ac91f40f_setTitle:(NSString *)title +{ + [self _ac91f40f_setTitle:title]; + + UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; + if (targetItem != nil) { + [targetItem setTitle:title]; + } else { + [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] enumerateItems:^(UINavigationItemSetTitleListener listener) { + listener(title, false); + }]; + } +} + +- (void)setTitle:(NSString * _Nullable)title animated:(bool)animated { + [self _ac91f40f_setTitle:title]; + + UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; + if (targetItem != nil) { + [targetItem setTitle:title]; + } else { + [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] enumerateItems:^(UINavigationItemSetTitleListener listener) { + listener(title, animated); + }]; + } +} + +- (void)_ac91f40f_setTitleView:(UIView *)titleView +{ + [self _ac91f40f_setTitleView:titleView]; + + UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; + if (targetItem != nil) { + [targetItem setTitleView:titleView]; + } else { + [(NSBag *)[self associatedObjectForKey:setTitleViewListenerBagKey] enumerateItems:^(UINavigationItemSetTitleViewListener listener) { + listener(titleView); + }]; + } +} + +- (void)_ac91f40f_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem { + [self setLeftBarButtonItem:leftBarButtonItem animated:false]; +} + +- (void)_ac91f40f_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem animated:(BOOL)animated +{ + UIBarButtonItem *previousItem = self.leftBarButtonItem; + + [self _ac91f40f_setLeftBarButtonItem:leftBarButtonItem animated:animated]; + + UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; + if (targetItem != nil) { + [targetItem setLeftBarButtonItem:leftBarButtonItem animated:animated]; + } else { + [(NSBag *)[self associatedObjectForKey:setLeftBarButtonItemListenerBagKey] enumerateItems:^(UINavigationItemSetBarButtonItemListener listener) { + listener(previousItem, leftBarButtonItem, animated); + }]; + } +} + +- (void)_ac91f40f_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem { + [self setRightBarButtonItem:rightBarButtonItem animated:false]; +} + +- (void)_ac91f40f_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem animated:(BOOL)animated +{ + UIBarButtonItem *previousItem = self.rightBarButtonItem; + + [self _ac91f40f_setRightBarButtonItem:rightBarButtonItem animated:animated]; + + UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; + if (targetItem != nil) { + [targetItem setRightBarButtonItem:rightBarButtonItem animated:animated]; + } else { + [(NSBag *)[self associatedObjectForKey:setRightBarButtonItemListenerBagKey] enumerateItems:^(UINavigationItemSetBarButtonItemListener listener) { + listener(previousItem, rightBarButtonItem, animated); + }]; + } +} + +- (void)_ac91f40f_setRightBarButtonItems:(NSArray *)rightBarButtonItems { + [self setRightBarButtonItems:rightBarButtonItems animated:false]; +} + +- (void)_ac91f40f_setRightBarButtonItems:(NSArray *)rightBarButtonItems animated:(BOOL)animated +{ + [self _ac91f40f_setRightBarButtonItems:rightBarButtonItems animated:animated]; + + UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; + if (targetItem != nil) { + [targetItem setRightBarButtonItems:rightBarButtonItems animated:animated]; + } else { + [(NSBag *)[self associatedObjectForKey:setMultipleRightBarButtonItemsListenerKey] enumerateItems:^(UINavigationItemSetMutipleBarButtonItemsListener listener) { + listener(rightBarButtonItems, animated); + }]; + } +} + +- (void)_ac91f40f_setBackBarButtonItem:(UIBarButtonItem *)backBarButtonItem +{ + UIBarButtonItem *previousItem = self.rightBarButtonItem; + + [self _ac91f40f_setBackBarButtonItem:backBarButtonItem]; + + UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; + if (targetItem != nil) { + [targetItem setBackBarButtonItem:backBarButtonItem]; + } else { + [(NSBag *)[self associatedObjectForKey:setBackBarButtonItemListenerBagKey] enumerateItems:^(UINavigationItemSetBarButtonItemListener listener) { + listener(previousItem, backBarButtonItem, false); + }]; + } +} + +- (void)setTargetItem:(UINavigationItem *)targetItem { + NSWeakReference *previousSourceItem = [targetItem associatedObjectForKey:sourceItemKey]; + [(UINavigationItem *)previousSourceItem.value setAssociatedObject:nil forKey:targetItemKey associationPolicy:NSObjectAssociationPolicyRetain]; + + [self setAssociatedObject:targetItem forKey:targetItemKey associationPolicy:NSObjectAssociationPolicyRetain]; + [targetItem setAssociatedObject:[[NSWeakReference alloc] initWithValue:self] forKey:sourceItemKey associationPolicy:NSObjectAssociationPolicyRetain]; + + if ((targetItem.title != nil) != (self.title != nil) || ![targetItem.title isEqualToString:self.title]) { + targetItem.title = self.title; + } + if (targetItem.titleView != self.titleView) { + [targetItem setTitleView:self.titleView]; + } + if (targetItem.leftBarButtonItem != self.leftBarButtonItem) { + [targetItem setLeftBarButtonItem:self.leftBarButtonItem]; + } + if (targetItem.rightBarButtonItem != self.rightBarButtonItem) { + [targetItem setRightBarButtonItem:self.rightBarButtonItem]; + } + if (targetItem.backBarButtonItem != self.backBarButtonItem) { + [targetItem setBackBarButtonItem:self.backBarButtonItem]; + } +} + +- (BOOL)hasTargetItem { + return [self associatedObjectForKey:targetItemKey] != nil; +} + +- (NSInteger)addSetTitleListener:(UINavigationItemSetTitleListener)listener +{ + NSBag *bag = [self associatedObjectForKey:setTitleListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setTitleListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetTitleListener:(NSInteger)key +{ + [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetTitleViewListener:(UINavigationItemSetTitleViewListener)listener +{ + NSBag *bag = [self associatedObjectForKey:setTitleViewListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setTitleViewListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetTitleViewListener:(NSInteger)key +{ + [(NSBag *)[self associatedObjectForKey:setTitleViewListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetLeftBarButtonItemListener:(UINavigationItemSetBarButtonItemListener)listener +{ + NSBag *bag = [self associatedObjectForKey:setLeftBarButtonItemListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setLeftBarButtonItemListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetLeftBarButtonItemListener:(NSInteger)key +{ + [(NSBag *)[self associatedObjectForKey:setLeftBarButtonItemListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetRightBarButtonItemListener:(UINavigationItemSetBarButtonItemListener)listener +{ + NSBag *bag = [self associatedObjectForKey:setRightBarButtonItemListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setRightBarButtonItemListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetRightBarButtonItemListener:(NSInteger)key +{ + [(NSBag *)[self associatedObjectForKey:setRightBarButtonItemListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetMultipleRightBarButtonItemsListener:(UINavigationItemSetMutipleBarButtonItemsListener _Nonnull)listener { + NSBag *bag = [self associatedObjectForKey:setMultipleRightBarButtonItemsListenerKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setMultipleRightBarButtonItemsListenerKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetMultipleRightBarButtonItemsListener:(NSInteger)key { + [(NSBag *)[self associatedObjectForKey:setMultipleRightBarButtonItemsListenerKey] removeItem:key]; +} + +- (NSInteger)addSetBackBarButtonItemListener:(UINavigationItemSetBarButtonItemListener)listener { + NSBag *bag = [self associatedObjectForKey:setBackBarButtonItemListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setBackBarButtonItemListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetBackBarButtonItemListener:(NSInteger)key { + [(NSBag *)[self associatedObjectForKey:setBackBarButtonItemListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetBadgeListener:(UITabBarItemSetBadgeListener)listener { + NSBag *bag = [self associatedObjectForKey:setBadgeListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setBadgeListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetBadgeListener:(NSInteger)key { + [(NSBag *)[self associatedObjectForKey:setBadgeListenerBagKey] removeItem:key]; +} + +- (void)setBadge:(NSString *)badge { + [self setAssociatedObject:badge forKey:badgeKey]; + + [(NSBag *)[self associatedObjectForKey:setBadgeListenerBagKey] enumerateItems:^(UITabBarItemSetBadgeListener listener) { + listener(badge); + }]; +} + +- (NSString *)badge { + return [self associatedObjectForKey:badgeKey]; +} + +@end + +@implementation UITabBarItem (Proxy) + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + [RuntimeUtils swizzleInstanceMethodOfClass:[UITabBarItem class] currentSelector:@selector(setBadgeValue:) newSelector:@selector(_ac91f40f_setBadgeValue:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UITabBarItem class] currentSelector:@selector(setTitle:) newSelector:@selector(_ac91f40f_setTitle:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UITabBarItem class] currentSelector:@selector(setImage:) newSelector:@selector(_ac91f40f_setImage:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UITabBarItem class] currentSelector:@selector(setSelectedImage:) newSelector:@selector(_ac91f40f_setSelectedImage:)]; + }); +} + +NSInteger UITabBarItem_addSetBadgeListener(UITabBarItem *item, UITabBarItemSetBadgeListener listener) { + NSBag *bag = [item associatedObjectForKey:setBadgeListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [item setAssociatedObject:bag forKey:setBadgeListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetBadgeListener:(NSInteger)key { + [(NSBag *)[self associatedObjectForKey:setBadgeListenerBagKey] removeItem:key]; +} + +- (void)_ac91f40f_setBadgeValue:(NSString *)value { + [self _ac91f40f_setBadgeValue:value]; + + [(NSBag *)[self associatedObjectForKey:setBadgeListenerBagKey] enumerateItems:^(UITabBarItemSetBadgeListener listener) { + listener(value); + }]; +} + +- (void)_ac91f40f_setTitle:(NSString *)value { + [self _ac91f40f_setTitle:value]; + + [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] enumerateItems:^(UINavigationItemSetTitleListener listener) { + listener(value, false); + }]; +} + +- (void)_ac91f40f_setImage:(UIImage *)value { + [self _ac91f40f_setImage:value]; + + [(NSBag *)[self associatedObjectForKey:setImageListenerBagKey] enumerateItems:^(UINavigationItemSetImageListener listener) { + listener(value); + }]; +} + +- (void)_ac91f40f_setSelectedImage:(UIImage *)value { + [self _ac91f40f_setSelectedImage:value]; + + [(NSBag *)[self associatedObjectForKey:setSelectedImageListenerBagKey] enumerateItems:^(UINavigationItemSetImageListener listener) { + listener(value); + }]; +} + +- (NSInteger)addSetTitleListener:(UINavigationItemSetTitleListener)listener { + NSBag *bag = [self associatedObjectForKey:setTitleListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setTitleListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetTitleListener:(NSInteger)key { + [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetImageListener:(UINavigationItemSetImageListener)listener { + NSBag *bag = [self associatedObjectForKey:setImageListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setImageListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetImageListener:(NSInteger)key { + [(NSBag *)[self associatedObjectForKey:setImageListenerBagKey] removeItem:key]; +} + +- (NSInteger)addSetSelectedImageListener:(UINavigationItemSetImageListener)listener { + NSBag *bag = [self associatedObjectForKey:setSelectedImageListenerBagKey]; + if (bag == nil) + { + bag = [[NSBag alloc] init]; + [self setAssociatedObject:bag forKey:setSelectedImageListenerBagKey]; + } + return [bag addItem:[listener copy]]; +} + +- (void)removeSetSelectedImageListener:(NSInteger)key { + [(NSBag *)[self associatedObjectForKey:setSelectedImageListenerBagKey] removeItem:key]; +} + +- (NSObject * _Nullable)userInfo { + return [self associatedObjectForKey:userInfoKey]; +} + +- (void)setUserInfo:(NSObject * _Nullable)userInfo { + [self setAssociatedObject:userInfo forKey:userInfoKey]; +} + +@end diff --git a/submodules/Display/Display/UITracingLayerView.swift b/submodules/Display/Display/UITracingLayerView.swift new file mode 100644 index 0000000000..7834962799 --- /dev/null +++ b/submodules/Display/Display/UITracingLayerView.swift @@ -0,0 +1,36 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +#if BUCK +import DisplayPrivate +#endif + +open class UITracingLayerView: UIView { + private var scheduledWithLayout: (() -> Void)? + + open func schedule(layout f: @escaping () -> Void) { + self.scheduledWithLayout = f + self.setNeedsLayout() + } + + override open var autoresizingMask: UIViewAutoresizing { + get { + return [] + } set(value) { + } + } + + override open class var layerClass: AnyClass { + return CATracingLayer.self + } + + override open func layoutSubviews() { + super.layoutSubviews() + + if let scheduledWithLayout = self.scheduledWithLayout { + self.scheduledWithLayout = nil + scheduledWithLayout() + } + } +} diff --git a/submodules/Display/Display/UIViewController+Navigation.h b/submodules/Display/Display/UIViewController+Navigation.h new file mode 100644 index 0000000000..5c71ced132 --- /dev/null +++ b/submodules/Display/Display/UIViewController+Navigation.h @@ -0,0 +1,35 @@ +#import + +typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { + UIResponderDisableAutomaticKeyboardHandlingForward = 1 << 0, + UIResponderDisableAutomaticKeyboardHandlingBackward = 1 << 1 +}; + +@interface UIViewController (Navigation) + +- (void)setHintWillBePresentedInPreviewingContext:(BOOL)value; +- (BOOL)isPresentedInPreviewingContext; +- (void)setIgnoreAppearanceMethodInvocations:(BOOL)ignoreAppearanceMethodInvocations; +- (BOOL)ignoreAppearanceMethodInvocations; +- (void)navigation_setNavigationController:(UINavigationController * _Nullable)navigationControlller; +- (void)navigation_setPresentingViewController:(UIViewController * _Nullable)presentingViewController; +- (void)navigation_setDismiss:(void (^_Nullable)())dismiss rootController:( UIViewController * _Nullable )rootController; +- (void)state_setNeedsStatusBarAppearanceUpdate:(void (^_Nullable)())block; + +@end + +@interface UIView (Navigation) + +@property (nonatomic) bool disablesInteractiveTransitionGestureRecognizer; +@property (nonatomic, copy) bool (^ disablesInteractiveTransitionGestureRecognizerNow)(); + +@property (nonatomic) UIResponderDisableAutomaticKeyboardHandling disableAutomaticKeyboardHandling; + +@property (nonatomic, copy) BOOL (^_Nullable interactiveTransitionGestureRecognizerTest)(CGPoint); + +- (void)input_setInputAccessoryHeightProvider:(CGFloat (^_Nullable)())block; +- (CGFloat)input_getInputAccessoryHeight; + +@end + +void applyKeyboardAutocorrection(); diff --git a/submodules/Display/Display/UIViewController+Navigation.m b/submodules/Display/Display/UIViewController+Navigation.m new file mode 100644 index 0000000000..cfef8b7103 --- /dev/null +++ b/submodules/Display/Display/UIViewController+Navigation.m @@ -0,0 +1,310 @@ +#import "UIViewController+Navigation.h" + +#import "RuntimeUtils.h" +#import + +#import "NSWeakReference.h" + +@interface UIViewControllerPresentingProxy : UIViewController + +@property (nonatomic, copy) void (^dismiss)(); +@property (nonatomic, strong, readonly) UIViewController *rootController; + +@end + +@implementation UIViewControllerPresentingProxy + +- (instancetype)initWithRootController:(UIViewController *)rootController { + _rootController = rootController; + return self; +} + +- (void)dismissViewControllerAnimated:(BOOL)__unused flag completion:(void (^)(void))completion { + if (_dismiss) { + _dismiss(); + } + if (completion) { + completion(); + } +} + +@end + +static const void *UIViewControllerIgnoreAppearanceMethodInvocationsKey = &UIViewControllerIgnoreAppearanceMethodInvocationsKey; +static const void *UIViewControllerNavigationControllerKey = &UIViewControllerNavigationControllerKey; +static const void *UIViewControllerPresentingControllerKey = &UIViewControllerPresentingControllerKey; +static const void *UIViewControllerPresentingProxyControllerKey = &UIViewControllerPresentingProxyControllerKey; +static const void *disablesInteractiveTransitionGestureRecognizerKey = &disablesInteractiveTransitionGestureRecognizerKey; +static const void *disablesInteractiveTransitionGestureRecognizerNowKey = &disablesInteractiveTransitionGestureRecognizerNowKey; +static const void *disableAutomaticKeyboardHandlingKey = &disableAutomaticKeyboardHandlingKey; +static const void *setNeedsStatusBarAppearanceUpdateKey = &setNeedsStatusBarAppearanceUpdateKey; +static const void *inputAccessoryHeightProviderKey = &inputAccessoryHeightProviderKey; +static const void *interactiveTransitionGestureRecognizerTestKey = &interactiveTransitionGestureRecognizerTestKey; +static const void *UIViewControllerHintWillBePresentedInPreviewingContextKey = &UIViewControllerHintWillBePresentedInPreviewingContextKey; + +static bool notyfyingShiftState = false; + +@interface UIKeyboardImpl_65087dc8: UIView + +@end + +@implementation UIKeyboardImpl_65087dc8 + +- (void)notifyShiftState { + static void (*impl)(id, SEL) = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Method m = class_getInstanceMethod([UIKeyboardImpl_65087dc8 class], @selector(notifyShiftState)); + impl = (typeof(impl))method_getImplementation(m); + }); + if (impl) { + notyfyingShiftState = true; + impl(self, @selector(notifyShiftState)); + notyfyingShiftState = false; + } +} + +@end + +@interface UIInputWindowController_65087dc8: UIViewController + +@end + +@implementation UIInputWindowController_65087dc8 + +- (void)updateViewConstraints { + static void (*impl)(id, SEL) = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Method m = class_getInstanceMethod([UIInputWindowController_65087dc8 class], @selector(updateViewConstraints)); + impl = (typeof(impl))method_getImplementation(m); + }); + if (impl) { + if (!notyfyingShiftState) { + impl(self, @selector(updateViewConstraints)); + } + } +} + +@end + +@implementation UIViewController (Navigation) + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(viewWillAppear:) newSelector:@selector(_65087dc8_viewWillAppear:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(viewDidAppear:) newSelector:@selector(_65087dc8_viewDidAppear:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(viewWillDisappear:) newSelector:@selector(_65087dc8_viewWillDisappear:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(viewDidDisappear:) newSelector:@selector(_65087dc8_viewDidDisappear:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(navigationController) newSelector:@selector(_65087dc8_navigationController)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentingViewController) newSelector:@selector(_65087dc8_presentingViewController)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentViewController:animated:completion:) newSelector:@selector(_65087dc8_presentViewController:animated:completion:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(setNeedsStatusBarAppearanceUpdate) newSelector:@selector(_65087dc8_setNeedsStatusBarAppearanceUpdate)]; + + //[RuntimeUtils swizzleInstanceMethodOfClass:NSClassFromString(@"UIKeyboardImpl") currentSelector:@selector(notifyShiftState) withAnotherClass:[UIKeyboardImpl_65087dc8 class] newSelector:@selector(notifyShiftState)]; + //[RuntimeUtils swizzleInstanceMethodOfClass:NSClassFromString(@"UIInputWindowController") currentSelector:@selector(updateViewConstraints) withAnotherClass:[UIInputWindowController_65087dc8 class] newSelector:@selector(updateViewConstraints)]; + }); +} + +- (void)setHintWillBePresentedInPreviewingContext:(BOOL)value { + [self setAssociatedObject:@(value) forKey:UIViewControllerHintWillBePresentedInPreviewingContextKey]; +} + +- (BOOL)isPresentedInPreviewingContext { + if ([[self associatedObjectForKey:UIViewControllerHintWillBePresentedInPreviewingContextKey] boolValue]) { + return true; + } else { + return false; + } +} + +- (void)setIgnoreAppearanceMethodInvocations:(BOOL)ignoreAppearanceMethodInvocations +{ + [self setAssociatedObject:@(ignoreAppearanceMethodInvocations) forKey:UIViewControllerIgnoreAppearanceMethodInvocationsKey]; +} + +- (BOOL)ignoreAppearanceMethodInvocations +{ + return [[self associatedObjectForKey:UIViewControllerIgnoreAppearanceMethodInvocationsKey] boolValue]; +} + +- (void)_65087dc8_viewWillAppear:(BOOL)animated +{ + if (![self ignoreAppearanceMethodInvocations]) + [self _65087dc8_viewWillAppear:animated]; +} + +- (void)_65087dc8_viewDidAppear:(BOOL)animated +{ + if (![self ignoreAppearanceMethodInvocations]) + [self _65087dc8_viewDidAppear:animated]; +} + +- (void)_65087dc8_viewWillDisappear:(BOOL)animated +{ + if (![self ignoreAppearanceMethodInvocations]) + [self _65087dc8_viewWillDisappear:animated]; +} + +- (void)_65087dc8_viewDidDisappear:(BOOL)animated +{ + if (![self ignoreAppearanceMethodInvocations]) + [self _65087dc8_viewDidDisappear:animated]; +} + +- (void)navigation_setNavigationController:(UINavigationController * _Nullable)navigationControlller { + [self setAssociatedObject:[[NSWeakReference alloc] initWithValue:navigationControlller] forKey:UIViewControllerNavigationControllerKey]; +} + +- (UINavigationController *)_65087dc8_navigationController { + UINavigationController *navigationController = self._65087dc8_navigationController; + if (navigationController != nil) { + return navigationController; + } + + UIViewController *parentController = self.parentViewController; + + navigationController = parentController.navigationController; + if (navigationController != nil) { + return navigationController; + } + + return ((NSWeakReference *)[self associatedObjectForKey:UIViewControllerNavigationControllerKey]).value; +} + +- (void)navigation_setPresentingViewController:(UIViewController *)presentingViewController { + [self setAssociatedObject:[[NSWeakReference alloc] initWithValue:presentingViewController] forKey:UIViewControllerPresentingControllerKey]; +} + +- (void)navigation_setDismiss:(void (^_Nullable)())dismiss rootController:(UIViewController *)rootController { + UIViewControllerPresentingProxy *proxy = [[UIViewControllerPresentingProxy alloc] initWithRootController:rootController]; + proxy.dismiss = dismiss; + [self setAssociatedObject:proxy forKey:UIViewControllerPresentingProxyControllerKey]; +} + +- (UIViewController *)_65087dc8_presentingViewController { + UINavigationController *navigationController = self.navigationController; + if (navigationController.presentingViewController != nil) { + return navigationController.presentingViewController; + } + + UIViewController *controller = ((NSWeakReference *)[self associatedObjectForKey:UIViewControllerPresentingControllerKey]).value; + if (controller != nil) { + return controller; + } + + UIViewController *result = [self associatedObjectForKey:UIViewControllerPresentingProxyControllerKey]; + if (result != nil) { + return result; + } + + return [self _65087dc8_presentingViewController]; +} + +- (void)_65087dc8_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion { + [self _65087dc8_presentViewController:viewControllerToPresent animated:flag completion:completion]; +} + +- (void)_65087dc8_setNeedsStatusBarAppearanceUpdate { + [self _65087dc8_setNeedsStatusBarAppearanceUpdate]; + + void (^block)() = [self associatedObjectForKey:setNeedsStatusBarAppearanceUpdateKey]; + if (block) { + block(); + } +} + +- (void)state_setNeedsStatusBarAppearanceUpdate:(void (^_Nullable)())block { + [self setAssociatedObject:[block copy] forKey:setNeedsStatusBarAppearanceUpdateKey]; +} + +@end + +@implementation UIView (Navigation) + +- (bool)disablesInteractiveTransitionGestureRecognizer { + return [[self associatedObjectForKey:disablesInteractiveTransitionGestureRecognizerKey] boolValue]; +} + +- (void)setDisablesInteractiveTransitionGestureRecognizer:(bool)disablesInteractiveTransitionGestureRecognizer { + [self setAssociatedObject:@(disablesInteractiveTransitionGestureRecognizer) forKey:disablesInteractiveTransitionGestureRecognizerKey]; +} + +- (bool (^)())disablesInteractiveTransitionGestureRecognizerNow { + return [self associatedObjectForKey:disablesInteractiveTransitionGestureRecognizerNowKey]; +} + +- (void)setDisablesInteractiveTransitionGestureRecognizerNow:(bool (^)())disablesInteractiveTransitionGestureRecognizerNow { + [self setAssociatedObject:[disablesInteractiveTransitionGestureRecognizerNow copy] forKey:disablesInteractiveTransitionGestureRecognizerNowKey]; +} + +- (BOOL (^)(CGPoint))interactiveTransitionGestureRecognizerTest { + return [self associatedObjectForKey:interactiveTransitionGestureRecognizerTestKey]; +} + +- (void)setInteractiveTransitionGestureRecognizerTest:(BOOL (^)(CGPoint))block { + [self setAssociatedObject:[block copy] forKey:interactiveTransitionGestureRecognizerTestKey]; +} + +- (UIResponderDisableAutomaticKeyboardHandling)disableAutomaticKeyboardHandling { + return (UIResponderDisableAutomaticKeyboardHandling)[[self associatedObjectForKey:disableAutomaticKeyboardHandlingKey] unsignedIntegerValue]; +} + +- (void)setDisableAutomaticKeyboardHandling:(UIResponderDisableAutomaticKeyboardHandling)disableAutomaticKeyboardHandling { + [self setAssociatedObject:@(disableAutomaticKeyboardHandling) forKey:disableAutomaticKeyboardHandlingKey]; +} + +- (void)input_setInputAccessoryHeightProvider:(CGFloat (^_Nullable)())block { + [self setAssociatedObject:[block copy] forKey:inputAccessoryHeightProviderKey]; +} + +- (CGFloat)input_getInputAccessoryHeight { + CGFloat (^block)() = [self associatedObjectForKey:inputAccessoryHeightProviderKey]; + if (block) { + return block(); + } + return 0.0f; +} + +@end + +static NSString *TGEncodeText(NSString *string, int key) +{ + NSMutableString *result = [[NSMutableString alloc] init]; + + for (int i = 0; i < (int)[string length]; i++) + { + unichar c = [string characterAtIndex:i]; + c += key; + [result appendString:[NSString stringWithCharacters:&c length:1]]; + } + + return result; +} + +void applyKeyboardAutocorrection() { + static Class keyboardClass = NULL; + static SEL currentInstanceSelector = NULL; + static SEL applyVariantSelector = NULL; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keyboardClass = NSClassFromString(TGEncodeText(@"VJLfzcpbse", -1)); + + currentInstanceSelector = NSSelectorFromString(TGEncodeText(@"bdujwfLfzcpbse", -1)); + applyVariantSelector = NSSelectorFromString(TGEncodeText(@"bddfquBvupdpssfdujpo", -1)); + }); + + if ([keyboardClass respondsToSelector:currentInstanceSelector]) + { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + id currentInstance = [keyboardClass performSelector:currentInstanceSelector]; + if ([currentInstance respondsToSelector:applyVariantSelector]) + [currentInstance performSelector:applyVariantSelector]; +#pragma clang diagnostic pop + } +} diff --git a/submodules/Display/Display/UIWindow+OrientationChange.h b/submodules/Display/Display/UIWindow+OrientationChange.h new file mode 100644 index 0000000000..b6f1320618 --- /dev/null +++ b/submodules/Display/Display/UIWindow+OrientationChange.h @@ -0,0 +1,11 @@ +#import + +@interface UIWindow (OrientationChange) + +- (bool)isRotating; ++ (void)addPostDeviceOrientationDidChangeBlock:(void (^)())block; ++ (bool)isDeviceRotating; + +- (void)_updateToInterfaceOrientation:(int)arg1 duration:(double)arg2 force:(BOOL)arg3; + +@end diff --git a/submodules/Display/Display/UIWindow+OrientationChange.m b/submodules/Display/Display/UIWindow+OrientationChange.m new file mode 100644 index 0000000000..f44d682e5e --- /dev/null +++ b/submodules/Display/Display/UIWindow+OrientationChange.m @@ -0,0 +1,121 @@ +#import "UIWindow+OrientationChange.h" + +#import "RuntimeUtils.h" +#import "NotificationCenterUtils.h" + +static const void *isRotatingKey = &isRotatingKey; + +static NSMutableArray *postDeviceDidChangeOrientationBlocks() { + static NSMutableArray *array = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + array = [[NSMutableArray alloc] init]; + }); + return array; +} + +static bool _isDeviceRotating = false; + +@implementation UIWindow (OrientationChange) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ { + [NotificationCenterUtils addNotificationHandler:^bool(NSString *name, id object, NSDictionary *userInfo, void (^passNotification)()) { + if ([name isEqualToString:@"UIWindowWillRotateNotification"]) { + [(UIWindow *)object setRotating:true]; + + if (NSClassFromString(@"NSUserActivity") == NULL) { + UIInterfaceOrientation orientation = [userInfo[@"UIWindowNewOrientationUserInfoKey"] integerValue]; + CGSize screenSize = [UIScreen mainScreen].bounds.size; + if (screenSize.width > screenSize.height) + { + CGFloat tmp = screenSize.height; + screenSize.height = screenSize.width; + screenSize.width = tmp; + } + CGSize windowSize = CGSizeZero; + CGFloat windowRotation = 0.0; + bool landscape = false; + switch (orientation) { + case UIInterfaceOrientationPortrait: + windowSize = screenSize; + break; + case UIInterfaceOrientationPortraitUpsideDown: + windowRotation = (CGFloat)(M_PI); + windowSize = screenSize; + break; + case UIInterfaceOrientationLandscapeLeft: + landscape = true; + windowRotation = (CGFloat)(-M_PI / 2.0); + windowSize = CGSizeMake(screenSize.height, screenSize.width); + break; + case UIInterfaceOrientationLandscapeRight: + landscape = true; + windowRotation = (CGFloat)(M_PI / 2.0); + windowSize = CGSizeMake(screenSize.height, screenSize.width); + break; + default: + break; + } + + [UIView animateWithDuration:0.3 animations:^ + { + CGAffineTransform transform = CGAffineTransformIdentity; + transform = CGAffineTransformRotate(transform, windowRotation); + ((UIWindow *)object).transform = transform; + ((UIWindow *)object).bounds = CGRectMake(0.0f, 0.0f, windowSize.width, windowSize.height); + }]; + } + + passNotification(); + + return true; + } else if ([name isEqualToString:@"UIWindowDidRotateNotification"]) { + [(UIWindow *)object setRotating:false]; + } else if ([name isEqualToString:UIDeviceOrientationDidChangeNotification]) { + //NSLog(@"notification start: %@", name); + + _isDeviceRotating = true; + + passNotification(); + + if (postDeviceDidChangeOrientationBlocks().count != 0) { + NSArray *blocks = [postDeviceDidChangeOrientationBlocks() copy]; + [postDeviceDidChangeOrientationBlocks() removeAllObjects]; + for (dispatch_block_t block in blocks) { + block(); + } + } + + _isDeviceRotating = false; + + //NSLog(@"notification end: %@", name); + + return true; + } + + return false; + }]; + }); +} + ++ (void)addPostDeviceOrientationDidChangeBlock:(void (^)())block { + [postDeviceDidChangeOrientationBlocks() addObject:[block copy]]; +} + +- (void)setRotating:(bool)rotating +{ + [self setAssociatedObject:@(rotating) forKey:isRotatingKey]; +} + +- (bool)isRotating +{ + return [[self associatedObjectForKey:isRotatingKey] boolValue]; +} + ++ (bool)isDeviceRotating { + return _isDeviceRotating; +} + +@end diff --git a/submodules/Display/Display/UniversalMasterController.swift b/submodules/Display/Display/UniversalMasterController.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/submodules/Display/Display/UniversalTapRecognizer.swift b/submodules/Display/Display/UniversalTapRecognizer.swift new file mode 100644 index 0000000000..a98268147b --- /dev/null +++ b/submodules/Display/Display/UniversalTapRecognizer.swift @@ -0,0 +1,44 @@ +import UIKit +import UIKit.UIGestureRecognizerSubclass + +private class TimerTargetWrapper: NSObject { + let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + } + + @objc func timerEvent() { + self.f() + } +} + +class UniversalTapRecognizer: UITapGestureRecognizer { + private let tapMaxDelay: Double = 0.15 + + private var timer: Timer? + + deinit { + self.timer?.invalidate() + } + + override func reset() { + super.reset() + + self.timer?.invalidate() + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + let timer = Timer(timeInterval: self.tapMaxDelay, target: TimerTargetWrapper({ [weak self] in + if let strongSelf = self { + if strongSelf.state != .ended { + strongSelf.state = .failed + } + } + }), selector: #selector(TimerTargetWrapper.timerEvent), userInfo: nil, repeats: false) + self.timer = timer + RunLoop.main.add(timer, forMode: .commonModes) + } +} diff --git a/submodules/Display/Display/ViewController.swift b/submodules/Display/Display/ViewController.swift new file mode 100644 index 0000000000..fea78f9d79 --- /dev/null +++ b/submodules/Display/Display/ViewController.swift @@ -0,0 +1,568 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +#if BUCK +import DisplayPrivate +#endif + +private func findCurrentResponder(_ view: UIView) -> UIResponder? { + if view.isFirstResponder { + return view + } else { + for subview in view.subviews { + if let result = findCurrentResponder(subview) { + return result + } + } + return nil + } +} + +private func findWindow(_ view: UIView) -> WindowHost? { + if let view = view as? WindowHost { + return view + } else if let superview = view.superview { + return findWindow(superview) + } else { + return nil + } +} + +public enum ViewControllerPresentationAnimation { + case none + case modalSheet +} + +public struct ViewControllerSupportedOrientations { + public var regularSize: UIInterfaceOrientationMask + public var compactSize: UIInterfaceOrientationMask + + public init(regularSize: UIInterfaceOrientationMask, compactSize: UIInterfaceOrientationMask) { + self.regularSize = regularSize + self.compactSize = compactSize + } + + public func intersection(_ other: ViewControllerSupportedOrientations) -> ViewControllerSupportedOrientations { + return ViewControllerSupportedOrientations(regularSize: self.regularSize.intersection(other.regularSize), compactSize: self.compactSize.intersection(other.compactSize)) + } +} + +open class ViewControllerPresentationArguments { + public let presentationAnimation: ViewControllerPresentationAnimation + public let completion: (() -> Void)? + + public init(presentationAnimation: ViewControllerPresentationAnimation, completion: (() -> Void)? = nil) { + self.presentationAnimation = presentationAnimation + self.completion = completion + } +} + +@objc open class ViewController: UIViewController, ContainableController { + private var validLayout: ContainerViewLayout? + public var currentlyAppliedLayout: ContainerViewLayout? { + return self.validLayout + } + + private let presentationContext: PresentationContext + + public final var supportedOrientations: ViewControllerSupportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) + public final var lockedOrientation: UIInterfaceOrientationMask? + public final var lockOrientation: Bool = false { + didSet { + if self.lockOrientation != oldValue { + if !self.lockOrientation { + self.lockedOrientation = nil + } + } + } + } + + public final var isOpaqueWhenInOverlay: Bool = false + public final var blocksBackgroundWhenInOverlay: Bool = false + + public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations { + return self.supportedOrientations + } + + public final var deferScreenEdgeGestures: UIRectEdge = [] { + didSet { + if self.deferScreenEdgeGestures != oldValue { + self.window?.invalidateDeferScreenEdgeGestures() + } + } + } + + public final var preferNavigationUIHidden: Bool = false { + didSet { + if self.preferNavigationUIHidden != oldValue { + self.window?.invalidatePreferNavigationUIHidden() + } + } + } + + override open func prefersHomeIndicatorAutoHidden() -> Bool { + return self.preferNavigationUIHidden + } + + public var presentationArguments: Any? + + public var tabBarItemDebugTapAction: (() -> Void)? + + private var _displayNode: ASDisplayNode? + public final var displayNode: ASDisplayNode { + get { + if let value = self._displayNode { + return value + } + else { + self.loadDisplayNode() + if self._displayNode == nil { + fatalError("displayNode should be initialized after loadDisplayNode()") + } + return self._displayNode! + } + } + set(value) { + self._displayNode = value + } + } + + public final var isNodeLoaded: Bool { + return self._displayNode != nil + } + + public let statusBar: StatusBar + public let navigationBar: NavigationBar? + private(set) var toolbar: Toolbar? + + private var previewingContext: Any? + + public var displayNavigationBar = true + + private weak var activeInputViewCandidate: UIResponder? + private weak var activeInputView: UIResponder? + + open var hasActiveInput: Bool = false + + private var navigationBarOrigin: CGFloat = 0.0 + + public var navigationOffset: CGFloat = 0.0 { + didSet { + if let navigationBar = self.navigationBar { + var navigationBarFrame = navigationBar.frame + navigationBarFrame.origin.y = self.navigationBarOrigin + self.navigationOffset + navigationBar.frame = navigationBarFrame + } + } + } + + open var navigationHeight: CGFloat { + if let navigationBar = self.navigationBar { + return navigationBar.frame.maxY + } else { + return 0.0 + } + } + + open var navigationInsetHeight: CGFloat { + if let navigationBar = self.navigationBar { + var height = navigationBar.frame.maxY + if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode { + height += contentNode.nominalHeight - contentNode.height + } + return height + } else { + return 0.0 + } + } + + open var visualNavigationInsetHeight: CGFloat { + if let navigationBar = self.navigationBar { + var height = navigationBar.frame.maxY + if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode { + //height += contentNode.height + } + return height + } else { + return 0.0 + } + } + + private let _ready = Promise(true) + open var ready: Promise { + return self._ready + } + + private var scrollToTopView: ScrollToTopView? + public var scrollToTop: (() -> Void)? { + didSet { + if self.isViewLoaded { + self.updateScrollToTopView() + } + } + } + public var scrollToTopWithTabBar: (() -> Void)? + public var longTapWithTabBar: (() -> Void)? + + public var attemptNavigation: (@escaping () -> Void) -> Bool = { _ in + return true + } + + private func updateScrollToTopView() { + if self.scrollToTop != nil { + if let displayNode = self._displayNode , self.scrollToTopView == nil { + let scrollToTopView = ScrollToTopView(frame: CGRect(x: 0.0, y: -1.0, width: displayNode.frame.size.width, height: 1.0)) + scrollToTopView.action = { [weak self] in + if let scrollToTop = self?.scrollToTop { + scrollToTop() + } + } + self.scrollToTopView = scrollToTopView + self.view.addSubview(scrollToTopView) + } + } else if let scrollToTopView = self.scrollToTopView { + scrollToTopView.removeFromSuperview() + self.scrollToTopView = nil + } + } + + public init(navigationBarPresentationData: NavigationBarPresentationData?) { + self.statusBar = StatusBar() + if let navigationBarPresentationData = navigationBarPresentationData { + self.navigationBar = NavigationBar(presentationData: navigationBarPresentationData) + } else { + self.navigationBar = nil + } + self.presentationContext = PresentationContext() + + super.init(nibName: nil, bundle: nil) + + self.navigationBar?.backPressed = { [weak self] in + if let strongSelf = self, strongSelf.attemptNavigation({ + self?.navigationController?.popViewController(animated: true) + }) { + strongSelf.navigationController?.popViewController(animated: true) + } + } + self.navigationBar?.requestContainerLayout = { [weak self] transition in + self?.requestLayout(transition: transition) + } + self.navigationBar?.item = self.navigationItem + self.automaticallyAdjustsScrollViewInsets = false + + self.scrollToTopWithTabBar = { [weak self] in + self?.scrollToTop?() + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + + } + + open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + if !self.isViewLoaded { + self.loadView() + } + transition.updateFrame(node: self.displayNode, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) + if let _ = layout.statusBarHeight { + self.statusBar.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 40.0)) + } + + let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 + let navigationBarHeight: CGFloat = max(20.0, statusBarHeight) + (self.navigationBar?.contentHeight ?? 44.0) + let navigationBarOffset: CGFloat + if statusBarHeight.isZero { + navigationBarOffset = -20.0 + } else { + navigationBarOffset = 0.0 + } + var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: navigationBarHeight)) + if layout.statusBarHeight == nil { + navigationBarFrame.size.height = (self.navigationBar?.contentHeight ?? 44.0) + 20.0 + } + + if !self.displayNavigationBar { + navigationBarFrame.origin.y = -navigationBarFrame.size.height + } + + self.navigationBarOrigin = navigationBarFrame.origin.y + navigationBarFrame.origin.y += self.navigationOffset + + if let navigationBar = self.navigationBar { + if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar { + navigationBarFrame.origin.y += contentNode.height + statusBarHeight + } + navigationBar.updateLayout(size: navigationBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) + if !transition.isAnimated { + navigationBar.layer.cancelAnimationsRecursive(key: "bounds") + navigationBar.layer.cancelAnimationsRecursive(key: "position") + } + transition.updateFrame(node: navigationBar, frame: navigationBarFrame) + navigationBar.setHidden(!self.displayNavigationBar, animated: transition.isAnimated) + } + + self.presentationContext.containerLayoutUpdated(layout, transition: transition) + + if let scrollToTopView = self.scrollToTopView { + scrollToTopView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 10.0) + } + } + + open func navigationStackConfigurationUpdated(next: [ViewController]) { + } + + open override func loadView() { + self.view = self.displayNode.view + if let navigationBar = self.navigationBar { + if navigationBar.supernode == nil { + self.displayNode.addSubnode(navigationBar) + } + } + self.view.autoresizingMask = [] + self.view.addSubview(self.statusBar.view) + self.presentationContext.view = self.view + } + + open func loadDisplayNode() { + self.displayNode = ASDisplayNode() + self.displayNodeDidLoad() + } + + open func displayNodeDidLoad() { + if let layer = self.displayNode.layer as? CATracingLayer { + layer.setTraceableInfo(CATracingLayerInfo(shouldBeAdjustedToInverseTransform: false, userData: self.displayNode.layer, tracingTag: WindowTracingTags.keyboard, disableChildrenTracingTags: 0)) + } + self.updateScrollToTopView() + if let backgroundColor = self.displayNode.backgroundColor, backgroundColor.alpha.isEqual(to: 1.0) { + self.blocksBackgroundWhenInOverlay = true + self.isOpaqueWhenInOverlay = true + } + } + + public func requestLayout(transition: ContainedViewLayoutTransition) { + if self.isViewLoaded, let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: transition) + } + } + + open func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { + + } + + public func setDisplayNavigationBar(_ displayNavigationBar: Bool, transition: ContainedViewLayoutTransition = .immediate) { + if displayNavigationBar != self.displayNavigationBar { + self.displayNavigationBar = displayNavigationBar + if let parent = self.parent as? TabBarController { + if parent.currentController === self { + parent.displayNavigationBar = displayNavigationBar + parent.requestLayout(transition: transition) + } + } else { + self.requestLayout(transition: transition) + } + } + } + + override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { + super.present(viewControllerToPresent, animated: flag, completion: completion) + return + } + + override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + if let navigationController = self.navigationController as? NavigationController { + navigationController.dismiss(animated: flag, completion: completion) + } else { + super.dismiss(animated: flag, completion: completion) + } + } + + public final var window: WindowHost? { + if let window = self.view.window as? WindowHost { + return window + } else if let result = findWindow(self.view) { + return result + } else { + if let parent = self.parent as? ViewController { + return parent.window + } + return nil + } + } + + public func present(_ controller: ViewController, in context: PresentationContextType, with arguments: Any? = nil, blockInteraction: Bool = false, completion: @escaping () -> Void = {}) { + controller.presentationArguments = arguments + switch context { + case .current: + self.presentationContext.present(controller, on: PresentationSurfaceLevel(rawValue: 0), completion: completion) + case let .window(level): + self.window?.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) + } + } + + public func forEachController(_ f: (ContainableController) -> Bool) { + for (controller, _) in self.presentationContext.controllers { + if !f(controller) { + break + } + } + } + + public func presentInGlobalOverlay(_ controller: ViewController, with arguments: Any? = nil) { + controller.presentationArguments = arguments + self.window?.presentInGlobalOverlay(controller) + } + + open override func viewWillDisappear(_ animated: Bool) { + self.activeInputViewCandidate = findCurrentResponder(self.view) + + super.viewWillDisappear(animated) + } + + open override func viewDidDisappear(_ animated: Bool) { + self.activeInputView = self.activeInputViewCandidate + + super.viewDidDisappear(animated) + } + + open override func viewDidAppear(_ animated: Bool) { + self.activeInputView = nil + + super.viewDidAppear(animated) + } + + open func dismiss(completion: (() -> Void)? = nil) { + } + + @available(iOSApplicationExtension 9.0, iOS 9.0, *) + open func registerForPreviewing(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme, onlyNative: Bool) { + if self.traitCollection.forceTouchCapability == .available { + let _ = super.registerForPreviewing(with: delegate, sourceView: sourceView) + } else if !onlyNative { + if self.previewingContext == nil { + let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in + self?.presentInGlobalOverlay(c, with: a) + }) + self.previewingContext = previewingContext + } + } + } + + @available(iOSApplicationExtension 9.0, iOS 9.0, *) + public func registerForPreviewingNonNative(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme) { + if self.traitCollection.forceTouchCapability != .available { + if self.previewingContext == nil { + let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in + self?.presentInGlobalOverlay(c, with: a) + }) + self.previewingContext = previewingContext + } + } + } + + @available(iOSApplicationExtension 9.0, iOS 9.0, *) + open override func unregisterForPreviewing(withContext previewing: UIViewControllerPreviewing) { + if self.previewingContext != nil { + self.previewingContext = nil + } else { + super.unregisterForPreviewing(withContext: previewing) + } + } + + public final func navigationNextSibling() -> UIViewController? { + if let navigationController = self.navigationController as? NavigationController { + if let index = navigationController.viewControllers.index(where: { $0 === self }) { + if index != navigationController.viewControllers.count - 1 { + return navigationController.viewControllers[index + 1] + } + } + } + return nil + } + + public func traceVisibility() -> Bool { + if !self.isViewLoaded { + return false + } + return traceViewVisibility(view: self.view, rect: self.view.bounds) + } + + open func setToolbar(_ toolbar: Toolbar?, transition: ContainedViewLayoutTransition) { + if self.toolbar != toolbar { + self.toolbar = toolbar + if let parent = self.parent as? TabBarController { + if parent.currentController === self { + parent.requestLayout(transition: transition) + } + } + } + } + + open func toolbarActionSelected(action: ToolbarActionOption) { + } +} + +func traceIsOpaque(layer: CALayer, rect: CGRect) -> Bool { + if layer.bounds.contains(rect) { + if layer.isHidden { + return false + } + if layer.opacity < 0.01 { + return false + } + if let backgroundColor = layer.backgroundColor { + var alpha: CGFloat = 0.0 + UIColor(cgColor: backgroundColor).getRed(nil, green: nil, blue: nil, alpha: &alpha) + if alpha > 0.95 { + return true + } + } + if let sublayers = layer.sublayers { + for sublayer in sublayers { + let sublayerRect = layer.convert(rect, to: sublayer) + if traceIsOpaque(layer: sublayer, rect: sublayerRect) { + return true + } + } + } + return false + } else { + return false + } +} + +private func traceViewVisibility(view: UIView, rect: CGRect) -> Bool { + if view.isHidden { + return false + } + if view is UIWindow { + return true + } else if let superview = view.superview, let siblings = superview.layer.sublayers { + if view.window == nil { + return false + } + if let index = siblings.index(where: { $0 === view.layer }) { + let viewFrame = view.convert(rect, to: superview) + for i in (index + 1) ..< siblings.count { + if siblings[i].frame.contains(viewFrame) { + let siblingSubframe = view.layer.convert(viewFrame, to: siblings[i]) + if traceIsOpaque(layer: siblings[i], rect: siblingSubframe) { + return false + } + } + } + return traceViewVisibility(view: superview, rect: viewFrame) + } else { + return false + } + } else { + return false + } +} diff --git a/submodules/Display/Display/ViewControllerPreviewing.swift b/submodules/Display/Display/ViewControllerPreviewing.swift new file mode 100644 index 0000000000..fafdce84e9 --- /dev/null +++ b/submodules/Display/Display/ViewControllerPreviewing.swift @@ -0,0 +1,131 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +@available(iOSApplicationExtension 9.0, iOS 9.0, *) +private final class ViewControllerPeekContent: PeekControllerContent { + private let controller: ViewController + private let menu: [PeekControllerMenuItem] + + init(controller: ViewController) { + self.controller = controller + var menu: [PeekControllerMenuItem] = [] + for item in controller.previewActionItems { + menu.append(PeekControllerMenuItem(title: item.title, color: .accent, action: { [weak controller] in + if let controller = controller, let item = item as? UIPreviewAction { + item.handler(item, controller) + } + })) + } + self.menu = menu + } + + func presentation() -> PeekControllerContentPresentation { + return .contained + } + + func menuActivation() -> PeerkControllerMenuActivation { + return .drag + } + + func menuItems() -> [PeekControllerMenuItem] { + return self.menu + } + + func node() -> PeekControllerContentNode & ASDisplayNode { + return ViewControllerPeekContentNode(controller: self.controller) + } + + func topAccessoryNode() -> ASDisplayNode? { + return nil + } + + func isEqual(to: PeekControllerContent) -> Bool { + if let to = to as? ViewControllerPeekContent { + return self.controller === to.controller + } else { + return false + } + } +} + +private final class ViewControllerPeekContentNode: ASDisplayNode, PeekControllerContentNode { + private let controller: ViewController + private var hasValidLayout = false + + init(controller: ViewController) { + self.controller = controller + + super.init() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + if !self.hasValidLayout { + self.hasValidLayout = true + self.controller.view.frame = CGRect(origin: CGPoint(), size: size) + self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) + self.controller.setIgnoreAppearanceMethodInvocations(true) + self.view.addSubview(self.controller.view) + self.controller.setIgnoreAppearanceMethodInvocations(false) + self.controller.viewWillAppear(false) + self.controller.viewDidAppear(false) + } else { + self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + } + + return size + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + return self.view + } + return nil + } +} + +@available(iOSApplicationExtension 9.0, iOS 9.0, *) +final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreviewing { + weak var delegateImpl: UIViewControllerPreviewingDelegate? + var delegate: UIViewControllerPreviewingDelegate { + return self.delegateImpl! + } + let recognizer: PeekControllerGestureRecognizer + var previewingGestureRecognizerForFailureRelationship: UIGestureRecognizer { + return self.recognizer + } + let sourceView: UIView + let node: ASDisplayNode + + var sourceRect: CGRect = CGRect() + + init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void) { + self.delegateImpl = delegate + self.sourceView = sourceView + self.node = node + var contentAtPointImpl: ((CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?)? + self.recognizer = PeekControllerGestureRecognizer(contentAtPoint: { point in + return contentAtPointImpl?(point) + }, present: { content, sourceNode in + let controller = PeekController(theme: theme, content: content, sourceNode: { + return sourceNode + }) + present(controller, nil) + return controller + }) + + node.view.addGestureRecognizer(self.recognizer) + + super.init() + + contentAtPointImpl = { [weak self] point in + if let strongSelf = self, let delegate = strongSelf.delegateImpl { + if let controller = delegate.previewingContext(strongSelf, viewControllerForLocation: point) as? ViewController { + return .single((strongSelf.node, ViewControllerPeekContent(controller: controller))) + } + } + return nil + } + } +} diff --git a/submodules/Display/Display/ViewControllerTracingNode.swift b/submodules/Display/Display/ViewControllerTracingNode.swift new file mode 100644 index 0000000000..91f678585a --- /dev/null +++ b/submodules/Display/Display/ViewControllerTracingNode.swift @@ -0,0 +1,37 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +private final class ViewControllerTracingNodeView: UITracingLayerView { + private var inHitTest = false + var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.inHitTest { + return super.hitTest(point, with: event) + } else { + self.inHitTest = true + let result = self.hitTestImpl?(point, event) + self.inHitTest = false + return result + } + } +} + +open class ViewControllerTracingNode: ASDisplayNode { + override public init() { + super.init() + + self.setViewBlock({ + return ViewControllerTracingNodeView() + }) + } + + override open func didLoad() { + super.didLoad() + + (self.view as! ViewControllerTracingNodeView).hitTestImpl = { [weak self] point, event in + return self?.hitTest(point, with: event) + } + } +} diff --git a/submodules/Display/Display/VolumeControlStatusBar.swift b/submodules/Display/Display/VolumeControlStatusBar.swift new file mode 100644 index 0000000000..35501ba9d5 --- /dev/null +++ b/submodules/Display/Display/VolumeControlStatusBar.swift @@ -0,0 +1,273 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import MediaPlayer +import SwiftSignalKit + +private let volumeNotificationKey = "AVSystemController_SystemVolumeDidChangeNotification" +private let volumeParameterKey = "AVSystemController_AudioVolumeNotificationParameter" +private let changeReasonParameterKey = "AVSystemController_AudioVolumeChangeReasonNotificationParameter" +private let explicitChangeReasonValue = "ExplicitVolumeChange" + +private final class VolumeView: MPVolumeView { + @objc func _updateWirelessRouteStatus() { + } +} + +final class VolumeControlStatusBar: UIView { + private let control: VolumeView + private var observer: Any? + private var currentValue: Float + + var valueChanged: ((Float, Float) -> Void)? + + private var disposable: Disposable? + private var ignoreAdjustmentOnce = false + + init(frame: CGRect, shouldBeVisible: Signal) { + self.control = VolumeView(frame: CGRect(origin: CGPoint(x: -100.0, y: -100.0), size: CGSize(width: 100.0, height: 20.0))) + self.control.alpha = 0.0001 + self.currentValue = AVAudioSession.sharedInstance().outputVolume + + super.init(frame: frame) + + self.addSubview(self.control) + self.observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: volumeNotificationKey), object: nil, queue: OperationQueue.main, using: { [weak self] notification in + if let strongSelf = self, let userInfo = notification.userInfo { + if let volume = userInfo[volumeParameterKey] as? Float { + let previous = strongSelf.currentValue + if !previous.isEqual(to: volume) { + strongSelf.currentValue = volume + if strongSelf.ignoreAdjustmentOnce { + strongSelf.ignoreAdjustmentOnce = false + } else { + if strongSelf.control.superview != nil { + if let reason = userInfo[changeReasonParameterKey], reason as? String != explicitChangeReasonValue { + return + } + strongSelf.valueChanged?(previous, volume) + } + } + } + } + } + }) + + self.disposable = (shouldBeVisible + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + if value { + if strongSelf.control.superview == nil { + strongSelf.ignoreAdjustmentOnce = true + strongSelf.addSubview(strongSelf.control) + } + } else { + strongSelf.control.removeFromSuperview() + strongSelf.ignoreAdjustmentOnce = false + } + }) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let observer = self.observer { + NotificationCenter.default.removeObserver(observer) + } + self.disposable?.dispose() + } +} + +final class VolumeControlStatusBarNode: ASDisplayNode { + var innerGraphics: (UIImage, UIImage, UIImage, Bool)? + var graphics: (UIImage, UIImage, UIImage)? = nil { + didSet { + if self.isDark { + self.innerGraphics = generateDarkGraphics(self.graphics) + } else { + if let graphics = self.graphics { + self.innerGraphics = (graphics.0, graphics.1, graphics.2, false) + } else { + self.innerGraphics = nil + } + } + } + } + private let outlineNode: ASImageNode + private let backgroundNode: ASImageNode + private let iconNode: ASImageNode + private let foregroundNode: ASImageNode + private let foregroundClippingNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + + var isDark: Bool = false { + didSet { + if self.isDark != oldValue { + if self.isDark { + self.outlineNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: UIColor(white: 0.0, alpha: 0.7)) + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(white: 0.6, alpha: 1.0)) + self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .white) + + self.innerGraphics = generateDarkGraphics(self.graphics) + } else { + self.outlineNode.image = nil + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(rgb: 0xc5c5c5)) + self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .black) + + if let graphics = self.graphics { + self.innerGraphics = (graphics.0, graphics.1, graphics.2, false) + } + } + self.updateIcon() + } + } + } + private var value: CGFloat = 1.0 + + override init() { + self.outlineNode = ASImageNode() + self.outlineNode.isLayerBacked = true + self.outlineNode.displaysAsynchronously = false + self.outlineNode.displayWithoutProcessing = true + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(rgb: 0xc5c5c5)) + + self.foregroundNode = ASImageNode() + self.foregroundNode.isLayerBacked = true + self.foregroundNode.displaysAsynchronously = false + self.foregroundNode.displayWithoutProcessing = true + self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .black) + + self.foregroundClippingNode = ASDisplayNode() + self.foregroundClippingNode.clipsToBounds = true + self.foregroundClippingNode.addSubnode(self.foregroundNode) + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.outlineNode) + self.addSubnode(self.backgroundNode) + self.addSubnode(self.foregroundClippingNode) + self.addSubnode(self.iconNode) + } + + func generateDarkGraphics(_ graphics: (UIImage, UIImage, UIImage)?) -> (UIImage, UIImage, UIImage, Bool)? { + if var (offImage, halfImage, onImage) = graphics { + offImage = generateTintedImage(image: offImage, color: UIColor.white)! + halfImage = generateTintedImage(image: halfImage, color: UIColor.white)! + onImage = generateTintedImage(image: onImage, color: UIColor.white)! + return (offImage, halfImage, onImage, true) + } else { + return nil + } + } + + func updateGraphics() { + if self.isDark { + self.outlineNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: UIColor(white: 0.0, alpha: 0.7)) + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(white: 0.6, alpha: 1.0)) + self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .white) + } else { + self.outlineNode.image = nil + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(white: 0.6, alpha: 1.0)) + self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .black) + } + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + let barHeight: CGFloat = 4.0 + var barWidth: CGFloat + + let statusBarHeight: CGFloat + var sideInset: CGFloat + if let actual = layout.statusBarHeight { + statusBarHeight = actual + } else { + statusBarHeight = 24.0 + } + if layout.safeInsets.left.isZero && layout.safeInsets.top.isZero && layout.intrinsicInsets.left.isZero && layout.intrinsicInsets.top.isZero { + sideInset = 4.0 + } else { + sideInset = 12.0 + } + + let iconRect = CGRect(x: sideInset + 4.0, y: 14.0, width: 21.0, height: 16.0) + if !layout.intrinsicInsets.bottom.isZero { + if layout.size.width > 375.0 { + barWidth = 88.0 - sideInset * 2.0 + } else { + barWidth = 80.0 - sideInset * 2.0 + } + if layout.size.width < layout.size.height { + self.outlineNode.isHidden = true + } else { + self.outlineNode.isHidden = false + } + if self.graphics != nil { + if layout.size.width < layout.size.height { + self.iconNode.isHidden = false + barWidth -= iconRect.width - 8.0 + sideInset += iconRect.width + 8.0 + } else { + sideInset += layout.safeInsets.left + self.iconNode.isHidden = true + } + } + } else { + self.iconNode.isHidden = true + barWidth = layout.size.width - sideInset * 2.0 + } + + let boundingRect = CGRect(origin: CGPoint(x: sideInset, y: floor((statusBarHeight - barHeight) / 2.0)), size: CGSize(width: barWidth, height: barHeight)) + + transition.updateFrame(node: self.iconNode, frame: iconRect) + transition.updateFrame(node: self.outlineNode, frame: boundingRect.insetBy(dx: -4.0, dy: -4.0)) + transition.updateFrame(node: self.backgroundNode, frame: boundingRect) + transition.updateFrame(node: self.foregroundNode, frame: CGRect(origin: CGPoint(), size: boundingRect.size)) + transition.updateFrame(node: self.foregroundClippingNode, frame: CGRect(origin: boundingRect.origin, size: CGSize(width: self.value * boundingRect.width, height: boundingRect.height))) + } + + func updateValue(from fromValue: CGFloat, to toValue: CGFloat) { + if let layout = self.validLayout { + if self.foregroundClippingNode.layer.animation(forKey: "bounds") == nil { + self.value = fromValue + self.updateLayout(layout: layout, transition: .immediate) + } + self.value = toValue + self.updateLayout(layout: layout, transition: .animated(duration: 0.25, curve: .spring)) + + self.updateIcon() + } else { + self.value = toValue + } + } + + private func updateIcon() { + if let graphics = self.innerGraphics { + if self.value > 0.5 { + self.iconNode.image = graphics.2 + } else if self.value > 0.001 { + self.iconNode.image = graphics.1 + } else { + self.iconNode.image = graphics.0 + } + } + } +} diff --git a/submodules/Display/Display/WindowContent.swift b/submodules/Display/Display/WindowContent.swift new file mode 100644 index 0000000000..be3b7610ff --- /dev/null +++ b/submodules/Display/Display/WindowContent.swift @@ -0,0 +1,1083 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit + +#if BUCK +import DisplayPrivate +#endif + +private struct WindowLayout: Equatable { + let size: CGSize + let metrics: LayoutMetrics + let statusBarHeight: CGFloat? + let forceInCallStatusBarText: String? + let inputHeight: CGFloat? + let safeInsets: UIEdgeInsets + let onScreenNavigationHeight: CGFloat? + let upperKeyboardInputPositionBound: CGFloat? + let inVoiceOver: Bool +} + +private struct UpdatingLayout { + var layout: WindowLayout + var transition: ContainedViewLayoutTransition + + mutating func update(transition: ContainedViewLayoutTransition, override: Bool) { + var update = false + if case .immediate = self.transition { + update = true + } else if override { + update = true + } + if update { + self.transition = transition + } + } + + mutating func update(size: CGSize, metrics: LayoutMetrics, safeInsets: UIEdgeInsets, forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: size, metrics: metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) + } + + + mutating func update(forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) + } + + mutating func update(statusBarHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) + } + + mutating func update(inputHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) + } + + mutating func update(safeInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) + } + + mutating func update(onScreenNavigationHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) + } + + mutating func update(upperKeyboardInputPositionBound: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) + } + + mutating func update(inVoiceOver: Bool) { + self.update(transition: transition, override: false) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: inVoiceOver) + } +} + +private let statusBarHiddenInLandscape: Bool = UIDevice.current.userInterfaceIdiom == .phone + +private func inputHeightOffsetForLayout(_ layout: WindowLayout) -> CGFloat { + if let inputHeight = layout.inputHeight, let upperBound = layout.upperKeyboardInputPositionBound { + return max(0.0, upperBound - (layout.size.height - inputHeight)) + } + return 0.0 +} + +private func containedLayoutForWindowLayout(_ layout: WindowLayout, hasOnScreenNavigation: Bool) -> ContainerViewLayout { + let resolvedStatusBarHeight: CGFloat? + if let statusBarHeight = layout.statusBarHeight { + if layout.forceInCallStatusBarText != nil { + resolvedStatusBarHeight = max(40.0, layout.safeInsets.top) + } else { + resolvedStatusBarHeight = statusBarHeight + } + } else { + resolvedStatusBarHeight = nil + } + + var updatedInputHeight = layout.inputHeight + if let inputHeight = updatedInputHeight, let _ = layout.upperKeyboardInputPositionBound { + updatedInputHeight = inputHeight - inputHeightOffsetForLayout(layout) + } + + var resolvedSafeInsets = layout.safeInsets + var standardInputHeight: CGFloat = 216.0 + var predictiveHeight: CGFloat = 42.0 + + if let metrics = DeviceMetrics.forScreenSize(layout.size, hintHasOnScreenNavigation: hasOnScreenNavigation) { + let isLandscape = layout.size.width > layout.size.height + let safeAreaInsets = metrics.safeAreaInsets(inLandscape: isLandscape) + if !safeAreaInsets.left.isZero { + resolvedSafeInsets.left = safeAreaInsets.left + resolvedSafeInsets.right = safeAreaInsets.right + } + + standardInputHeight = metrics.standardInputHeight(inLandscape: isLandscape) + predictiveHeight = metrics.predictiveInputHeight(inLandscape: isLandscape) + } + + standardInputHeight += predictiveHeight + + return ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.onScreenNavigationHeight ?? 00, right: 0.0), safeInsets: resolvedSafeInsets, statusBarHeight: resolvedStatusBarHeight, inputHeight: updatedInputHeight, standardInputHeight: standardInputHeight, inputHeightIsInteractivellyChanging: layout.upperKeyboardInputPositionBound != nil && layout.upperKeyboardInputPositionBound != layout.size.height && layout.inputHeight != nil, inVoiceOver: layout.inVoiceOver) +} + +private func encodeText(_ string: String, _ key: Int) -> String { + var result = "" + for c in string.unicodeScalars { + result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!)) + } + return result +} + +public func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UIView) -> Bool { + if view.disablesInteractiveTransitionGestureRecognizer { + return true + } + if let f = view.disablesInteractiveTransitionGestureRecognizerNow, f() { + return true + } + if let superview = view.superview { + return doesViewTreeDisableInteractiveTransitionGestureRecognizer(superview) + } + return false +} + +private let transitionClass: AnyClass? = NSClassFromString(encodeText("VJUsbotjujpoWjfx", -1)) +private let previewingClass: AnyClass? = NSClassFromString("UIVisualEffectView") +private let previewingActionGroupClass: AnyClass? = NSClassFromString("UIInterfaceActionGroupView") +private func checkIsPreviewingView(_ view: UIView) -> Bool { + if let transitionClass = transitionClass, view.isKind(of: transitionClass) { + for subview in view.subviews { + if let previewingClass = previewingClass, subview.isKind(of: previewingClass) { + return true + } + } + } + return false +} + +private func applyThemeToPreviewingView(_ view: UIView, accentColor: UIColor, darkBlur: Bool) { + if let previewingActionGroupClass = previewingActionGroupClass, view.isKind(of: previewingActionGroupClass) { + view.tintColor = accentColor + if darkBlur { + applyThemeToPreviewingEffectView(view) + } + return + } + + for subview in view.subviews { + applyThemeToPreviewingView(subview, accentColor: accentColor, darkBlur: darkBlur) + } +} + +private func applyThemeToPreviewingEffectView(_ view: UIView) { + if let previewingClass = previewingClass, view.isKind(of: previewingClass) { + if let view = view as? UIVisualEffectView { + view.effect = UIBlurEffect(style: .dark) + } + } + + for subview in view.subviews { + applyThemeToPreviewingEffectView(subview) + } +} + +public func getFirstResponderAndAccessoryHeight(_ view: UIView, _ accessoryHeight: CGFloat? = nil) -> (UIView?, CGFloat?) { + if view.isFirstResponder { + return (view, accessoryHeight) + } else { + var updatedAccessoryHeight = accessoryHeight + if let view = view as? WindowInputAccessoryHeightProvider { + updatedAccessoryHeight = view.getWindowInputAccessoryHeight() + } + for subview in view.subviews { + let (result, resultHeight) = getFirstResponderAndAccessoryHeight(subview, updatedAccessoryHeight) + if let result = result { + return (result, resultHeight) + } + } + return (nil, nil) + } +} + +public final class WindowHostView { + public let containerView: UIView + public let eventView: UIView + public let isRotating: () -> Bool + + let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void + let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void + let updatePreferNavigationUIHidden: (Bool) -> Void + + var present: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)? + var presentInGlobalOverlay: ((_ controller: ContainableController) -> Void)? + var presentNative: ((UIViewController) -> Void)? + var updateSize: ((CGSize, Double) -> Void)? + var layoutSubviews: (() -> Void)? + var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)? + var isUpdatingOrientationLayout = false + var hitTest: ((CGPoint, UIEvent?) -> UIView?)? + var invalidateDeferScreenEdgeGesture: (() -> Void)? + var invalidatePreferNavigationUIHidden: (() -> Void)? + var cancelInteractiveKeyboardGestures: (() -> Void)? + var forEachController: (((ContainableController) -> Void) -> Void)? + var getAccessibilityElements: (() -> [Any]?)? + + init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePreferNavigationUIHidden: @escaping (Bool) -> Void) { + self.containerView = containerView + self.eventView = eventView + self.isRotating = isRotating + self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations + self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures + self.updatePreferNavigationUIHidden = updatePreferNavigationUIHidden + } + + var hasOnScreenNavigation: Bool { + if #available(iOSApplicationExtension 11.0, *) { + return !self.eventView.safeAreaInsets.bottom.isZero + } else { + return false + } + } +} + +public struct WindowTracingTags { + public static let statusBar: Int32 = 1 << 0 + public static let keyboard: Int32 = 1 << 1 +} + +public protocol WindowHost { + func forEachController(_ f: (ContainableController) -> Void) + func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) + func presentInGlobalOverlay(_ controller: ContainableController) + func invalidateDeferScreenEdgeGestures() + func invalidatePreferNavigationUIHidden() + func cancelInteractiveKeyboardGestures() +} + +private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics { + if size.width > 690.0 && size.height > 690.0 { + return LayoutMetrics(widthClass: .regular, heightClass: .regular) + } else { + return LayoutMetrics(widthClass: .compact, heightClass: .compact) + } +} + +private func safeInsetsForScreenSize(_ size: CGSize, hasOnScreenNavigation: Bool) -> UIEdgeInsets { + return DeviceMetrics.forScreenSize(size, hintHasOnScreenNavigation: hasOnScreenNavigation)?.safeAreaInsets(inLandscape: size.width > size.height) ?? UIEdgeInsets.zero +} + +public final class WindowKeyboardGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } +} + +public class Window1 { + public let hostView: WindowHostView + + private let statusBarHost: StatusBarHost? + private let statusBarManager: StatusBarManager? + private let keyboardManager: KeyboardManager? + private var statusBarChangeObserver: AnyObject? + private var keyboardRotationChangeObserver: AnyObject? + private var keyboardFrameChangeObserver: AnyObject? + private var keyboardTypeChangeObserver: AnyObject? + private var voiceOverStatusObserver: AnyObject? + + private var windowLayout: WindowLayout + private var updatingLayout: UpdatingLayout? + private var updatedContainerLayout: ContainerViewLayout? + private var upperKeyboardInputPositionBound: CGFloat? + private var cachedWindowSubviewCount: Int = 0 + private var cachedHasPreview: Bool = false + + private let presentationContext: PresentationContext + private let overlayPresentationContext: GlobalOverlayPresentationContext + + private var tracingStatusBarsInvalidated = false + private var shouldUpdateDeferScreenEdgeGestures = false + private var shouldInvalidatePreferNavigationUIHidden = false + + private var statusBarHidden = false + + public var previewThemeAccentColor: UIColor = .blue + public var previewThemeDarkBlur: Bool = false + + public private(set) var forceInCallStatusBarText: String? = nil + public var inCallNavigate: (() -> Void)? { + didSet { + self.statusBarManager?.inCallNavigate = self.inCallNavigate + } + } + + private var windowPanRecognizer: WindowPanRecognizer? + private let keyboardGestureRecognizerDelegate = WindowKeyboardGestureRecognizerDelegate() + private var keyboardGestureBeginLocation: CGPoint? + private var keyboardGestureAccessoryHeight: CGFloat? + + private var keyboardTypeChangeTimer: SwiftSignalKit.Timer? + + private let volumeControlStatusBar: VolumeControlStatusBar + private let volumeControlStatusBarNode: VolumeControlStatusBarNode + + private var isInteractionBlocked = false + + /*private var accessibilityElements: [Any]? { + return self.viewController?.view.accessibilityElements + }*/ + + public init(hostView: WindowHostView, statusBarHost: StatusBarHost?) { + self.hostView = hostView + + self.volumeControlStatusBar = VolumeControlStatusBar(frame: CGRect(origin: CGPoint(x: 0.0, y: -20.0), size: CGSize(width: 100.0, height: 20.0)), shouldBeVisible: statusBarHost?.handleVolumeControl ?? .single(false)) + self.volumeControlStatusBarNode = VolumeControlStatusBarNode() + self.volumeControlStatusBarNode.isHidden = true + + let boundsSize = self.hostView.eventView.bounds.size + let deviceMetrics = DeviceMetrics.forScreenSize(boundsSize, hintHasOnScreenNavigation: self.hostView.hasOnScreenNavigation) + + self.statusBarHost = statusBarHost + let statusBarHeight: CGFloat + if let statusBarHost = statusBarHost { + statusBarHeight = statusBarHost.statusBarFrame.size.height + self.statusBarManager = StatusBarManager(host: statusBarHost, volumeControlStatusBar: self.volumeControlStatusBar, volumeControlStatusBarNode: self.volumeControlStatusBarNode) + self.keyboardManager = KeyboardManager(host: statusBarHost) + } else { + statusBarHeight = deviceMetrics?.statusBarHeight ?? 20.0 + self.statusBarManager = nil + self.keyboardManager = nil + } + + let onScreenNavigationHeight = deviceMetrics?.onScreenNavigationHeight(inLandscape: boundsSize.width > boundsSize.height) + + self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsetsForScreenSize(boundsSize, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning) + self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) + self.presentationContext = PresentationContext() + self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost) + + self.presentationContext.updateIsInteractionBlocked = { [weak self] value in + self?.isInteractionBlocked = value + } + + self.presentationContext.updateHasOpaqueOverlay = { [weak self] value in + self?._rootController?.displayNode.accessibilityElementsHidden = value + } + + self.hostView.present = { [weak self] controller, level, blockInteraction, completion in + self?.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) + } + + self.hostView.presentInGlobalOverlay = { [weak self] controller in + self?.presentInGlobalOverlay(controller) + } + + self.hostView.presentNative = { [weak self] controller in + self?.presentNative(controller) + } + + self.hostView.updateSize = { [weak self] size, duration in + self?.updateSize(size, duration: duration) + } + + self.hostView.eventView.layer.setInvalidateTracingSublayers { [weak self] in + self?.invalidateTracingStatusBars() + } + + self.hostView.layoutSubviews = { [weak self] in + self?.layoutSubviews() + } + + self.hostView.updateToInterfaceOrientation = { [weak self] orientation in + self?.updateToInterfaceOrientation(orientation) + } + + self.hostView.hitTest = { [weak self] point, event in + return self?.hitTest(point, with: event) + } + + self.hostView.invalidateDeferScreenEdgeGesture = { [weak self] in + self?.invalidateDeferScreenEdgeGestures() + } + + self.hostView.invalidatePreferNavigationUIHidden = { [weak self] in + self?.invalidatePreferNavigationUIHidden() + } + + self.hostView.cancelInteractiveKeyboardGestures = { [weak self] in + self?.cancelInteractiveKeyboardGestures() + } + + self.hostView.forEachController = { [weak self] f in + self?.forEachViewController({ controller in + f(controller) + return true + }) + } + + /*self.hostView.getAccessibilityElements = { [weak self] in + return self?.accessibilityElements + }*/ + + self.presentationContext.view = self.hostView.containerView + self.presentationContext.volumeControlStatusBarNodeView = self.volumeControlStatusBarNode.view + self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate) + self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate) + + self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil, queue: OperationQueue.main, using: { [weak self] notification in + if let strongSelf = self { + let statusBarHeight: CGFloat = max(20.0, (notification.userInfo?[UIApplicationStatusBarFrameUserInfoKey] as? NSValue)?.cgRectValue.height ?? 20.0) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .easeInOut) + strongSelf.updateLayout { $0.update(statusBarHeight: statusBarHeight, transition: transition, overrideTransition: false) } + } + }) + self.keyboardRotationChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name("UITextEffectsWindowDidRotateNotification"), object: nil, queue: nil, using: { [weak self] notification in + if let strongSelf = self { + if !strongSelf.hostView.isUpdatingOrientationLayout { + return + } + let keyboardHeight = max(0.0, strongSelf.keyboardManager?.getCurrentKeyboardHeight() ?? 0.0) + var duration: Double = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0 + if duration > Double.ulpOfOne { + duration = 0.5 + } + let curve: UInt = (notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 + + let transitionCurve: ContainedViewLayoutTransitionCurve + if curve == 7 { + transitionCurve = .spring + } else { + transitionCurve = .easeInOut + } + + strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: .animated(duration: duration, curve: transitionCurve), overrideTransition: false) } + } + }) + + self.keyboardFrameChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil, queue: nil, using: { [weak self] notification in + if let strongSelf = self { + let keyboardFrame: CGRect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? CGRect() + + let screenHeight: CGFloat + + if keyboardFrame.width.isEqual(to: UIScreen.main.bounds.width) { + if abs(strongSelf.windowLayout.size.height - UIScreen.main.bounds.height) > 41.0 { + screenHeight = UIScreen.main.bounds.height + } else { + screenHeight = strongSelf.windowLayout.size.height + } + } else { + screenHeight = UIScreen.main.bounds.width + } + + let keyboardHeight = max(0.0, screenHeight - keyboardFrame.minY) + var duration: Double = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0 + if duration > Double.ulpOfOne { + duration = 0.5 + } + let curve: UInt = (notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 + + let transitionCurve: ContainedViewLayoutTransitionCurve + if curve == 7 { + transitionCurve = .spring + } else { + transitionCurve = .easeInOut + } + + strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: .animated(duration: duration, curve: transitionCurve), overrideTransition: false) } + } + }) + + if #available(iOSApplicationExtension 11.0, *) { + self.keyboardTypeChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UITextInputCurrentInputModeDidChange, object: nil, queue: OperationQueue.main, using: { [weak self] notification in + if let strongSelf = self, let initialInputHeight = strongSelf.windowLayout.inputHeight, let firstResponder = getFirstResponderAndAccessoryHeight(strongSelf.hostView.eventView).0 { + if firstResponder.textInputMode?.primaryLanguage != nil { + return + } + + strongSelf.keyboardTypeChangeTimer?.invalidate() + let timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { + if let strongSelf = self, let firstResponder = getFirstResponderAndAccessoryHeight(strongSelf.hostView.eventView).0 { + if firstResponder.textInputMode?.primaryLanguage != nil { + return + } + + if let keyboardManager = strongSelf.keyboardManager { + let updatedKeyboardHeight = keyboardManager.getCurrentKeyboardHeight() + if !updatedKeyboardHeight.isEqual(to: initialInputHeight) { + strongSelf.updateLayout({ $0.update(inputHeight: updatedKeyboardHeight, transition: .immediate, overrideTransition: false) }) + } + } + } + }, queue: Queue.mainQueue()) + strongSelf.keyboardTypeChangeTimer = timer + timer.start() + } + }) + } + + if #available(iOSApplicationExtension 11.0, *) { + self.voiceOverStatusObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIAccessibilityVoiceOverStatusDidChange, object: nil, queue: OperationQueue.main, using: { [weak self] _ in + if let strongSelf = self { + strongSelf.updateLayout { $0.update(inVoiceOver: UIAccessibility.isVoiceOverRunning) } + } + }) + } + + let recognizer = WindowPanRecognizer(target: self, action: #selector(self.panGesture(_:))) + recognizer.cancelsTouchesInView = false + recognizer.delaysTouchesBegan = false + recognizer.delaysTouchesEnded = false + recognizer.delegate = self.keyboardGestureRecognizerDelegate + recognizer.began = { [weak self] point in + self?.panGestureBegan(location: point) + } + recognizer.moved = { [weak self] point in + self?.panGestureMoved(location: point) + } + recognizer.ended = { [weak self] point, velocity in + self?.panGestureEnded(location: point, velocity: velocity) + } + self.windowPanRecognizer = recognizer + self.hostView.containerView.addGestureRecognizer(recognizer) + + self.hostView.containerView.addSubview(self.volumeControlStatusBar) + self.hostView.containerView.addSubview(self.volumeControlStatusBarNode.view) + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let statusBarChangeObserver = self.statusBarChangeObserver { + NotificationCenter.default.removeObserver(statusBarChangeObserver) + } + if let keyboardRotationChangeObserver = self.keyboardRotationChangeObserver { + NotificationCenter.default.removeObserver(keyboardRotationChangeObserver) + } + if let keyboardFrameChangeObserver = self.keyboardFrameChangeObserver { + NotificationCenter.default.removeObserver(keyboardFrameChangeObserver) + } + if let keyboardTypeChangeObserver = self.keyboardTypeChangeObserver { + NotificationCenter.default.removeObserver(keyboardTypeChangeObserver) + } + if let voiceOverStatusObserver = self.voiceOverStatusObserver { + NotificationCenter.default.removeObserver(voiceOverStatusObserver) + } + } + + public func setupVolumeControlStatusBarGraphics(_ graphics: (UIImage, UIImage, UIImage)) { + self.volumeControlStatusBarNode.graphics = graphics + } + + public func setForceInCallStatusBar(_ forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)) { + if self.forceInCallStatusBarText != forceInCallStatusBarText { + self.forceInCallStatusBarText = forceInCallStatusBarText + + self.updateLayout { $0.update(forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } + + self.invalidateTracingStatusBars() + } + } + + private func invalidateTracingStatusBars() { + self.tracingStatusBarsInvalidated = true + self.hostView.eventView.setNeedsLayout() + } + + public func invalidateDeferScreenEdgeGestures() { + self.shouldUpdateDeferScreenEdgeGestures = true + self.hostView.eventView.setNeedsLayout() + } + + public func invalidatePreferNavigationUIHidden() { + self.shouldInvalidatePreferNavigationUIHidden = true + self.hostView.eventView.setNeedsLayout() + } + + public func cancelInteractiveKeyboardGestures() { + self.windowPanRecognizer?.isEnabled = false + self.windowPanRecognizer?.isEnabled = true + + if self.windowLayout.upperKeyboardInputPositionBound != nil { + self.updateLayout { + $0.update(upperKeyboardInputPositionBound: nil, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) + } + } + + if self.keyboardGestureBeginLocation != nil { + self.keyboardGestureBeginLocation = nil + } + } + + public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.isInteractionBlocked { + return nil + } + if let coveringView = self.coveringView, !coveringView.isHidden, coveringView.superview != nil, coveringView.frame.contains(point) { + return coveringView.hitTest(point, with: event) + } + + for view in self.hostView.eventView.subviews.reversed() { + if NSStringFromClass(type(of: view)) == "UITransitionView" { + if let result = view.hitTest(point, with: event) { + return result + } + } + } + + if let result = self.overlayPresentationContext.hitTest(point, with: event) { + return result + } + + for controller in self._topLevelOverlayControllers.reversed() { + if let result = controller.view.hitTest(point, with: event) { + return result + } + } + + if let result = self.presentationContext.hitTest(point, with: event) { + return result + } + return self.viewController?.view.hitTest(point, with: event) + } + + func updateSize(_ value: CGSize, duration: Double) { + let transition: ContainedViewLayoutTransition + if !duration.isZero { + transition = .animated(duration: duration, curve: .easeInOut) + } else { + transition = .immediate + } + self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(value), safeInsets: safeInsetsForScreenSize(value, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } + } + + private var _rootController: ContainableController? + public var viewController: ContainableController? { + get { + return _rootController + } + set(value) { + if let rootController = self._rootController { + rootController.view.removeFromSuperview() + } + self._rootController = value + + if let rootController = self._rootController { + if !self.windowLayout.size.width.isZero && !self.windowLayout.size.height.isZero { + rootController.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate) + } + + self.hostView.containerView.insertSubview(rootController.view, at: 0) + } + + self.hostView.eventView.setNeedsLayout() + } + } + + private var _topLevelOverlayControllers: [ContainableController] = [] + public var topLevelOverlayControllers: [ContainableController] { + get { + return _topLevelOverlayControllers + } + set(value) { + for controller in self._topLevelOverlayControllers { + controller.view.removeFromSuperview() + } + self._topLevelOverlayControllers = value + + let layout = containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation) + for controller in self._topLevelOverlayControllers { + controller.containerLayoutUpdated(layout, transition: .immediate) + + if let coveringView = self.coveringView { + self.hostView.containerView.insertSubview(controller.view, belowSubview: coveringView) + } else { + self.hostView.containerView.insertSubview(controller.view, belowSubview: self.volumeControlStatusBarNode.view) + } + } + + self.presentationContext.topLevelSubview = self._topLevelOverlayControllers.first?.view + } + } + + public var coveringView: WindowCoveringView? { + didSet { + if self.coveringView !== oldValue { + if let oldValue = oldValue { + oldValue.layer.allowsGroupOpacity = true + oldValue.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak oldValue] _ in + oldValue?.removeFromSuperview() + }) + } + if let coveringView = self.coveringView { + coveringView.layer.removeAnimation(forKey: "opacity") + coveringView.layer.allowsGroupOpacity = false + coveringView.alpha = 1.0 + self.hostView.containerView.insertSubview(coveringView, belowSubview: self.volumeControlStatusBarNode.view) + if !self.windowLayout.size.width.isZero { + coveringView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) + coveringView.updateLayout(self.windowLayout.size) + } + } + } + } + } + + private func layoutSubviews() { + var hasPreview = false + var updatedHasPreview = false + for subview in self.hostView.eventView.subviews { + if checkIsPreviewingView(subview) { + applyThemeToPreviewingView(subview, accentColor: self.previewThemeAccentColor, darkBlur: self.previewThemeDarkBlur) + hasPreview = true + break + } + } + if hasPreview != self.cachedHasPreview { + self.cachedHasPreview = hasPreview + updatedHasPreview = true + } + + if self.tracingStatusBarsInvalidated || updatedHasPreview, let statusBarManager = statusBarManager, let keyboardManager = keyboardManager { + self.tracingStatusBarsInvalidated = false + + if self.statusBarHidden { + statusBarManager.updateState(surfaces: [], withSafeInsets: false, forceInCallStatusBarText: nil, forceHiddenBySystemWindows: false, animated: false) + } else { + var statusBarSurfaces: [StatusBarSurface] = [] + for layers in self.hostView.containerView.layer.traceableLayerSurfaces(withTag: WindowTracingTags.statusBar) { + let surface = StatusBarSurface() + for layer in layers { + let traceableInfo = layer.traceableInfo() + if let statusBar = traceableInfo?.userData as? StatusBar { + surface.addStatusBar(statusBar) + } + } + statusBarSurfaces.append(surface) + } + self.hostView.containerView.layer.adjustTraceableLayerTransforms(CGSize()) + var animatedUpdate = false + if let updatingLayout = self.updatingLayout { + if case .animated = updatingLayout.transition { + animatedUpdate = true + } + } + self.cachedWindowSubviewCount = self.hostView.containerView.window?.subviews.count ?? 0 + statusBarManager.updateState(surfaces: statusBarSurfaces, withSafeInsets: !self.windowLayout.safeInsets.top.isZero, forceInCallStatusBarText: self.forceInCallStatusBarText, forceHiddenBySystemWindows: hasPreview, animated: animatedUpdate) + } + + var keyboardSurfaces: [KeyboardSurface] = [] + for layers in self.hostView.containerView.layer.traceableLayerSurfaces(withTag: WindowTracingTags.keyboard) { + for layer in layers { + if let view = layer.delegate as? UITracingLayerView { + keyboardSurfaces.append(KeyboardSurface(host: view)) + } + } + } + keyboardManager.surfaces = keyboardSurfaces + + var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) + let orientationToLock: UIInterfaceOrientationMask + if self.windowLayout.size.width < self.windowLayout.size.height { + orientationToLock = .portrait + } else { + orientationToLock = .landscape + } + if let _rootController = self._rootController { + supportedOrientations = supportedOrientations.intersection(_rootController.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) + } + supportedOrientations = supportedOrientations.intersection(self.presentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) + supportedOrientations = supportedOrientations.intersection(self.overlayPresentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) + + var resolvedOrientations: UIInterfaceOrientationMask + switch self.windowLayout.metrics.widthClass { + case .regular: + resolvedOrientations = supportedOrientations.regularSize + case .compact: + resolvedOrientations = supportedOrientations.compactSize + } + if resolvedOrientations.isEmpty { + resolvedOrientations = [.portrait] + } + self.hostView.updateSupportedInterfaceOrientations(resolvedOrientations) + + self.hostView.updateDeferScreenEdgeGestures(self.collectScreenEdgeGestures()) + self.hostView.updatePreferNavigationUIHidden(self.collectPreferNavigationUIHidden()) + + self.shouldUpdateDeferScreenEdgeGestures = false + self.shouldInvalidatePreferNavigationUIHidden = false + } else if self.shouldUpdateDeferScreenEdgeGestures || self.shouldInvalidatePreferNavigationUIHidden { + self.shouldUpdateDeferScreenEdgeGestures = false + self.shouldInvalidatePreferNavigationUIHidden = false + + self.hostView.updateDeferScreenEdgeGestures(self.collectScreenEdgeGestures()) + self.hostView.updatePreferNavigationUIHidden(self.collectPreferNavigationUIHidden()) + } + + if !UIWindow.isDeviceRotating() { + if !self.hostView.isUpdatingOrientationLayout { + self.commitUpdatingLayout() + } else { + self.addPostUpdateToInterfaceOrientationBlock(f: { [weak self] in + if let strongSelf = self { + strongSelf.hostView.eventView.setNeedsLayout() + } + }) + } + } else { + UIWindow.addPostDeviceOrientationDidChange({ [weak self] in + if let strongSelf = self { + strongSelf.hostView.eventView.setNeedsLayout() + } + }) + } + } + + var postUpdateToInterfaceOrientationBlocks: [() -> Void] = [] + + private func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { + let blocks = self.postUpdateToInterfaceOrientationBlocks + self.postUpdateToInterfaceOrientationBlocks = [] + for f in blocks { + f() + } + self._rootController?.updateToInterfaceOrientation(orientation) + self.presentationContext.updateToInterfaceOrientation(orientation) + self.overlayPresentationContext.updateToInterfaceOrientation(orientation) + + for controller in self.topLevelOverlayControllers { + controller.updateToInterfaceOrientation(orientation) + } + } + + public func addPostUpdateToInterfaceOrientationBlock(f: @escaping () -> Void) { + postUpdateToInterfaceOrientationBlocks.append(f) + } + + private func updateLayout(_ update: (inout UpdatingLayout) -> ()) { + if self.updatingLayout == nil { + var updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) + update(&updatingLayout) + if updatingLayout.layout != self.windowLayout { + self.updatingLayout = updatingLayout + self.hostView.eventView.setNeedsLayout() + } + } else { + update(&self.updatingLayout!) + self.hostView.eventView.setNeedsLayout() + } + } + + private var isFirstLayout = true + + private func commitUpdatingLayout() { + if let updatingLayout = self.updatingLayout { + self.updatingLayout = nil + if updatingLayout.layout != self.windowLayout || self.isFirstLayout { + self.isFirstLayout = false + + let boundsSize = updatingLayout.layout.size + let deviceMetrics = DeviceMetrics.forScreenSize(boundsSize, hintHasOnScreenNavigation: self.hostView.hasOnScreenNavigation) + + var statusBarHeight: CGFloat? + if let statusBarHost = self.statusBarHost { + statusBarHeight = statusBarHost.statusBarFrame.size.height + } else { + statusBarHeight = deviceMetrics?.statusBarHeight ?? 20.0 + } + let statusBarWasHidden = self.statusBarHidden + if statusBarHiddenInLandscape && updatingLayout.layout.size.width > updatingLayout.layout.size.height { + statusBarHeight = nil + self.statusBarHidden = true + } else { + self.statusBarHidden = false + } + if self.statusBarHidden != statusBarWasHidden { + self.tracingStatusBarsInvalidated = true + self.hostView.eventView.setNeedsLayout() + } + let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout) + + let onScreenNavigationHeight = deviceMetrics?.onScreenNavigationHeight(inLandscape: boundsSize.width > boundsSize.height) + + self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver) + + let childLayout = containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation) + let childLayoutUpdated = self.updatedContainerLayout != childLayout + self.updatedContainerLayout = childLayout + + if childLayoutUpdated { + var rootLayout = childLayout + let rootTransition = updatingLayout.transition + if self.presentationContext.isCurrentlyOpaque { + rootLayout.inputHeight = nil + } + self._rootController?.containerLayoutUpdated(rootLayout, transition: rootTransition) + self.presentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) + self.overlayPresentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) + + for controller in self.topLevelOverlayControllers { + controller.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) + } + } + + let updatedInputOffset = inputHeightOffsetForLayout(self.windowLayout) + if !previousInputOffset.isEqual(to: updatedInputOffset) { + let hide = updatingLayout.transition.isAnimated && updatingLayout.layout.upperKeyboardInputPositionBound == updatingLayout.layout.size.height + self.keyboardManager?.updateInteractiveInputOffset(updatedInputOffset, transition: updatingLayout.transition, completion: { [weak self] in + if let strongSelf = self, hide { + strongSelf.updateLayout { + $0.update(upperKeyboardInputPositionBound: nil, transition: .immediate, overrideTransition: false) + } + strongSelf.hostView.eventView.endEditing(true) + } + }) + } + + self.volumeControlStatusBarNode.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) + self.volumeControlStatusBarNode.updateLayout(layout: childLayout, transition: updatingLayout.transition) + + if let coveringView = self.coveringView { + coveringView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) + coveringView.updateLayout(self.windowLayout.size) + } + } + } + } + + public func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void = {}) { + self.presentationContext.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) + } + + public func presentInGlobalOverlay(_ controller: ContainableController) { + self.overlayPresentationContext.present(controller) + } + + public func presentNative(_ controller: UIViewController) { + + } + + private func panGestureBegan(location: CGPoint) { + if self.windowLayout.upperKeyboardInputPositionBound != nil { + return + } + + let keyboardGestureBeginLocation = location + let view = self.hostView.containerView + let (firstResponder, accessoryHeight) = getFirstResponderAndAccessoryHeight(view) + if let inputHeight = self.windowLayout.inputHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < self.windowLayout.size.height - inputHeight - (accessoryHeight ?? 0.0) { + var enableGesture = true + if let view = self.hostView.containerView.hitTest(location, with: nil) { + if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view) { + enableGesture = false + } + } + if enableGesture, let _ = firstResponder { + self.keyboardGestureBeginLocation = keyboardGestureBeginLocation + self.keyboardGestureAccessoryHeight = accessoryHeight + } + } + } + + private func panGestureMoved(location: CGPoint) { + if let keyboardGestureBeginLocation = self.keyboardGestureBeginLocation { + let currentLocation = location + let deltaY = keyboardGestureBeginLocation.y - location.y + if deltaY * deltaY >= 3.0 * 3.0 || self.windowLayout.upperKeyboardInputPositionBound != nil { + self.updateLayout { + $0.update(upperKeyboardInputPositionBound: currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0), transition: .immediate, overrideTransition: false) + } + } + } + } + + private func panGestureEnded(location: CGPoint, velocity: CGPoint?) { + if self.keyboardGestureBeginLocation == nil { + return + } + + self.keyboardGestureBeginLocation = nil + let currentLocation = location + + let accessoryHeight = (self.keyboardGestureAccessoryHeight ?? 0.0) + + var canDismiss = false + if let upperKeyboardInputPositionBound = self.windowLayout.upperKeyboardInputPositionBound, upperKeyboardInputPositionBound >= self.windowLayout.size.height - accessoryHeight { + canDismiss = true + } else if let velocity = velocity, velocity.y > 100.0 { + canDismiss = true + } + + if canDismiss, let inputHeight = self.windowLayout.inputHeight, currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0) > self.windowLayout.size.height - inputHeight { + self.updateLayout { + $0.update(upperKeyboardInputPositionBound: self.windowLayout.size.height, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) + } + } else { + self.updateLayout { + $0.update(upperKeyboardInputPositionBound: nil, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) + } + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.panGestureBegan(location: recognizer.location(in: recognizer.view)) + case .changed: + self.panGestureMoved(location: recognizer.location(in: recognizer.view)) + case .ended: + self.panGestureEnded(location: recognizer.location(in: recognizer.view), velocity: recognizer.velocity(in: recognizer.view)) + case .cancelled: + self.panGestureEnded(location: recognizer.location(in: recognizer.view), velocity: nil) + default: + break + } + } + + private func collectScreenEdgeGestures() -> UIRectEdge { + var edges = self.presentationContext.combinedDeferScreenEdgeGestures() + + for controller in self.topLevelOverlayControllers { + if let controller = controller as? ViewController { + edges = edges.union(controller.deferScreenEdgeGestures) + } + } + + return edges + } + + private func collectPreferNavigationUIHidden() -> Bool { + return false + } + + public func forEachViewController(_ f: (ContainableController) -> Bool) { + for (controller, _) in self.presentationContext.controllers { + if !f(controller) { + break + } + } + for controller in self.topLevelOverlayControllers { + if !f(controller) { + break + } + } + } +} diff --git a/submodules/Display/Display/WindowCoveringView.swift b/submodules/Display/Display/WindowCoveringView.swift new file mode 100644 index 0000000000..6ba76a89c6 --- /dev/null +++ b/submodules/Display/Display/WindowCoveringView.swift @@ -0,0 +1,7 @@ +import Foundation +import UIKit + +open class WindowCoveringView: UIView { + open func updateLayout(_ size: CGSize) { + } +} diff --git a/submodules/Display/Display/WindowInputAccessoryHeightProvider.swift b/submodules/Display/Display/WindowInputAccessoryHeightProvider.swift new file mode 100644 index 0000000000..ef51090cbc --- /dev/null +++ b/submodules/Display/Display/WindowInputAccessoryHeightProvider.swift @@ -0,0 +1,6 @@ +import Foundation +import UIKit + +public protocol WindowInputAccessoryHeightProvider: class { + func getWindowInputAccessoryHeight() -> CGFloat +} diff --git a/submodules/Display/Display/WindowPanRecognizer.swift b/submodules/Display/Display/WindowPanRecognizer.swift new file mode 100644 index 0000000000..d9180edbaa --- /dev/null +++ b/submodules/Display/Display/WindowPanRecognizer.swift @@ -0,0 +1,81 @@ +import Foundation +import UIKit + +public final class WindowPanRecognizer: UIGestureRecognizer { + public var began: ((CGPoint) -> Void)? + public var moved: ((CGPoint) -> Void)? + public var ended: ((CGPoint, CGPoint?) -> Void)? + + private var previousPoints: [(CGPoint, Double)] = [] + + override public func reset() { + super.reset() + + self.previousPoints.removeAll() + } + + private func addPoint(_ point: CGPoint) { + self.previousPoints.append((point, CACurrentMediaTime())) + if self.previousPoints.count > 6 { + self.previousPoints.removeFirst() + } + } + + private func estimateVerticalVelocity() -> CGFloat { + let timestamp = CACurrentMediaTime() + var sum: CGFloat = 0.0 + var count = 0 + if self.previousPoints.count > 1 { + for i in 1 ..< self.previousPoints.count { + if self.previousPoints[i].1 >= timestamp - 0.1 { + sum += (self.previousPoints[i].0.y - self.previousPoints[i - 1].0.y) / CGFloat(self.previousPoints[i].1 - self.previousPoints[i - 1].1) + count += 1 + } + } + } + + if count != 0 { + return sum / CGFloat(count * 5) + } else { + return 0.0 + } + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let touch = touches.first { + let location = touch.location(in: self.view) + self.addPoint(location) + self.began?(location) + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if let touch = touches.first { + let location = touch.location(in: self.view) + self.addPoint(location) + self.moved?(location) + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if let touch = touches.first { + let location = touch.location(in: self.view) + self.addPoint(location) + self.ended?(location, CGPoint(x: 0.0, y: self.estimateVerticalVelocity())) + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + if let touch = touches.first { + self.ended?(touch.location(in: self.view), nil) + } + } +} diff --git a/submodules/Display/DisplayTests/DisplayTests.swift b/submodules/Display/DisplayTests/DisplayTests.swift new file mode 100644 index 0000000000..3f8d2f268a --- /dev/null +++ b/submodules/Display/DisplayTests/DisplayTests.swift @@ -0,0 +1,36 @@ +// +// DisplayTests.swift +// DisplayTests +// +// Created by Peter on 29/07/15. +// Copyright © 2015 Telegram. All rights reserved. +// + +import XCTest +@testable import Display + +class DisplayTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/submodules/Display/DisplayTests/Info.plist b/submodules/Display/DisplayTests/Info.plist new file mode 100644 index 0000000000..ba72822e87 --- /dev/null +++ b/submodules/Display/DisplayTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/Display/Display_Xcode.xcodeproj/project.pbxproj b/submodules/Display/Display_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..8edc164afd --- /dev/null +++ b/submodules/Display/Display_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,1759 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 09167E20229803DC005734A7 /* UIMenuItem+Icons.h in Headers */ = {isa = PBXBuildFile; fileRef = 09167E1E229803DC005734A7 /* UIMenuItem+Icons.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 09167E21229803DC005734A7 /* UIMenuItem+Icons.m in Sources */ = {isa = PBXBuildFile; fileRef = 09167E1F229803DC005734A7 /* UIMenuItem+Icons.m */; }; + 09C147D8216CCEF700390252 /* KeyShortcutsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C147D7216CCEF700390252 /* KeyShortcutsController.swift */; }; + 09C147DA216CD7E500390252 /* KeyShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C147D9216CD7E500390252 /* KeyShortcut.swift */; }; + 09DD88EB21BCA5E0000766BC /* EditableTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DD88EA21BCA5E0000766BC /* EditableTextNode.swift */; }; + 09E12476214D0978009FC9C3 /* DeviceMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09E12475214D0978009FC9C3 /* DeviceMetrics.swift */; }; + D00701982029CAD6006B9E34 /* TooltipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00701972029CAD6006B9E34 /* TooltipController.swift */; }; + D007019A2029CAE2006B9E34 /* TooltipControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00701992029CAE2006B9E34 /* TooltipControllerNode.swift */; }; + D0076F2021AC9C930059500A /* ToolbarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0076F1F21AC9C930059500A /* ToolbarNode.swift */; }; + D0076F2221ACA5020059500A /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0076F2121ACA5020059500A /* Toolbar.swift */; }; + D0078A681C92B21400DF6D92 /* StatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0078A671C92B21400DF6D92 /* StatusBar.swift */; }; + D00C7CD21E3657570080C3D5 /* TextFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00C7CD11E3657570080C3D5 /* TextFieldNode.swift */; }; + D015F7521D1AE08D00E269B5 /* ContainableController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7511D1AE08D00E269B5 /* ContainableController.swift */; }; + D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7571D1B467200E269B5 /* ActionSheetController.swift */; }; + D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.swift */; }; + D01847661FFA72E000075256 /* ContainedViewLayoutTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01847651FFA72E000075256 /* ContainedViewLayoutTransition.swift */; }; + D01E2BDE1D9049620066BF65 /* GridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BDD1D9049620066BF65 /* GridNode.swift */; }; + D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */; }; + D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE11D9049F60066BF65 /* GridItemNode.swift */; }; + D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE31D904A000066BF65 /* GridItem.swift */; }; + D01EA41B203227BA00B4B0B5 /* ViewControllerPreviewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EA41A203227BA00B4B0B5 /* ViewControllerPreviewing.swift */; }; + D01F728221F13891006AB634 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F728121F13891006AB634 /* Accessibility.swift */; }; + D02383801DDF7916004018B6 /* LegacyPresentedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023837F1DDF7916004018B6 /* LegacyPresentedController.swift */; }; + D02383821DDF798E004018B6 /* LegacyPresentedControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02383811DDF798E004018B6 /* LegacyPresentedControllerNode.swift */; }; + D02383861DE0E3B4004018B6 /* ListViewIntermediateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02383851DE0E3B4004018B6 /* ListViewIntermediateState.swift */; }; + D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */; }; + D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */; }; + D02C61F322A94BD600D4EC86 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C61F222A94BD600D4EC86 /* Keyboard.swift */; }; + D03310B3213F232600FC83CD /* ListViewTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03310B2213F232600FC83CD /* ListViewTapGestureRecognizer.swift */; }; + D033874E223D3E86007A2CE4 /* AccessibilityAreaNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033874D223D3E86007A2CE4 /* AccessibilityAreaNode.swift */; }; + D0338750223EE5A4007A2CE4 /* ActionSheetSwitchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033874F223EE5A4007A2CE4 /* ActionSheetSwitchItem.swift */; }; + D036574B1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036574A1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift */; }; + D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C01D6DF594007FC290 /* ContextMenuNode.swift */; }; + D03725C31D6DF7A6007FC290 /* ContextMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C21D6DF7A6007FC290 /* ContextMenuAction.swift */; }; + D03725C51D6DF8B9007FC290 /* ContextMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C41D6DF8B9007FC290 /* ContextMenuController.swift */; }; + D03AA4D5202C793E0056C405 /* PeekController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4D4202C793E0056C405 /* PeekController.swift */; }; + D03AA4D7202C79600056C405 /* PeekControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4D6202C79600056C405 /* PeekControllerNode.swift */; }; + D03AA4D9202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4D8202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift */; }; + D03AA4DB202DA6D60056C405 /* PeekControllerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DA202DA6D60056C405 /* PeekControllerContent.swift */; }; + D03AA4DD202DB1840056C405 /* PeekControllerGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DC202DB1840056C405 /* PeekControllerGestureRecognizer.swift */; }; + D03AA4E1202DD4490056C405 /* PeekControllerMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E0202DD4490056C405 /* PeekControllerMenuNode.swift */; }; + D03AA4E3202DD52E0056C405 /* PeekControllerMenuItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E2202DD52E0056C405 /* PeekControllerMenuItemNode.swift */; }; + D03AA4E9202E02070056C405 /* ListViewReorderingItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E8202E02070056C405 /* ListViewReorderingItemNode.swift */; }; + D03AA4EB202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4EA202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift */; }; + D03AA5162030C5F80056C405 /* ListViewTempItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA5152030C5F80056C405 /* ListViewTempItemNode.swift */; }; + D03B0E701D6331FB00955575 /* StatusBarHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B0E6F1D6331FB00955575 /* StatusBarHost.swift */; }; + D03E7DE41C96A90100C07816 /* NavigationShadow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D03E7DE31C96A90100C07816 /* NavigationShadow@2x.png */; }; + D03E7DE61C96B96E00C07816 /* NavigationBarTransitionContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E7DE51C96B96E00C07816 /* NavigationBarTransitionContainer.swift */; }; + D03E7DF81C96C5F200C07816 /* NSWeakReference.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E7DF61C96C5F200C07816 /* NSWeakReference.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D03E7DF91C96C5F200C07816 /* NSWeakReference.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E7DF71C96C5F200C07816 /* NSWeakReference.m */; }; + D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E7DFE1C96F7B400C07816 /* StatusBarManager.swift */; }; + D03E7E011C974AB300C07816 /* DisplayLinkDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E7E001C974AB300C07816 /* DisplayLinkDispatcher.swift */; }; + D04554AA21BDB93E007A6DD9 /* CollectionIndexNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */; }; + D04C468E1F4C97BE00D30FE1 /* PageControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04C468D1F4C97BE00D30FE1 /* PageControlNode.swift */; }; + D05174B31EAA833200A1BF36 /* CASeeThroughTracingLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D05174B11EAA833200A1BF36 /* CASeeThroughTracingLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05174B41EAA833200A1BF36 /* CASeeThroughTracingLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D05174B21EAA833200A1BF36 /* CASeeThroughTracingLayer.m */; }; + D053CB601D22B4F200DD41DF /* CATracingLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D053CB5E1D22B4F200DD41DF /* CATracingLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D053CB5F1D22B4F200DD41DF /* CATracingLayer.m */; }; + D053DAD12018ECF900993D32 /* WindowCoveringView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D053DAD02018ECF900993D32 /* WindowCoveringView.swift */; }; + D05BE4AB1D1F25E3002BD72C /* PresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05BE4AA1D1F25E3002BD72C /* PresentationContext.swift */; }; + D05CC2671B69316F00E235A3 /* Display.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC2661B69316F00E235A3 /* Display.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC26E1B69316F00E235A3 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D05CC2631B69316F00E235A3 /* Display.framework */; }; + D05CC2731B69316F00E235A3 /* DisplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2721B69316F00E235A3 /* DisplayTests.swift */; }; + D05CC2A01B69326400E235A3 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC29F1B69326400E235A3 /* NavigationController.swift */; }; + D05CC2A21B69326C00E235A3 /* WindowContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2A11B69326C00E235A3 /* WindowContent.swift */; }; + D05CC2E71B69555800E235A3 /* CAAnimationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2E41B69555800E235A3 /* CAAnimationUtils.swift */; }; + D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2EA1B69558A00E235A3 /* RuntimeUtils.m */; }; + D05CC2ED1B69558A00E235A3 /* RuntimeUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC2EB1B69558A00E235A3 /* RuntimeUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC2F71B6955D000E235A3 /* UIKitUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2EE1B6955D000E235A3 /* UIKitUtils.swift */; }; + D05CC2F81B6955D000E235A3 /* UIViewController+Navigation.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2EF1B6955D000E235A3 /* UIViewController+Navigation.m */; }; + D05CC2F91B6955D000E235A3 /* UIViewController+Navigation.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC2F01B6955D000E235A3 /* UIViewController+Navigation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC2FA1B6955D000E235A3 /* UINavigationItem+Proxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2F11B6955D000E235A3 /* UINavigationItem+Proxy.m */; }; + D05CC2FB1B6955D000E235A3 /* UINavigationItem+Proxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC2F21B6955D000E235A3 /* UINavigationItem+Proxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC2FC1B6955D000E235A3 /* UIKitUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2F31B6955D000E235A3 /* UIKitUtils.m */; }; + D05CC2FD1B6955D000E235A3 /* UIKitUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC2F41B6955D000E235A3 /* UIKitUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC2FE1B6955D000E235A3 /* UIWindow+OrientationChange.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2F51B6955D000E235A3 /* UIWindow+OrientationChange.m */; }; + D05CC2FF1B6955D000E235A3 /* UIWindow+OrientationChange.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC2F61B6955D000E235A3 /* UIWindow+OrientationChange.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC3031B69568600E235A3 /* NotificationCenterUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC3011B69568600E235A3 /* NotificationCenterUtils.m */; }; + D05CC3041B69568600E235A3 /* NotificationCenterUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC3021B69568600E235A3 /* NotificationCenterUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC3071B69575900E235A3 /* NSBag.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC3051B69575900E235A3 /* NSBag.m */; }; + D05CC3081B69575900E235A3 /* NSBag.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC3061B69575900E235A3 /* NSBag.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC3151B695A9600E235A3 /* NavigationTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC3091B695A9500E235A3 /* NavigationTransitionCoordinator.swift */; }; + D05CC3161B695A9600E235A3 /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC30A1B695A9500E235A3 /* NavigationBar.swift */; }; + D05CC3191B695A9600E235A3 /* NavigationBackButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC30D1B695A9500E235A3 /* NavigationBackButtonNode.swift */; }; + D05CC31A1B695A9600E235A3 /* NavigationButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC30E1B695A9500E235A3 /* NavigationButtonNode.swift */; }; + D05CC31B1B695A9600E235A3 /* NavigationTitleNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC30F1B695A9500E235A3 /* NavigationTitleNode.swift */; }; + D05CC31D1B695A9600E235A3 /* UIBarButtonItem+Proxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC3111B695A9600E235A3 /* UIBarButtonItem+Proxy.m */; }; + D05CC31E1B695A9600E235A3 /* UIBarButtonItem+Proxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC3121B695A9600E235A3 /* UIBarButtonItem+Proxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC31F1B695A9600E235A3 /* NavigationControllerProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC3131B695A9600E235A3 /* NavigationControllerProxy.m */; }; + D05CC3201B695A9600E235A3 /* NavigationControllerProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC3141B695A9600E235A3 /* NavigationControllerProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC3241B695B0700E235A3 /* NavigationBarProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC3221B695B0700E235A3 /* NavigationBarProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D05CC3251B695B0700E235A3 /* NavigationBarProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC3231B695B0700E235A3 /* NavigationBarProxy.m */; }; + D05CC3271B69725400E235A3 /* NavigationBackArrowLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D05CC3261B69725400E235A3 /* NavigationBackArrowLight@2x.png */; }; + D05CC3291B69750D00E235A3 /* InteractiveTransitionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC3281B69750D00E235A3 /* InteractiveTransitionGestureRecognizer.swift */; }; + D06B76DB20592A97006E9EEA /* LayoutSizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06B76DA20592A97006E9EEA /* LayoutSizes.swift */; }; + D06D37A220779C82009219B6 /* VolumeControlStatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06D37A120779C82009219B6 /* VolumeControlStatusBar.swift */; }; + D06EE8451B7140FF00837186 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06EE8441B7140FF00837186 /* Font.swift */; }; + D077B8E91F4637040046D27A /* NavigationBarBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D077B8E81F4637040046D27A /* NavigationBarBadge.swift */; }; + D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081229C1D19AA1C005F7395 /* ContainerViewLayout.swift */; }; + D087BFB51F75181D003FD209 /* ChildWindowHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB41F75181D003FD209 /* ChildWindowHostView.swift */; }; + D08B61BE22A752FD000A46A8 /* UITracingLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B61BD22A752FD000A46A8 /* UITracingLayerView.swift */; }; + D08CAA7B1ED73C990000FDA8 /* ViewControllerTracingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08CAA7A1ED73C990000FDA8 /* ViewControllerTracingNode.swift */; }; + D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E90391D24159200533158 /* ActionSheetItem.swift */; }; + D08E903C1D2417E000533158 /* ActionSheetButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E903B1D2417E000533158 /* ActionSheetButtonItem.swift */; }; + D08E903E1D24187900533158 /* ActionSheetItemGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E903D1D24187900533158 /* ActionSheetItemGroup.swift */; }; + D08E90471D243C2F00533158 /* HighlightTrackingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E90461D243C2F00533158 /* HighlightTrackingButton.swift */; }; + D096A4501EA64F580000A7AE /* ActionSheetCheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D096A44F1EA64F580000A7AE /* ActionSheetCheckboxItem.swift */; }; + D0A1346220336C350059716A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1346120336C350059716A /* ViewController.swift */; }; + D0A134642034DE580059716A /* TabBarTapRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A134632034DE580059716A /* TabBarTapRecognizer.swift */; }; + D0A749951E3A9E7B00AD786E /* SwitchNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A749941E3A9E7B00AD786E /* SwitchNode.swift */; }; + D0AA840E1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA840D1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift */; }; + D0AA84101FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA840F1FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift */; }; + D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AE2CA51C94548900F2FD3C /* GenerateImage.swift */; }; + D0AE3D4D1D25C816001CCE13 /* NavigationBarTransitionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AE3D4C1D25C816001CCE13 /* NavigationBarTransitionState.swift */; }; + D0B367201C94A53A00346D2E /* StatusBarProxyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B3671F1C94A53A00346D2E /* StatusBarProxyNode.swift */; }; + D0BB4EBA1F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BB4EB91F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift */; }; + D0BE93191E8ED71100DCC1E6 /* NativeWindowHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE93181E8ED71100DCC1E6 /* NativeWindowHostView.swift */; }; + D0C0B5991EDF3BC9000F4D2C /* ActionSheetTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5981EDF3BC9000F4D2C /* ActionSheetTextItem.swift */; }; + D0C0B59D1EE022CC000F4D2C /* NavigationBarContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B59C1EE022CC000F4D2C /* NavigationBarContentNode.swift */; }; + D0C0D28F1C997110001D2851 /* FBAnimationPerformanceTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D0C0D28D1C997110001D2851 /* FBAnimationPerformanceTracker.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0C0D2901C997110001D2851 /* FBAnimationPerformanceTracker.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0C0D28E1C997110001D2851 /* FBAnimationPerformanceTracker.mm */; }; + D0C12A1A1F3375B400B3F66D /* NavigationBarTitleTransitionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C12A191F3375B400B3F66D /* NavigationBarTitleTransitionNode.swift */; }; + D0C2DFC61CC4431D0044FF83 /* ASTransformLayerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFBB1CC4431D0044FF83 /* ASTransformLayerNode.swift */; }; + D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFBC1CC4431D0044FF83 /* ListViewItemNode.swift */; }; + D0C2DFC81CC4431D0044FF83 /* Spring.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFBD1CC4431D0044FF83 /* Spring.swift */; }; + D0C2DFCA1CC4431D0044FF83 /* ListViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFBF1CC4431D0044FF83 /* ListViewItem.swift */; }; + D0C2DFCB1CC4431D0044FF83 /* ListViewAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFC01CC4431D0044FF83 /* ListViewAnimation.swift */; }; + D0C2DFCD1CC4431D0044FF83 /* ListViewTransactionQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFC21CC4431D0044FF83 /* ListViewTransactionQueue.swift */; }; + D0C2DFCE1CC4431D0044FF83 /* ListViewAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFC31CC4431D0044FF83 /* ListViewAccessoryItem.swift */; }; + D0C2DFCF1CC4431D0044FF83 /* ListViewScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFC41CC4431D0044FF83 /* ListViewScroller.swift */; }; + D0C2DFD01CC4431D0044FF83 /* ListViewAccessoryItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFC51CC4431D0044FF83 /* ListViewAccessoryItemNode.swift */; }; + D0C2DFFC1CC528B70044FF83 /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C2DFFB1CC528B70044FF83 /* SwiftSignalKit.framework */; }; + D0C85DD01D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C85DCF1D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift */; }; + D0C85DD21D1C08AE00124894 /* ActionSheetItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C85DD11D1C08AE00124894 /* ActionSheetItemNode.swift */; }; + D0C85DD41D1C1E6A00124894 /* ActionSheetItemGroupNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C85DD31D1C1E6A00124894 /* ActionSheetItemGroupNode.swift */; }; + D0CA3F8A2073F7650042D2B6 /* LinkHighlightingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CA3F892073F7650042D2B6 /* LinkHighlightingNode.swift */; }; + D0CA3F8C2073F8240042D2B6 /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CA3F8B2073F8240042D2B6 /* TapLongTapOrDoubleTapGestureRecognizer.swift */; }; + D0CB78901F9822F8004AB79B /* WindowPanRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CB788F1F9822F8004AB79B /* WindowPanRecognizer.swift */; }; + D0CD12161CCFEB4E000DE7BC /* ScrollToTopProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */; }; + D0CE67921F7DA11700FFB557 /* ActionSheetTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE67911F7DA11700FFB557 /* ActionSheetTheme.swift */; }; + D0CE8CE91F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE81F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift */; }; + D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D94A161D3814F900740E02 /* UniversalTapRecognizer.swift */; }; + D0DA444C1E4DCA4A005FDCA7 /* AlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */; }; + D0DA444E1E4DCA6E005FDCA7 /* AlertControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA444D1E4DCA6E005FDCA7 /* AlertControllerNode.swift */; }; + D0DA44501E4DCBDE005FDCA7 /* AlertContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA444F1E4DCBDE005FDCA7 /* AlertContentNode.swift */; }; + D0DA44521E4DCC11005FDCA7 /* TextAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA44511E4DCC11005FDCA7 /* TextAlertController.swift */; }; + D0DC48541BF93D8B00F672FD /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC48531BF93D8A00F672FD /* TabBarController.swift */; }; + D0DC48561BF945DD00F672FD /* TabBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC48551BF945DD00F672FD /* TabBarNode.swift */; }; + D0DC485F1BF949FB00F672FD /* TabBarContollerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC485E1BF949FB00F672FD /* TabBarContollerNode.swift */; }; + D0E1D6721CBC201E00B04029 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E1D6711CBC201E00B04029 /* AsyncDisplayKit.framework */; }; + D0E35A031DE473B900BC6096 /* HighlightableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E35A021DE473B900BC6096 /* HighlightableButton.swift */; }; + D0F1132F1D6F3C20008C3597 /* ContextMenuActionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F1132E1D6F3C20008C3597 /* ContextMenuActionNode.swift */; }; + D0F7AB371DCFF6F8009AD9A1 /* ListViewItemHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F7AB361DCFF6F8009AD9A1 /* ListViewItemHeader.swift */; }; + D0F8C3932014FB7C00236FC5 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2DFBE1CC4431D0044FF83 /* ListView.swift */; }; + D0FA08C220487A8600DD23FC /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA08C120487A8600DD23FC /* HapticFeedback.swift */; }; + D0FA08C42048803C00DD23FC /* TextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA08C32048803C00DD23FC /* TextNode.swift */; }; + D0FA08C6204880C900DD23FC /* ImmediateTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA08C5204880C900DD23FC /* ImmediateTextNode.swift */; }; + D0FF9B301E7196F6000C66DB /* KeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FF9B2F1E7196F6000C66DB /* KeyboardManager.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + D05CC26F1B69316F00E235A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05CC25A1B69316F00E235A3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D05CC2621B69316F00E235A3; + remoteInfo = Display; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 09167E1E229803DC005734A7 /* UIMenuItem+Icons.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIMenuItem+Icons.h"; sourceTree = ""; }; + 09167E1F229803DC005734A7 /* UIMenuItem+Icons.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIMenuItem+Icons.m"; sourceTree = ""; }; + 09C147D7216CCEF700390252 /* KeyShortcutsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyShortcutsController.swift; sourceTree = ""; }; + 09C147D9216CD7E500390252 /* KeyShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyShortcut.swift; sourceTree = ""; }; + 09DD88EA21BCA5E0000766BC /* EditableTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextNode.swift; sourceTree = ""; }; + 09E12475214D0978009FC9C3 /* DeviceMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetrics.swift; sourceTree = ""; }; + D00701972029CAD6006B9E34 /* TooltipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipController.swift; sourceTree = ""; }; + D00701992029CAE2006B9E34 /* TooltipControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipControllerNode.swift; sourceTree = ""; }; + D0076F1F21AC9C930059500A /* ToolbarNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarNode.swift; sourceTree = ""; }; + D0076F2121ACA5020059500A /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; + D0078A671C92B21400DF6D92 /* StatusBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBar.swift; sourceTree = ""; }; + D00C7CD11E3657570080C3D5 /* TextFieldNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldNode.swift; sourceTree = ""; }; + D015F7511D1AE08D00E269B5 /* ContainableController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainableController.swift; sourceTree = ""; }; + D015F7571D1B467200E269B5 /* ActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetController.swift; sourceTree = ""; }; + D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetControllerNode.swift; sourceTree = ""; }; + D01847651FFA72E000075256 /* ContainedViewLayoutTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainedViewLayoutTransition.swift; sourceTree = ""; }; + D01C06C61FC2558F001561AB /* SwiftSignalKitMac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKitMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D01E2BDD1D9049620066BF65 /* GridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNode.swift; sourceTree = ""; }; + D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNodeScroller.swift; sourceTree = ""; }; + D01E2BE11D9049F60066BF65 /* GridItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItemNode.swift; sourceTree = ""; }; + D01E2BE31D904A000066BF65 /* GridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItem.swift; sourceTree = ""; }; + D01EA41A203227BA00B4B0B5 /* ViewControllerPreviewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerPreviewing.swift; sourceTree = ""; }; + D01F728121F13891006AB634 /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; + D023837F1DDF7916004018B6 /* LegacyPresentedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyPresentedController.swift; sourceTree = ""; }; + D02383811DDF798E004018B6 /* LegacyPresentedControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyPresentedControllerNode.swift; sourceTree = ""; }; + D02383851DE0E3B4004018B6 /* ListViewIntermediateState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewIntermediateState.swift; sourceTree = ""; }; + D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuContainerNode.swift; sourceTree = ""; }; + D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeUtils.swift; sourceTree = ""; }; + D02C61F222A94BD600D4EC86 /* Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = ""; }; + D03310B2213F232600FC83CD /* ListViewTapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewTapGestureRecognizer.swift; sourceTree = ""; }; + D033874D223D3E86007A2CE4 /* AccessibilityAreaNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityAreaNode.swift; sourceTree = ""; }; + D033874F223EE5A4007A2CE4 /* ActionSheetSwitchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionSheetSwitchItem.swift; sourceTree = ""; }; + D036574A1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinimizeKeyboardGestureRecognizer.swift; sourceTree = ""; }; + D03725C01D6DF594007FC290 /* ContextMenuNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuNode.swift; sourceTree = ""; }; + D03725C21D6DF7A6007FC290 /* ContextMenuAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuAction.swift; sourceTree = ""; }; + D03725C41D6DF8B9007FC290 /* ContextMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuController.swift; sourceTree = ""; }; + D03AA4D4202C793E0056C405 /* PeekController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekController.swift; sourceTree = ""; }; + D03AA4D6202C79600056C405 /* PeekControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerNode.swift; sourceTree = ""; }; + D03AA4D8202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalOverlayPresentationContext.swift; sourceTree = ""; }; + D03AA4DA202DA6D60056C405 /* PeekControllerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerContent.swift; sourceTree = ""; }; + D03AA4DC202DB1840056C405 /* PeekControllerGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerGestureRecognizer.swift; sourceTree = ""; }; + D03AA4E0202DD4490056C405 /* PeekControllerMenuNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerMenuNode.swift; sourceTree = ""; }; + D03AA4E2202DD52E0056C405 /* PeekControllerMenuItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerMenuItemNode.swift; sourceTree = ""; }; + D03AA4E8202E02070056C405 /* ListViewReorderingItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewReorderingItemNode.swift; sourceTree = ""; }; + D03AA4EA202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewReorderingGestureRecognizer.swift; sourceTree = ""; }; + D03AA5152030C5F80056C405 /* ListViewTempItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewTempItemNode.swift; sourceTree = ""; }; + D03B0E6F1D6331FB00955575 /* StatusBarHost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarHost.swift; sourceTree = ""; }; + D03E7DE31C96A90100C07816 /* NavigationShadow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "NavigationShadow@2x.png"; sourceTree = ""; }; + D03E7DE51C96B96E00C07816 /* NavigationBarTransitionContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarTransitionContainer.swift; sourceTree = ""; }; + D03E7DF61C96C5F200C07816 /* NSWeakReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSWeakReference.h; sourceTree = ""; }; + D03E7DF71C96C5F200C07816 /* NSWeakReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSWeakReference.m; sourceTree = ""; }; + D03E7DFE1C96F7B400C07816 /* StatusBarManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarManager.swift; sourceTree = ""; }; + D03E7E001C974AB300C07816 /* DisplayLinkDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayLinkDispatcher.swift; sourceTree = ""; }; + D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionIndexNode.swift; sourceTree = ""; }; + D04C468D1F4C97BE00D30FE1 /* PageControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControlNode.swift; sourceTree = ""; }; + D05174B11EAA833200A1BF36 /* CASeeThroughTracingLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CASeeThroughTracingLayer.h; sourceTree = ""; }; + D05174B21EAA833200A1BF36 /* CASeeThroughTracingLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CASeeThroughTracingLayer.m; sourceTree = ""; }; + D053CB5E1D22B4F200DD41DF /* CATracingLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CATracingLayer.h; sourceTree = ""; }; + D053CB5F1D22B4F200DD41DF /* CATracingLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CATracingLayer.m; sourceTree = ""; }; + D053DAD02018ECF900993D32 /* WindowCoveringView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCoveringView.swift; sourceTree = ""; }; + D05BE4AA1D1F25E3002BD72C /* PresentationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationContext.swift; sourceTree = ""; }; + D05CC2631B69316F00E235A3 /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D05CC2661B69316F00E235A3 /* Display.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Display.h; sourceTree = ""; }; + D05CC2681B69316F00E235A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D05CC26D1B69316F00E235A3 /* DisplayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DisplayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D05CC2721B69316F00E235A3 /* DisplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTests.swift; sourceTree = ""; }; + D05CC2741B69316F00E235A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D05CC29F1B69326400E235A3 /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; + D05CC2A11B69326C00E235A3 /* WindowContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowContent.swift; sourceTree = ""; }; + D05CC2E41B69555800E235A3 /* CAAnimationUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAAnimationUtils.swift; sourceTree = ""; }; + D05CC2EA1B69558A00E235A3 /* RuntimeUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RuntimeUtils.m; sourceTree = ""; }; + D05CC2EB1B69558A00E235A3 /* RuntimeUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RuntimeUtils.h; sourceTree = ""; }; + D05CC2EE1B6955D000E235A3 /* UIKitUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitUtils.swift; sourceTree = ""; }; + D05CC2EF1B6955D000E235A3 /* UIViewController+Navigation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Navigation.m"; sourceTree = ""; }; + D05CC2F01B6955D000E235A3 /* UIViewController+Navigation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Navigation.h"; sourceTree = ""; }; + D05CC2F11B6955D000E235A3 /* UINavigationItem+Proxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UINavigationItem+Proxy.m"; sourceTree = ""; }; + D05CC2F21B6955D000E235A3 /* UINavigationItem+Proxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UINavigationItem+Proxy.h"; sourceTree = ""; }; + D05CC2F31B6955D000E235A3 /* UIKitUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIKitUtils.m; sourceTree = ""; }; + D05CC2F41B6955D000E235A3 /* UIKitUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIKitUtils.h; sourceTree = ""; }; + D05CC2F51B6955D000E235A3 /* UIWindow+OrientationChange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWindow+OrientationChange.m"; sourceTree = ""; }; + D05CC2F61B6955D000E235A3 /* UIWindow+OrientationChange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIWindow+OrientationChange.h"; sourceTree = ""; }; + D05CC3011B69568600E235A3 /* NotificationCenterUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationCenterUtils.m; sourceTree = ""; }; + D05CC3021B69568600E235A3 /* NotificationCenterUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationCenterUtils.h; sourceTree = ""; }; + D05CC3051B69575900E235A3 /* NSBag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSBag.m; sourceTree = ""; }; + D05CC3061B69575900E235A3 /* NSBag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSBag.h; sourceTree = ""; }; + D05CC3091B695A9500E235A3 /* NavigationTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationTransitionCoordinator.swift; sourceTree = ""; }; + D05CC30A1B695A9500E235A3 /* NavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; + D05CC30D1B695A9500E235A3 /* NavigationBackButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBackButtonNode.swift; sourceTree = ""; }; + D05CC30E1B695A9500E235A3 /* NavigationButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationButtonNode.swift; sourceTree = ""; }; + D05CC30F1B695A9500E235A3 /* NavigationTitleNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationTitleNode.swift; sourceTree = ""; }; + D05CC3111B695A9600E235A3 /* UIBarButtonItem+Proxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIBarButtonItem+Proxy.m"; sourceTree = ""; }; + D05CC3121B695A9600E235A3 /* UIBarButtonItem+Proxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIBarButtonItem+Proxy.h"; sourceTree = ""; }; + D05CC3131B695A9600E235A3 /* NavigationControllerProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NavigationControllerProxy.m; sourceTree = ""; }; + D05CC3141B695A9600E235A3 /* NavigationControllerProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NavigationControllerProxy.h; sourceTree = ""; }; + D05CC3221B695B0700E235A3 /* NavigationBarProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NavigationBarProxy.h; sourceTree = ""; }; + D05CC3231B695B0700E235A3 /* NavigationBarProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NavigationBarProxy.m; sourceTree = ""; }; + D05CC3261B69725400E235A3 /* NavigationBackArrowLight@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "NavigationBackArrowLight@2x.png"; sourceTree = ""; }; + D05CC3281B69750D00E235A3 /* InteractiveTransitionGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveTransitionGestureRecognizer.swift; sourceTree = ""; }; + D06B76DA20592A97006E9EEA /* LayoutSizes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutSizes.swift; sourceTree = ""; }; + D06D37A120779C82009219B6 /* VolumeControlStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeControlStatusBar.swift; sourceTree = ""; }; + D06EE8441B7140FF00837186 /* Font.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = ""; }; + D077B8E81F4637040046D27A /* NavigationBarBadge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarBadge.swift; sourceTree = ""; }; + D081229C1D19AA1C005F7395 /* ContainerViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerViewLayout.swift; sourceTree = ""; }; + D087BFB41F75181D003FD209 /* ChildWindowHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChildWindowHostView.swift; sourceTree = ""; }; + D08B61BD22A752FD000A46A8 /* UITracingLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITracingLayerView.swift; sourceTree = ""; }; + D08CAA7A1ED73C990000FDA8 /* ViewControllerTracingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerTracingNode.swift; sourceTree = ""; }; + D08E90391D24159200533158 /* ActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItem.swift; sourceTree = ""; }; + D08E903B1D2417E000533158 /* ActionSheetButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetButtonItem.swift; sourceTree = ""; }; + D08E903D1D24187900533158 /* ActionSheetItemGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemGroup.swift; sourceTree = ""; }; + D08E90461D243C2F00533158 /* HighlightTrackingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightTrackingButton.swift; sourceTree = ""; }; + D096A44F1EA64F580000A7AE /* ActionSheetCheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetCheckboxItem.swift; sourceTree = ""; }; + D0A1346120336C350059716A /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + D0A134632034DE580059716A /* TabBarTapRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTapRecognizer.swift; sourceTree = ""; }; + D0A749941E3A9E7B00AD786E /* SwitchNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchNode.swift; sourceTree = ""; }; + D0AA840D1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewFloatingHeaderNode.swift; sourceTree = ""; }; + D0AA840F1FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewOverscrollBackgroundNode.swift; sourceTree = ""; }; + D0AE2CA51C94548900F2FD3C /* GenerateImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateImage.swift; sourceTree = ""; }; + D0AE3D4C1D25C816001CCE13 /* NavigationBarTransitionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarTransitionState.swift; sourceTree = ""; }; + D0B3671F1C94A53A00346D2E /* StatusBarProxyNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarProxyNode.swift; sourceTree = ""; }; + D0BB4EB91F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowInputAccessoryHeightProvider.swift; sourceTree = ""; }; + D0BE93181E8ED71100DCC1E6 /* NativeWindowHostView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeWindowHostView.swift; sourceTree = ""; }; + D0C0B5981EDF3BC9000F4D2C /* ActionSheetTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetTextItem.swift; sourceTree = ""; }; + D0C0B59C1EE022CC000F4D2C /* NavigationBarContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarContentNode.swift; sourceTree = ""; }; + D0C0D28D1C997110001D2851 /* FBAnimationPerformanceTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBAnimationPerformanceTracker.h; sourceTree = ""; }; + D0C0D28E1C997110001D2851 /* FBAnimationPerformanceTracker.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FBAnimationPerformanceTracker.mm; sourceTree = ""; }; + D0C12A191F3375B400B3F66D /* NavigationBarTitleTransitionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarTitleTransitionNode.swift; sourceTree = ""; }; + D0C2DFBB1CC4431D0044FF83 /* ASTransformLayerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ASTransformLayerNode.swift; sourceTree = ""; }; + D0C2DFBC1CC4431D0044FF83 /* ListViewItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewItemNode.swift; sourceTree = ""; }; + D0C2DFBD1CC4431D0044FF83 /* Spring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spring.swift; sourceTree = ""; }; + D0C2DFBE1CC4431D0044FF83 /* ListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; + D0C2DFBF1CC4431D0044FF83 /* ListViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewItem.swift; sourceTree = ""; }; + D0C2DFC01CC4431D0044FF83 /* ListViewAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewAnimation.swift; sourceTree = ""; }; + D0C2DFC21CC4431D0044FF83 /* ListViewTransactionQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewTransactionQueue.swift; sourceTree = ""; }; + D0C2DFC31CC4431D0044FF83 /* ListViewAccessoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewAccessoryItem.swift; sourceTree = ""; }; + D0C2DFC41CC4431D0044FF83 /* ListViewScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewScroller.swift; sourceTree = ""; }; + D0C2DFC51CC4431D0044FF83 /* ListViewAccessoryItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewAccessoryItemNode.swift; sourceTree = ""; }; + D0C2DFFB1CC528B70044FF83 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0C85DCF1D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemGroupsContainerNode.swift; sourceTree = ""; }; + D0C85DD11D1C08AE00124894 /* ActionSheetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemNode.swift; sourceTree = ""; }; + D0C85DD31D1C1E6A00124894 /* ActionSheetItemGroupNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemGroupNode.swift; sourceTree = ""; }; + D0CA3F892073F7650042D2B6 /* LinkHighlightingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkHighlightingNode.swift; sourceTree = ""; }; + D0CA3F8B2073F8240042D2B6 /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = ""; }; + D0CB788F1F9822F8004AB79B /* WindowPanRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowPanRecognizer.swift; sourceTree = ""; }; + D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollToTopProxyView.swift; sourceTree = ""; }; + D0CE67911F7DA11700FFB557 /* ActionSheetTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionSheetTheme.swift; sourceTree = ""; }; + D0CE8CE81F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarTitleView.swift; sourceTree = ""; }; + D0D94A161D3814F900740E02 /* UniversalTapRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalTapRecognizer.swift; sourceTree = ""; }; + D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertController.swift; sourceTree = ""; }; + D0DA444D1E4DCA6E005FDCA7 /* AlertControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertControllerNode.swift; sourceTree = ""; }; + D0DA444F1E4DCBDE005FDCA7 /* AlertContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertContentNode.swift; sourceTree = ""; }; + D0DA44511E4DCC11005FDCA7 /* TextAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAlertController.swift; sourceTree = ""; }; + D0DC48531BF93D8A00F672FD /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; + D0DC48551BF945DD00F672FD /* TabBarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarNode.swift; sourceTree = ""; }; + D0DC485E1BF949FB00F672FD /* TabBarContollerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarContollerNode.swift; sourceTree = ""; }; + D0E1D6351CBC159C00B04029 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + D0E1D6711CBC201E00B04029 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0E35A021DE473B900BC6096 /* HighlightableButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightableButton.swift; sourceTree = ""; }; + D0F1132E1D6F3C20008C3597 /* ContextMenuActionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuActionNode.swift; sourceTree = ""; }; + D0F7AB361DCFF6F8009AD9A1 /* ListViewItemHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewItemHeader.swift; sourceTree = ""; }; + D0FA08C120487A8600DD23FC /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; + D0FA08C32048803C00DD23FC /* TextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextNode.swift; sourceTree = ""; }; + D0FA08C5204880C900DD23FC /* ImmediateTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImmediateTextNode.swift; sourceTree = ""; }; + D0FF9B2F1E7196F6000C66DB /* KeyboardManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D05CC25F1B69316F00E235A3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0C2DFFC1CC528B70044FF83 /* SwiftSignalKit.framework in Frameworks */, + D0E1D6721CBC201E00B04029 /* AsyncDisplayKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D05CC26A1B69316F00E235A3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D05CC26E1B69316F00E235A3 /* Display.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 09C147D6216CCE9700390252 /* Key Shortcuts */ = { + isa = PBXGroup; + children = ( + 09C147D9216CD7E500390252 /* KeyShortcut.swift */, + 09C147D7216CCEF700390252 /* KeyShortcutsController.swift */, + ); + name = "Key Shortcuts"; + sourceTree = ""; + }; + D00701962029CAC4006B9E34 /* Tooltip */ = { + isa = PBXGroup; + children = ( + D00701972029CAD6006B9E34 /* TooltipController.swift */, + D00701992029CAE2006B9E34 /* TooltipControllerNode.swift */, + ); + name = Tooltip; + sourceTree = ""; + }; + D015F7501D1ADC6800E269B5 /* Window */ = { + isa = PBXGroup; + children = ( + D05CC2A11B69326C00E235A3 /* WindowContent.swift */, + D0BE93181E8ED71100DCC1E6 /* NativeWindowHostView.swift */, + D087BFB41F75181D003FD209 /* ChildWindowHostView.swift */, + D0BB4EB91F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift */, + D0CB788F1F9822F8004AB79B /* WindowPanRecognizer.swift */, + D053DAD02018ECF900993D32 /* WindowCoveringView.swift */, + D03AA4D8202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift */, + ); + name = Window; + sourceTree = ""; + }; + D015F7551D1B142300E269B5 /* Tab Bar */ = { + isa = PBXGroup; + children = ( + D0DC48531BF93D8A00F672FD /* TabBarController.swift */, + D0DC485E1BF949FB00F672FD /* TabBarContollerNode.swift */, + D0DC48551BF945DD00F672FD /* TabBarNode.swift */, + D0A134632034DE580059716A /* TabBarTapRecognizer.swift */, + D0076F2121ACA5020059500A /* Toolbar.swift */, + D0076F1F21AC9C930059500A /* ToolbarNode.swift */, + ); + name = "Tab Bar"; + sourceTree = ""; + }; + D015F7561D1B465600E269B5 /* Action Sheet */ = { + isa = PBXGroup; + children = ( + D015F7571D1B467200E269B5 /* ActionSheetController.swift */, + D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.swift */, + D0C85DCF1D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift */, + D0C85DD31D1C1E6A00124894 /* ActionSheetItemGroupNode.swift */, + D0C85DD11D1C08AE00124894 /* ActionSheetItemNode.swift */, + D08E90391D24159200533158 /* ActionSheetItem.swift */, + D08E903D1D24187900533158 /* ActionSheetItemGroup.swift */, + D08E903B1D2417E000533158 /* ActionSheetButtonItem.swift */, + D096A44F1EA64F580000A7AE /* ActionSheetCheckboxItem.swift */, + D0C0B5981EDF3BC9000F4D2C /* ActionSheetTextItem.swift */, + D0CE67911F7DA11700FFB557 /* ActionSheetTheme.swift */, + D033874F223EE5A4007A2CE4 /* ActionSheetSwitchItem.swift */, + ); + name = "Action Sheet"; + sourceTree = ""; + }; + D01E2BDC1D90494A0066BF65 /* Grid Node */ = { + isa = PBXGroup; + children = ( + D01E2BDD1D9049620066BF65 /* GridNode.swift */, + D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */, + D01E2BE31D904A000066BF65 /* GridItem.swift */, + D01E2BE11D9049F60066BF65 /* GridItemNode.swift */, + ); + name = "Grid Node"; + sourceTree = ""; + }; + D01E2BE51D904A530066BF65 /* Collection Nodes */ = { + isa = PBXGroup; + children = ( + D0C2DFBA1CC443080044FF83 /* List Node */, + D01E2BDC1D90494A0066BF65 /* Grid Node */, + ); + name = "Collection Nodes"; + sourceTree = ""; + }; + D02BDAEC1B6A7053008AFAD2 /* Nodes */ = { + isa = PBXGroup; + children = ( + D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */, + D08E90461D243C2F00533158 /* HighlightTrackingButton.swift */, + D0E35A021DE473B900BC6096 /* HighlightableButton.swift */, + D00C7CD11E3657570080C3D5 /* TextFieldNode.swift */, + D0A749941E3A9E7B00AD786E /* SwitchNode.swift */, + D04C468D1F4C97BE00D30FE1 /* PageControlNode.swift */, + D0FA08C32048803C00DD23FC /* TextNode.swift */, + D0FA08C5204880C900DD23FC /* ImmediateTextNode.swift */, + D0CA3F892073F7650042D2B6 /* LinkHighlightingNode.swift */, + D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */, + 09DD88EA21BCA5E0000766BC /* EditableTextNode.swift */, + ); + name = Nodes; + sourceTree = ""; + }; + D03725BF1D6DF57B007FC290 /* Context Menu */ = { + isa = PBXGroup; + children = ( + D03725C21D6DF7A6007FC290 /* ContextMenuAction.swift */, + D03725C41D6DF8B9007FC290 /* ContextMenuController.swift */, + D03725C01D6DF594007FC290 /* ContextMenuNode.swift */, + D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */, + D0F1132E1D6F3C20008C3597 /* ContextMenuActionNode.swift */, + ); + name = "Context Menu"; + sourceTree = ""; + }; + D03AA4D3202C790D0056C405 /* Peek */ = { + isa = PBXGroup; + children = ( + D03AA4D4202C793E0056C405 /* PeekController.swift */, + D03AA4D6202C79600056C405 /* PeekControllerNode.swift */, + D03AA4DA202DA6D60056C405 /* PeekControllerContent.swift */, + D03AA4DC202DB1840056C405 /* PeekControllerGestureRecognizer.swift */, + D03AA4E0202DD4490056C405 /* PeekControllerMenuNode.swift */, + D03AA4E2202DD52E0056C405 /* PeekControllerMenuItemNode.swift */, + ); + name = Peek; + sourceTree = ""; + }; + D05BE4AC1D217F33002BD72C /* Utils */ = { + isa = PBXGroup; + children = ( + D053CB5E1D22B4F200DD41DF /* CATracingLayer.h */, + D053CB5F1D22B4F200DD41DF /* CATracingLayer.m */, + D05174B11EAA833200A1BF36 /* CASeeThroughTracingLayer.h */, + D05174B21EAA833200A1BF36 /* CASeeThroughTracingLayer.m */, + D01847651FFA72E000075256 /* ContainedViewLayoutTransition.swift */, + D0FA08C120487A8600DD23FC /* HapticFeedback.swift */, + D06B76DA20592A97006E9EEA /* LayoutSizes.swift */, + D0CA3F8B2073F8240042D2B6 /* TapLongTapOrDoubleTapGestureRecognizer.swift */, + 09E12475214D0978009FC9C3 /* DeviceMetrics.swift */, + D033874D223D3E86007A2CE4 /* AccessibilityAreaNode.swift */, + D08B61BD22A752FD000A46A8 /* UITracingLayerView.swift */, + ); + name = Utils; + sourceTree = ""; + }; + D05CC2591B69316F00E235A3 = { + isa = PBXGroup; + children = ( + D05CC2A31B6932D500E235A3 /* Frameworks */, + D05CC2651B69316F00E235A3 /* Display */, + D05CC2711B69316F00E235A3 /* DisplayTests */, + D05CC2641B69316F00E235A3 /* Products */, + ); + sourceTree = ""; + }; + D05CC2641B69316F00E235A3 /* Products */ = { + isa = PBXGroup; + children = ( + D05CC2631B69316F00E235A3 /* Display.framework */, + D05CC26D1B69316F00E235A3 /* DisplayTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + D05CC2651B69316F00E235A3 /* Display */ = { + isa = PBXGroup; + children = ( + D08122991D19A9E0005F7395 /* User Interface */, + D01E2BE51D904A530066BF65 /* Collection Nodes */, + D05CC3001B6955D500E235A3 /* Utils */, + D07921AA1B6FC911005C23D9 /* Status Bar */, + D0FF9B2E1E7196E2000C66DB /* Keyboard */, + D05CC3211B695AA600E235A3 /* Navigation */, + D02BDAEC1B6A7053008AFAD2 /* Nodes */, + D05CC2E11B69534100E235A3 /* Supporting Files */, + ); + path = Display; + sourceTree = ""; + }; + D05CC2711B69316F00E235A3 /* DisplayTests */ = { + isa = PBXGroup; + children = ( + D05CC2721B69316F00E235A3 /* DisplayTests.swift */, + D05CC2741B69316F00E235A3 /* Info.plist */, + ); + path = DisplayTests; + sourceTree = ""; + }; + D05CC2A31B6932D500E235A3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D01C06C61FC2558F001561AB /* SwiftSignalKitMac.framework */, + D0C2DFFB1CC528B70044FF83 /* SwiftSignalKit.framework */, + D0E1D6711CBC201E00B04029 /* AsyncDisplayKit.framework */, + D0E1D6351CBC159C00B04029 /* AVFoundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D05CC2E11B69534100E235A3 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D03E7DE31C96A90100C07816 /* NavigationShadow@2x.png */, + D05CC3261B69725400E235A3 /* NavigationBackArrowLight@2x.png */, + D05CC2661B69316F00E235A3 /* Display.h */, + D05CC2681B69316F00E235A3 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D05CC3001B6955D500E235A3 /* Utils */ = { + isa = PBXGroup; + children = ( + D05CC2EB1B69558A00E235A3 /* RuntimeUtils.h */, + D05CC2EA1B69558A00E235A3 /* RuntimeUtils.m */, + D05CC2F01B6955D000E235A3 /* UIViewController+Navigation.h */, + D05CC2EF1B6955D000E235A3 /* UIViewController+Navigation.m */, + D05CC2EE1B6955D000E235A3 /* UIKitUtils.swift */, + D05CC2F41B6955D000E235A3 /* UIKitUtils.h */, + D05CC2F31B6955D000E235A3 /* UIKitUtils.m */, + D05CC2F21B6955D000E235A3 /* UINavigationItem+Proxy.h */, + D05CC2F11B6955D000E235A3 /* UINavigationItem+Proxy.m */, + D05CC2F61B6955D000E235A3 /* UIWindow+OrientationChange.h */, + D05CC2F51B6955D000E235A3 /* UIWindow+OrientationChange.m */, + D05CC3021B69568600E235A3 /* NotificationCenterUtils.h */, + D05CC3011B69568600E235A3 /* NotificationCenterUtils.m */, + D05CC3061B69575900E235A3 /* NSBag.h */, + D05CC3051B69575900E235A3 /* NSBag.m */, + D05CC3121B695A9600E235A3 /* UIBarButtonItem+Proxy.h */, + D05CC3111B695A9600E235A3 /* UIBarButtonItem+Proxy.m */, + D05CC3141B695A9600E235A3 /* NavigationControllerProxy.h */, + D05CC3131B695A9600E235A3 /* NavigationControllerProxy.m */, + D05CC3221B695B0700E235A3 /* NavigationBarProxy.h */, + D05CC3231B695B0700E235A3 /* NavigationBarProxy.m */, + D05CC2E41B69555800E235A3 /* CAAnimationUtils.swift */, + D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */, + D06EE8441B7140FF00837186 /* Font.swift */, + D0AE2CA51C94548900F2FD3C /* GenerateImage.swift */, + D03E7DF61C96C5F200C07816 /* NSWeakReference.h */, + D03E7DF71C96C5F200C07816 /* NSWeakReference.m */, + D03E7E001C974AB300C07816 /* DisplayLinkDispatcher.swift */, + D0C0D28D1C997110001D2851 /* FBAnimationPerformanceTracker.h */, + D0C0D28E1C997110001D2851 /* FBAnimationPerformanceTracker.mm */, + D0D94A161D3814F900740E02 /* UniversalTapRecognizer.swift */, + D01F728121F13891006AB634 /* Accessibility.swift */, + 09167E1E229803DC005734A7 /* UIMenuItem+Icons.h */, + 09167E1F229803DC005734A7 /* UIMenuItem+Icons.m */, + D02C61F222A94BD600D4EC86 /* Keyboard.swift */, + ); + name = Utils; + sourceTree = ""; + }; + D05CC3211B695AA600E235A3 /* Navigation */ = { + isa = PBXGroup; + children = ( + D05CC3091B695A9500E235A3 /* NavigationTransitionCoordinator.swift */, + D03E7DE51C96B96E00C07816 /* NavigationBarTransitionContainer.swift */, + D05CC30A1B695A9500E235A3 /* NavigationBar.swift */, + D0C0B59C1EE022CC000F4D2C /* NavigationBarContentNode.swift */, + D05CC30D1B695A9500E235A3 /* NavigationBackButtonNode.swift */, + D05CC30E1B695A9500E235A3 /* NavigationButtonNode.swift */, + D05CC30F1B695A9500E235A3 /* NavigationTitleNode.swift */, + D0C12A191F3375B400B3F66D /* NavigationBarTitleTransitionNode.swift */, + D05CC3281B69750D00E235A3 /* InteractiveTransitionGestureRecognizer.swift */, + D0AE3D4C1D25C816001CCE13 /* NavigationBarTransitionState.swift */, + D077B8E81F4637040046D27A /* NavigationBarBadge.swift */, + D0CE8CE81F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift */, + ); + name = Navigation; + sourceTree = ""; + }; + D07921AA1B6FC911005C23D9 /* Status Bar */ = { + isa = PBXGroup; + children = ( + D0078A671C92B21400DF6D92 /* StatusBar.swift */, + D0B3671F1C94A53A00346D2E /* StatusBarProxyNode.swift */, + D03E7DFE1C96F7B400C07816 /* StatusBarManager.swift */, + D03B0E6F1D6331FB00955575 /* StatusBarHost.swift */, + D06D37A120779C82009219B6 /* VolumeControlStatusBar.swift */, + ); + name = "Status Bar"; + sourceTree = ""; + }; + D08122991D19A9E0005F7395 /* User Interface */ = { + isa = PBXGroup; + children = ( + D05BE4AC1D217F33002BD72C /* Utils */, + D015F7501D1ADC6800E269B5 /* Window */, + D081229B1D19A9F0005F7395 /* Controllers */, + ); + name = "User Interface"; + sourceTree = ""; + }; + D081229A1D19A9EB005F7395 /* Navigation */ = { + isa = PBXGroup; + children = ( + D05CC29F1B69326400E235A3 /* NavigationController.swift */, + ); + name = Navigation; + sourceTree = ""; + }; + D081229B1D19A9F0005F7395 /* Controllers */ = { + isa = PBXGroup; + children = ( + D0A1346120336C350059716A /* ViewController.swift */, + D081229C1D19AA1C005F7395 /* ContainerViewLayout.swift */, + D015F7511D1AE08D00E269B5 /* ContainableController.swift */, + D05BE4AA1D1F25E3002BD72C /* PresentationContext.swift */, + D08CAA7A1ED73C990000FDA8 /* ViewControllerTracingNode.swift */, + D01EA41A203227BA00B4B0B5 /* ViewControllerPreviewing.swift */, + D023837F1DDF7916004018B6 /* LegacyPresentedController.swift */, + D02383811DDF798E004018B6 /* LegacyPresentedControllerNode.swift */, + D081229A1D19A9EB005F7395 /* Navigation */, + D015F7551D1B142300E269B5 /* Tab Bar */, + D015F7561D1B465600E269B5 /* Action Sheet */, + D03725BF1D6DF57B007FC290 /* Context Menu */, + D00701962029CAC4006B9E34 /* Tooltip */, + D0DA444A1E4DCA36005FDCA7 /* Alert */, + D03AA4D3202C790D0056C405 /* Peek */, + ); + name = Controllers; + sourceTree = ""; + }; + D0C2DFBA1CC443080044FF83 /* List Node */ = { + isa = PBXGroup; + children = ( + D0C2DFBB1CC4431D0044FF83 /* ASTransformLayerNode.swift */, + D0C2DFBD1CC4431D0044FF83 /* Spring.swift */, + D0C2DFBE1CC4431D0044FF83 /* ListView.swift */, + D02383851DE0E3B4004018B6 /* ListViewIntermediateState.swift */, + D0C2DFBF1CC4431D0044FF83 /* ListViewItem.swift */, + D0C2DFBC1CC4431D0044FF83 /* ListViewItemNode.swift */, + D0C2DFC01CC4431D0044FF83 /* ListViewAnimation.swift */, + D0C2DFC21CC4431D0044FF83 /* ListViewTransactionQueue.swift */, + D0C2DFC31CC4431D0044FF83 /* ListViewAccessoryItem.swift */, + D0C2DFC41CC4431D0044FF83 /* ListViewScroller.swift */, + D0C2DFC51CC4431D0044FF83 /* ListViewAccessoryItemNode.swift */, + D0F7AB361DCFF6F8009AD9A1 /* ListViewItemHeader.swift */, + D0AA840D1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift */, + D0AA840F1FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift */, + D03AA4E8202E02070056C405 /* ListViewReorderingItemNode.swift */, + D03AA4EA202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift */, + D03AA5152030C5F80056C405 /* ListViewTempItemNode.swift */, + D03310B2213F232600FC83CD /* ListViewTapGestureRecognizer.swift */, + ); + name = "List Node"; + sourceTree = ""; + }; + D0DA444A1E4DCA36005FDCA7 /* Alert */ = { + isa = PBXGroup; + children = ( + D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */, + D0DA444D1E4DCA6E005FDCA7 /* AlertControllerNode.swift */, + D0DA444F1E4DCBDE005FDCA7 /* AlertContentNode.swift */, + D0DA44511E4DCC11005FDCA7 /* TextAlertController.swift */, + ); + name = Alert; + sourceTree = ""; + }; + D0FF9B2E1E7196E2000C66DB /* Keyboard */ = { + isa = PBXGroup; + children = ( + 09C147D6216CCE9700390252 /* Key Shortcuts */, + D0FF9B2F1E7196F6000C66DB /* KeyboardManager.swift */, + D036574A1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift */, + ); + name = Keyboard; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D05CC2601B69316F00E235A3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D05CC3041B69568600E235A3 /* NotificationCenterUtils.h in Headers */, + D05CC2ED1B69558A00E235A3 /* RuntimeUtils.h in Headers */, + D05CC3201B695A9600E235A3 /* NavigationControllerProxy.h in Headers */, + D03E7DF81C96C5F200C07816 /* NSWeakReference.h in Headers */, + D05CC2FB1B6955D000E235A3 /* UINavigationItem+Proxy.h in Headers */, + D05CC3241B695B0700E235A3 /* NavigationBarProxy.h in Headers */, + D05CC31E1B695A9600E235A3 /* UIBarButtonItem+Proxy.h in Headers */, + D05CC2FF1B6955D000E235A3 /* UIWindow+OrientationChange.h in Headers */, + D053CB601D22B4F200DD41DF /* CATracingLayer.h in Headers */, + D05CC2FD1B6955D000E235A3 /* UIKitUtils.h in Headers */, + D05174B31EAA833200A1BF36 /* CASeeThroughTracingLayer.h in Headers */, + D05CC3081B69575900E235A3 /* NSBag.h in Headers */, + 09167E20229803DC005734A7 /* UIMenuItem+Icons.h in Headers */, + D0C0D28F1C997110001D2851 /* FBAnimationPerformanceTracker.h in Headers */, + D05CC2671B69316F00E235A3 /* Display.h in Headers */, + D05CC2F91B6955D000E235A3 /* UIViewController+Navigation.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D05CC2621B69316F00E235A3 /* Display */ = { + isa = PBXNativeTarget; + buildConfigurationList = D05CC2771B69316F00E235A3 /* Build configuration list for PBXNativeTarget "Display" */; + buildPhases = ( + D05CC25E1B69316F00E235A3 /* Sources */, + D05CC25F1B69316F00E235A3 /* Frameworks */, + D05CC2601B69316F00E235A3 /* Headers */, + D05CC2611B69316F00E235A3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Display; + productName = Display; + productReference = D05CC2631B69316F00E235A3 /* Display.framework */; + productType = "com.apple.product-type.framework"; + }; + D05CC26C1B69316F00E235A3 /* DisplayTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D05CC27A1B69316F00E235A3 /* Build configuration list for PBXNativeTarget "DisplayTests" */; + buildPhases = ( + D05CC2691B69316F00E235A3 /* Sources */, + D05CC26A1B69316F00E235A3 /* Frameworks */, + D05CC26B1B69316F00E235A3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D05CC2701B69316F00E235A3 /* PBXTargetDependency */, + ); + name = DisplayTests; + productName = DisplayTests; + productReference = D05CC26D1B69316F00E235A3 /* DisplayTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D05CC25A1B69316F00E235A3 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + D05CC2621B69316F00E235A3 = { + CreatedOnToolsVersion = 7.0; + ProvisioningStyle = Manual; + }; + D05CC26C1B69316F00E235A3 = { + CreatedOnToolsVersion = 7.0; + }; + }; + }; + buildConfigurationList = D05CC25D1B69316F00E235A3 /* Build configuration list for PBXProject "Display_Xcode" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = D05CC2591B69316F00E235A3; + productRefGroup = D05CC2641B69316F00E235A3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D05CC2621B69316F00E235A3 /* Display */, + D05CC26C1B69316F00E235A3 /* DisplayTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D05CC2611B69316F00E235A3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D03E7DE41C96A90100C07816 /* NavigationShadow@2x.png in Resources */, + D05CC3271B69725400E235A3 /* NavigationBackArrowLight@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D05CC26B1B69316F00E235A3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D05CC25E1B69316F00E235A3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D08E903C1D2417E000533158 /* ActionSheetButtonItem.swift in Sources */, + D0FA08C220487A8600DD23FC /* HapticFeedback.swift in Sources */, + D03AA5162030C5F80056C405 /* ListViewTempItemNode.swift in Sources */, + D087BFB51F75181D003FD209 /* ChildWindowHostView.swift in Sources */, + D0FA08C6204880C900DD23FC /* ImmediateTextNode.swift in Sources */, + D0078A681C92B21400DF6D92 /* StatusBar.swift in Sources */, + D05CC2F81B6955D000E235A3 /* UIViewController+Navigation.m in Sources */, + D03310B3213F232600FC83CD /* ListViewTapGestureRecognizer.swift in Sources */, + D0F1132F1D6F3C20008C3597 /* ContextMenuActionNode.swift in Sources */, + D053DAD12018ECF900993D32 /* WindowCoveringView.swift in Sources */, + D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */, + D0BB4EBA1F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift in Sources */, + D05CC31F1B695A9600E235A3 /* NavigationControllerProxy.m in Sources */, + D05CC3031B69568600E235A3 /* NotificationCenterUtils.m in Sources */, + D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */, + D05BE4AB1D1F25E3002BD72C /* PresentationContext.swift in Sources */, + D0C2DFCA1CC4431D0044FF83 /* ListViewItem.swift in Sources */, + D06D37A220779C82009219B6 /* VolumeControlStatusBar.swift in Sources */, + D05CC2A01B69326400E235A3 /* NavigationController.swift in Sources */, + D06EE8451B7140FF00837186 /* Font.swift in Sources */, + 09167E21229803DC005734A7 /* UIMenuItem+Icons.m in Sources */, + D0CA3F8A2073F7650042D2B6 /* LinkHighlightingNode.swift in Sources */, + D0C2DFCB1CC4431D0044FF83 /* ListViewAnimation.swift in Sources */, + D0BE93191E8ED71100DCC1E6 /* NativeWindowHostView.swift in Sources */, + D04554AA21BDB93E007A6DD9 /* CollectionIndexNode.swift in Sources */, + D05CC3251B695B0700E235A3 /* NavigationBarProxy.m in Sources */, + D05174B41EAA833200A1BF36 /* CASeeThroughTracingLayer.m in Sources */, + D03AA4D9202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift in Sources */, + D0076F2221ACA5020059500A /* Toolbar.swift in Sources */, + D0F8C3932014FB7C00236FC5 /* ListView.swift in Sources */, + D03E7DE61C96B96E00C07816 /* NavigationBarTransitionContainer.swift in Sources */, + D0C85DD01D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift in Sources */, + D05CC2F71B6955D000E235A3 /* UIKitUtils.swift in Sources */, + D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */, + D05CC3161B695A9600E235A3 /* NavigationBar.swift in Sources */, + D05CC31D1B695A9600E235A3 /* UIBarButtonItem+Proxy.m in Sources */, + D01E2BDE1D9049620066BF65 /* GridNode.swift in Sources */, + D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */, + D0C2DFD01CC4431D0044FF83 /* ListViewAccessoryItemNode.swift in Sources */, + D06B76DB20592A97006E9EEA /* LayoutSizes.swift in Sources */, + D0DA44501E4DCBDE005FDCA7 /* AlertContentNode.swift in Sources */, + D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */, + D00701982029CAD6006B9E34 /* TooltipController.swift in Sources */, + D01EA41B203227BA00B4B0B5 /* ViewControllerPreviewing.swift in Sources */, + D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */, + D0DA444C1E4DCA4A005FDCA7 /* AlertController.swift in Sources */, + D03E7DF91C96C5F200C07816 /* NSWeakReference.m in Sources */, + D0338750223EE5A4007A2CE4 /* ActionSheetSwitchItem.swift in Sources */, + D0DC48541BF93D8B00F672FD /* TabBarController.swift in Sources */, + D03E7E011C974AB300C07816 /* DisplayLinkDispatcher.swift in Sources */, + D05CC3191B695A9600E235A3 /* NavigationBackButtonNode.swift in Sources */, + D0C2DFC81CC4431D0044FF83 /* Spring.swift in Sources */, + D05CC3071B69575900E235A3 /* NSBag.m in Sources */, + D0AE3D4D1D25C816001CCE13 /* NavigationBarTransitionState.swift in Sources */, + D0C85DD21D1C08AE00124894 /* ActionSheetItemNode.swift in Sources */, + D0DC48561BF945DD00F672FD /* TabBarNode.swift in Sources */, + D03AA4D5202C793E0056C405 /* PeekController.swift in Sources */, + D05CC31A1B695A9600E235A3 /* NavigationButtonNode.swift in Sources */, + D05CC2E71B69555800E235A3 /* CAAnimationUtils.swift in Sources */, + D05CC31B1B695A9600E235A3 /* NavigationTitleNode.swift in Sources */, + D04C468E1F4C97BE00D30FE1 /* PageControlNode.swift in Sources */, + D0C2DFCF1CC4431D0044FF83 /* ListViewScroller.swift in Sources */, + D00C7CD21E3657570080C3D5 /* TextFieldNode.swift in Sources */, + D0DC485F1BF949FB00F672FD /* TabBarContollerNode.swift in Sources */, + D08B61BE22A752FD000A46A8 /* UITracingLayerView.swift in Sources */, + D05CC2FA1B6955D000E235A3 /* UINavigationItem+Proxy.m in Sources */, + 09C147D8216CCEF700390252 /* KeyShortcutsController.swift in Sources */, + D096A4501EA64F580000A7AE /* ActionSheetCheckboxItem.swift in Sources */, + D0C2DFCE1CC4431D0044FF83 /* ListViewAccessoryItem.swift in Sources */, + D0CE8CE91F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift in Sources */, + D0C12A1A1F3375B400B3F66D /* NavigationBarTitleTransitionNode.swift in Sources */, + D03AA4E1202DD4490056C405 /* PeekControllerMenuNode.swift in Sources */, + D0A749951E3A9E7B00AD786E /* SwitchNode.swift in Sources */, + D03AA4D7202C79600056C405 /* PeekControllerNode.swift in Sources */, + D03725C51D6DF8B9007FC290 /* ContextMenuController.swift in Sources */, + D03725C31D6DF7A6007FC290 /* ContextMenuAction.swift in Sources */, + D033874E223D3E86007A2CE4 /* AccessibilityAreaNode.swift in Sources */, + D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */, + D01847661FFA72E000075256 /* ContainedViewLayoutTransition.swift in Sources */, + D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */, + D03AA4DB202DA6D60056C405 /* PeekControllerContent.swift in Sources */, + D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */, + D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */, + D0DA44521E4DCC11005FDCA7 /* TextAlertController.swift in Sources */, + D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */, + D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */, + D0FF9B301E7196F6000C66DB /* KeyboardManager.swift in Sources */, + D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */, + D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */, + D0A1346220336C350059716A /* ViewController.swift in Sources */, + D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */, + D03AA4E9202E02070056C405 /* ListViewReorderingItemNode.swift in Sources */, + D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */, + D0E35A031DE473B900BC6096 /* HighlightableButton.swift in Sources */, + D01F728221F13891006AB634 /* Accessibility.swift in Sources */, + D0FA08C42048803C00DD23FC /* TextNode.swift in Sources */, + D0CA3F8C2073F8240042D2B6 /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */, + D0CD12161CCFEB4E000DE7BC /* ScrollToTopProxyView.swift in Sources */, + D02C61F322A94BD600D4EC86 /* Keyboard.swift in Sources */, + D0AA840E1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift in Sources */, + D03AA4EB202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift in Sources */, + D0C2DFCD1CC4431D0044FF83 /* ListViewTransactionQueue.swift in Sources */, + D0AA84101FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift in Sources */, + D02383821DDF798E004018B6 /* LegacyPresentedControllerNode.swift in Sources */, + D05CC2FC1B6955D000E235A3 /* UIKitUtils.m in Sources */, + D0C2DFC61CC4431D0044FF83 /* ASTransformLayerNode.swift in Sources */, + D03AA4DD202DB1840056C405 /* PeekControllerGestureRecognizer.swift in Sources */, + D007019A2029CAE2006B9E34 /* TooltipControllerNode.swift in Sources */, + D05CC3291B69750D00E235A3 /* InteractiveTransitionGestureRecognizer.swift in Sources */, + D077B8E91F4637040046D27A /* NavigationBarBadge.swift in Sources */, + D0CE67921F7DA11700FFB557 /* ActionSheetTheme.swift in Sources */, + D0A134642034DE580059716A /* TabBarTapRecognizer.swift in Sources */, + D03AA4E3202DD52E0056C405 /* PeekControllerMenuItemNode.swift in Sources */, + D0C0D2901C997110001D2851 /* FBAnimationPerformanceTracker.mm in Sources */, + D015F7521D1AE08D00E269B5 /* ContainableController.swift in Sources */, + D036574B1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift in Sources */, + D0CB78901F9822F8004AB79B /* WindowPanRecognizer.swift in Sources */, + D05CC2FE1B6955D000E235A3 /* UIWindow+OrientationChange.m in Sources */, + D0C85DD41D1C1E6A00124894 /* ActionSheetItemGroupNode.swift in Sources */, + D0C0B5991EDF3BC9000F4D2C /* ActionSheetTextItem.swift in Sources */, + D0C0B59D1EE022CC000F4D2C /* NavigationBarContentNode.swift in Sources */, + D08E903E1D24187900533158 /* ActionSheetItemGroup.swift in Sources */, + 09E12476214D0978009FC9C3 /* DeviceMetrics.swift in Sources */, + D02383861DE0E3B4004018B6 /* ListViewIntermediateState.swift in Sources */, + D0DA444E1E4DCA6E005FDCA7 /* AlertControllerNode.swift in Sources */, + D0B367201C94A53A00346D2E /* StatusBarProxyNode.swift in Sources */, + 09DD88EB21BCA5E0000766BC /* EditableTextNode.swift in Sources */, + D05CC2A21B69326C00E235A3 /* WindowContent.swift in Sources */, + D05CC3151B695A9600E235A3 /* NavigationTransitionCoordinator.swift in Sources */, + D03B0E701D6331FB00955575 /* StatusBarHost.swift in Sources */, + D0076F2021AC9C930059500A /* ToolbarNode.swift in Sources */, + D02383801DDF7916004018B6 /* LegacyPresentedController.swift in Sources */, + D08E90471D243C2F00533158 /* HighlightTrackingButton.swift in Sources */, + D0F7AB371DCFF6F8009AD9A1 /* ListViewItemHeader.swift in Sources */, + 09C147DA216CD7E500390252 /* KeyShortcut.swift in Sources */, + D08CAA7B1ED73C990000FDA8 /* ViewControllerTracingNode.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D05CC2691B69316F00E235A3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D05CC2731B69316F00E235A3 /* DisplayTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D05CC2701B69316F00E235A3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D05CC2621B69316F00E235A3 /* Display */; + targetProxy = D05CC26F1B69316F00E235A3 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + D021D4F4219CB1AD0064BEBA /* DebugFork */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugFork; + }; + D021D4F5219CB1AD0064BEBA /* DebugFork */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = DebugFork; + }; + D021D4F6219CB1AD0064BEBA /* DebugFork */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = DebugFork; + }; + D05CC2751B69316F00E235A3 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D05CC2761B69316F00E235A3 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D05CC2781B69316F00E235A3 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = DebugHockeyapp; + }; + D05CC2791B69316F00E235A3 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = ReleaseHockeyapp; + }; + D05CC27B1B69316F00E235A3 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = DebugHockeyapp; + }; + D05CC27C1B69316F00E235A3 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + }; + name = ReleaseHockeyapp; + }; + D079FD091F06BD9C0038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D079FD0A1F06BD9C0038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = DebugAppStore; + }; + D079FD0B1F06BD9C0038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = DebugAppStore; + }; + D086A56E1CC0115D00F08284 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D086A56F1CC0115D00F08284 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = ReleaseAppStore; + }; + D086A5701CC0115D00F08284 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = ReleaseAppStore; + }; + D0924FD41FE52BE9003F693F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyappInternal; + }; + D0924FD51FE52BE9003F693F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = ReleaseHockeyappInternal; + }; + D0924FD61FE52BE9003F693F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + }; + name = ReleaseHockeyappInternal; + }; + D0ADF920212B3ABC00310BBC /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStoreLLC; + }; + D0ADF921212B3ABC00310BBC /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = DebugAppStoreLLC; + }; + D0ADF922212B3ABC00310BBC /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = DebugAppStoreLLC; + }; + D0CE6EE1213DB54B00BCD44B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "-DMINIMAL_ASDK=1"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStoreLLC; + }; + D0CE6EE2213DB54B00BCD44B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + INFOPLIST_FILE = Display/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Display; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 4.0; + }; + name = ReleaseAppStoreLLC; + }; + D0CE6EE3213DB54B00BCD44B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = DisplayTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.DisplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = ReleaseAppStoreLLC; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D05CC25D1B69316F00E235A3 /* Build configuration list for PBXProject "Display_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D05CC2751B69316F00E235A3 /* DebugHockeyapp */, + D021D4F4219CB1AD0064BEBA /* DebugFork */, + D079FD091F06BD9C0038FADE /* DebugAppStore */, + D0ADF920212B3ABC00310BBC /* DebugAppStoreLLC */, + D05CC2761B69316F00E235A3 /* ReleaseHockeyapp */, + D0924FD41FE52BE9003F693F /* ReleaseHockeyappInternal */, + D086A56E1CC0115D00F08284 /* ReleaseAppStore */, + D0CE6EE1213DB54B00BCD44B /* ReleaseAppStoreLLC */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + D05CC2771B69316F00E235A3 /* Build configuration list for PBXNativeTarget "Display" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D05CC2781B69316F00E235A3 /* DebugHockeyapp */, + D021D4F5219CB1AD0064BEBA /* DebugFork */, + D079FD0A1F06BD9C0038FADE /* DebugAppStore */, + D0ADF921212B3ABC00310BBC /* DebugAppStoreLLC */, + D05CC2791B69316F00E235A3 /* ReleaseHockeyapp */, + D0924FD51FE52BE9003F693F /* ReleaseHockeyappInternal */, + D086A56F1CC0115D00F08284 /* ReleaseAppStore */, + D0CE6EE2213DB54B00BCD44B /* ReleaseAppStoreLLC */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + D05CC27A1B69316F00E235A3 /* Build configuration list for PBXNativeTarget "DisplayTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D05CC27B1B69316F00E235A3 /* DebugHockeyapp */, + D021D4F6219CB1AD0064BEBA /* DebugFork */, + D079FD0B1F06BD9C0038FADE /* DebugAppStore */, + D0ADF922212B3ABC00310BBC /* DebugAppStoreLLC */, + D05CC27C1B69316F00E235A3 /* ReleaseHockeyapp */, + D0924FD61FE52BE9003F693F /* ReleaseHockeyappInternal */, + D086A5701CC0115D00F08284 /* ReleaseAppStore */, + D0CE6EE3213DB54B00BCD44B /* ReleaseAppStoreLLC */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D05CC25A1B69316F00E235A3 /* Project object */; +} diff --git a/submodules/Display/Display_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/Display/Display_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1752e17513 --- /dev/null +++ b/submodules/Display/Display_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + +