├── .gitignore ├── .gitmodules ├── Display.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── peter.xcuserdatad │ └── xcschemes │ └── Display.xcscheme ├── Display ├── ASTransformLayerNode.swift ├── Accessibility.swift ├── AccessibilityAreaNode.swift ├── ActionSheetButtonItem.swift ├── ActionSheetCheckboxItem.swift ├── ActionSheetController.swift ├── ActionSheetControllerNode.swift ├── ActionSheetItem.swift ├── ActionSheetItemGroup.swift ├── ActionSheetItemGroupNode.swift ├── ActionSheetItemGroupsContainerNode.swift ├── ActionSheetItemNode.swift ├── ActionSheetSwitchItem.swift ├── ActionSheetTextItem.swift ├── ActionSheetTheme.swift ├── AlertContentNode.swift ├── AlertController.swift ├── AlertControllerNode.swift ├── CAAnimationUtils.swift ├── CASeeThroughTracingLayer.h ├── CASeeThroughTracingLayer.m ├── CATracingLayer.h ├── CATracingLayer.m ├── ChildWindowHostView.swift ├── CollectionIndexNode.swift ├── ContainableController.swift ├── ContainedViewLayoutTransition.swift ├── ContainerViewLayout.swift ├── ContextMenuAction.swift ├── ContextMenuActionNode.swift ├── ContextMenuContainerNode.swift ├── ContextMenuController.swift ├── ContextMenuNode.swift ├── DeviceMetrics.swift ├── Display.h ├── DisplayLinkDispatcher.swift ├── EditableTextNode.swift ├── FBAnimationPerformanceTracker.h ├── FBAnimationPerformanceTracker.mm ├── Font.swift ├── GenerateImage.swift ├── GlobalOverlayPresentationContext.swift ├── GridItem.swift ├── GridItemNode.swift ├── GridNode.swift ├── GridNodeScroller.swift ├── HapticFeedback.swift ├── HighlightTrackingButton.swift ├── HighlightableButton.swift ├── ImmediateTextNode.swift ├── Info.plist ├── InteractiveTransitionGestureRecognizer.swift ├── KeyShortcut.swift ├── KeyShortcutsController.swift ├── KeyboardManager.swift ├── LayoutSizes.swift ├── LegacyPresentedController.swift ├── LegacyPresentedControllerNode.swift ├── LinkHighlightingNode.swift ├── ListView.swift ├── ListViewAccessoryItem.swift ├── ListViewAccessoryItemNode.swift ├── ListViewAnimation.swift ├── ListViewFloatingHeaderNode.swift ├── ListViewIntermediateState.swift ├── ListViewItem.swift ├── ListViewItemHeader.swift ├── ListViewItemNode.swift ├── ListViewOverscrollBackgroundNode.swift ├── ListViewReorderingGestureRecognizer.swift ├── ListViewReorderingItemNode.swift ├── ListViewScroller.swift ├── ListViewTapGestureRecognizer.swift ├── ListViewTempItemNode.swift ├── ListViewTransactionQueue.swift ├── MinimizeKeyboardGestureRecognizer.swift ├── NSBag.h ├── NSBag.m ├── NSWeakReference.h ├── NSWeakReference.m ├── NativeWindowHostView.swift ├── NavigationBackArrowLight@2x.png ├── NavigationBackButtonNode.swift ├── NavigationBar.swift ├── NavigationBarBadge.swift ├── NavigationBarContentNode.swift ├── NavigationBarProxy.h ├── NavigationBarProxy.m ├── NavigationBarTitleTransitionNode.swift ├── NavigationBarTitleView.swift ├── NavigationBarTransitionContainer.swift ├── NavigationBarTransitionState.swift ├── NavigationButtonNode.swift ├── NavigationController.swift ├── NavigationControllerProxy.h ├── NavigationControllerProxy.m ├── NavigationShadow@2x.png ├── NavigationTitleNode.swift ├── NavigationTransitionCoordinator.swift ├── NotificationCenterUtils.h ├── NotificationCenterUtils.m ├── PageControlNode.swift ├── PeekController.swift ├── PeekControllerContent.swift ├── PeekControllerGestureRecognizer.swift ├── PeekControllerMenuItemNode.swift ├── PeekControllerMenuNode.swift ├── PeekControllerNode.swift ├── PresentationContext.swift ├── RuntimeUtils.h ├── RuntimeUtils.m ├── RuntimeUtils.swift ├── ScrollToTopProxyView.swift ├── Spring.swift ├── StatusBar.swift ├── StatusBarHost.swift ├── StatusBarManager.swift ├── StatusBarProxyNode.swift ├── SwitchNode.swift ├── TabBarContollerNode.swift ├── TabBarController.swift ├── TabBarNode.swift ├── TabBarTapRecognizer.swift ├── TapLongTapOrDoubleTapGestureRecognizer.swift ├── TextAlertController.swift ├── TextFieldNode.swift ├── TextNode.swift ├── Theme.swift ├── Toolbar.swift ├── ToolbarNode.swift ├── TooltipController.swift ├── TooltipControllerNode.swift ├── UIBarButtonItem+Proxy.h ├── UIBarButtonItem+Proxy.m ├── UIKitUtils.h ├── UIKitUtils.m ├── UIKitUtils.swift ├── UIMenuItem+Icons.h ├── UIMenuItem+Icons.m ├── UINavigationItem+Proxy.h ├── UINavigationItem+Proxy.m ├── UIViewController+Navigation.h ├── UIViewController+Navigation.m ├── UIWindow+OrientationChange.h ├── UIWindow+OrientationChange.m ├── UniversalMasterController.swift ├── UniversalTapRecognizer.swift ├── ViewController.swift ├── ViewControllerPreviewing.swift ├── ViewControllerTracingNode.swift ├── VolumeControlStatusBar.swift ├── WindowContent.swift ├── WindowCoveringView.swift ├── WindowInputAccessoryHeightProvider.swift └── WindowPanRecognizer.swift └── DisplayTests ├── DisplayTests.swift └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | fastlane/README.md 2 | fastlane/report.xml 3 | fastlane/test_output/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.xcscmblueprint 15 | *.moved-aside 16 | DerivedData 17 | *.hmap 18 | *.ipa 19 | *.xcuserstate 20 | .DS_Store 21 | *.dSYM 22 | *.dSYM.zip 23 | *.ipa 24 | */xcuserdata/* 25 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peter-iakovlev/Display/ffb131ac55bed21c37052a09c27150d5d380321e/.gitmodules -------------------------------------------------------------------------------- /Display.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/Display.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Display/ASTransformLayerNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | class ASTransformLayer: CATransformLayer { 5 | override var contents: Any? { 6 | get { 7 | return nil 8 | } set(value) { 9 | 10 | } 11 | } 12 | 13 | override var backgroundColor: CGColor? { 14 | get { 15 | return nil 16 | } set(value) { 17 | 18 | } 19 | } 20 | 21 | override func setNeedsLayout() { 22 | } 23 | 24 | override func layoutSublayers() { 25 | } 26 | } 27 | 28 | class ASTransformView: UIView { 29 | override class var layerClass: AnyClass { 30 | return ASTransformLayer.self 31 | } 32 | } 33 | 34 | open class ASTransformLayerNode: ASDisplayNode { 35 | public override init() { 36 | super.init() 37 | self.setLayerBlock({ 38 | return ASTransformLayer() 39 | }) 40 | } 41 | } 42 | 43 | open class ASTransformViewNode: ASDisplayNode { 44 | public override init() { 45 | super.init() 46 | 47 | self.setViewBlock({ 48 | return ASTransformView() 49 | }) 50 | } 51 | } 52 | 53 | open class ASTransformNode: ASDisplayNode { 54 | public init(layerBacked: Bool = true) { 55 | if layerBacked { 56 | super.init() 57 | self.setLayerBlock({ 58 | return ASTransformLayer() 59 | }) 60 | } else { 61 | super.init() 62 | 63 | self.setViewBlock({ 64 | return ASTransformView() 65 | }) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Display/Accessibility.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import AsyncDisplayKit 4 | 5 | public func addAccessibilityChildren(of node: ASDisplayNode, container: Any, to list: inout [Any]) { 6 | if node.isAccessibilityElement { 7 | let element = UIAccessibilityElement(accessibilityContainer: container) 8 | element.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view) 9 | element.accessibilityLabel = node.accessibilityLabel 10 | element.accessibilityValue = node.accessibilityValue 11 | element.accessibilityTraits = node.accessibilityTraits 12 | element.accessibilityHint = node.accessibilityHint 13 | element.accessibilityIdentifier = node.accessibilityIdentifier 14 | 15 | //node.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view) 16 | list.append(element) 17 | } else if let accessibilityElements = node.accessibilityElements { 18 | list.append(contentsOf: accessibilityElements) 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Display/AccessibilityAreaNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public final class AccessibilityAreaNode: ASDisplayNode { 5 | public var activate: (() -> Bool)? 6 | 7 | override public init() { 8 | super.init() 9 | 10 | self.isAccessibilityElement = true 11 | } 12 | 13 | override public func accessibilityActivate() -> Bool { 14 | return self.activate?() ?? false 15 | } 16 | 17 | override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 18 | return nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Display/ActionSheetButtonItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public enum ActionSheetButtonColor { 5 | case accent 6 | case destructive 7 | case disabled 8 | } 9 | 10 | 11 | public enum ActionSheetButtonFont { 12 | case `default` 13 | case bold 14 | } 15 | 16 | public class ActionSheetButtonItem: ActionSheetItem { 17 | public let title: String 18 | public let color: ActionSheetButtonColor 19 | public let font: ActionSheetButtonFont 20 | public let enabled: Bool 21 | public let action: () -> Void 22 | 23 | public init(title: String, color: ActionSheetButtonColor = .accent, font: ActionSheetButtonFont = .default, enabled: Bool = true, action: @escaping () -> Void) { 24 | self.title = title 25 | self.color = color 26 | self.font = font 27 | self.enabled = enabled 28 | self.action = action 29 | } 30 | 31 | public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { 32 | let node = ActionSheetButtonNode(theme: theme) 33 | node.setItem(self) 34 | return node 35 | } 36 | 37 | public func updateNode(_ node: ActionSheetItemNode) { 38 | guard let node = node as? ActionSheetButtonNode else { 39 | assertionFailure() 40 | return 41 | } 42 | 43 | node.setItem(self) 44 | } 45 | } 46 | 47 | public class ActionSheetButtonNode: ActionSheetItemNode { 48 | private let theme: ActionSheetControllerTheme 49 | 50 | public static let defaultFont: UIFont = Font.regular(20.0) 51 | public static let boldFont: UIFont = Font.medium(20.0) 52 | 53 | private var item: ActionSheetButtonItem? 54 | 55 | private let button: HighlightTrackingButton 56 | private let label: ASTextNode 57 | 58 | override public init(theme: ActionSheetControllerTheme) { 59 | self.theme = theme 60 | 61 | self.button = HighlightTrackingButton() 62 | 63 | self.label = ASTextNode() 64 | self.label.isUserInteractionEnabled = false 65 | self.label.maximumNumberOfLines = 1 66 | self.label.displaysAsynchronously = false 67 | self.label.truncationMode = .byTruncatingTail 68 | 69 | super.init(theme: theme) 70 | 71 | self.view.addSubview(self.button) 72 | 73 | self.label.isUserInteractionEnabled = false 74 | self.addSubnode(self.label) 75 | 76 | self.button.highligthedChanged = { [weak self] highlighted in 77 | if let strongSelf = self { 78 | if highlighted { 79 | strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor 80 | } else { 81 | UIView.animate(withDuration: 0.3, animations: { 82 | strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor 83 | }) 84 | } 85 | } 86 | } 87 | 88 | self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) 89 | } 90 | 91 | func setItem(_ item: ActionSheetButtonItem) { 92 | self.item = item 93 | 94 | let textColor: UIColor 95 | let textFont: UIFont 96 | switch item.color { 97 | case .accent: 98 | textColor = self.theme.standardActionTextColor 99 | case .destructive: 100 | textColor = self.theme.destructiveActionTextColor 101 | case .disabled: 102 | textColor = self.theme.disabledActionTextColor 103 | } 104 | switch item.font { 105 | case .default: 106 | textFont = ActionSheetButtonNode.defaultFont 107 | case .bold: 108 | textFont = ActionSheetButtonNode.boldFont 109 | } 110 | self.label.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) 111 | 112 | self.button.isEnabled = item.enabled 113 | 114 | self.setNeedsLayout() 115 | } 116 | 117 | public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 118 | return CGSize(width: constrainedSize.width, height: 57.0) 119 | } 120 | 121 | public override func layout() { 122 | super.layout() 123 | 124 | let size = self.bounds.size 125 | 126 | self.button.frame = CGRect(origin: CGPoint(), size: size) 127 | 128 | let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 10.0), height: size.height)) 129 | self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) 130 | } 131 | 132 | @objc func buttonPressed() { 133 | if let item = self.item { 134 | item.action() 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Display/ActionSheetController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class ActionSheetController: ViewController, PresentableController { 4 | private var actionSheetNode: ActionSheetControllerNode { 5 | return self.displayNode as! ActionSheetControllerNode 6 | } 7 | 8 | public var theme: ActionSheetControllerTheme { 9 | didSet { 10 | if oldValue != self.theme { 11 | self.actionSheetNode.theme = self.theme 12 | } 13 | } 14 | } 15 | 16 | private var groups: [ActionSheetItemGroup] = [] 17 | 18 | private var isDismissed: Bool = false 19 | 20 | public var dismissed: ((Bool) -> Void)? 21 | 22 | public init(theme: ActionSheetControllerTheme) { 23 | self.theme = theme 24 | 25 | super.init(navigationBarPresentationData: nil) 26 | 27 | self.blocksBackgroundWhenInOverlay = true 28 | } 29 | 30 | required public init(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | public func dismissAnimated() { 35 | if !self.isDismissed { 36 | self.isDismissed = true 37 | self.actionSheetNode.animateOut(cancelled: false) 38 | } 39 | } 40 | 41 | open override func loadDisplayNode() { 42 | self.displayNode = ActionSheetControllerNode(theme: self.theme) 43 | self.displayNodeDidLoad() 44 | 45 | self.actionSheetNode.dismiss = { [weak self] cancelled in 46 | self?.dismissed?(cancelled) 47 | self?.presentingViewController?.dismiss(animated: false) 48 | } 49 | 50 | self.actionSheetNode.setGroups(self.groups) 51 | } 52 | 53 | override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { 54 | super.containerLayoutUpdated(layout, transition: transition) 55 | 56 | self.actionSheetNode.containerLayoutUpdated(layout, transition: transition) 57 | } 58 | 59 | open override func viewDidAppear(_ animated: Bool) { 60 | super.viewDidAppear(animated) 61 | 62 | self.viewDidAppear(completion: {}) 63 | } 64 | 65 | public func viewDidAppear(completion: @escaping () -> Void) { 66 | self.actionSheetNode.animateIn(completion: completion) 67 | } 68 | 69 | public func setItemGroups(_ groups: [ActionSheetItemGroup]) { 70 | self.groups = groups 71 | if self.isViewLoaded { 72 | self.actionSheetNode.setGroups(groups) 73 | } 74 | } 75 | 76 | public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) { 77 | if self.isViewLoaded { 78 | self.actionSheetNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Display/ActionSheetItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ActionSheetItem { 4 | func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode 5 | func updateNode(_ node: ActionSheetItemNode) -> Void 6 | } 7 | -------------------------------------------------------------------------------- /Display/ActionSheetItemGroup.swift: -------------------------------------------------------------------------------- 1 | 2 | public final class ActionSheetItemGroup { 3 | let items: [ActionSheetItem] 4 | let leadingVisibleNodeCount: CGFloat? 5 | 6 | public init(items: [ActionSheetItem], leadingVisibleNodeCount: CGFloat? = nil) { 7 | self.items = items 8 | self.leadingVisibleNodeCount = leadingVisibleNodeCount 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Display/ActionSheetItemGroupsContainerNode.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AsyncDisplayKit 3 | 4 | private let groupSpacing: CGFloat = 8.0 5 | 6 | final class ActionSheetItemGroupsContainerNode: ASDisplayNode { 7 | var theme: ActionSheetControllerTheme { 8 | didSet { 9 | self.setGroups(self.groups) 10 | self.setNeedsLayout() 11 | } 12 | } 13 | 14 | private var groups: [ActionSheetItemGroup] = [] 15 | private var groupNodes: [ActionSheetItemGroupNode] = [] 16 | 17 | init(theme: ActionSheetControllerTheme) { 18 | self.theme = theme 19 | 20 | super.init() 21 | } 22 | 23 | func setGroups(_ groups: [ActionSheetItemGroup]) { 24 | self.groups = groups 25 | 26 | for groupNode in self.groupNodes { 27 | groupNode.removeFromSupernode() 28 | } 29 | self.groupNodes.removeAll() 30 | 31 | for group in groups { 32 | let groupNode = ActionSheetItemGroupNode(theme: self.theme) 33 | groupNode.updateItemNodes(group.items.map({ $0.node(theme: self.theme) }), leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0) 34 | self.groupNodes.append(groupNode) 35 | self.addSubnode(groupNode) 36 | } 37 | } 38 | 39 | override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 40 | var groupsHeight: CGFloat = 0.0 41 | 42 | for groupNode in self.groupNodes.reversed() { 43 | if CGFloat(0.0).isLess(than: groupsHeight) { 44 | groupsHeight += groupSpacing 45 | } 46 | 47 | let size = groupNode.measure(CGSize(width: constrainedSize.width, height: max(0.0, constrainedSize.height - groupsHeight))) 48 | groupsHeight += size.height 49 | } 50 | 51 | return CGSize(width: constrainedSize.width, height: min(groupsHeight, constrainedSize.height)) 52 | } 53 | 54 | override func layout() { 55 | var groupsHeight: CGFloat = 0.0 56 | for i in 0 ..< self.groupNodes.count { 57 | let groupNode = self.groupNodes[i] 58 | 59 | let size = groupNode.calculatedSize 60 | 61 | if i != 0 { 62 | groupsHeight += groupSpacing 63 | self.groupNodes[i - 1].trailingDimView.frame = CGRect(x: 0.0, y: groupNodes[i - 1].bounds.size.height, width: size.width, height: groupSpacing) 64 | } 65 | 66 | groupNode.frame = CGRect(origin: CGPoint(x: 0.0, y: groupsHeight), size: size) 67 | groupNode.trailingDimView.frame = CGRect() 68 | 69 | groupsHeight += size.height 70 | } 71 | } 72 | 73 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 74 | for groupNode in self.groupNodes { 75 | if groupNode.frame.contains(point) { 76 | return groupNode.hitTest(self.convert(point, to: groupNode), with: event) 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) { 83 | for node in self.groupNodes { 84 | node.animateDimViewsAlpha(from: from, to: to, duration: duration) 85 | } 86 | } 87 | 88 | public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) { 89 | var item = self.groups[groupIndex].items[itemIndex] 90 | let itemNode = self.groupNodes[groupIndex].itemNode(at: itemIndex) 91 | item = f(item) 92 | item.updateNode(itemNode) 93 | 94 | var groupItems = self.groups[groupIndex].items 95 | groupItems[itemIndex] = item 96 | 97 | self.groups[groupIndex] = ActionSheetItemGroup(items: groupItems) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Display/ActionSheetItemNode.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AsyncDisplayKit 3 | 4 | open class ActionSheetItemNode: ASDisplayNode { 5 | private let theme: ActionSheetControllerTheme 6 | 7 | public let backgroundNode: ASDisplayNode 8 | private let overflowSeparatorNode: ASDisplayNode 9 | 10 | public init(theme: ActionSheetControllerTheme) { 11 | self.theme = theme 12 | 13 | self.backgroundNode = ASDisplayNode() 14 | self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor 15 | 16 | self.overflowSeparatorNode = ASDisplayNode() 17 | self.overflowSeparatorNode.backgroundColor = self.theme.itemHighlightedBackgroundColor 18 | 19 | super.init() 20 | 21 | self.addSubnode(self.backgroundNode) 22 | self.addSubnode(self.overflowSeparatorNode) 23 | } 24 | 25 | open override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 26 | return CGSize(width: constrainedSize.width, height: 57.0) 27 | } 28 | 29 | open override func layout() { 30 | self.backgroundNode.frame = CGRect(origin: CGPoint(), size: self.calculatedSize) 31 | self.overflowSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.calculatedSize.height), size: CGSize(width: self.calculatedSize.width, height: UIScreenPixel)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Display/ActionSheetSwitchItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public class ActionSheetSwitchItem: ActionSheetItem { 5 | public let title: String 6 | public let isOn: Bool 7 | public let action: (Bool) -> Void 8 | 9 | public init(title: String, isOn: Bool, action: @escaping (Bool) -> Void) { 10 | self.title = title 11 | self.isOn = isOn 12 | self.action = action 13 | } 14 | 15 | public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { 16 | let node = ActionSheetSwitchNode(theme: theme) 17 | node.setItem(self) 18 | return node 19 | } 20 | 21 | public func updateNode(_ node: ActionSheetItemNode) { 22 | guard let node = node as? ActionSheetSwitchNode else { 23 | assertionFailure() 24 | return 25 | } 26 | 27 | node.setItem(self) 28 | } 29 | } 30 | 31 | public class ActionSheetSwitchNode: ActionSheetItemNode { 32 | private let theme: ActionSheetControllerTheme 33 | 34 | private var item: ActionSheetSwitchItem? 35 | 36 | private let button: HighlightTrackingButton 37 | private let label: ASTextNode 38 | private let switchNode: SwitchNode 39 | 40 | override public init(theme: ActionSheetControllerTheme) { 41 | self.theme = theme 42 | 43 | self.button = HighlightTrackingButton() 44 | 45 | self.label = ASTextNode() 46 | self.label.isUserInteractionEnabled = false 47 | self.label.maximumNumberOfLines = 1 48 | self.label.displaysAsynchronously = false 49 | self.label.truncationMode = .byTruncatingTail 50 | 51 | self.switchNode = SwitchNode() 52 | self.switchNode.frameColor = theme.switchFrameColor 53 | self.switchNode.contentColor = theme.switchContentColor 54 | self.switchNode.handleColor = theme.switchHandleColor 55 | 56 | super.init(theme: theme) 57 | 58 | self.view.addSubview(self.button) 59 | 60 | self.label.isUserInteractionEnabled = false 61 | self.addSubnode(self.label) 62 | self.addSubnode(self.switchNode) 63 | 64 | self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) 65 | self.switchNode.valueUpdated = { [weak self] value in 66 | self?.item?.action(value) 67 | } 68 | } 69 | 70 | func setItem(_ item: ActionSheetSwitchItem) { 71 | self.item = item 72 | 73 | self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: self.theme.primaryTextColor) 74 | 75 | self.switchNode.isOn = item.isOn 76 | 77 | self.setNeedsLayout() 78 | } 79 | 80 | public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 81 | return CGSize(width: constrainedSize.width, height: 57.0) 82 | } 83 | 84 | public override func layout() { 85 | super.layout() 86 | 87 | let size = self.bounds.size 88 | 89 | self.button.frame = CGRect(origin: CGPoint(), size: size) 90 | 91 | let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 51.0 - 16.0 * 2.0), height: size.height)) 92 | self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) 93 | 94 | let switchSize = CGSize(width: 51.0, height: 31.0) 95 | self.switchNode.frame = CGRect(origin: CGPoint(x: size.width - 16.0 - switchSize.width, y: floor((size.height - switchSize.height) / 2.0)), size: switchSize) 96 | } 97 | 98 | @objc func buttonPressed() { 99 | let value = !self.switchNode.isOn 100 | self.switchNode.setOn(value, animated: true) 101 | self.item?.action(value) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Display/ActionSheetTextItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public class ActionSheetTextItem: ActionSheetItem { 5 | public let title: String 6 | 7 | public init(title: String) { 8 | self.title = title 9 | } 10 | 11 | public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { 12 | let node = ActionSheetTextNode(theme: theme) 13 | node.setItem(self) 14 | return node 15 | } 16 | 17 | public func updateNode(_ node: ActionSheetItemNode) { 18 | guard let node = node as? ActionSheetTextNode else { 19 | assertionFailure() 20 | return 21 | } 22 | 23 | node.setItem(self) 24 | } 25 | } 26 | 27 | public class ActionSheetTextNode: ActionSheetItemNode { 28 | public static let defaultFont: UIFont = Font.regular(13.0) 29 | 30 | private let theme: ActionSheetControllerTheme 31 | 32 | private var item: ActionSheetTextItem? 33 | 34 | private let label: ASTextNode 35 | 36 | override public init(theme: ActionSheetControllerTheme) { 37 | self.theme = theme 38 | 39 | self.label = ASTextNode() 40 | self.label.isUserInteractionEnabled = false 41 | self.label.maximumNumberOfLines = 0 42 | self.label.displaysAsynchronously = false 43 | self.label.truncationMode = .byTruncatingTail 44 | 45 | super.init(theme: theme) 46 | 47 | self.label.isUserInteractionEnabled = false 48 | self.addSubnode(self.label) 49 | } 50 | 51 | func setItem(_ item: ActionSheetTextItem) { 52 | self.item = item 53 | 54 | self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetTextNode.defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) 55 | 56 | self.setNeedsLayout() 57 | } 58 | 59 | public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 60 | let labelSize = self.label.measure(CGSize(width: max(1.0, constrainedSize.width - 20.0), height: constrainedSize.height)) 61 | return CGSize(width: constrainedSize.width, height: max(57.0, labelSize.height + 32.0)) 62 | } 63 | 64 | public override func layout() { 65 | super.layout() 66 | 67 | let size = self.bounds.size 68 | 69 | let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 20.0), height: size.height)) 70 | self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Display/ActionSheetTheme.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum ActionSheetControllerThemeBackgroundType { 5 | case light 6 | case dark 7 | } 8 | 9 | public final class ActionSheetControllerTheme: Equatable { 10 | public let dimColor: UIColor 11 | public let backgroundType: ActionSheetControllerThemeBackgroundType 12 | public let itemBackgroundColor: UIColor 13 | public let itemHighlightedBackgroundColor: UIColor 14 | public let standardActionTextColor: UIColor 15 | public let destructiveActionTextColor: UIColor 16 | public let disabledActionTextColor: UIColor 17 | public let primaryTextColor: UIColor 18 | public let secondaryTextColor: UIColor 19 | public let controlAccentColor: UIColor 20 | public let controlColor: UIColor 21 | public let switchFrameColor: UIColor 22 | public let switchContentColor: UIColor 23 | public let switchHandleColor: UIColor 24 | 25 | 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) { 26 | self.dimColor = dimColor 27 | self.backgroundType = backgroundType 28 | self.itemBackgroundColor = itemBackgroundColor 29 | self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor 30 | self.standardActionTextColor = standardActionTextColor 31 | self.destructiveActionTextColor = destructiveActionTextColor 32 | self.disabledActionTextColor = disabledActionTextColor 33 | self.primaryTextColor = primaryTextColor 34 | self.secondaryTextColor = secondaryTextColor 35 | self.controlAccentColor = controlAccentColor 36 | self.controlColor = controlColor 37 | self.switchFrameColor = switchFrameColor 38 | self.switchContentColor = switchContentColor 39 | self.switchHandleColor = switchHandleColor 40 | } 41 | 42 | public static func ==(lhs: ActionSheetControllerTheme, rhs: ActionSheetControllerTheme) -> Bool { 43 | if lhs.dimColor != rhs.dimColor { 44 | return false 45 | } 46 | if lhs.backgroundType != rhs.backgroundType { 47 | return false 48 | } 49 | if lhs.itemBackgroundColor != rhs.itemBackgroundColor { 50 | return false 51 | } 52 | if lhs.itemHighlightedBackgroundColor != rhs.itemHighlightedBackgroundColor { 53 | return false 54 | } 55 | if lhs.standardActionTextColor != rhs.standardActionTextColor { 56 | return false 57 | } 58 | if lhs.destructiveActionTextColor != rhs.destructiveActionTextColor { 59 | return false 60 | } 61 | if lhs.disabledActionTextColor != rhs.disabledActionTextColor { 62 | return false 63 | } 64 | if lhs.primaryTextColor != rhs.primaryTextColor { 65 | return false 66 | } 67 | if lhs.secondaryTextColor != rhs.secondaryTextColor { 68 | return false 69 | } 70 | if lhs.controlAccentColor != rhs.controlAccentColor { 71 | return false 72 | } 73 | if lhs.controlColor != rhs.controlColor { 74 | return false 75 | } 76 | if lhs.switchFrameColor != rhs.switchFrameColor { 77 | return false 78 | } 79 | if lhs.switchContentColor != rhs.switchContentColor { 80 | return false 81 | } 82 | if lhs.switchHandleColor != rhs.switchHandleColor { 83 | return false 84 | } 85 | return true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Display/AlertContentNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | open class AlertContentNode: ASDisplayNode { 5 | open var requestLayout: ((ContainedViewLayoutTransition) -> Void)? 6 | 7 | open var dismissOnOutsideTap: Bool { 8 | return true 9 | } 10 | 11 | open func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { 12 | assertionFailure() 13 | 14 | return CGSize() 15 | } 16 | 17 | open func updateTheme(_ theme: AlertControllerTheme) { 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Display/AlertController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public final class AlertControllerTheme: Equatable { 5 | public let backgroundColor: UIColor 6 | public let separatorColor: UIColor 7 | public let highlightedItemColor: UIColor 8 | public let primaryColor: UIColor 9 | public let secondaryColor: UIColor 10 | public let accentColor: UIColor 11 | public let destructiveColor: UIColor 12 | public let disabledColor: UIColor 13 | 14 | public init(backgroundColor: UIColor, separatorColor: UIColor, highlightedItemColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disabledColor: UIColor) { 15 | self.backgroundColor = backgroundColor 16 | self.separatorColor = separatorColor 17 | self.highlightedItemColor = highlightedItemColor 18 | self.primaryColor = primaryColor 19 | self.secondaryColor = secondaryColor 20 | self.accentColor = accentColor 21 | self.destructiveColor = destructiveColor 22 | self.disabledColor = disabledColor 23 | } 24 | 25 | public static func ==(lhs: AlertControllerTheme, rhs: AlertControllerTheme) -> Bool { 26 | if lhs.backgroundColor != rhs.backgroundColor { 27 | return false 28 | } 29 | if lhs.separatorColor != rhs.separatorColor { 30 | return false 31 | } 32 | if lhs.highlightedItemColor != rhs.highlightedItemColor { 33 | return false 34 | } 35 | if lhs.primaryColor != rhs.primaryColor { 36 | return false 37 | } 38 | if lhs.secondaryColor != rhs.secondaryColor { 39 | return false 40 | } 41 | if lhs.accentColor != rhs.accentColor { 42 | return false 43 | } 44 | if lhs.destructiveColor != rhs.destructiveColor { 45 | return false 46 | } 47 | if lhs.disabledColor != rhs.disabledColor { 48 | return false 49 | } 50 | return true 51 | } 52 | } 53 | 54 | open class AlertController: ViewController { 55 | private var controllerNode: AlertControllerNode { 56 | return self.displayNode as! AlertControllerNode 57 | } 58 | 59 | public var theme: AlertControllerTheme { 60 | didSet { 61 | if oldValue != self.theme { 62 | self.controllerNode.updateTheme(self.theme) 63 | } 64 | } 65 | } 66 | private let contentNode: AlertContentNode 67 | private let allowInputInset: Bool 68 | 69 | public var dismissed: (() -> Void)? 70 | 71 | public init(theme: AlertControllerTheme, contentNode: AlertContentNode, allowInputInset: Bool = true) { 72 | self.theme = theme 73 | self.contentNode = contentNode 74 | self.allowInputInset = allowInputInset 75 | 76 | super.init(navigationBarPresentationData: nil) 77 | 78 | self.blocksBackgroundWhenInOverlay = true 79 | 80 | self.statusBar.statusBarStyle = .Ignore 81 | } 82 | 83 | required public init(coder aDecoder: NSCoder) { 84 | fatalError("init(coder:) has not been implemented") 85 | } 86 | 87 | override open func loadDisplayNode() { 88 | self.displayNode = AlertControllerNode(contentNode: self.contentNode, theme: self.theme, allowInputInset: self.allowInputInset) 89 | self.displayNodeDidLoad() 90 | 91 | self.controllerNode.dismiss = { [weak self] in 92 | if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap { 93 | strongSelf.controllerNode.animateOut { 94 | self?.dismiss() 95 | } 96 | } 97 | } 98 | } 99 | 100 | override open func viewDidAppear(_ animated: Bool) { 101 | super.viewDidAppear(animated) 102 | 103 | self.controllerNode.animateIn() 104 | } 105 | 106 | override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { 107 | super.containerLayoutUpdated(layout, transition: transition) 108 | 109 | self.controllerNode.containerLayoutUpdated(layout, transition: transition) 110 | } 111 | 112 | override open func dismiss(completion: (() -> Void)? = nil) { 113 | self.dismissed?() 114 | self.presentingViewController?.dismiss(animated: false, completion: completion) 115 | } 116 | 117 | public func dismissAnimated() { 118 | self.controllerNode.animateOut { [weak self] in 119 | self?.dismiss() 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Display/AlertControllerNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | final class AlertControllerNode: ASDisplayNode { 5 | private let dimmingNode: ASDisplayNode 6 | private let containerNode: ASDisplayNode 7 | private let effectNode: ASDisplayNode 8 | private let contentNode: AlertContentNode 9 | private let allowInputInset: Bool 10 | 11 | private var containerLayout: ContainerViewLayout? 12 | 13 | var dismiss: (() -> Void)? 14 | 15 | init(contentNode: AlertContentNode, theme: AlertControllerTheme, allowInputInset: Bool) { 16 | self.allowInputInset = allowInputInset 17 | self.dimmingNode = ASDisplayNode() 18 | self.dimmingNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) 19 | 20 | self.containerNode = ASDisplayNode() 21 | self.containerNode.backgroundColor = theme.backgroundColor 22 | self.containerNode.layer.cornerRadius = 14.0 23 | self.containerNode.layer.masksToBounds = true 24 | 25 | self.effectNode = ASDisplayNode(viewBlock: { 26 | let view = UIView() 27 | return view 28 | }) 29 | 30 | self.contentNode = contentNode 31 | 32 | super.init() 33 | 34 | self.addSubnode(self.dimmingNode) 35 | 36 | self.addSubnode(self.effectNode) 37 | 38 | self.containerNode.addSubnode(self.contentNode) 39 | self.addSubnode(self.containerNode) 40 | 41 | self.contentNode.requestLayout = { [weak self] transition in 42 | if let strongSelf = self, let containerLayout = self?.containerLayout { 43 | strongSelf.containerLayoutUpdated(containerLayout, transition: transition) 44 | } 45 | } 46 | } 47 | 48 | override func didLoad() { 49 | super.didLoad() 50 | 51 | self.dimmingNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:)))) 52 | } 53 | 54 | func updateTheme(_ theme: AlertControllerTheme) { 55 | self.containerNode.backgroundColor = theme.backgroundColor 56 | self.contentNode.updateTheme(theme) 57 | } 58 | 59 | func animateIn() { 60 | self.dimmingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) 61 | self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) 62 | 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) 63 | } 64 | 65 | func animateOut(completion: @escaping () -> Void) { 66 | self.dimmingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) 67 | self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) 68 | self.containerNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false, completion: { _ in 69 | completion() 70 | }) 71 | } 72 | 73 | func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { 74 | self.containerLayout = layout 75 | 76 | transition.updateFrame(node: self.dimmingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) 77 | 78 | var insetOptions: ContainerViewLayoutInsetOptions = [.statusBar] 79 | if self.allowInputInset { 80 | insetOptions.insert(.input) 81 | } 82 | var insets = layout.insets(options: insetOptions) 83 | let maxWidth = min(240.0, layout.size.width - 70.0) 84 | insets.left = floor((layout.size.width - maxWidth) / 2.0) 85 | insets.right = floor((layout.size.width - maxWidth) / 2.0) 86 | 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)) 87 | let contentSize = self.contentNode.updateLayout(size: contentAvailableFrame.size, transition: transition) 88 | let containerSize = CGSize(width: contentSize.width, height: contentSize.height) 89 | 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) 90 | 91 | transition.updateFrame(node: self.containerNode, frame: containerFrame) 92 | transition.updateFrame(node: self.effectNode, frame: containerFrame) 93 | transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size)) 94 | } 95 | 96 | @objc func dimmingNodeTapGesture(_ recognizer: UITapGestureRecognizer) { 97 | if case .ended = recognizer.state { 98 | self.dismiss?() 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Display/CASeeThroughTracingLayer.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface CASeeThroughTracingLayer : CALayer 4 | 5 | @end 6 | 7 | @interface CASeeThroughTracingView : UIView 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Display/CASeeThroughTracingLayer.m: -------------------------------------------------------------------------------- 1 | #import "CASeeThroughTracingLayer.h" 2 | 3 | @interface CASeeThroughTracingLayer () { 4 | CGPoint _parentOffset; 5 | } 6 | 7 | @end 8 | 9 | @implementation CASeeThroughTracingLayer 10 | 11 | - (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key { 12 | [super addAnimation:anim forKey:key]; 13 | } 14 | 15 | - (void)setFrame:(CGRect)frame { 16 | [super setFrame:frame]; 17 | 18 | [self _mirrorTransformToSublayers]; 19 | } 20 | 21 | - (void)setBounds:(CGRect)bounds { 22 | [super setBounds:bounds]; 23 | 24 | [self _mirrorTransformToSublayers]; 25 | } 26 | 27 | - (void)setPosition:(CGPoint)position { 28 | [super setPosition:position]; 29 | 30 | [self _mirrorTransformToSublayers]; 31 | } 32 | 33 | - (void)_mirrorTransformToSublayers { 34 | CGRect bounds = self.bounds; 35 | CGPoint position = self.position; 36 | 37 | CGPoint sublayerParentOffset = _parentOffset; 38 | sublayerParentOffset.x += position.x - (bounds.size.width) / 2.0f; 39 | sublayerParentOffset.y += position.y - (bounds.size.width) / 2.0f; 40 | 41 | for (CALayer *sublayer in self.sublayers) { 42 | if ([sublayer isKindOfClass:[CASeeThroughTracingLayer class]]) { 43 | ((CASeeThroughTracingLayer *)sublayer)->_parentOffset = sublayerParentOffset; 44 | [(CASeeThroughTracingLayer *)sublayer _mirrorTransformToSublayers]; 45 | } 46 | } 47 | } 48 | 49 | @end 50 | 51 | @implementation CASeeThroughTracingView 52 | 53 | + (Class)layerClass { 54 | return [CASeeThroughTracingLayer class]; 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /Display/CATracingLayer.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface CATracingLayer : CALayer 4 | 5 | @end 6 | 7 | @interface CATracingLayerInfo : NSObject 8 | 9 | @property (nonatomic, readonly) bool shouldBeAdjustedToInverseTransform; 10 | @property (nonatomic, weak, readonly) id _Nullable userData; 11 | @property (nonatomic, readonly) int32_t tracingTag; 12 | @property (nonatomic, readonly) int32_t disableChildrenTracingTags; 13 | 14 | - (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag disableChildrenTracingTags:(int32_t)disableChildrenTracingTags; 15 | 16 | @end 17 | 18 | @interface UITracingLayerView : UIView 19 | 20 | - (void)scheduleWithLayout:(void (^_Nonnull)())block; 21 | 22 | @end 23 | 24 | @interface CALayer (Tracing) 25 | 26 | - (CATracingLayerInfo * _Nullable)traceableInfo; 27 | - (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info; 28 | 29 | - (bool)hasPositionOrOpacityAnimations; 30 | - (bool)hasPositionAnimations; 31 | 32 | - (void)setInvalidateTracingSublayers:(void (^_Nullable)())block; 33 | - (NSArray *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag; 34 | - (void)adjustTraceableLayerTransforms:(CGSize)offset; 35 | 36 | - (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget; 37 | 38 | - (void)invalidateUpTheTree; 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /Display/ChildWindowHostView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | private final class ChildWindowHostView: UIView, WindowHost { 5 | var updateSize: ((CGSize) -> Void)? 6 | var layoutSubviewsEvent: (() -> Void)? 7 | var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? 8 | var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)? 9 | var invalidateDeferScreenEdgeGestureImpl: (() -> Void)? 10 | var invalidatePreferNavigationUIHiddenImpl: (() -> Void)? 11 | var cancelInteractiveKeyboardGesturesImpl: (() -> Void)? 12 | var forEachControllerImpl: (((ContainableController) -> Void) -> Void)? 13 | var getAccessibilityElementsImpl: (() -> [Any]?)? 14 | 15 | override var frame: CGRect { 16 | didSet { 17 | if self.frame.size != oldValue.size { 18 | self.updateSize?(self.frame.size) 19 | } 20 | } 21 | } 22 | 23 | override func layoutSubviews() { 24 | super.layoutSubviews() 25 | 26 | self.layoutSubviewsEvent?() 27 | } 28 | 29 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 30 | return self.hitTestImpl?(point, event) 31 | } 32 | 33 | func invalidateDeferScreenEdgeGestures() { 34 | self.invalidateDeferScreenEdgeGestureImpl?() 35 | } 36 | 37 | func invalidatePreferNavigationUIHidden() { 38 | self.invalidatePreferNavigationUIHiddenImpl?() 39 | } 40 | 41 | func cancelInteractiveKeyboardGestures() { 42 | self.cancelInteractiveKeyboardGesturesImpl?() 43 | } 44 | 45 | func forEachController(_ f: (ContainableController) -> Void) { 46 | self.forEachControllerImpl?(f) 47 | } 48 | 49 | func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) { 50 | self.presentController?(controller, level, blockInteraction, completion) 51 | } 52 | 53 | func presentInGlobalOverlay(_ controller: ContainableController) { 54 | } 55 | } 56 | 57 | public func childWindowHostView(parent: UIView) -> WindowHostView { 58 | let view = ChildWindowHostView() 59 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 60 | 61 | let hostView = WindowHostView(containerView: view, eventView: view, isRotating: { 62 | return false 63 | }, updateSupportedInterfaceOrientations: { orientations in 64 | }, updateDeferScreenEdgeGestures: { edges in 65 | }, updatePreferNavigationUIHidden: { value in 66 | }) 67 | 68 | view.updateSize = { [weak hostView] size in 69 | hostView?.updateSize?(size, 0.0) 70 | } 71 | 72 | view.layoutSubviewsEvent = { [weak hostView] in 73 | hostView?.layoutSubviews?() 74 | } 75 | 76 | /*window.updateIsUpdatingOrientationLayout = { [weak hostView] value in 77 | hostView?.isUpdatingOrientationLayout = value 78 | } 79 | 80 | window.updateToInterfaceOrientation = { [weak hostView] in 81 | hostView?.updateToInterfaceOrientation?() 82 | }*/ 83 | 84 | view.presentController = { [weak hostView] controller, level, block, f in 85 | hostView?.present?(controller, level, block, f) 86 | } 87 | 88 | /*view.presentNativeImpl = { [weak hostView] controller in 89 | hostView?.presentNative?(controller) 90 | }*/ 91 | 92 | view.hitTestImpl = { [weak hostView] point, event in 93 | return hostView?.hitTest?(point, event) 94 | } 95 | 96 | view.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in 97 | return hostView?.invalidateDeferScreenEdgeGesture?() 98 | } 99 | 100 | view.invalidatePreferNavigationUIHiddenImpl = { [weak hostView] in 101 | return hostView?.invalidatePreferNavigationUIHidden?() 102 | } 103 | 104 | view.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in 105 | hostView?.cancelInteractiveKeyboardGestures?() 106 | } 107 | 108 | view.forEachControllerImpl = { [weak hostView] f in 109 | hostView?.forEachController?(f) 110 | } 111 | 112 | view.getAccessibilityElementsImpl = { [weak hostView] in 113 | return hostView?.getAccessibilityElements?() 114 | } 115 | 116 | return hostView 117 | } 118 | -------------------------------------------------------------------------------- /Display/ContainableController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AsyncDisplayKit 3 | import SwiftSignalKit 4 | 5 | public protocol PresentableController: class { 6 | func viewDidAppear(completion: @escaping () -> Void) 7 | } 8 | 9 | public protocol ContainableController: class { 10 | var view: UIView! { get } 11 | var displayNode: ASDisplayNode { get } 12 | var isViewLoaded: Bool { get } 13 | var isOpaqueWhenInOverlay: Bool { get } 14 | var blocksBackgroundWhenInOverlay: Bool { get } 15 | var ready: Promise { get } 16 | 17 | func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations 18 | var deferScreenEdgeGestures: UIRectEdge { get } 19 | 20 | func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) 21 | func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) 22 | 23 | func viewWillAppear(_ animated: Bool) 24 | func viewWillDisappear(_ animated: Bool) 25 | func viewDidAppear(_ animated: Bool) 26 | func viewDidDisappear(_ animated: Bool) 27 | } 28 | -------------------------------------------------------------------------------- /Display/ContainerViewLayout.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct ContainerViewLayoutInsetOptions: OptionSet { 3 | public let rawValue: Int 4 | 5 | public init(rawValue: Int) { 6 | self.rawValue = rawValue 7 | } 8 | 9 | public init() { 10 | self.rawValue = 0 11 | } 12 | 13 | public static let statusBar = ContainerViewLayoutInsetOptions(rawValue: 1 << 0) 14 | public static let input = ContainerViewLayoutInsetOptions(rawValue: 1 << 1) 15 | } 16 | 17 | public enum ContainerViewLayoutSizeClass { 18 | case compact 19 | case regular 20 | } 21 | 22 | public struct LayoutMetrics: Equatable { 23 | public let widthClass: ContainerViewLayoutSizeClass 24 | public let heightClass: ContainerViewLayoutSizeClass 25 | 26 | public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) { 27 | self.widthClass = widthClass 28 | self.heightClass = heightClass 29 | } 30 | 31 | public init() { 32 | self.widthClass = .compact 33 | self.heightClass = .compact 34 | } 35 | } 36 | 37 | public struct ContainerViewLayout: Equatable { 38 | public let size: CGSize 39 | public let metrics: LayoutMetrics 40 | public let intrinsicInsets: UIEdgeInsets 41 | public let safeInsets: UIEdgeInsets 42 | public let statusBarHeight: CGFloat? 43 | public var inputHeight: CGFloat? 44 | public let standardInputHeight: CGFloat 45 | public let inputHeightIsInteractivellyChanging: Bool 46 | public let inVoiceOver: Bool 47 | 48 | public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, standardInputHeight: CGFloat, inputHeightIsInteractivellyChanging: Bool, inVoiceOver: Bool) { 49 | self.size = size 50 | self.metrics = metrics 51 | self.intrinsicInsets = intrinsicInsets 52 | self.safeInsets = safeInsets 53 | self.statusBarHeight = statusBarHeight 54 | self.inputHeight = inputHeight 55 | self.standardInputHeight = standardInputHeight 56 | self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging 57 | self.inVoiceOver = inVoiceOver 58 | } 59 | 60 | public func insets(options: ContainerViewLayoutInsetOptions) -> UIEdgeInsets { 61 | var insets = self.intrinsicInsets 62 | if let statusBarHeight = self.statusBarHeight , options.contains(.statusBar) { 63 | insets.top += statusBarHeight 64 | } 65 | if let inputHeight = self.inputHeight , options.contains(.input) { 66 | insets.bottom = max(inputHeight, insets.bottom) 67 | } 68 | return insets 69 | } 70 | 71 | public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout { 72 | 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) 73 | } 74 | 75 | public func withUpdatedSize(_ size: CGSize) -> ContainerViewLayout { 76 | 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) 77 | } 78 | 79 | public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout { 80 | 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) 81 | } 82 | 83 | public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout { 84 | 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) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Display/ContextMenuAction.swift: -------------------------------------------------------------------------------- 1 | 2 | public enum ContextMenuActionContent { 3 | case text(title: String, accessibilityLabel: String) 4 | case icon(UIImage) 5 | } 6 | 7 | public struct ContextMenuAction { 8 | public let content: ContextMenuActionContent 9 | public let action: () -> Void 10 | 11 | public init(content: ContextMenuActionContent, action: @escaping () -> Void) { 12 | self.content = content 13 | self.action = action 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Display/ContextMenuActionNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | final private class ContextMenuActionButton: HighlightTrackingButton { 5 | override func convert(_ point: CGPoint, from view: UIView?) -> CGPoint { 6 | if view is UIWindow { 7 | return super.convert(point, from: nil) 8 | } else { 9 | return super.convert(point, from: view) 10 | } 11 | } 12 | } 13 | 14 | final class ContextMenuActionNode: ASDisplayNode { 15 | private let textNode: ImmediateTextNode? 16 | private var textSize: CGSize? 17 | private let iconNode: ASImageNode? 18 | private let action: () -> Void 19 | private let button: ContextMenuActionButton 20 | private let actionArea: AccessibilityAreaNode 21 | 22 | var dismiss: (() -> Void)? 23 | 24 | init(action: ContextMenuAction) { 25 | self.actionArea = AccessibilityAreaNode() 26 | self.actionArea.accessibilityTraits = UIAccessibilityTraitButton 27 | 28 | switch action.content { 29 | case let .text(title, accessibilityLabel): 30 | self.actionArea.accessibilityLabel = accessibilityLabel 31 | 32 | let textNode = ImmediateTextNode() 33 | textNode.isUserInteractionEnabled = false 34 | textNode.displaysAsynchronously = false 35 | textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: UIColor.white) 36 | textNode.isAccessibilityElement = false 37 | 38 | self.textNode = textNode 39 | self.iconNode = nil 40 | case let .icon(image): 41 | let iconNode = ASImageNode() 42 | iconNode.displaysAsynchronously = false 43 | iconNode.displayWithoutProcessing = true 44 | iconNode.image = image 45 | 46 | self.iconNode = iconNode 47 | self.textNode = nil 48 | } 49 | self.action = action.action 50 | 51 | self.button = ContextMenuActionButton() 52 | self.button.isAccessibilityElement = false 53 | 54 | super.init() 55 | 56 | self.backgroundColor = UIColor(white: 0.0, alpha: 0.8) 57 | if let textNode = self.textNode { 58 | self.addSubnode(textNode) 59 | } 60 | if let iconNode = self.iconNode { 61 | self.addSubnode(iconNode) 62 | } 63 | 64 | self.button.highligthedChanged = { [weak self] highlighted in 65 | self?.backgroundColor = highlighted ? UIColor(white: 0.0, alpha: 0.4) : UIColor(white: 0.0, alpha: 0.8) 66 | } 67 | self.view.addSubview(self.button) 68 | self.addSubnode(self.actionArea) 69 | 70 | self.actionArea.activate = { [weak self] in 71 | self?.buttonPressed() 72 | return true 73 | } 74 | } 75 | 76 | override func didLoad() { 77 | super.didLoad() 78 | 79 | self.button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) 80 | } 81 | 82 | @objc private func buttonPressed() { 83 | self.backgroundColor = UIColor(white: 0.0, alpha: 0.4) 84 | 85 | self.action() 86 | if let dismiss = self.dismiss { 87 | dismiss() 88 | } 89 | } 90 | 91 | override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 92 | if let textNode = self.textNode { 93 | let textSize = textNode.updateLayout(constrainedSize) 94 | self.textSize = textSize 95 | return CGSize(width: textSize.width + 36.0, height: 54.0) 96 | } else if let iconNode = self.iconNode, let image = iconNode.image { 97 | return CGSize(width: image.size.width + 36.0, height: 54.0) 98 | } else { 99 | return CGSize(width: 36.0, height: 54.0) 100 | } 101 | } 102 | 103 | override func layout() { 104 | super.layout() 105 | 106 | self.button.frame = self.bounds 107 | self.actionArea.frame = self.bounds 108 | 109 | if let textNode = self.textNode, let textSize = self.textSize { 110 | 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) 111 | } 112 | if let iconNode = self.iconNode, let image = iconNode.image { 113 | let iconSize = image.size 114 | 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) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Display/ContextMenuContainerNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | private struct CachedMaskParams: Equatable { 5 | let size: CGSize 6 | let relativeArrowPosition: CGFloat 7 | let arrowOnBottom: Bool 8 | } 9 | 10 | private final class ContextMenuContainerMaskView: UIView { 11 | override class var layerClass: AnyClass { 12 | return CAShapeLayer.self 13 | } 14 | } 15 | 16 | public final class ContextMenuContainerNode: ASDisplayNode { 17 | private var cachedMaskParams: CachedMaskParams? 18 | private let maskView = ContextMenuContainerMaskView() 19 | 20 | public var relativeArrowPosition: (CGFloat, Bool)? 21 | 22 | //private let effectView: UIVisualEffectView 23 | 24 | override public init() { 25 | //self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) 26 | 27 | super.init() 28 | 29 | self.backgroundColor = UIColor(rgb: 0xeaecec) 30 | //self.view.addSubview(self.effectView) 31 | //self.effectView.mask = self.maskView 32 | self.view.mask = self.maskView 33 | } 34 | 35 | override public func didLoad() { 36 | super.didLoad() 37 | 38 | self.layer.allowsGroupOpacity = true 39 | } 40 | 41 | override public func layout() { 42 | super.layout() 43 | 44 | self.updateLayout(transition: .immediate) 45 | } 46 | 47 | public func updateLayout(transition: ContainedViewLayoutTransition) { 48 | //self.effectView.frame = self.bounds 49 | 50 | let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true) 51 | if self.cachedMaskParams != maskParams { 52 | let path = UIBezierPath() 53 | let cornerRadius: CGFloat = 6.0 54 | let verticalInset: CGFloat = 9.0 55 | let arrowWidth: CGFloat = 18.0 56 | let requestedArrowPosition = maskParams.relativeArrowPosition 57 | let arrowPosition = max(cornerRadius + arrowWidth / 2.0, min(maskParams.size.width - cornerRadius - arrowWidth / 2.0, requestedArrowPosition)) 58 | let arrowOnBottom = maskParams.arrowOnBottom 59 | 60 | path.move(to: CGPoint(x: 0.0, y: verticalInset + cornerRadius)) 61 | path.addArc(withCenter: CGPoint(x: cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: CGFloat(3.0 * CGFloat.pi / 2.0), clockwise: true) 62 | if !arrowOnBottom { 63 | path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: verticalInset)) 64 | path.addLine(to: CGPoint(x: arrowPosition, y: 0.0)) 65 | path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: verticalInset)) 66 | } 67 | path.addLine(to: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset)) 68 | 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) 69 | path.addLine(to: CGPoint(x: maskParams.size.width, y: maskParams.size.height - cornerRadius - verticalInset)) 70 | 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) 71 | if arrowOnBottom { 72 | path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: maskParams.size.height - verticalInset)) 73 | path.addLine(to: CGPoint(x: arrowPosition, y: maskParams.size.height)) 74 | path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: maskParams.size.height - verticalInset)) 75 | } 76 | path.addLine(to: CGPoint(x: cornerRadius, y: maskParams.size.height - verticalInset)) 77 | 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) 78 | path.close() 79 | 80 | self.cachedMaskParams = maskParams 81 | if let layer = self.maskView.layer as? CAShapeLayer { 82 | if case let .animated(duration, curve) = transition, let previousPath = layer.path { 83 | layer.animate(from: previousPath, to: path.cgPath, keyPath: "path", timingFunction: curve.timingFunction, duration: duration) 84 | } 85 | layer.path = path.cgPath 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Display/ContextMenuController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public final class ContextMenuControllerPresentationArguments { 5 | fileprivate let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)? 6 | 7 | public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?) { 8 | self.sourceNodeAndRect = sourceNodeAndRect 9 | } 10 | } 11 | 12 | public final class ContextMenuController: ViewController { 13 | private var contextMenuNode: ContextMenuNode { 14 | return self.displayNode as! ContextMenuNode 15 | } 16 | 17 | private let actions: [ContextMenuAction] 18 | private let catchTapsOutside: Bool 19 | private let hasHapticFeedback: Bool 20 | 21 | private var layout: ContainerViewLayout? 22 | 23 | public var dismissed: (() -> Void)? 24 | 25 | public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false) { 26 | self.actions = actions 27 | self.catchTapsOutside = catchTapsOutside 28 | self.hasHapticFeedback = hasHapticFeedback 29 | 30 | super.init(navigationBarPresentationData: nil) 31 | } 32 | 33 | required public init(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override public func loadDisplayNode() { 38 | self.displayNode = ContextMenuNode(actions: self.actions, dismiss: { [weak self] in 39 | self?.dismissed?() 40 | self?.contextMenuNode.animateOut { 41 | self?.presentingViewController?.dismiss(animated: false) 42 | } 43 | }, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback) 44 | self.displayNodeDidLoad() 45 | } 46 | 47 | override public func viewDidAppear(_ animated: Bool) { 48 | super.viewDidAppear(animated) 49 | 50 | self.contextMenuNode.animateIn() 51 | } 52 | 53 | override public func dismiss(completion: (() -> Void)? = nil) { 54 | self.dismissed?() 55 | self.contextMenuNode.animateOut { [weak self] in 56 | self?.presentingViewController?.dismiss(animated: false) 57 | } 58 | } 59 | 60 | override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { 61 | super.containerLayoutUpdated(layout, transition: transition) 62 | 63 | if self.layout != nil && self.layout! != layout { 64 | self.dismissed?() 65 | self.contextMenuNode.animateOut { [weak self] in 66 | self?.presentingViewController?.dismiss(animated: false) 67 | } 68 | } else { 69 | self.layout = layout 70 | 71 | if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() { 72 | self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil) 73 | self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil) 74 | } else { 75 | self.contextMenuNode.sourceRect = nil 76 | self.contextMenuNode.containerRect = nil 77 | } 78 | 79 | self.contextMenuNode.containerLayoutUpdated(layout, transition: transition) 80 | } 81 | } 82 | 83 | override public func viewWillAppear(_ animated: Bool) { 84 | super.viewWillAppear(animated) 85 | 86 | self.contextMenuNode.animateIn() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Display/Display.h: -------------------------------------------------------------------------------- 1 | // 2 | // Display.h 3 | // Display 4 | // 5 | // Created by Peter on 29/07/15. 6 | // Copyright © 2015 Telegram. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Display. 12 | FOUNDATION_EXPORT double DisplayVersionNumber; 13 | 14 | //! Project version string for Display. 15 | FOUNDATION_EXPORT const unsigned char DisplayVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | #import 20 | #import 21 | #import 22 | #import 23 | #import 24 | #import 25 | #import 26 | #import 27 | #import 28 | #import 29 | #import 30 | #import 31 | #import 32 | #import 33 | #import 34 | #import 35 | -------------------------------------------------------------------------------- /Display/DisplayLinkDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class DisplayLinkDispatcher: NSObject { 4 | private var displayLink: CADisplayLink! 5 | private var blocksToDispatch: [() -> Void] = [] 6 | private let limit: Int 7 | 8 | public init(limit: Int = 0) { 9 | self.limit = limit 10 | 11 | super.init() 12 | 13 | if #available(iOS 10.0, *) { 14 | //self.displayLink.preferredFramesPerSecond = 60 15 | } else { 16 | self.displayLink = CADisplayLink(target: self, selector: #selector(self.run)) 17 | self.displayLink.isPaused = true 18 | self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) 19 | } 20 | } 21 | 22 | public func dispatch(f: @escaping () -> Void) { 23 | if self.displayLink == nil { 24 | if Thread.isMainThread { 25 | f() 26 | } else { 27 | DispatchQueue.main.async(execute: f) 28 | } 29 | } else { 30 | self.blocksToDispatch.append(f) 31 | self.displayLink.isPaused = false 32 | } 33 | } 34 | 35 | @objc func run() { 36 | for _ in 0 ..< (self.limit == 0 ? 1000 : self.limit) { 37 | if self.blocksToDispatch.count == 0 { 38 | self.displayLink.isPaused = true 39 | break 40 | } else { 41 | let f = self.blocksToDispatch.removeFirst() 42 | f() 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Display/EditableTextNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public class EditableTextNode: ASEditableTextNode { 5 | override public var keyboardAppearance: UIKeyboardAppearance { 6 | get { 7 | return super.keyboardAppearance 8 | } 9 | set { 10 | guard newValue != self.keyboardAppearance else { 11 | return 12 | } 13 | let resigning = self.isFirstResponder() 14 | if resigning { 15 | self.resignFirstResponder() 16 | } 17 | super.keyboardAppearance = newValue 18 | if resigning { 19 | self.becomeFirstResponder() 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Display/FBAnimationPerformanceTracker.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /* 4 | * This is an example provided by Facebook are for non-commercial testing and 5 | * evaluation purposes only. 6 | * 7 | * Facebook reserves all rights not expressly granted. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 10 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 11 | * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL 12 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 13 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | * 16 | * 17 | * FBAnimationPerformanceTracker 18 | * ----------------------------------------------------------------------- 19 | * 20 | * This class provides animation performance tracking functionality. It basically 21 | * measures the app's frame rate during an operation, and reports this information. 22 | * 23 | * 1) In Foo's designated initializer, construct a tracker object 24 | * 25 | * 2) Add calls to -start and -stop in appropriate places, e.g. for a ScrollView 26 | * 27 | * - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { 28 | * [_apTracker start]; 29 | * } 30 | * 31 | * - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 32 | * { 33 | * if (!scrollView.dragging) { 34 | * [_apTracker stop]; 35 | * } 36 | * } 37 | * 38 | * - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { 39 | * if (!decelerate) { 40 | * [_apTracker stop]; 41 | * } 42 | * } 43 | * 44 | * Notes 45 | * ----- 46 | * [] The tracker operates by creating a CADisplayLink object to measure the frame rate of the display 47 | * during start/stop interval. 48 | * 49 | * [] Calls to -stop that were not preceded by a matching call to -start have no effect. 50 | * 51 | * [] 2 calls to -start in a row will trash the data accumulated so far and not log anything. 52 | * 53 | * 54 | * Configuration object for the core tracker 55 | * 56 | * =============================================================================== 57 | * I highly recommend for you to use the standard configuration provided 58 | * These are essentially here so that the computation of the metric is transparent 59 | * and you can feel confident in what the numbers mean. 60 | * =============================================================================== 61 | */ 62 | struct FBAnimationPerformanceTrackerConfig 63 | { 64 | // Number of frame drop that defines a "small" drop event. By default, 1. 65 | NSInteger smallDropEventFrameNumber; 66 | // Number of frame drop that defines a "large" drop event. By default, 4. 67 | NSInteger largeDropEventFrameNumber; 68 | // Number of maximum frame drops to which the drop will be trimmed down to. Currently 15. 69 | NSInteger maxFrameDropAccount; 70 | 71 | // If YES, will report stack traces 72 | BOOL reportStackTraces; 73 | }; 74 | typedef struct FBAnimationPerformanceTrackerConfig FBAnimationPerformanceTrackerConfig; 75 | 76 | 77 | @protocol FBAnimationPerformanceTrackerDelegate 78 | 79 | /** 80 | * Core Metric 81 | * 82 | * You are responsible for the aggregation of these metrics (it being on the client or the server). I recommend to implement both 83 | * to limit the payload you are sending to the server. 84 | * 85 | * The final recommended metric being: - SUM(duration) / SUM(smallDropEvent) aka the number of seconds between one frame drop or more 86 | * - SUM(duration) / SUM(largeDropEvent) aka the number of seconds between four frame drops or more 87 | * 88 | * The first metric will tell you how smooth is your scroll view. 89 | * The second metric will tell you how clowny your scroll view can get. 90 | * 91 | * Every time stop is called, this event will fire reporting the performance. 92 | * 93 | * NOTE on this metric: 94 | * - It has been tested at scale on many Facebook apps. 95 | * - It follows the curves of devices. 96 | * - You will need about 100K calls for the number to converge. 97 | * - 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) 98 | * - We report fraction of drops. 7 frame drop = 1.75 of a large frame drop if a large drop is 4 frame drop. 99 | * This is to preserve the correlation mentionned above. 100 | */ 101 | - (void)reportDurationInMS:(NSInteger)duration smallDropEvent:(double)smallDropEvent largeDropEvent:(double)largeDropEvent; 102 | 103 | /** 104 | * Stack traces 105 | * 106 | * Dark magic of the animation tracker. In case of a frame drop, this will return a stack trace. 107 | * This will NOT be reported on the main-thread, but off-main thread to save a few CPU cycles. 108 | * 109 | * The slide is constant value that needs to be reported with the stack for processing. 110 | * This currently only allows for symbolication of your own image. 111 | * 112 | * Future work includes symbolicating all modules. I personnaly find it usually 113 | * good enough to know the name of the module. 114 | * 115 | * The stack will have the following format: 116 | * Foundation:0x123|MyApp:0x234|MyApp:0x345| 117 | * 118 | * The slide will have the following format: 119 | * 0x456 120 | */ 121 | - (void)reportStackTrace:(NSString *)stack withSlide:(NSString *)slide; 122 | 123 | @end 124 | 125 | @interface FBAnimationPerformanceTracker : NSObject 126 | 127 | - (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config; 128 | 129 | + (FBAnimationPerformanceTrackerConfig)standardConfig; 130 | 131 | @property (weak, nonatomic, readwrite) id delegate; 132 | 133 | - (void)start; 134 | - (void)stop; 135 | 136 | @end 137 | -------------------------------------------------------------------------------- /Display/Font.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public struct Font { 5 | public static func regular(_ size: CGFloat) -> UIFont { 6 | return UIFont.systemFont(ofSize: size) 7 | } 8 | 9 | public static func medium(_ size: CGFloat) -> UIFont { 10 | if #available(iOS 8.2, *) { 11 | return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.medium) 12 | } else { 13 | return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil) 14 | } 15 | } 16 | 17 | public static func semibold(_ size: CGFloat) -> UIFont { 18 | if #available(iOS 8.2, *) { 19 | return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.semibold) 20 | } else { 21 | return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil) 22 | } 23 | } 24 | 25 | public static func bold(_ size: CGFloat) -> UIFont { 26 | if #available(iOS 8.2, *) { 27 | return UIFont.boldSystemFont(ofSize: size) 28 | } else { 29 | return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil) 30 | } 31 | } 32 | 33 | public static func light(_ size: CGFloat) -> UIFont { 34 | if #available(iOS 8.2, *) { 35 | return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light) 36 | } else { 37 | return CTFontCreateWithName("HelveticaNeue-Light" as CFString, size, nil) 38 | } 39 | } 40 | 41 | public static func semiboldItalic(_ size: CGFloat) -> UIFont { 42 | if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) { 43 | return UIFont(descriptor: descriptor, size: size) 44 | } else { 45 | return UIFont.italicSystemFont(ofSize: size) 46 | } 47 | } 48 | 49 | public static func monospace(_ size: CGFloat) -> UIFont { 50 | return UIFont(name: "Menlo-Regular", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 51 | } 52 | 53 | public static func semiboldMonospace(_ size: CGFloat) -> UIFont { 54 | return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 55 | } 56 | 57 | public static func italicMonospace(_ size: CGFloat) -> UIFont { 58 | return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 59 | } 60 | 61 | public static func semiboldItalicMonospace(_ size: CGFloat) -> UIFont { 62 | return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 63 | } 64 | 65 | public static func italic(_ size: CGFloat) -> UIFont { 66 | return UIFont.italicSystemFont(ofSize: size) 67 | } 68 | } 69 | 70 | public extension NSAttributedString { 71 | convenience init(string: String, font: UIFont? = nil, textColor: UIColor = UIColor.black, paragraphAlignment: NSTextAlignment? = nil) { 72 | var attributes: [NSAttributedStringKey: AnyObject] = [:] 73 | if let font = font { 74 | attributes[NSAttributedStringKey.font] = font 75 | } 76 | attributes[NSAttributedStringKey.foregroundColor] = textColor 77 | if let paragraphAlignment = paragraphAlignment { 78 | let paragraphStyle = NSMutableParagraphStyle() 79 | paragraphStyle.alignment = paragraphAlignment 80 | attributes[NSAttributedStringKey.paragraphStyle] = paragraphStyle 81 | } 82 | self.init(string: string, attributes: attributes) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Display/GridItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol GridSection { 4 | var height: CGFloat { get } 5 | var hashValue: Int { get } 6 | 7 | func isEqual(to: GridSection) -> Bool 8 | func node() -> ASDisplayNode 9 | } 10 | 11 | public protocol GridItem { 12 | var section: GridSection? { get } 13 | func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode 14 | func update(node: GridItemNode) 15 | var aspectRatio: CGFloat { get } 16 | var fillsRowWithHeight: CGFloat? { get } 17 | var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? { get } 18 | } 19 | 20 | public extension GridItem { 21 | var aspectRatio: CGFloat { 22 | return 1.0 23 | } 24 | 25 | var fillsRowWithHeight: CGFloat? { 26 | return nil 27 | } 28 | 29 | var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? { 30 | return nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Display/GridItemNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | open class GridItemNode: ASDisplayNode { 5 | open var isVisibleInGrid = false 6 | open var isGridScrolling = false 7 | 8 | final var cachedFrame: CGRect = CGRect() 9 | override open var frame: CGRect { 10 | get { 11 | return self.cachedFrame 12 | } set(value) { 13 | self.cachedFrame = value 14 | super.frame = value 15 | } 16 | } 17 | 18 | open func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Display/GridNodeScroller.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | private class GridNodeScrollerLayer: CALayer { 4 | override func setNeedsDisplay() { 5 | } 6 | } 7 | 8 | private class GridNodeScrollerView: UIScrollView { 9 | override class var layerClass: AnyClass { 10 | return GridNodeScrollerLayer.self 11 | } 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | if #available(iOSApplicationExtension 11.0, *) { 17 | self.contentInsetAdjustmentBehavior = .never 18 | } 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | override func touchesShouldCancel(in view: UIView) -> Bool { 26 | return true 27 | } 28 | 29 | @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 30 | return false 31 | } 32 | } 33 | 34 | open class GridNodeScroller: ASDisplayNode, UIGestureRecognizerDelegate { 35 | public var scrollView: UIScrollView { 36 | return self.view as! UIScrollView 37 | } 38 | 39 | override init() { 40 | super.init() 41 | 42 | self.setViewBlock({ 43 | return GridNodeScrollerView(frame: CGRect()) 44 | }) 45 | 46 | self.scrollView.scrollsToTop = false 47 | } 48 | 49 | required public init?(coder aDecoder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Display/HapticFeedback.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum ImpactHapticFeedbackStyle: Hashable { 5 | case light 6 | case medium 7 | case heavy 8 | } 9 | 10 | @available(iOSApplicationExtension 10.0, *) 11 | private final class HapticFeedbackImpl { 12 | private lazy var impactGenerator: [ImpactHapticFeedbackStyle : UIImpactFeedbackGenerator] = { 13 | [.light: UIImpactFeedbackGenerator(style: .light), 14 | .medium: UIImpactFeedbackGenerator(style: .medium), 15 | .heavy: UIImpactFeedbackGenerator(style: .heavy)] }() 16 | private lazy var selectionGenerator = { UISelectionFeedbackGenerator() }() 17 | private lazy var notificationGenerator = { UINotificationFeedbackGenerator() }() 18 | 19 | func prepareTap() { 20 | self.selectionGenerator.prepare() 21 | } 22 | 23 | func tap() { 24 | self.selectionGenerator.selectionChanged() 25 | } 26 | 27 | func prepareImpact(_ style: ImpactHapticFeedbackStyle) { 28 | self.impactGenerator[style]?.prepare() 29 | } 30 | 31 | func impact(_ style: ImpactHapticFeedbackStyle) { 32 | self.impactGenerator[style]?.impactOccurred() 33 | } 34 | 35 | func success() { 36 | self.notificationGenerator.notificationOccurred(.success) 37 | } 38 | 39 | func prepareError() { 40 | self.notificationGenerator.prepare() 41 | } 42 | 43 | func error() { 44 | self.notificationGenerator.notificationOccurred(.error) 45 | } 46 | 47 | @objc dynamic func f() { 48 | } 49 | } 50 | 51 | public final class HapticFeedback { 52 | private var impl: AnyObject? 53 | 54 | public init() { 55 | } 56 | 57 | deinit { 58 | let impl = self.impl 59 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { 60 | if #available(iOSApplicationExtension 10.0, *) { 61 | if let impl = impl as? HapticFeedbackImpl { 62 | impl.f() 63 | } 64 | } 65 | }) 66 | } 67 | 68 | @available(iOSApplicationExtension 10.0, *) 69 | private func withImpl(_ f: (HapticFeedbackImpl) -> Void) { 70 | if self.impl == nil { 71 | self.impl = HapticFeedbackImpl() 72 | } 73 | f(self.impl as! HapticFeedbackImpl) 74 | } 75 | 76 | public func prepareTap() { 77 | if #available(iOSApplicationExtension 10.0, *) { 78 | self.withImpl { impl in 79 | impl.prepareTap() 80 | } 81 | } 82 | } 83 | 84 | public func tap() { 85 | if #available(iOSApplicationExtension 10.0, *) { 86 | self.withImpl { impl in 87 | impl.tap() 88 | } 89 | } 90 | } 91 | 92 | public func prepareImpact(_ style: ImpactHapticFeedbackStyle = .medium) { 93 | if #available(iOSApplicationExtension 10.0, *) { 94 | self.withImpl { impl in 95 | impl.prepareImpact(style) 96 | } 97 | } 98 | } 99 | 100 | public func impact(_ style: ImpactHapticFeedbackStyle = .medium) { 101 | if #available(iOSApplicationExtension 10.0, *) { 102 | self.withImpl { impl in 103 | impl.impact(style) 104 | } 105 | } 106 | } 107 | 108 | public func success() { 109 | if #available(iOSApplicationExtension 10.0, *) { 110 | self.withImpl { impl in 111 | impl.success() 112 | } 113 | } 114 | } 115 | 116 | public func prepareError() { 117 | if #available(iOSApplicationExtension 10.0, *) { 118 | self.withImpl { impl in 119 | impl.prepareError() 120 | } 121 | } 122 | } 123 | 124 | public func error() { 125 | if #available(iOSApplicationExtension 10.0, *) { 126 | self.withImpl { impl in 127 | impl.error() 128 | } 129 | } 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /Display/HighlightTrackingButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class HighlightTrackingButton: UIButton { 4 | private var internalHighlighted = false 5 | 6 | public var internalHighligthedChanged: (Bool) -> Void = { _ in } 7 | public var highligthedChanged: (Bool) -> Void = { _ in } 8 | 9 | open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { 10 | if !self.internalHighlighted { 11 | self.internalHighlighted = true 12 | self.highligthedChanged(true) 13 | self.internalHighligthedChanged(true) 14 | } 15 | 16 | return super.beginTracking(touch, with: event) 17 | } 18 | 19 | open override func endTracking(_ touch: UITouch?, with event: UIEvent?) { 20 | if self.internalHighlighted { 21 | self.internalHighlighted = false 22 | self.highligthedChanged(false) 23 | self.internalHighligthedChanged(false) 24 | } 25 | 26 | super.endTracking(touch, with: event) 27 | } 28 | 29 | open override func cancelTracking(with event: UIEvent?) { 30 | if self.internalHighlighted { 31 | self.internalHighlighted = false 32 | self.highligthedChanged(false) 33 | self.internalHighligthedChanged(false) 34 | } 35 | 36 | super.cancelTracking(with: event) 37 | } 38 | 39 | open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 40 | if self.internalHighlighted { 41 | self.internalHighlighted = false 42 | self.highligthedChanged(false) 43 | self.internalHighligthedChanged(false) 44 | } 45 | 46 | super.touchesCancelled(touches, with: event) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Display/HighlightableButton.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import AsyncDisplayKit 4 | 5 | open class HighlightableButton: HighlightTrackingButton { 6 | override public init(frame: CGRect) { 7 | super.init(frame: frame) 8 | 9 | self.adjustsImageWhenHighlighted = false 10 | self.adjustsImageWhenDisabled = false 11 | self.internalHighligthedChanged = { [weak self] highlighted in 12 | if let strongSelf = self { 13 | if highlighted { 14 | strongSelf.layer.removeAnimation(forKey: "opacity") 15 | strongSelf.alpha = 0.4 16 | } else { 17 | strongSelf.alpha = 1.0 18 | strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) 19 | } 20 | } 21 | } 22 | } 23 | 24 | required public init?(coder aDecoder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | } 28 | 29 | open class HighlightTrackingButtonNode: ASButtonNode { 30 | private var internalHighlighted = false 31 | 32 | public var highligthedChanged: (Bool) -> Void = { _ in } 33 | 34 | open override func beginTracking(with touch: UITouch, with event: UIEvent?) -> Bool { 35 | if !self.internalHighlighted { 36 | self.internalHighlighted = true 37 | self.highligthedChanged(true) 38 | } 39 | 40 | return super.beginTracking(with: touch, with: event) 41 | } 42 | 43 | open override func endTracking(with touch: UITouch?, with event: UIEvent?) { 44 | if self.internalHighlighted { 45 | self.internalHighlighted = false 46 | self.highligthedChanged(false) 47 | } 48 | 49 | super.endTracking(with: touch, with: event) 50 | } 51 | 52 | open override func cancelTracking(with event: UIEvent?) { 53 | if self.internalHighlighted { 54 | self.internalHighlighted = false 55 | self.highligthedChanged(false) 56 | } 57 | 58 | super.cancelTracking(with: event) 59 | } 60 | 61 | open override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { 62 | super.touchesCancelled(touches, with: event) 63 | 64 | if self.internalHighlighted { 65 | self.internalHighlighted = false 66 | self.highligthedChanged(false) 67 | } 68 | } 69 | } 70 | 71 | open class HighlightableButtonNode: HighlightTrackingButtonNode { 72 | override public init() { 73 | super.init() 74 | 75 | self.highligthedChanged = { [weak self] highlighted in 76 | if let strongSelf = self { 77 | if highlighted { 78 | strongSelf.layer.removeAnimation(forKey: "opacity") 79 | strongSelf.alpha = 0.4 80 | } else { 81 | strongSelf.alpha = 1.0 82 | strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Display/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Display/InteractiveTransitionGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool { 5 | if view.disablesInteractiveTransitionGestureRecognizer { 6 | return true 7 | } 8 | 9 | if let point = point, let test = view.interactiveTransitionGestureRecognizerTest, test(point) { 10 | return true 11 | } 12 | 13 | if let view = view as? ListViewBackingView { 14 | let transform = view.transform 15 | let angle: Double = Double(atan2f(Float(transform.b), Float(transform.a))) 16 | let term1: Double = abs(angle - Double.pi / 2.0) 17 | let term2: Double = abs(angle + Double.pi / 2.0) 18 | let term3: Double = abs(angle - Double.pi * 3.0 / 2.0) 19 | if term1 < 0.001 || term2 < 0.001 || term3 < 0.001 { 20 | return true 21 | } 22 | } 23 | 24 | if let superview = view.superview { 25 | return hasHorizontalGestures(superview, point: point != nil ? view.convert(point!, to: superview) : nil) 26 | } else { 27 | return false 28 | } 29 | } 30 | 31 | class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { 32 | var validatedGesture = false 33 | var firstLocation: CGPoint = CGPoint() 34 | 35 | override init(target: Any?, action: Selector?) { 36 | super.init(target: target, action: action) 37 | 38 | self.maximumNumberOfTouches = 1 39 | } 40 | 41 | override func reset() { 42 | super.reset() 43 | 44 | validatedGesture = false 45 | } 46 | 47 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 48 | super.touchesBegan(touches, with: event) 49 | 50 | let touch = touches.first! 51 | self.firstLocation = touch.location(in: self.view) 52 | 53 | if let target = self.view?.hitTest(self.firstLocation, with: event) { 54 | if hasHorizontalGestures(target, point: self.view?.convert(self.firstLocation, to: target)) { 55 | self.state = .cancelled 56 | } 57 | } 58 | } 59 | 60 | override func touchesMoved(_ touches: Set, with event: UIEvent) { 61 | let location = touches.first!.location(in: self.view) 62 | let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y) 63 | 64 | let absTranslationX: CGFloat = abs(translation.x) 65 | let absTranslationY: CGFloat = abs(translation.y) 66 | 67 | if !validatedGesture { 68 | if self.firstLocation.x < 16.0 { 69 | validatedGesture = true 70 | } else if translation.x < 0.0 { 71 | self.state = .failed 72 | } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { 73 | self.state = .failed 74 | } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { 75 | validatedGesture = true 76 | } 77 | } 78 | 79 | if validatedGesture { 80 | super.touchesMoved(touches, with: event) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Display/KeyShortcut.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public struct KeyShortcut: Hashable { 4 | let title: String 5 | let input: String 6 | let modifiers: UIKeyModifierFlags 7 | let action: () -> Void 8 | 9 | public init(title: String = "", input: String = "", modifiers: UIKeyModifierFlags = [], action: @escaping () -> Void = {}) { 10 | self.title = title 11 | self.input = input 12 | self.modifiers = modifiers 13 | self.action = action 14 | } 15 | 16 | public var hashValue: Int { 17 | return input.hashValue ^ modifiers.hashValue 18 | } 19 | 20 | public static func ==(lhs: KeyShortcut, rhs: KeyShortcut) -> Bool { 21 | return lhs.hashValue == rhs.hashValue 22 | } 23 | } 24 | 25 | extension UIKeyModifierFlags: Hashable { 26 | public var hashValue: Int { 27 | return self.rawValue 28 | } 29 | } 30 | 31 | extension KeyShortcut { 32 | var uiKeyCommand: UIKeyCommand { 33 | if #available(iOSApplicationExtension 9.0, *), !self.title.isEmpty { 34 | return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title) 35 | } else { 36 | return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:))) 37 | } 38 | } 39 | 40 | func isEqual(to command: UIKeyCommand) -> Bool { 41 | return self.input == command.input && self.modifiers == command.modifierFlags 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Display/KeyShortcutsController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol KeyShortcutResponder { 4 | var keyShortcuts: [KeyShortcut] { get }; 5 | } 6 | 7 | public class KeyShortcutsController: UIResponder { 8 | private var effectiveShortcuts: [KeyShortcut]? 9 | private var viewControllerEnumerator: ((ContainableController) -> Bool) -> Void 10 | 11 | public static var isAvailable: Bool { 12 | if #available(iOSApplicationExtension 8.0, *), UIDevice.current.userInterfaceIdiom == .pad { 13 | return true 14 | } else { 15 | return false 16 | } 17 | } 18 | 19 | public init(enumerator: @escaping ((ContainableController) -> Bool) -> Void) { 20 | self.viewControllerEnumerator = enumerator 21 | super.init() 22 | } 23 | 24 | public override var keyCommands: [UIKeyCommand]? { 25 | var convertedCommands: [UIKeyCommand] = [] 26 | var shortcuts: [KeyShortcut] = [] 27 | 28 | self.viewControllerEnumerator({ viewController -> Bool in 29 | guard let viewController = viewController as? KeyShortcutResponder else { 30 | return true 31 | } 32 | shortcuts.append(contentsOf: viewController.keyShortcuts) 33 | return true 34 | }) 35 | 36 | // iOS 8 fix 37 | convertedCommands.append(KeyShortcut(modifiers:[.command]).uiKeyCommand) 38 | convertedCommands.append(KeyShortcut(modifiers:[.alternate]).uiKeyCommand) 39 | 40 | convertedCommands.append(contentsOf: shortcuts.map { $0.uiKeyCommand }) 41 | 42 | self.effectiveShortcuts = shortcuts 43 | 44 | return convertedCommands 45 | } 46 | 47 | @objc func handleKeyCommand(_ command: UIKeyCommand) { 48 | if let shortcut = findShortcut(for: command) { 49 | shortcut.action() 50 | } 51 | } 52 | 53 | private func findShortcut(for command: UIKeyCommand) -> KeyShortcut? { 54 | if let shortcuts = self.effectiveShortcuts { 55 | for shortcut in shortcuts { 56 | if shortcut.isEqual(to: command) { 57 | return shortcut 58 | } 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { 65 | if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) { 66 | return true 67 | } else { 68 | return super.canPerformAction(action, withSender: sender) 69 | } 70 | } 71 | 72 | public override func target(forAction action: Selector, withSender sender: Any?) -> Any? { 73 | if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) { 74 | return self 75 | } else { 76 | return super.target(forAction: action, withSender: sender) 77 | } 78 | } 79 | 80 | public override var canBecomeFirstResponder: Bool { 81 | return true 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Display/KeyboardManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | struct KeyboardSurface { 5 | let host: UIView 6 | } 7 | 8 | private func getFirstResponder(_ view: UIView) -> UIView? { 9 | if view.isFirstResponder { 10 | return view 11 | } else { 12 | for subview in view.subviews { 13 | if let result = getFirstResponder(subview) { 14 | return result 15 | } 16 | } 17 | return nil 18 | } 19 | } 20 | 21 | class KeyboardManager { 22 | private let host: StatusBarHost 23 | 24 | private weak var previousPositionAnimationMirrorSource: CATracingLayer? 25 | private weak var previousFirstResponderView: UIView? 26 | private var interactiveInputOffset: CGFloat = 0.0 27 | 28 | var surfaces: [KeyboardSurface] = [] { 29 | didSet { 30 | self.updateSurfaces(oldValue) 31 | } 32 | } 33 | 34 | init(host: StatusBarHost) { 35 | self.host = host 36 | } 37 | 38 | func getCurrentKeyboardHeight() -> CGFloat { 39 | guard let keyboardView = self.host.keyboardView else { 40 | return 0.0 41 | } 42 | return keyboardView.bounds.height 43 | } 44 | 45 | func updateInteractiveInputOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { 46 | guard let keyboardView = self.host.keyboardView else { 47 | return 48 | } 49 | 50 | self.interactiveInputOffset = offset 51 | 52 | let previousBounds = keyboardView.bounds 53 | let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: -offset), size: previousBounds.size) 54 | keyboardView.layer.bounds = updatedBounds 55 | if transition.isAnimated { 56 | transition.animateOffsetAdditive(layer: keyboardView.layer, offset: previousBounds.minY - updatedBounds.minY, completion: completion) 57 | } else { 58 | completion() 59 | } 60 | 61 | //transition.updateSublayerTransformOffset(layer: keyboardView.layer, offset: CGPoint(x: 0.0, y: offset)) 62 | } 63 | 64 | private func updateSurfaces(_ previousSurfaces: [KeyboardSurface]) { 65 | guard let keyboardWindow = self.host.keyboardWindow else { 66 | return 67 | } 68 | 69 | var firstResponderView: UIView? 70 | var firstResponderDisableAutomaticKeyboardHandling: UIResponderDisableAutomaticKeyboardHandling = [] 71 | for surface in self.surfaces { 72 | if let view = getFirstResponder(surface.host) { 73 | firstResponderView = surface.host 74 | firstResponderDisableAutomaticKeyboardHandling = view.disableAutomaticKeyboardHandling 75 | break 76 | } 77 | } 78 | 79 | if let firstResponderView = firstResponderView { 80 | let containerOrigin = firstResponderView.convert(CGPoint(), to: nil) 81 | var filteredTranslation = containerOrigin.x 82 | if firstResponderDisableAutomaticKeyboardHandling.contains(.forward) { 83 | filteredTranslation = max(0.0, filteredTranslation) 84 | } 85 | if firstResponderDisableAutomaticKeyboardHandling.contains(.backward) { 86 | filteredTranslation = min(0.0, filteredTranslation) 87 | } 88 | let horizontalTranslation = CATransform3DMakeTranslation(filteredTranslation, 0.0, 0.0) 89 | let currentTransform = keyboardWindow.layer.sublayerTransform 90 | if !CATransform3DEqualToTransform(horizontalTranslation, currentTransform) { 91 | //print("set to \(CGPoint(x: containerOrigin.x, y: self.interactiveInputOffset))") 92 | keyboardWindow.layer.sublayerTransform = horizontalTranslation 93 | } 94 | if let tracingLayer = firstResponderView.layer as? CATracingLayer, firstResponderDisableAutomaticKeyboardHandling.isEmpty { 95 | if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource, previousPositionAnimationMirrorSource !== tracingLayer { 96 | previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) 97 | } 98 | tracingLayer.setPositionAnimationMirrorTarget(keyboardWindow.layer) 99 | self.previousPositionAnimationMirrorSource = tracingLayer 100 | } else if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource { 101 | previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) 102 | self.previousPositionAnimationMirrorSource = nil 103 | } 104 | } else { 105 | keyboardWindow.layer.sublayerTransform = CATransform3DIdentity 106 | if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource { 107 | previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) 108 | self.previousPositionAnimationMirrorSource = nil 109 | } 110 | if let previousFirstResponderView = previousFirstResponderView { 111 | if previousFirstResponderView.window == nil { 112 | keyboardWindow.isHidden = true 113 | keyboardWindow.layer.cancelAnimationsRecursive(key: "position") 114 | keyboardWindow.layer.cancelAnimationsRecursive(key: "bounds") 115 | keyboardWindow.isHidden = false 116 | } 117 | } 118 | } 119 | 120 | self.previousFirstResponderView = firstResponderView 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Display/LayoutSizes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func horizontalContainerFillingSizeForLayout(layout: ContainerViewLayout, sideInset: CGFloat) -> CGFloat { 4 | if case .regular = layout.metrics.widthClass { 5 | return min(layout.size.width, 414.0) - sideInset * 2.0 6 | } else { 7 | return layout.size.width - sideInset * 2.0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Display/LegacyPresentedControllerNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | final class LegacyPresentedControllerNode: ASDisplayNode { 5 | private var containerLayout: ContainerViewLayout? 6 | 7 | var controllerView: UIView? { 8 | didSet { 9 | if let controllerView = self.controllerView, let containerLayout = self.containerLayout { 10 | controllerView.frame = CGRect(origin: CGPoint(), size: containerLayout.size) 11 | } 12 | } 13 | } 14 | 15 | override init() { 16 | super.init() 17 | 18 | self.setViewBlock({ 19 | return UITracingLayerView() 20 | }) 21 | } 22 | 23 | func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { 24 | self.containerLayout = layout 25 | if let controllerView = self.controllerView { 26 | controllerView.frame = CGRect(origin: CGPoint(), size: layout.size) 27 | } 28 | } 29 | 30 | func animateModalIn() { 31 | 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) 32 | } 33 | 34 | func animateModalOut(completion: @escaping () -> Void) { 35 | 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 36 | completion() 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Display/ListViewAccessoryItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ListViewAccessoryItem { 4 | func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool 5 | func node() -> ListViewAccessoryItemNode 6 | } 7 | -------------------------------------------------------------------------------- /Display/ListViewAccessoryItemNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(macOS) 3 | #else 4 | import AsyncDisplayKit 5 | #endif 6 | 7 | open class ListViewAccessoryItemNode: ASDisplayNode { 8 | var transitionOffset: CGPoint = CGPoint() { 9 | didSet { 10 | self.bounds = CGRect(origin: self.transitionOffset, size: self.bounds.size) 11 | } 12 | } 13 | 14 | private var transitionOffsetAnimation: ListViewAnimation? 15 | 16 | final func animateTransitionOffset(_ from: CGPoint, beginAt: Double, duration: Double, curve: @escaping (CGFloat) -> CGFloat) { 17 | self.transitionOffset = from 18 | self.transitionOffsetAnimation = ListViewAnimation(from: from, to: CGPoint(), duration: duration, curve: curve, beginAt: beginAt, update: { [weak self] _, currentValue in 19 | if let strongSelf = self { 20 | strongSelf.transitionOffset = currentValue 21 | } 22 | }) 23 | } 24 | 25 | final func removeAllAnimations() { 26 | self.transitionOffsetAnimation = nil 27 | self.transitionOffset = CGPoint() 28 | } 29 | 30 | final func animate(_ timestamp: Double) -> Bool { 31 | if let animation = self.transitionOffsetAnimation { 32 | animation.applyAt(timestamp) 33 | 34 | if animation.completeAt(timestamp) { 35 | self.transitionOffsetAnimation = nil 36 | } else { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | 44 | override open func layout() { 45 | super.layout() 46 | 47 | self.updateLayout(size: self.bounds.size, leftInset: 0.0, rightInset: 0.0) 48 | } 49 | 50 | open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Display/ListViewFloatingHeaderNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | open class ListViewFloatingHeaderNode: ASDisplayNode { 5 | open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { 6 | return 0.0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Display/ListViewItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(macOS) 3 | import SwiftSignalKitMac 4 | #else 5 | import SwiftSignalKit 6 | #endif 7 | 8 | public enum ListViewItemUpdateAnimation { 9 | case None 10 | case System(duration: Double) 11 | 12 | public var isAnimated: Bool { 13 | if case .None = self { 14 | return false 15 | } else { 16 | return true 17 | } 18 | } 19 | } 20 | 21 | public struct ListViewItemConfigureNodeFlags: OptionSet { 22 | public var rawValue: Int32 23 | 24 | public init() { 25 | self.rawValue = 0 26 | } 27 | 28 | public init(rawValue: Int32) { 29 | self.rawValue = rawValue 30 | } 31 | 32 | public static let preferSynchronousResourceLoading = ListViewItemConfigureNodeFlags(rawValue: 1 << 0) 33 | } 34 | 35 | public struct ListViewItemApply { 36 | public let isOnScreen: Bool 37 | 38 | public init(isOnScreen: Bool) { 39 | self.isOnScreen = isOnScreen 40 | } 41 | } 42 | 43 | public protocol ListViewItem { 44 | func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) 45 | 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) 46 | 47 | var accessoryItem: ListViewAccessoryItem? { get } 48 | var headerAccessoryItem: ListViewAccessoryItem? { get } 49 | var selectable: Bool { get } 50 | var approximateHeight: CGFloat { get } 51 | 52 | func selected(listView: ListView) 53 | } 54 | 55 | public extension ListViewItem { 56 | var accessoryItem: ListViewAccessoryItem? { 57 | return nil 58 | } 59 | 60 | var headerAccessoryItem: ListViewAccessoryItem? { 61 | return nil 62 | } 63 | 64 | var selectable: Bool { 65 | return false 66 | } 67 | 68 | var approximateHeight: CGFloat { 69 | return 44.0 70 | } 71 | 72 | func selected(listView: ListView) { 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Display/ListViewOverscrollBackgroundNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(macOS) 3 | #else 4 | import AsyncDisplayKit 5 | #endif 6 | 7 | final class ListViewOverscrollBackgroundNode: ASDisplayNode { 8 | private let backgroundNode: ASDisplayNode 9 | 10 | var color: UIColor { 11 | didSet { 12 | self.backgroundNode.backgroundColor = color 13 | } 14 | } 15 | 16 | init(color: UIColor) { 17 | self.color = color 18 | 19 | self.backgroundNode = ASDisplayNode() 20 | self.backgroundNode.backgroundColor = color 21 | self.backgroundNode.isLayerBacked = true 22 | 23 | super.init() 24 | 25 | self.addSubnode(self.backgroundNode) 26 | } 27 | 28 | func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { 29 | transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Display/ListViewReorderingGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | final class ListViewReorderingGestureRecognizer: UIGestureRecognizer { 5 | private let shouldBegin: (CGPoint) -> Bool 6 | private let ended: () -> Void 7 | private let moved: (CGFloat) -> Void 8 | 9 | private var initialLocation: CGPoint? 10 | 11 | init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) { 12 | self.shouldBegin = shouldBegin 13 | self.ended = ended 14 | self.moved = moved 15 | 16 | super.init(target: nil, action: nil) 17 | } 18 | 19 | override func reset() { 20 | super.reset() 21 | 22 | self.initialLocation = nil 23 | } 24 | 25 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 26 | super.touchesBegan(touches, with: event) 27 | 28 | if self.state == .possible { 29 | if let location = touches.first?.location(in: self.view), self.shouldBegin(location) { 30 | self.initialLocation = location 31 | self.state = .began 32 | } else { 33 | self.state = .failed 34 | } 35 | } 36 | } 37 | 38 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 39 | super.touchesEnded(touches, with: event) 40 | 41 | if self.state == .began || self.state == .changed { 42 | self.ended() 43 | self.state = .failed 44 | } 45 | } 46 | 47 | override func touchesCancelled(_ touches: Set, with event: UIEvent) { 48 | super.touchesCancelled(touches, with: event) 49 | 50 | if self.state == .began || self.state == .changed { 51 | self.ended() 52 | self.state = .failed 53 | } 54 | } 55 | 56 | override func touchesMoved(_ touches: Set, with event: UIEvent) { 57 | super.touchesMoved(touches, with: event) 58 | 59 | if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { 60 | self.state = .changed 61 | let offset = location.y - initialLocation.y 62 | self.moved(offset) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Display/ListViewReorderingItemNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | final class ListViewReorderingItemNode: ASDisplayNode { 5 | weak var itemNode: ListViewItemNode? 6 | 7 | var currentState: (Int, Int)? 8 | 9 | private let copyView: UIView? 10 | private let initialLocation: CGPoint 11 | 12 | init(itemNode: ListViewItemNode, initialLocation: CGPoint) { 13 | self.itemNode = itemNode 14 | self.copyView = itemNode.view.snapshotView(afterScreenUpdates: false) 15 | self.initialLocation = initialLocation 16 | 17 | super.init() 18 | 19 | if let copyView = self.copyView { 20 | self.view.addSubview(copyView) 21 | copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: copyView.bounds.size) 22 | copyView.bounds = itemNode.bounds 23 | } 24 | } 25 | 26 | func updateOffset(offset: CGFloat) { 27 | if let copyView = self.copyView { 28 | copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size) 29 | } 30 | } 31 | 32 | func currentOffset() -> CGFloat? { 33 | if let copyView = self.copyView { 34 | return copyView.center.y 35 | } 36 | return nil 37 | } 38 | 39 | func animateCompletion(completion: @escaping () -> Void) { 40 | if let copyView = self.copyView, let itemNode = self.itemNode { 41 | itemNode.isHidden = false 42 | itemNode.transitionOffset = itemNode.apparentFrame.midY - copyView.frame.midY 43 | itemNode.addTransitionOffsetAnimation(0.0, duration: 0.2, beginAt: CACurrentMediaTime()) 44 | completion() 45 | } else { 46 | completion() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Display/ListViewScroller.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | #else 3 | import UIKit 4 | #endif 5 | 6 | class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { 7 | override init(frame: CGRect) { 8 | super.init(frame: frame) 9 | 10 | #if os(iOS) 11 | self.scrollsToTop = false 12 | if #available(iOSApplicationExtension 11.0, *) { 13 | self.contentInsetAdjustmentBehavior = .never 14 | } 15 | #endif 16 | } 17 | 18 | required init?(coder aDecoder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 23 | if otherGestureRecognizer is ListViewTapGestureRecognizer { 24 | return true 25 | } 26 | return false 27 | } 28 | 29 | #if os(iOS) 30 | override func touchesShouldCancel(in view: UIView) -> Bool { 31 | return true 32 | } 33 | #endif 34 | } 35 | -------------------------------------------------------------------------------- /Display/ListViewTapGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public final class ListViewTapGestureRecognizer: UITapGestureRecognizer { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /Display/ListViewTempItemNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class ListViewTempItemNode: ListViewItemNode { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /Display/ListViewTransactionQueue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(iOS) 3 | import SwiftSignalKit 4 | #else 5 | import SwiftSignalKitMac 6 | #endif 7 | 8 | public typealias ListViewTransaction = (@escaping () -> Void) -> Void 9 | 10 | public final class ListViewTransactionQueue { 11 | private var transactions: [ListViewTransaction] = [] 12 | public final var transactionCompleted: () -> Void = { } 13 | 14 | public init() { 15 | } 16 | 17 | public func addTransaction(_ transaction: @escaping ListViewTransaction) { 18 | precondition(Thread.isMainThread) 19 | let beginTransaction = self.transactions.count == 0 20 | self.transactions.append(transaction) 21 | 22 | if beginTransaction { 23 | transaction({ [weak self] in 24 | precondition(Thread.isMainThread) 25 | 26 | if Thread.isMainThread { 27 | if let strongSelf = self { 28 | strongSelf.endTransaction() 29 | } 30 | } else { 31 | Queue.mainQueue().async { 32 | if let strongSelf = self { 33 | strongSelf.endTransaction() 34 | } 35 | } 36 | } 37 | }) 38 | } 39 | } 40 | 41 | private func endTransaction() { 42 | precondition(Thread.isMainThread) 43 | Queue.mainQueue().async { 44 | self.transactionCompleted() 45 | if !self.transactions.isEmpty { 46 | let _ = self.transactions.removeFirst() 47 | } 48 | 49 | if let nextTransaction = self.transactions.first { 50 | nextTransaction({ [weak self] in 51 | precondition(Thread.isMainThread) 52 | 53 | if Thread.isMainThread { 54 | if let strongSelf = self { 55 | strongSelf.endTransaction() 56 | } 57 | } else { 58 | Queue.mainQueue().async { 59 | if let strongSelf = self { 60 | strongSelf.endTransaction() 61 | } 62 | } 63 | } 64 | }) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Display/MinimizeKeyboardGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate { 5 | override init(target: Any?, action: Selector?) { 6 | super.init(target: target, action: action) 7 | 8 | self.cancelsTouchesInView = false 9 | self.delaysTouchesBegan = false 10 | self.delaysTouchesEnded = false 11 | self.delegate = self 12 | 13 | self.direction = [.left, .right] 14 | self.numberOfTouchesRequired = 2 15 | } 16 | 17 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 18 | return true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Display/NSBag.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSBag : NSObject 4 | 5 | - (NSInteger)addItem:(id)item; 6 | - (void)enumerateItems:(void (^)(id))block; 7 | - (void)removeItem:(NSInteger)key; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Display/NSBag.m: -------------------------------------------------------------------------------- 1 | #import "NSBag.h" 2 | 3 | @interface NSBag () 4 | { 5 | NSInteger _nextKey; 6 | NSMutableArray *_items; 7 | NSMutableArray *_itemKeys; 8 | } 9 | 10 | @end 11 | 12 | @implementation NSBag 13 | 14 | - (instancetype)init 15 | { 16 | self = [super init]; 17 | if (self != nil) 18 | { 19 | _items = [[NSMutableArray alloc] init]; 20 | _itemKeys = [[NSMutableArray alloc] init]; 21 | } 22 | return self; 23 | } 24 | 25 | - (NSInteger)addItem:(id)item 26 | { 27 | if (item == nil) 28 | return -1; 29 | 30 | NSInteger key = _nextKey; 31 | [_items addObject:item]; 32 | [_itemKeys addObject:@(key)]; 33 | _nextKey++; 34 | 35 | return key; 36 | } 37 | 38 | - (void)enumerateItems:(void (^)(id))block 39 | { 40 | if (block) 41 | { 42 | for (id item in _items) 43 | { 44 | block(item); 45 | } 46 | } 47 | } 48 | 49 | - (void)removeItem:(NSInteger)key 50 | { 51 | NSUInteger index = 0; 52 | for (NSNumber *itemKey in _itemKeys) 53 | { 54 | if ([itemKey integerValue] == key) 55 | { 56 | [_items removeObjectAtIndex:index]; 57 | [_itemKeys removeObjectAtIndex:index]; 58 | break; 59 | } 60 | index++; 61 | } 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /Display/NSWeakReference.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSWeakReference : NSObject 4 | 5 | @property (nonatomic, weak) id value; 6 | 7 | - (instancetype)initWithValue:(id)value; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Display/NSWeakReference.m: -------------------------------------------------------------------------------- 1 | #import "NSWeakReference.h" 2 | 3 | @implementation NSWeakReference 4 | 5 | - (instancetype)initWithValue:(id)value { 6 | self = [super init]; 7 | if (self != nil) { 8 | self.value = value; 9 | } 10 | return self; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Display/NavigationBackArrowLight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peter-iakovlev/Display/ffb131ac55bed21c37052a09c27150d5d380321e/Display/NavigationBackArrowLight@2x.png -------------------------------------------------------------------------------- /Display/NavigationBarBadge.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | final class NavigationBarBadgeNode: ASDisplayNode { 5 | private var fillColor: UIColor 6 | private var strokeColor: UIColor 7 | private var textColor: UIColor 8 | 9 | private let textNode: ASTextNode 10 | private let backgroundNode: ASImageNode 11 | 12 | private let font: UIFont = Font.regular(13.0) 13 | 14 | var text: String = "" { 15 | didSet { 16 | self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) 17 | self.invalidateCalculatedLayout() 18 | } 19 | } 20 | 21 | init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { 22 | self.fillColor = fillColor 23 | self.strokeColor = strokeColor 24 | self.textColor = textColor 25 | 26 | self.textNode = ASTextNode() 27 | self.textNode.isUserInteractionEnabled = false 28 | self.textNode.displaysAsynchronously = false 29 | 30 | self.backgroundNode = ASImageNode() 31 | self.backgroundNode.isLayerBacked = true 32 | self.backgroundNode.displayWithoutProcessing = true 33 | self.backgroundNode.displaysAsynchronously = false 34 | self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) 35 | 36 | super.init() 37 | 38 | self.addSubnode(self.backgroundNode) 39 | self.addSubnode(self.textNode) 40 | } 41 | 42 | func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { 43 | self.fillColor = fillColor 44 | self.strokeColor = strokeColor 45 | self.textColor = textColor 46 | self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) 47 | self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) 48 | } 49 | 50 | override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 51 | let badgeSize = self.textNode.measure(constrainedSize) 52 | let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) 53 | let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) 54 | self.backgroundNode.frame = backgroundFrame 55 | 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) 56 | 57 | return backgroundSize 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Display/NavigationBarContentNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public enum NavigationBarContentMode { 5 | case replacement 6 | case expansion 7 | } 8 | 9 | open class NavigationBarContentNode: ASDisplayNode { 10 | open var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in } 11 | 12 | open var height: CGFloat { 13 | return self.nominalHeight 14 | } 15 | 16 | open var clippedHeight: CGFloat { 17 | return self.nominalHeight 18 | } 19 | 20 | open var nominalHeight: CGFloat { 21 | return 44.0 22 | } 23 | 24 | open var mode: NavigationBarContentMode { 25 | return .replacement 26 | } 27 | 28 | open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Display/NavigationBarProxy.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NavigationBarProxy : UINavigationBar 4 | 5 | @property (nonatomic, copy) void (^setItemsProxy)(NSArray *, NSArray *, bool); 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Display/NavigationBarProxy.m: -------------------------------------------------------------------------------- 1 | #import "NavigationBarProxy.h" 2 | 3 | @interface NavigationBarProxy () 4 | { 5 | NSArray *_items; 6 | } 7 | 8 | @end 9 | 10 | @implementation NavigationBarProxy 11 | 12 | - (instancetype)initWithFrame:(CGRect)frame 13 | { 14 | self = [super initWithFrame:frame]; 15 | if (self != nil) 16 | { 17 | } 18 | return self; 19 | } 20 | 21 | - (void)pushNavigationItem:(UINavigationItem *)item animated:(BOOL)animated 22 | { 23 | [self setItems:[[self items] arrayByAddingObject:item] animated:animated]; 24 | } 25 | 26 | - (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated 27 | { 28 | NSMutableArray *items = [[NSMutableArray alloc] initWithArray:[self items]]; 29 | UINavigationItem *lastItem = [items lastObject]; 30 | [items removeLastObject]; 31 | [self setItems:items animated:animated]; 32 | return lastItem; 33 | } 34 | 35 | - (UINavigationItem *)topItem 36 | { 37 | return [[self items] lastObject]; 38 | } 39 | 40 | - (UINavigationItem *)backItem 41 | { 42 | NSLog(@"backItem"); 43 | return nil; 44 | } 45 | 46 | - (NSArray *)items 47 | { 48 | if (_items == nil) 49 | return @[]; 50 | return _items; 51 | } 52 | 53 | - (void)setItems:(NSArray *)items 54 | { 55 | [self setItems:items animated:false]; 56 | } 57 | 58 | - (void)setItems:(NSArray *)items animated:(BOOL)animated 59 | { 60 | NSArray *previousItems = _items; 61 | _items = items; 62 | 63 | if (_setItemsProxy) 64 | _setItemsProxy(previousItems, items, animated); 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /Display/NavigationBarTitleTransitionNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public protocol NavigationBarTitleTransitionNode { 5 | func makeTransitionMirrorNode() -> ASDisplayNode 6 | } 7 | -------------------------------------------------------------------------------- /Display/NavigationBarTitleView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public protocol NavigationBarTitleView { 5 | func animateLayoutTransition() 6 | 7 | func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) 8 | } 9 | -------------------------------------------------------------------------------- /Display/NavigationBarTransitionContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | class NavigationBarTransitionContainer: ASDisplayNode { 5 | var progress: CGFloat = 0.0 { 6 | didSet { 7 | self.layout() 8 | } 9 | } 10 | 11 | let transition: NavigationTransition 12 | let topNavigationBar: NavigationBar 13 | let bottomNavigationBar: NavigationBar 14 | 15 | let topClippingNode: ASDisplayNode 16 | let bottomClippingNode: ASDisplayNode 17 | 18 | let topNavigationBarSupernode: ASDisplayNode? 19 | let bottomNavigationBarSupernode: ASDisplayNode? 20 | 21 | init(transition: NavigationTransition, topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) { 22 | self.transition = transition 23 | 24 | self.topNavigationBar = topNavigationBar 25 | self.topNavigationBarSupernode = topNavigationBar.supernode 26 | 27 | self.bottomNavigationBar = bottomNavigationBar 28 | self.bottomNavigationBarSupernode = bottomNavigationBar.supernode 29 | 30 | self.topClippingNode = ASDisplayNode() 31 | self.topClippingNode.clipsToBounds = true 32 | self.bottomClippingNode = ASDisplayNode() 33 | self.bottomClippingNode.clipsToBounds = true 34 | 35 | super.init() 36 | 37 | self.topClippingNode.addSubnode(self.topNavigationBar) 38 | self.bottomClippingNode.addSubnode(self.bottomNavigationBar) 39 | 40 | self.addSubnode(self.bottomClippingNode) 41 | self.addSubnode(self.topClippingNode) 42 | } 43 | 44 | func complete() { 45 | self.topNavigationBarSupernode?.addSubnode(self.topNavigationBar) 46 | self.bottomNavigationBarSupernode?.addSubnode(self.bottomNavigationBar) 47 | } 48 | 49 | override func layout() { 50 | super.layout() 51 | 52 | let size = self.bounds.size 53 | 54 | let position: CGFloat 55 | switch self.transition { 56 | case .Push: 57 | position = 1.0 - progress 58 | case .Pop: 59 | position = progress 60 | } 61 | 62 | let offset = floorToScreenPixels(size.width * position) 63 | 64 | self.topClippingNode.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: size) 65 | 66 | self.bottomClippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: offset, height: size.height)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Display/NavigationBarTransitionState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum NavigationBarTransitionRole { 4 | case top 5 | case bottom 6 | } 7 | 8 | final class NavigationBarTransitionState { 9 | weak var navigationBar: NavigationBar? 10 | let transition: NavigationTransition 11 | let role: NavigationBarTransitionRole 12 | let progress: CGFloat 13 | 14 | init(navigationBar: NavigationBar, transition: NavigationTransition, role: NavigationBarTransitionRole, progress: CGFloat) { 15 | self.navigationBar = navigationBar 16 | self.transition = transition 17 | self.role = role 18 | self.progress = progress 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Display/NavigationControllerProxy.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NavigationControllerProxy : UINavigationController 4 | 5 | - (instancetype)init; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Display/NavigationControllerProxy.m: -------------------------------------------------------------------------------- 1 | #import "NavigationControllerProxy.h" 2 | 3 | #import "NavigationBarProxy.h" 4 | 5 | @implementation NavigationControllerProxy 6 | 7 | - (instancetype)init 8 | { 9 | self = [super initWithNavigationBarClass:[NavigationBarProxy class] toolbarClass:[UIToolbar class]]; 10 | if (self != nil) { 11 | } 12 | return self; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Display/NavigationShadow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peter-iakovlev/Display/ffb131ac55bed21c37052a09c27150d5d380321e/Display/NavigationShadow@2x.png -------------------------------------------------------------------------------- /Display/NavigationTitleNode.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AsyncDisplayKit 3 | 4 | public class NavigationTitleNode: ASDisplayNode { 5 | private let label: ASTextNode 6 | 7 | private var _text: NSString = "" 8 | public var text: NSString { 9 | get { 10 | return self._text 11 | } 12 | set(value) { 13 | self._text = value 14 | self.setText(value) 15 | } 16 | } 17 | 18 | public var color: UIColor = UIColor.black { 19 | didSet { 20 | self.setText(self._text) 21 | } 22 | } 23 | 24 | public init(text: NSString) { 25 | self.label = ASTextNode() 26 | self.label.maximumNumberOfLines = 1 27 | self.label.truncationMode = .byTruncatingTail 28 | self.label.displaysAsynchronously = false 29 | 30 | super.init() 31 | 32 | self.addSubnode(self.label) 33 | 34 | self.setText(text) 35 | } 36 | 37 | public required init(coder aDecoder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | private func setText(_ text: NSString) { 42 | var titleAttributes = [NSAttributedStringKey : AnyObject]() 43 | titleAttributes[NSAttributedStringKey.font] = UIFont.boldSystemFont(ofSize: 17.0) 44 | titleAttributes[NSAttributedStringKey.foregroundColor] = self.color 45 | let titleString = NSAttributedString(string: text as String, attributes: titleAttributes) 46 | self.label.attributedText = titleString 47 | self.invalidateCalculatedLayout() 48 | } 49 | 50 | public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 51 | self.label.measure(constrainedSize) 52 | return self.label.calculatedSize 53 | } 54 | 55 | public override func layout() { 56 | self.label.frame = self.bounds 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Display/NotificationCenterUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef bool (^NotificationHandlerBlock)(NSString *, id, NSDictionary *, void (^)()); 4 | 5 | @interface NotificationCenterUtils : NSObject 6 | 7 | + (void)addNotificationHandler:(NotificationHandlerBlock)handler; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Display/NotificationCenterUtils.m: -------------------------------------------------------------------------------- 1 | #import "NotificationCenterUtils.h" 2 | 3 | #import "RuntimeUtils.h" 4 | #import 5 | 6 | static NSMutableArray *notificationHandlers() { 7 | static NSMutableArray *array = nil; 8 | static dispatch_once_t onceToken; 9 | dispatch_once(&onceToken, ^{ 10 | array = [[NSMutableArray alloc] init]; 11 | }); 12 | return array; 13 | } 14 | 15 | @interface NSNotificationCenter (_a65afc19) 16 | 17 | @end 18 | 19 | @implementation NSNotificationCenter (_a65afc19) 20 | 21 | - (void)_a65afc19_postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo 22 | { 23 | if ([NSThread isMainThread]) { 24 | for (NotificationHandlerBlock handler in notificationHandlers()) 25 | { 26 | if (handler(aName, anObject, aUserInfo, ^{ 27 | [self _a65afc19_postNotificationName:aName object:anObject userInfo:aUserInfo]; 28 | })) { 29 | return; 30 | } 31 | } 32 | } 33 | 34 | [self _a65afc19_postNotificationName:aName object:anObject userInfo:aUserInfo]; 35 | } 36 | 37 | @end 38 | 39 | @interface CATransaction (Swizzle) 40 | 41 | + (void)swizzle_flush; 42 | 43 | @end 44 | 45 | @implementation CATransaction (Swizzle) 46 | 47 | + (void)swizzle_flush { 48 | //printf("===flush\n"); 49 | 50 | [self swizzle_flush]; 51 | } 52 | 53 | @end 54 | 55 | @implementation NotificationCenterUtils 56 | 57 | + (void)load { 58 | static dispatch_once_t onceToken; 59 | dispatch_once(&onceToken, ^{ 60 | [RuntimeUtils swizzleInstanceMethodOfClass:[NSNotificationCenter class] currentSelector:@selector(postNotificationName:object:userInfo:) newSelector:@selector(_a65afc19_postNotificationName:object:userInfo:)]; 61 | 62 | //[RuntimeUtils swizzleClassMethodOfClass:[CATransaction class] currentSelector:@selector(flush) newSelector:@selector(swizzle_flush)]; 63 | }); 64 | } 65 | 66 | + (void)addNotificationHandler:(NotificationHandlerBlock)handler { 67 | [notificationHandlers() addObject:[handler copy]]; 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /Display/PageControlNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public final class PageControlNode: ASDisplayNode { 5 | private let dotSize: CGFloat 6 | private let dotSpacing: CGFloat 7 | public var dotColor: UIColor { 8 | didSet { 9 | if !oldValue.isEqual(self.dotColor) { 10 | self.normalDotImage = generateFilledCircleImage(diameter: dotSize, color: self.dotColor)! 11 | for dotNode in self.dotNodes { 12 | if dotNode === oldValue { 13 | dotNode.image = self.normalDotImage 14 | } 15 | } 16 | } 17 | } 18 | } 19 | public var inactiveDotColor: UIColor { 20 | didSet { 21 | if !oldValue.isEqual(self.inactiveDotColor) { 22 | self.inactiveDotImage = generateFilledCircleImage(diameter: dotSize, color: self.inactiveDotColor)! 23 | for dotNode in self.dotNodes { 24 | if dotNode === oldValue { 25 | dotNode.image = self.inactiveDotImage 26 | } 27 | } 28 | } 29 | } 30 | } 31 | private var dotNodes: [ASImageNode] = [] 32 | 33 | private var normalDotImage: UIImage 34 | private var inactiveDotImage: UIImage 35 | 36 | public init(dotSize: CGFloat = 7.0, dotSpacing: CGFloat = 9.0, dotColor: UIColor, inactiveDotColor: UIColor) { 37 | self.dotSize = dotSize 38 | self.dotSpacing = dotSpacing 39 | self.dotColor = dotColor 40 | self.inactiveDotColor = inactiveDotColor 41 | self.normalDotImage = generateFilledCircleImage(diameter: dotSize, color: dotColor)! 42 | self.inactiveDotImage = generateFilledCircleImage(diameter: dotSize, color: inactiveDotColor)! 43 | 44 | super.init() 45 | } 46 | 47 | public var pagesCount: Int = 0 { 48 | didSet { 49 | if self.pagesCount != oldValue { 50 | while self.dotNodes.count > self.pagesCount { 51 | self.dotNodes[self.dotNodes.count - 1].removeFromSupernode() 52 | self.dotNodes.removeLast() 53 | } 54 | while self.dotNodes.count < self.pagesCount { 55 | let dotNode = ASImageNode() 56 | dotNode.image = self.normalDotImage 57 | dotNode.displaysAsynchronously = false 58 | dotNode.displayWithoutProcessing = true 59 | dotNode.isUserInteractionEnabled = false 60 | self.dotNodes.append(dotNode) 61 | self.addSubnode(dotNode) 62 | } 63 | } 64 | } 65 | } 66 | 67 | public func setPage(_ pageValue: CGFloat) { 68 | let page = Int(round(pageValue)) 69 | 70 | for i in 0 ..< self.dotNodes.count { 71 | if i != page { 72 | self.dotNodes[i].image = self.inactiveDotImage 73 | } else { 74 | self.dotNodes[i].image = self.normalDotImage 75 | } 76 | } 77 | } 78 | 79 | override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 80 | return CGSize(width: self.dotSize * CGFloat(self.pagesCount) + self.dotSpacing * max(CGFloat(self.pagesCount - 1), 0.0), height: self.dotSize) 81 | } 82 | 83 | override public func layout() { 84 | super.layout() 85 | 86 | let dotSize = CGSize(width: self.dotSize, height: self.dotSize) 87 | 88 | let nominalWidth = self.dotSize * CGFloat(self.pagesCount) + self.dotSpacing * max(CGFloat(self.pagesCount - 1), 0.0) 89 | 90 | let startX = floor((self.bounds.size.width - nominalWidth) / 2) 91 | 92 | for i in 0 ..< self.dotNodes.count { 93 | self.dotNodes[i].frame = CGRect(origin: CGPoint(x: startX + CGFloat(i) * (dotSize.width + self.dotSpacing), y: 0.0), size: dotSize) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Display/PeekController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public final class PeekControllerTheme { 5 | public let isDark: Bool 6 | public let menuBackgroundColor: UIColor 7 | public let menuItemHighligtedColor: UIColor 8 | public let menuItemSeparatorColor: UIColor 9 | public let accentColor: UIColor 10 | public let destructiveColor: UIColor 11 | 12 | public init(isDark: Bool, menuBackgroundColor: UIColor, menuItemHighligtedColor: UIColor, menuItemSeparatorColor: UIColor, accentColor: UIColor, destructiveColor: UIColor) { 13 | self.isDark = isDark 14 | self.menuBackgroundColor = menuBackgroundColor 15 | self.menuItemHighligtedColor = menuItemHighligtedColor 16 | self.menuItemSeparatorColor = menuItemSeparatorColor 17 | self.accentColor = accentColor 18 | self.destructiveColor = destructiveColor 19 | } 20 | } 21 | 22 | public final class PeekController: ViewController { 23 | private var controllerNode: PeekControllerNode { 24 | return self.displayNode as! PeekControllerNode 25 | } 26 | 27 | private let theme: PeekControllerTheme 28 | private let content: PeekControllerContent 29 | var sourceNode: () -> ASDisplayNode? 30 | 31 | private var animatedIn = false 32 | 33 | public init(theme: PeekControllerTheme, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) { 34 | self.theme = theme 35 | self.content = content 36 | self.sourceNode = sourceNode 37 | 38 | super.init(navigationBarPresentationData: nil) 39 | } 40 | 41 | required public init(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | override public func loadDisplayNode() { 46 | self.displayNode = PeekControllerNode(theme: self.theme, content: self.content, requestDismiss: { [weak self] in 47 | self?.dismiss() 48 | }) 49 | self.displayNodeDidLoad() 50 | } 51 | 52 | private func getSourceRect() -> CGRect { 53 | if let sourceNode = self.sourceNode() { 54 | return sourceNode.view.convert(sourceNode.bounds, to: self.view) 55 | } else { 56 | let size = self.displayNode.bounds.size 57 | 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)) 58 | } 59 | } 60 | 61 | override public func viewDidAppear(_ animated: Bool) { 62 | super.viewDidAppear(animated) 63 | 64 | if !self.animatedIn { 65 | self.animatedIn = true 66 | self.controllerNode.animateIn(from: self.getSourceRect()) 67 | } 68 | } 69 | 70 | override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { 71 | super.containerLayoutUpdated(layout, transition: transition) 72 | 73 | self.controllerNode.containerLayoutUpdated(layout, transition: transition) 74 | } 75 | 76 | override public func dismiss(completion: (() -> Void)? = nil) { 77 | self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in 78 | self?.presentingViewController?.dismiss(animated: false, completion: nil) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Display/PeekControllerContent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public enum PeekControllerContentPresentation { 5 | case contained 6 | case freeform 7 | } 8 | 9 | public enum PeerkControllerMenuActivation { 10 | case drag 11 | case press 12 | } 13 | 14 | public protocol PeekControllerContent { 15 | func presentation() -> PeekControllerContentPresentation 16 | func menuActivation() -> PeerkControllerMenuActivation 17 | func menuItems() -> [PeekControllerMenuItem] 18 | func node() -> PeekControllerContentNode & ASDisplayNode 19 | 20 | func topAccessoryNode() -> ASDisplayNode? 21 | 22 | func isEqual(to: PeekControllerContent) -> Bool 23 | } 24 | 25 | public protocol PeekControllerContentNode { 26 | func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize 27 | } 28 | -------------------------------------------------------------------------------- /Display/PeekControllerMenuItemNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public enum PeekControllerMenuItemColor { 5 | case accent 6 | case destructive 7 | } 8 | 9 | public enum PeekControllerMenuItemFont { 10 | case `default` 11 | case bold 12 | } 13 | 14 | public struct PeekControllerMenuItem { 15 | public let title: String 16 | public let color: PeekControllerMenuItemColor 17 | public let font: PeekControllerMenuItemFont 18 | public let action: () -> Void 19 | 20 | public init(title: String, color: PeekControllerMenuItemColor, font: PeekControllerMenuItemFont = .default, action: @escaping () -> Void) { 21 | self.title = title 22 | self.color = color 23 | self.font = font 24 | self.action = action 25 | } 26 | } 27 | 28 | final class PeekControllerMenuItemNode: HighlightTrackingButtonNode { 29 | private let item: PeekControllerMenuItem 30 | private let activatedAction: () -> Void 31 | 32 | private let separatorNode: ASDisplayNode 33 | private let highlightedBackgroundNode: ASDisplayNode 34 | private let textNode: ASTextNode 35 | 36 | init(theme: PeekControllerTheme, item: PeekControllerMenuItem, activatedAction: @escaping () -> Void) { 37 | self.item = item 38 | self.activatedAction = activatedAction 39 | 40 | self.separatorNode = ASDisplayNode() 41 | self.separatorNode.isLayerBacked = true 42 | self.separatorNode.backgroundColor = theme.menuItemSeparatorColor 43 | 44 | self.highlightedBackgroundNode = ASDisplayNode() 45 | self.highlightedBackgroundNode.isLayerBacked = true 46 | self.highlightedBackgroundNode.backgroundColor = theme.menuItemHighligtedColor 47 | self.highlightedBackgroundNode.alpha = 0.0 48 | 49 | self.textNode = ASTextNode() 50 | self.textNode.isUserInteractionEnabled = false 51 | self.textNode.displaysAsynchronously = false 52 | 53 | let textColor: UIColor 54 | let textFont: UIFont 55 | switch item.color { 56 | case .accent: 57 | textColor = theme.accentColor 58 | case .destructive: 59 | textColor = theme.destructiveColor 60 | } 61 | switch item.font { 62 | case .default: 63 | textFont = Font.regular(20.0) 64 | case .bold: 65 | textFont = Font.medium(20.0) 66 | } 67 | self.textNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) 68 | 69 | super.init() 70 | 71 | self.addSubnode(self.separatorNode) 72 | self.addSubnode(self.highlightedBackgroundNode) 73 | self.addSubnode(self.textNode) 74 | 75 | self.highligthedChanged = { [weak self] highlighted in 76 | if let strongSelf = self { 77 | if highlighted { 78 | strongSelf.view.superview?.bringSubview(toFront: strongSelf.view) 79 | strongSelf.highlightedBackgroundNode.alpha = 1.0 80 | } else { 81 | strongSelf.highlightedBackgroundNode.alpha = 0.0 82 | strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) 83 | } 84 | } 85 | } 86 | 87 | self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) 88 | } 89 | 90 | func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { 91 | let height: CGFloat = 57.0 92 | transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) 93 | transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: width, height: UIScreenPixel))) 94 | 95 | let textSize = self.textNode.measure(CGSize(width: width - 10.0, height: height)) 96 | 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)) 97 | 98 | return height 99 | } 100 | 101 | @objc func buttonPressed() { 102 | self.activatedAction() 103 | self.item.action() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Display/PeekControllerMenuNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | final class PeekControllerMenuNode: ASDisplayNode { 5 | private let itemNodes: [PeekControllerMenuItemNode] 6 | 7 | init(theme: PeekControllerTheme, items: [PeekControllerMenuItem], activatedAction: @escaping () -> Void) { 8 | self.itemNodes = items.map { PeekControllerMenuItemNode(theme: theme, item: $0, activatedAction: activatedAction) } 9 | 10 | super.init() 11 | 12 | self.backgroundColor = theme.menuBackgroundColor 13 | self.cornerRadius = 16.0 14 | self.clipsToBounds = true 15 | 16 | for itemNode in self.itemNodes { 17 | self.addSubnode(itemNode) 18 | } 19 | } 20 | 21 | func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { 22 | var verticalOffset: CGFloat = 0.0 23 | for itemNode in self.itemNodes { 24 | let itemHeight = itemNode.updateLayout(width: width, transition: transition) 25 | transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: width, height: itemHeight))) 26 | verticalOffset += itemHeight 27 | } 28 | return verticalOffset - UIScreenPixel 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Display/RuntimeUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef enum { 4 | NSObjectAssociationPolicyRetain = 0, 5 | NSObjectAssociationPolicyCopy = 1 6 | } NSObjectAssociationPolicy; 7 | 8 | @interface RuntimeUtils : NSObject 9 | 10 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector; 11 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector withAnotherClass:(Class)anotherClass newSelector:(SEL)newSelector; 12 | + (void)swizzleClassMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector; 13 | 14 | @end 15 | 16 | @interface NSObject (AssociatedObject) 17 | 18 | - (void)setAssociatedObject:(id)object forKey:(void const *)key; 19 | - (void)setAssociatedObject:(id)object forKey:(void const *)key associationPolicy:(NSObjectAssociationPolicy)associationPolicy; 20 | - (id)associatedObjectForKey:(void const *)key; 21 | - (bool)checkObjectIsKindOfClass:(Class)targetClass; 22 | - (void)setClass:(Class)newClass; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Display/RuntimeUtils.m: -------------------------------------------------------------------------------- 1 | #import "RuntimeUtils.h" 2 | 3 | #import 4 | 5 | @implementation RuntimeUtils 6 | 7 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector { 8 | Method origMethod = nil, newMethod = nil; 9 | 10 | origMethod = class_getInstanceMethod(targetClass, currentSelector); 11 | newMethod = class_getInstanceMethod(targetClass, newSelector); 12 | if ((origMethod != nil) && (newMethod != nil)) { 13 | if(class_addMethod(targetClass, currentSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { 14 | class_replaceMethod(targetClass, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); 15 | } else { 16 | method_exchangeImplementations(origMethod, newMethod); 17 | } 18 | } 19 | } 20 | 21 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector withAnotherClass:(Class)anotherClass newSelector:(SEL)newSelector { 22 | Method origMethod = nil, newMethod = nil; 23 | 24 | origMethod = class_getInstanceMethod(targetClass, currentSelector); 25 | newMethod = class_getInstanceMethod(anotherClass, newSelector); 26 | if ((origMethod != nil) && (newMethod != nil)) { 27 | method_exchangeImplementations(origMethod, newMethod); 28 | } 29 | } 30 | 31 | + (void)swizzleClassMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector { 32 | Method origMethod = nil, newMethod = nil; 33 | 34 | origMethod = class_getClassMethod(targetClass, currentSelector); 35 | newMethod = class_getClassMethod(targetClass, newSelector); 36 | 37 | targetClass = object_getClass((id)targetClass); 38 | 39 | if ((origMethod != nil) && (newMethod != nil)) { 40 | if(class_addMethod(targetClass, currentSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { 41 | class_replaceMethod(targetClass, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); 42 | } else { 43 | method_exchangeImplementations(origMethod, newMethod); 44 | } 45 | } 46 | } 47 | 48 | @end 49 | 50 | @implementation NSObject (AssociatedObject) 51 | 52 | - (void)setAssociatedObject:(id)object forKey:(void const *)key 53 | { 54 | [self setAssociatedObject:object forKey:key associationPolicy:NSObjectAssociationPolicyRetain]; 55 | } 56 | 57 | - (void)setAssociatedObject:(id)object forKey:(void const *)key associationPolicy:(NSObjectAssociationPolicy)associationPolicy 58 | { 59 | int policy = 0; 60 | switch (associationPolicy) 61 | { 62 | case NSObjectAssociationPolicyRetain: 63 | policy = OBJC_ASSOCIATION_RETAIN_NONATOMIC; 64 | break; 65 | case NSObjectAssociationPolicyCopy: 66 | policy = OBJC_ASSOCIATION_COPY_NONATOMIC; 67 | break; 68 | default: 69 | policy = OBJC_ASSOCIATION_RETAIN_NONATOMIC; 70 | break; 71 | } 72 | objc_setAssociatedObject(self, key, object, policy); 73 | } 74 | 75 | - (id)associatedObjectForKey:(void const *)key 76 | { 77 | return objc_getAssociatedObject(self, key); 78 | } 79 | 80 | - (bool)checkObjectIsKindOfClass:(Class)targetClass { 81 | return [self isKindOfClass:targetClass]; 82 | } 83 | 84 | - (void)setClass:(Class)newClass { 85 | object_setClass(self, newClass); 86 | } 87 | 88 | static Class freedomMakeClass(Class superclass, Class subclass, SEL *copySelectors, int copySelectorsCount) 89 | { 90 | if (superclass == Nil || subclass == Nil) 91 | return nil; 92 | 93 | Class decoratedClass = objc_allocateClassPair(superclass, [[NSString alloc] initWithFormat:@"%@_%@", NSStringFromClass(superclass), NSStringFromClass(subclass)].UTF8String, 0); 94 | 95 | unsigned int count = 0; 96 | Method *methodList = class_copyMethodList(subclass, &count); 97 | if (methodList != NULL) { 98 | for (unsigned int i = 0; i < count; i++) { 99 | SEL methodName = method_getName(methodList[i]); 100 | class_addMethod(decoratedClass, methodName, method_getImplementation(methodList[i]), method_getTypeEncoding(methodList[i])); 101 | } 102 | 103 | free(methodList); 104 | } 105 | 106 | objc_registerClassPair(decoratedClass); 107 | 108 | return decoratedClass; 109 | } 110 | 111 | @end 112 | -------------------------------------------------------------------------------- /Display/RuntimeUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | private let systemVersion = { () -> (Int, Int) in 5 | let string = UIDevice.current.systemVersion as NSString 6 | var minor = 0 7 | let range = string.range(of: ".") 8 | if range.location != NSNotFound { 9 | minor = Int((string.substring(from: range.location + 1) as NSString).intValue) 10 | } 11 | return (Int(string.intValue), minor) 12 | }() 13 | 14 | public func matchMinimumSystemVersion(major: Int, minor: Int = 0) -> Bool { 15 | let version = systemVersion 16 | if version.0 == major { 17 | return version.1 >= minor 18 | } else if version.0 < major { 19 | return false 20 | } else { 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Display/ScrollToTopProxyView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ScrollToTopView: UIScrollView, UIScrollViewDelegate { 4 | var action: (() -> Void)? 5 | 6 | override init(frame: CGRect) { 7 | super.init(frame: frame) 8 | 9 | self.delegate = self 10 | self.scrollsToTop = true 11 | if #available(iOSApplicationExtension 11.0, *) { 12 | self.contentInsetAdjustmentBehavior = .never 13 | } 14 | } 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | override var frame: CGRect { 21 | didSet { 22 | let frame = self.frame 23 | self.contentSize = CGSize(width: frame.width, height: frame.height + 1.0) 24 | self.contentOffset = CGPoint(x: 0.0, y: 1.0) 25 | } 26 | } 27 | 28 | @objc func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { 29 | if let action = self.action { 30 | action() 31 | } 32 | 33 | return false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Display/Spring.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ViewportItemSpring { 4 | let stiffness: CGFloat 5 | let damping: CGFloat 6 | let mass: CGFloat 7 | var velocity: CGFloat = 0.0 8 | 9 | init(stiffness: CGFloat, damping: CGFloat, mass: CGFloat) { 10 | self.stiffness = stiffness 11 | self.damping = damping 12 | self.mass = mass 13 | } 14 | } 15 | 16 | private func a(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat 17 | { 18 | return 1.0 - 3.0 * a2 + 3.0 * a1 19 | } 20 | 21 | private func b(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat 22 | { 23 | return 3.0 * a2 - 6.0 * a1 24 | } 25 | 26 | private func c(_ a1: CGFloat) -> CGFloat 27 | { 28 | return 3.0 * a1 29 | } 30 | 31 | private func calcBezier(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat 32 | { 33 | return ((a(a1, a2)*t + b(a1, a2))*t + c(a1)) * t 34 | } 35 | 36 | private func calcSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat 37 | { 38 | return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1) 39 | } 40 | 41 | private func getTForX(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat { 42 | var t = x 43 | var i = 0 44 | while i < 4 { 45 | let currentSlope = calcSlope(t, x1, x2) 46 | if currentSlope == 0.0 { 47 | return t 48 | } else { 49 | let currentX = calcBezier(t, x1, x2) - x 50 | t -= currentX / currentSlope 51 | } 52 | 53 | i += 1 54 | } 55 | 56 | return t 57 | } 58 | 59 | func bezierPoint(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat) -> CGFloat 60 | { 61 | var value = calcBezier(getTForX(x, x1, x2), y1, y2) 62 | if value >= 0.997 { 63 | value = 1.0 64 | } 65 | return value 66 | } 67 | -------------------------------------------------------------------------------- /Display/StatusBarHost.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftSignalKit 3 | 4 | public protocol StatusBarHost { 5 | var statusBarFrame: CGRect { get } 6 | var statusBarStyle: UIStatusBarStyle { get set } 7 | var statusBarWindow: UIView? { get } 8 | var statusBarView: UIView? { get } 9 | 10 | var keyboardWindow: UIWindow? { get } 11 | var keyboardView: UIView? { get } 12 | 13 | var handleVolumeControl: Signal { get } 14 | } 15 | -------------------------------------------------------------------------------- /Display/SwitchNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | private final class SwitchNodeViewLayer: CALayer { 5 | override func setNeedsDisplay() { 6 | } 7 | } 8 | 9 | private final class SwitchNodeView: UISwitch { 10 | override class var layerClass: AnyClass { 11 | return SwitchNodeViewLayer.self 12 | } 13 | } 14 | 15 | open class SwitchNode: ASDisplayNode { 16 | public var valueUpdated: ((Bool) -> Void)? 17 | 18 | public var frameColor = UIColor(rgb: 0xe0e0e0) { 19 | didSet { 20 | if self.isNodeLoaded { 21 | (self.view as! UISwitch).tintColor = self.frameColor 22 | } 23 | } 24 | } 25 | public var handleColor = UIColor(rgb: 0xffffff) { 26 | didSet { 27 | if self.isNodeLoaded { 28 | //(self.view as! UISwitch).thumbTintColor = self.handleColor 29 | } 30 | } 31 | } 32 | public var contentColor = UIColor(rgb: 0x42d451) { 33 | didSet { 34 | if self.isNodeLoaded { 35 | (self.view as! UISwitch).onTintColor = self.contentColor 36 | } 37 | } 38 | } 39 | 40 | private var _isOn: Bool = false 41 | public var isOn: Bool { 42 | get { 43 | return self._isOn 44 | } set(value) { 45 | if (value != self._isOn) { 46 | self._isOn = value 47 | if self.isNodeLoaded { 48 | (self.view as! UISwitch).setOn(value, animated: false) 49 | } 50 | } 51 | } 52 | } 53 | 54 | override public init() { 55 | super.init() 56 | 57 | self.setViewBlock({ 58 | return SwitchNodeView() 59 | }) 60 | } 61 | 62 | override open func didLoad() { 63 | super.didLoad() 64 | 65 | self.view.isAccessibilityElement = false 66 | 67 | (self.view as! UISwitch).backgroundColor = self.backgroundColor 68 | (self.view as! UISwitch).tintColor = self.frameColor 69 | //(self.view as! UISwitch).thumbTintColor = self.handleColor 70 | (self.view as! UISwitch).onTintColor = self.contentColor 71 | 72 | (self.view as! UISwitch).setOn(self._isOn, animated: false) 73 | 74 | (self.view as! UISwitch).addTarget(self, action: #selector(switchValueChanged(_:)), for: .valueChanged) 75 | } 76 | 77 | public func setOn(_ value: Bool, animated: Bool) { 78 | self._isOn = value 79 | if self.isNodeLoaded { 80 | (self.view as! UISwitch).setOn(value, animated: animated) 81 | } 82 | } 83 | 84 | override open func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 85 | return CGSize(width: 51.0, height: 31.0) 86 | } 87 | 88 | @objc func switchValueChanged(_ view: UISwitch) { 89 | self._isOn = view.isOn 90 | self.valueUpdated?(view.isOn) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Display/TabBarContollerNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public enum ToolbarActionOption { 5 | case left 6 | case right 7 | case middle 8 | } 9 | 10 | final class TabBarControllerNode: ASDisplayNode { 11 | private var theme: TabBarControllerTheme 12 | let tabBarNode: TabBarNode 13 | private let navigationBar: NavigationBar? 14 | private var toolbarNode: ToolbarNode? 15 | private let toolbarActionSelected: (ToolbarActionOption) -> Void 16 | 17 | var currentControllerNode: ASDisplayNode? { 18 | didSet { 19 | oldValue?.removeFromSupernode() 20 | 21 | if let currentControllerNode = self.currentControllerNode { 22 | self.insertSubnode(currentControllerNode, at: 0) 23 | } 24 | } 25 | } 26 | 27 | init(theme: TabBarControllerTheme, navigationBar: NavigationBar?, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void) { 28 | self.theme = theme 29 | self.navigationBar = navigationBar 30 | self.tabBarNode = TabBarNode(theme: theme, itemSelected: itemSelected) 31 | self.toolbarActionSelected = toolbarActionSelected 32 | 33 | super.init() 34 | 35 | self.setViewBlock({ 36 | return UITracingLayerView() 37 | }) 38 | 39 | self.backgroundColor = theme.backgroundColor 40 | 41 | self.addSubnode(self.tabBarNode) 42 | } 43 | 44 | func updateTheme(_ theme: TabBarControllerTheme) { 45 | self.theme = theme 46 | self.backgroundColor = theme.backgroundColor 47 | 48 | self.tabBarNode.updateTheme(theme) 49 | self.toolbarNode?.updateTheme(theme) 50 | } 51 | 52 | func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) { 53 | var tabBarHeight: CGFloat 54 | var options: ContainerViewLayoutInsetOptions = [] 55 | if layout.metrics.widthClass == .regular { 56 | options.insert(.input) 57 | } 58 | let bottomInset: CGFloat = layout.insets(options: options).bottom 59 | if !layout.safeInsets.left.isZero { 60 | tabBarHeight = 34.0 + bottomInset 61 | } else { 62 | tabBarHeight = 49.0 + bottomInset 63 | } 64 | 65 | let tabBarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) 66 | 67 | transition.updateFrame(node: self.tabBarNode, frame: tabBarFrame) 68 | self.tabBarNode.updateLayout(size: layout.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: bottomInset, transition: transition) 69 | 70 | if let toolbar = toolbar { 71 | if let toolbarNode = self.toolbarNode { 72 | transition.updateFrame(node: toolbarNode, frame: tabBarFrame) 73 | toolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: bottomInset, toolbar: toolbar, transition: transition) 74 | } else { 75 | let toolbarNode = ToolbarNode(theme: self.theme, left: { [weak self] in 76 | self?.toolbarActionSelected(.left) 77 | }, right: { [weak self] in 78 | self?.toolbarActionSelected(.right) 79 | }, middle: { [weak self] in 80 | self?.toolbarActionSelected(.middle) 81 | }) 82 | toolbarNode.frame = tabBarFrame 83 | toolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate) 84 | self.addSubnode(toolbarNode) 85 | self.toolbarNode = toolbarNode 86 | if transition.isAnimated { 87 | toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) 88 | } 89 | } 90 | } else if let toolbarNode = self.toolbarNode { 91 | self.toolbarNode = nil 92 | transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in 93 | toolbarNode?.removeFromSupernode() 94 | }) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Display/TabBarTapRecognizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import SwiftSignalKit 4 | 5 | final class TabBarTapRecognizer: UIGestureRecognizer { 6 | private let tap: (CGPoint) -> Void 7 | private let longTap: (CGPoint) -> Void 8 | 9 | private var initialLocation: CGPoint? 10 | private var longTapTimer: SwiftSignalKit.Timer? 11 | 12 | init(tap: @escaping (CGPoint) -> Void, longTap: @escaping (CGPoint) -> Void) { 13 | self.tap = tap 14 | self.longTap = longTap 15 | 16 | super.init(target: nil, action: nil) 17 | } 18 | 19 | override func reset() { 20 | super.reset() 21 | 22 | self.initialLocation = nil 23 | self.longTapTimer?.invalidate() 24 | self.longTapTimer = nil 25 | } 26 | 27 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 28 | super.touchesBegan(touches, with: event) 29 | 30 | if self.initialLocation == nil { 31 | self.initialLocation = touches.first?.location(in: self.view) 32 | let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in 33 | guard let strongSelf = self else { 34 | return 35 | } 36 | if let initialLocation = strongSelf.initialLocation { 37 | strongSelf.initialLocation = nil 38 | strongSelf.longTap(initialLocation) 39 | strongSelf.state = .ended 40 | } 41 | }, queue: Queue.mainQueue()) 42 | self.longTapTimer?.invalidate() 43 | self.longTapTimer = longTapTimer 44 | longTapTimer.start() 45 | } 46 | } 47 | 48 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 49 | super.touchesEnded(touches, with: event) 50 | 51 | if let initialLocation = self.initialLocation { 52 | self.initialLocation = nil 53 | self.longTapTimer?.invalidate() 54 | self.longTapTimer = nil 55 | self.tap(initialLocation) 56 | self.state = .ended 57 | } 58 | } 59 | 60 | override func touchesMoved(_ touches: Set, with event: UIEvent) { 61 | super.touchesMoved(touches, with: event) 62 | 63 | if let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { 64 | let deltaX = initialLocation.x - location.x 65 | let deltaY = initialLocation.y - location.y 66 | if deltaX * deltaX + deltaY * deltaY > 4.0 { 67 | self.longTapTimer?.invalidate() 68 | self.longTapTimer = nil 69 | self.initialLocation = nil 70 | self.state = .failed 71 | } 72 | } 73 | } 74 | 75 | override func touchesCancelled(_ touches: Set, with event: UIEvent) { 76 | super.touchesCancelled(touches, with: event) 77 | 78 | self.initialLocation = nil 79 | self.longTapTimer?.invalidate() 80 | self.longTapTimer = nil 81 | self.state = .failed 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Display/TextFieldNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | public final class TextFieldNodeView: UITextField { 5 | public var didDeleteBackwardWhileEmpty: (() -> Void)? 6 | 7 | var fixOffset: Bool = true 8 | 9 | override public func editingRect(forBounds bounds: CGRect) -> CGRect { 10 | return bounds.offsetBy(dx: 0.0, dy: 0.0).integral 11 | } 12 | 13 | override public func textRect(forBounds bounds: CGRect) -> CGRect { 14 | return bounds.offsetBy(dx: 0.0, dy: 0.0).integral 15 | } 16 | 17 | override public func placeholderRect(forBounds bounds: CGRect) -> CGRect { 18 | return self.editingRect(forBounds: bounds.offsetBy(dx: 0.0, dy: -1.0)) 19 | } 20 | 21 | override public func deleteBackward() { 22 | if self.text == nil || self.text!.isEmpty { 23 | self.didDeleteBackwardWhileEmpty?() 24 | } 25 | super.deleteBackward() 26 | } 27 | 28 | override public var keyboardAppearance: UIKeyboardAppearance { 29 | get { 30 | return super.keyboardAppearance 31 | } 32 | set { 33 | guard newValue != self.keyboardAppearance else { 34 | return 35 | } 36 | let resigning = self.isFirstResponder 37 | if resigning { 38 | self.resignFirstResponder() 39 | } 40 | super.keyboardAppearance = newValue 41 | if resigning { 42 | self.becomeFirstResponder() 43 | } 44 | } 45 | } 46 | } 47 | 48 | public class TextFieldNode: ASDisplayNode { 49 | public var textField: TextFieldNodeView { 50 | return self.view as! TextFieldNodeView 51 | } 52 | 53 | public var fixOffset: Bool = true { 54 | didSet { 55 | self.textField.fixOffset = self.fixOffset 56 | } 57 | } 58 | 59 | override public init() { 60 | super.init() 61 | 62 | self.setViewBlock({ 63 | return TextFieldNodeView() 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Display/Theme.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class Theme { 4 | public let accentColor: UIColor 5 | 6 | public init(accentColor: UIColor) { 7 | self.accentColor = accentColor 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Display/Toolbar.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ToolbarAction: Equatable { 4 | public let title: String 5 | public let isEnabled: Bool 6 | 7 | public init(title: String, isEnabled: Bool) { 8 | self.title = title 9 | self.isEnabled = isEnabled 10 | } 11 | } 12 | 13 | public struct Toolbar: Equatable { 14 | public let leftAction: ToolbarAction? 15 | public let rightAction: ToolbarAction? 16 | public let middleAction: ToolbarAction? 17 | 18 | public init(leftAction: ToolbarAction?, rightAction: ToolbarAction?, middleAction: ToolbarAction?) { 19 | self.leftAction = leftAction 20 | self.rightAction = rightAction 21 | self.middleAction = middleAction 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Display/UIBarButtonItem+Proxy.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | typedef void (^UIBarButtonItemSetTitleListener)(NSString *); 5 | typedef void (^UIBarButtonItemSetEnabledListener)(BOOL); 6 | 7 | @interface UIBarButtonItem (Proxy) 8 | 9 | @property (nonatomic, strong, readonly) ASDisplayNode *customDisplayNode; 10 | @property (nonatomic, readonly) bool backButtonAppearance; 11 | 12 | - (instancetype)initWithCustomDisplayNode:(ASDisplayNode *)customDisplayNode; 13 | - (instancetype)initWithBackButtonAppearanceWithTitle:(NSString *)title target:(id)target action:(SEL)action; 14 | 15 | - (void)performActionOnTarget; 16 | 17 | - (NSInteger)addSetTitleListener:(UIBarButtonItemSetTitleListener)listener; 18 | - (void)removeSetTitleListener:(NSInteger)key; 19 | - (NSInteger)addSetEnabledListener:(UIBarButtonItemSetEnabledListener)listener; 20 | - (void)removeSetEnabledListener:(NSInteger)key; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Display/UIBarButtonItem+Proxy.m: -------------------------------------------------------------------------------- 1 | #import "UIBarButtonItem+Proxy.h" 2 | 3 | #import "NSBag.h" 4 | #import "RuntimeUtils.h" 5 | 6 | static const void *setEnabledListenerBagKey = &setEnabledListenerBagKey; 7 | static const void *setTitleListenerBagKey = &setTitleListenerBagKey; 8 | static const void *customDisplayNodeKey = &customDisplayNodeKey; 9 | static const void *backButtonAppearanceKey = &backButtonAppearanceKey; 10 | 11 | @implementation UIBarButtonItem (Proxy) 12 | 13 | + (void)load 14 | { 15 | static dispatch_once_t onceToken; 16 | dispatch_once(&onceToken, ^ 17 | { 18 | [RuntimeUtils swizzleInstanceMethodOfClass:[UIBarButtonItem class] currentSelector:@selector(setEnabled:) newSelector:@selector(_c1e56039_setEnabled:)]; 19 | [RuntimeUtils swizzleInstanceMethodOfClass:[UIBarButtonItem class] currentSelector:@selector(setTitle:) newSelector:@selector(_c1e56039_setTitle:)]; 20 | }); 21 | } 22 | 23 | - (instancetype)initWithCustomDisplayNode:(ASDisplayNode *)customDisplayNode { 24 | self = [self init]; 25 | if (self != nil) { 26 | [self setAssociatedObject:customDisplayNode forKey:customDisplayNodeKey]; 27 | } 28 | return self; 29 | } 30 | 31 | - (instancetype)initWithBackButtonAppearanceWithTitle:(NSString *)title target:(id)target action:(SEL)action { 32 | self = [self initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action]; 33 | if (self != nil) { 34 | [self setAssociatedObject:@true forKey:backButtonAppearanceKey]; 35 | } 36 | return self; 37 | } 38 | 39 | - (ASDisplayNode *)customDisplayNode { 40 | return [self associatedObjectForKey:customDisplayNodeKey]; 41 | } 42 | 43 | - (bool)backButtonAppearance { 44 | return [[self associatedObjectForKey:backButtonAppearanceKey] boolValue]; 45 | } 46 | 47 | - (void)_c1e56039_setEnabled:(BOOL)enabled 48 | { 49 | [self _c1e56039_setEnabled:enabled]; 50 | 51 | [(NSBag *)[self associatedObjectForKey:setEnabledListenerBagKey] enumerateItems:^(UIBarButtonItemSetEnabledListener listener) 52 | { 53 | listener(enabled); 54 | }]; 55 | } 56 | 57 | - (void)_c1e56039_setTitle:(NSString *)title 58 | { 59 | [self _c1e56039_setTitle:title]; 60 | 61 | [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] enumerateItems:^(UIBarButtonItemSetTitleListener listener) 62 | { 63 | listener(title); 64 | }]; 65 | } 66 | 67 | - (void)performActionOnTarget 68 | { 69 | if (self.target == nil) { 70 | return; 71 | } 72 | 73 | #pragma clang diagnostic push 74 | #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 75 | [self.target performSelector:self.action]; 76 | #pragma clang diagnostic pop 77 | } 78 | 79 | - (NSInteger)addSetTitleListener:(UIBarButtonItemSetTitleListener)listener 80 | { 81 | NSBag *bag = [self associatedObjectForKey:setTitleListenerBagKey]; 82 | if (bag == nil) 83 | { 84 | bag = [[NSBag alloc] init]; 85 | [self setAssociatedObject:bag forKey:setTitleListenerBagKey]; 86 | } 87 | return [bag addItem:[listener copy]]; 88 | } 89 | 90 | - (void)removeSetTitleListener:(NSInteger)key 91 | { 92 | [(NSBag *)[self associatedObjectForKey:setTitleListenerBagKey] removeItem:key]; 93 | } 94 | 95 | - (NSInteger)addSetEnabledListener:(UIBarButtonItemSetEnabledListener)listener 96 | { 97 | NSBag *bag = [self associatedObjectForKey:setEnabledListenerBagKey]; 98 | if (bag == nil) 99 | { 100 | bag = [[NSBag alloc] init]; 101 | [self setAssociatedObject:bag forKey:setEnabledListenerBagKey]; 102 | } 103 | return [bag addItem:[listener copy]]; 104 | } 105 | 106 | - (void)removeSetEnabledListener:(NSInteger)key 107 | { 108 | [(NSBag *)[self associatedObjectForKey:setEnabledListenerBagKey] removeItem:key]; 109 | } 110 | 111 | @end 112 | -------------------------------------------------------------------------------- /Display/UIKitUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface UIView (AnimationUtils) 5 | 6 | + (double)animationDurationFactor; 7 | 8 | @end 9 | 10 | CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath); 11 | CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping); 12 | CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t); 13 | 14 | -------------------------------------------------------------------------------- /Display/UIKitUtils.m: -------------------------------------------------------------------------------- 1 | #import "UIKitUtils.h" 2 | 3 | #import 4 | 5 | #if TARGET_IPHONE_SIMULATOR 6 | UIKIT_EXTERN float UIAnimationDragCoefficient(); // UIKit private drag coeffient, use judiciously 7 | #endif 8 | 9 | @implementation UIView (AnimationUtils) 10 | 11 | + (double)animationDurationFactor 12 | { 13 | #if TARGET_IPHONE_SIMULATOR 14 | return (double)UIAnimationDragCoefficient(); 15 | #endif 16 | 17 | return 1.0f; 18 | } 19 | 20 | @end 21 | 22 | @interface CASpringAnimation () 23 | 24 | @end 25 | 26 | @implementation CASpringAnimation (AnimationUtils) 27 | 28 | - (CGFloat)valueAt:(CGFloat)t { 29 | static dispatch_once_t onceToken; 30 | static float (*impl)(id, float) = NULL; 31 | static double (*dimpl)(id, double) = NULL; 32 | dispatch_once(&onceToken, ^{ 33 | Method method = class_getInstanceMethod([CASpringAnimation class], NSSelectorFromString([@"_" stringByAppendingString:@"solveForInput:"])); 34 | if (method) { 35 | const char *encoding = method_getTypeEncoding(method); 36 | NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:encoding]; 37 | const char *argType = [signature getArgumentTypeAtIndex:2]; 38 | if (strncmp(argType, "f", 1) == 0) { 39 | impl = (float (*)(id, float))method_getImplementation(method); 40 | } else if (strncmp(argType, "d", 1) == 0) { 41 | dimpl = (double (*)(id, double))method_getImplementation(method); 42 | } 43 | } 44 | }); 45 | if (impl) { 46 | float result = impl(self, (float)t); 47 | return (CGFloat)result; 48 | } else if (dimpl) { 49 | double result = dimpl(self, (double)t); 50 | return (CGFloat)result; 51 | } 52 | return t; 53 | } 54 | 55 | @end 56 | 57 | CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath) { 58 | CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath]; 59 | springAnimation.mass = 3.0f; 60 | springAnimation.stiffness = 1000.0f; 61 | springAnimation.damping = 500.0f; 62 | springAnimation.duration = 0.5; 63 | springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 64 | return springAnimation; 65 | } 66 | 67 | CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping) { 68 | CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath]; 69 | springAnimation.mass = 5.0f; 70 | springAnimation.stiffness = 900.0f; 71 | springAnimation.damping = damping; 72 | static bool canSetInitialVelocity = true; 73 | static dispatch_once_t onceToken; 74 | dispatch_once(&onceToken, ^{ 75 | canSetInitialVelocity = [springAnimation respondsToSelector:@selector(setInitialVelocity:)]; 76 | }); 77 | if (canSetInitialVelocity) { 78 | springAnimation.initialVelocity = initialVelocity; 79 | springAnimation.duration = springAnimation.settlingDuration; 80 | } else { 81 | springAnimation.duration = 0.1; 82 | } 83 | springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 84 | return springAnimation; 85 | } 86 | 87 | CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t) { 88 | return [(CASpringAnimation *)animation valueAt:t]; 89 | } 90 | -------------------------------------------------------------------------------- /Display/UIMenuItem+Icons.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface UIMenuItem (Icons) 4 | 5 | - (instancetype)initWithTitle:(NSString *)title icon:(UIImage *)icon action:(SEL)action; 6 | 7 | @end 8 | 9 | -------------------------------------------------------------------------------- /Display/UIMenuItem+Icons.m: -------------------------------------------------------------------------------- 1 | #import "UIMenuItem+Icons.h" 2 | 3 | #import "NSBag.h" 4 | #import "RuntimeUtils.h" 5 | 6 | static const void *imageKey = &imageKey; 7 | static const void *imageViewKey = &imageViewKey; 8 | static NSString *const imageItemIdetifier = @"\uFEFF\u200B"; 9 | 10 | @interface UIMenuController (Icons) 11 | 12 | @end 13 | 14 | @implementation UIMenuController (Icons) 15 | 16 | - (UIMenuItem *)findImageItemByTitle:(NSString *)title { 17 | if ([title hasSuffix:imageItemIdetifier]) { 18 | for (UIMenuItem *item in self.menuItems) { 19 | if ([item.title isEqualToString:title]) { 20 | return item; 21 | } 22 | } 23 | } 24 | return nil; 25 | } 26 | 27 | @end 28 | 29 | 30 | @implementation UIMenuItem (Icons) 31 | 32 | - (instancetype)initWithTitle:(NSString *)title icon:(UIImage *)icon action:(SEL)action { 33 | NSString *combinedTitle = title; 34 | if (icon != nil) { 35 | combinedTitle = [NSString stringWithFormat:@"%@%@", title, imageItemIdetifier]; 36 | } 37 | self = [self initWithTitle:combinedTitle action:action]; 38 | if (self != nil) { 39 | if (icon != nil) { 40 | [self _tg_setImage:icon]; 41 | } 42 | } 43 | return self; 44 | } 45 | 46 | - (UIImage *)_tg_image { 47 | return (UIImage *)[self associatedObjectForKey:imageKey]; 48 | } 49 | 50 | - (void)_tg_setImage:(UIImage *)image { 51 | [self setAssociatedObject:image forKey:imageKey associationPolicy:NSObjectAssociationPolicyRetain]; 52 | } 53 | 54 | @end 55 | 56 | @interface NSString (Items) 57 | 58 | @end 59 | 60 | @implementation NSString (Items) 61 | 62 | + (void)load 63 | { 64 | static dispatch_once_t onceToken; 65 | dispatch_once(&onceToken, ^ 66 | { 67 | [RuntimeUtils swizzleInstanceMethodOfClass:[NSString class] currentSelector:@selector(sizeWithAttributes:) newSelector:@selector(_78724db9_sizeWithAttributes:)]; 68 | }); 69 | } 70 | 71 | - (CGSize)_78724db9_sizeWithAttributes:(NSDictionary *)attrs { 72 | UIMenuItem *item = [[UIMenuController sharedMenuController] findImageItemByTitle:self]; 73 | UIImage *image = item._tg_image; 74 | if (image != nil) { 75 | return image.size; 76 | } else { 77 | return [self _78724db9_sizeWithAttributes:attrs]; 78 | } 79 | } 80 | 81 | @end 82 | 83 | 84 | @interface UILabel (Icons) 85 | 86 | @end 87 | 88 | @implementation UILabel (Icons) 89 | 90 | + (void)load 91 | { 92 | static dispatch_once_t onceToken; 93 | dispatch_once(&onceToken, ^ 94 | { 95 | [RuntimeUtils swizzleInstanceMethodOfClass:[UILabel class] currentSelector:@selector(drawTextInRect:) newSelector:@selector(_78724db9_drawTextInRect:)]; 96 | [RuntimeUtils swizzleInstanceMethodOfClass:[UILabel class] currentSelector:@selector(layoutSubviews) newSelector:@selector(_78724db9_layoutSubviews)]; 97 | [RuntimeUtils swizzleInstanceMethodOfClass:[UILabel class] currentSelector:@selector(setFrame:) newSelector:@selector(_78724db9_setFrame:)]; 98 | }); 99 | } 100 | 101 | - (void)_78724db9_drawTextInRect:(CGRect)rect { 102 | UIMenuItem *item = [[UIMenuController sharedMenuController] findImageItemByTitle:self.text]; 103 | UIImage *image = item._tg_image; 104 | if (image == nil) { 105 | [self _78724db9_drawTextInRect:rect]; 106 | } 107 | } 108 | 109 | - (void)_78724db9_layoutSubviews { 110 | UIMenuItem *item = [[UIMenuController sharedMenuController] findImageItemByTitle:self.text]; 111 | UIImage *image = item._tg_image; 112 | if (image == nil) { 113 | [self _78724db9_layoutSubviews]; 114 | return; 115 | } 116 | 117 | CGPoint point = CGPointMake(ceil((self.bounds.size.width - image.size.width) / 2.0), ceil((self.bounds.size.height - image.size.height) / 2.0)); 118 | UIImageView *imageView = [self associatedObjectForKey:imageViewKey]; 119 | if (imageView == nil) { 120 | imageView = [[UIImageView alloc] init]; 121 | [self addSubview:imageView]; 122 | [self setAssociatedObject:imageView forKey:imageViewKey associationPolicy:NSObjectAssociationPolicyRetain]; 123 | } 124 | 125 | imageView.image = image; 126 | imageView.frame = CGRectMake(point.x, point.y, image.size.width, image.size.height); 127 | } 128 | 129 | - (void)_78724db9_setFrame:(CGRect)frame 130 | { 131 | bool hasImage = [[UIMenuController sharedMenuController] findImageItemByTitle:self.text]._tg_image != nil; 132 | CGRect rect = frame; 133 | if (hasImage && self.superview != nil) { 134 | rect = self.superview.bounds; 135 | } 136 | [self _78724db9_setFrame:rect]; 137 | } 138 | 139 | @end 140 | -------------------------------------------------------------------------------- /Display/UINavigationItem+Proxy.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef void (^UINavigationItemSetTitleListener)(NSString * _Nullable, bool); 4 | typedef void (^UINavigationItemSetTitleViewListener)(UIView * _Nullable); 5 | typedef void (^UINavigationItemSetImageListener)(UIImage * _Nullable); 6 | typedef void (^UINavigationItemSetBarButtonItemListener)(UIBarButtonItem * _Nullable, UIBarButtonItem * _Nullable, BOOL); 7 | typedef void (^UINavigationItemSetMutipleBarButtonItemsListener)(NSArray * _Nullable, BOOL); 8 | typedef void (^UITabBarItemSetBadgeListener)(NSString * _Nullable); 9 | 10 | @interface UINavigationItem (Proxy) 11 | 12 | - (void)setTargetItem:(UINavigationItem * _Nullable)targetItem; 13 | - (BOOL)hasTargetItem; 14 | 15 | - (void)setTitle:(NSString * _Nullable)title animated:(bool)animated; 16 | 17 | - (NSInteger)addSetTitleListener:(UINavigationItemSetTitleListener _Nonnull)listener; 18 | - (void)removeSetTitleListener:(NSInteger)key; 19 | - (NSInteger)addSetTitleViewListener:(UINavigationItemSetTitleViewListener _Nonnull)listener; 20 | - (void)removeSetTitleViewListener:(NSInteger)key; 21 | - (NSInteger)addSetLeftBarButtonItemListener:(UINavigationItemSetBarButtonItemListener _Nonnull)listener; 22 | - (void)removeSetLeftBarButtonItemListener:(NSInteger)key; 23 | - (NSInteger)addSetRightBarButtonItemListener:(UINavigationItemSetBarButtonItemListener _Nonnull)listener; 24 | - (void)removeSetRightBarButtonItemListener:(NSInteger)key; 25 | - (NSInteger)addSetMultipleRightBarButtonItemsListener:(UINavigationItemSetMutipleBarButtonItemsListener _Nonnull)listener; 26 | - (void)removeSetMultipleRightBarButtonItemsListener:(NSInteger)key; 27 | - (NSInteger)addSetBackBarButtonItemListener:(UINavigationItemSetBarButtonItemListener _Nonnull)listener; 28 | - (void)removeSetBackBarButtonItemListener:(NSInteger)key; 29 | - (NSInteger)addSetBadgeListener:(UITabBarItemSetBadgeListener _Nonnull)listener; 30 | - (void)removeSetBadgeListener:(NSInteger)key; 31 | 32 | @property (nonatomic, strong) NSString * _Nullable badge; 33 | 34 | @end 35 | 36 | NSInteger UITabBarItem_addSetBadgeListener(UITabBarItem * _Nonnull item, UITabBarItemSetBadgeListener _Nonnull listener); 37 | 38 | @interface UITabBarItem (Proxy) 39 | 40 | - (void)removeSetBadgeListener:(NSInteger)key; 41 | 42 | - (NSInteger)addSetTitleListener:(UINavigationItemSetTitleListener _Nonnull)listener; 43 | - (void)removeSetTitleListener:(NSInteger)key; 44 | 45 | - (NSInteger)addSetImageListener:(UINavigationItemSetImageListener _Nonnull)listener; 46 | - (void)removeSetImageListener:(NSInteger)key; 47 | 48 | - (NSInteger)addSetSelectedImageListener:(UINavigationItemSetImageListener _Nonnull)listener; 49 | - (void)removeSetSelectedImageListener:(NSInteger)key; 50 | 51 | - (NSObject * _Nullable)userInfo; 52 | - (void)setUserInfo:(NSObject * _Nullable)userInfo; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Display/UIViewController+Navigation.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { 4 | UIResponderDisableAutomaticKeyboardHandlingForward = 1 << 0, 5 | UIResponderDisableAutomaticKeyboardHandlingBackward = 1 << 1 6 | }; 7 | 8 | @interface UIViewController (Navigation) 9 | 10 | - (void)setHintWillBePresentedInPreviewingContext:(BOOL)value; 11 | - (BOOL)isPresentedInPreviewingContext; 12 | - (void)setIgnoreAppearanceMethodInvocations:(BOOL)ignoreAppearanceMethodInvocations; 13 | - (BOOL)ignoreAppearanceMethodInvocations; 14 | - (void)navigation_setNavigationController:(UINavigationController * _Nullable)navigationControlller; 15 | - (void)navigation_setPresentingViewController:(UIViewController * _Nullable)presentingViewController; 16 | - (void)navigation_setDismiss:(void (^_Nullable)())dismiss rootController:( UIViewController * _Nullable )rootController; 17 | - (void)state_setNeedsStatusBarAppearanceUpdate:(void (^_Nullable)())block; 18 | 19 | @end 20 | 21 | @interface UIView (Navigation) 22 | 23 | @property (nonatomic) bool disablesInteractiveTransitionGestureRecognizer; 24 | @property (nonatomic, copy) bool (^ disablesInteractiveTransitionGestureRecognizerNow)(); 25 | 26 | @property (nonatomic) UIResponderDisableAutomaticKeyboardHandling disableAutomaticKeyboardHandling; 27 | 28 | @property (nonatomic, copy) BOOL (^_Nullable interactiveTransitionGestureRecognizerTest)(CGPoint); 29 | 30 | - (void)input_setInputAccessoryHeightProvider:(CGFloat (^_Nullable)())block; 31 | - (CGFloat)input_getInputAccessoryHeight; 32 | 33 | @end 34 | 35 | void applyKeyboardAutocorrection(); 36 | -------------------------------------------------------------------------------- /Display/UIWindow+OrientationChange.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface UIWindow (OrientationChange) 4 | 5 | - (bool)isRotating; 6 | + (void)addPostDeviceOrientationDidChangeBlock:(void (^)())block; 7 | + (bool)isDeviceRotating; 8 | 9 | - (void)_updateToInterfaceOrientation:(int)arg1 duration:(double)arg2 force:(BOOL)arg3; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Display/UIWindow+OrientationChange.m: -------------------------------------------------------------------------------- 1 | #import "UIWindow+OrientationChange.h" 2 | 3 | #import "RuntimeUtils.h" 4 | #import "NotificationCenterUtils.h" 5 | 6 | static const void *isRotatingKey = &isRotatingKey; 7 | 8 | static NSMutableArray *postDeviceDidChangeOrientationBlocks() { 9 | static NSMutableArray *array = nil; 10 | static dispatch_once_t onceToken; 11 | dispatch_once(&onceToken, ^{ 12 | array = [[NSMutableArray alloc] init]; 13 | }); 14 | return array; 15 | } 16 | 17 | static bool _isDeviceRotating = false; 18 | 19 | @implementation UIWindow (OrientationChange) 20 | 21 | + (void)load { 22 | static dispatch_once_t onceToken; 23 | dispatch_once(&onceToken, ^ { 24 | [NotificationCenterUtils addNotificationHandler:^bool(NSString *name, id object, NSDictionary *userInfo, void (^passNotification)()) { 25 | if ([name isEqualToString:@"UIWindowWillRotateNotification"]) { 26 | [(UIWindow *)object setRotating:true]; 27 | 28 | if (NSClassFromString(@"NSUserActivity") == NULL) { 29 | UIInterfaceOrientation orientation = [userInfo[@"UIWindowNewOrientationUserInfoKey"] integerValue]; 30 | CGSize screenSize = [UIScreen mainScreen].bounds.size; 31 | if (screenSize.width > screenSize.height) 32 | { 33 | CGFloat tmp = screenSize.height; 34 | screenSize.height = screenSize.width; 35 | screenSize.width = tmp; 36 | } 37 | CGSize windowSize = CGSizeZero; 38 | CGFloat windowRotation = 0.0; 39 | bool landscape = false; 40 | switch (orientation) { 41 | case UIInterfaceOrientationPortrait: 42 | windowSize = screenSize; 43 | break; 44 | case UIInterfaceOrientationPortraitUpsideDown: 45 | windowRotation = (CGFloat)(M_PI); 46 | windowSize = screenSize; 47 | break; 48 | case UIInterfaceOrientationLandscapeLeft: 49 | landscape = true; 50 | windowRotation = (CGFloat)(-M_PI / 2.0); 51 | windowSize = CGSizeMake(screenSize.height, screenSize.width); 52 | break; 53 | case UIInterfaceOrientationLandscapeRight: 54 | landscape = true; 55 | windowRotation = (CGFloat)(M_PI / 2.0); 56 | windowSize = CGSizeMake(screenSize.height, screenSize.width); 57 | break; 58 | default: 59 | break; 60 | } 61 | 62 | [UIView animateWithDuration:0.3 animations:^ 63 | { 64 | CGAffineTransform transform = CGAffineTransformIdentity; 65 | transform = CGAffineTransformRotate(transform, windowRotation); 66 | ((UIWindow *)object).transform = transform; 67 | ((UIWindow *)object).bounds = CGRectMake(0.0f, 0.0f, windowSize.width, windowSize.height); 68 | }]; 69 | } 70 | 71 | passNotification(); 72 | 73 | return true; 74 | } else if ([name isEqualToString:@"UIWindowDidRotateNotification"]) { 75 | [(UIWindow *)object setRotating:false]; 76 | } else if ([name isEqualToString:UIDeviceOrientationDidChangeNotification]) { 77 | //NSLog(@"notification start: %@", name); 78 | 79 | _isDeviceRotating = true; 80 | 81 | passNotification(); 82 | 83 | if (postDeviceDidChangeOrientationBlocks().count != 0) { 84 | NSArray *blocks = [postDeviceDidChangeOrientationBlocks() copy]; 85 | [postDeviceDidChangeOrientationBlocks() removeAllObjects]; 86 | for (dispatch_block_t block in blocks) { 87 | block(); 88 | } 89 | } 90 | 91 | _isDeviceRotating = false; 92 | 93 | //NSLog(@"notification end: %@", name); 94 | 95 | return true; 96 | } 97 | 98 | return false; 99 | }]; 100 | }); 101 | } 102 | 103 | + (void)addPostDeviceOrientationDidChangeBlock:(void (^)())block { 104 | [postDeviceDidChangeOrientationBlocks() addObject:[block copy]]; 105 | } 106 | 107 | - (void)setRotating:(bool)rotating 108 | { 109 | [self setAssociatedObject:@(rotating) forKey:isRotatingKey]; 110 | } 111 | 112 | - (bool)isRotating 113 | { 114 | return [[self associatedObjectForKey:isRotatingKey] boolValue]; 115 | } 116 | 117 | + (bool)isDeviceRotating { 118 | return _isDeviceRotating; 119 | } 120 | 121 | @end 122 | -------------------------------------------------------------------------------- /Display/UniversalMasterController.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peter-iakovlev/Display/ffb131ac55bed21c37052a09c27150d5d380321e/Display/UniversalMasterController.swift -------------------------------------------------------------------------------- /Display/UniversalTapRecognizer.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import UIKit.UIGestureRecognizerSubclass 3 | 4 | private class TimerTargetWrapper: NSObject { 5 | let f: () -> Void 6 | 7 | init(_ f: @escaping () -> Void) { 8 | self.f = f 9 | } 10 | 11 | @objc func timerEvent() { 12 | self.f() 13 | } 14 | } 15 | 16 | class UniversalTapRecognizer: UITapGestureRecognizer { 17 | private let tapMaxDelay: Double = 0.15 18 | 19 | private var timer: Timer? 20 | 21 | deinit { 22 | self.timer?.invalidate() 23 | } 24 | 25 | override func reset() { 26 | super.reset() 27 | 28 | self.timer?.invalidate() 29 | } 30 | 31 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 32 | super.touchesBegan(touches, with: event) 33 | 34 | let timer = Timer(timeInterval: self.tapMaxDelay, target: TimerTargetWrapper({ [weak self] in 35 | if let strongSelf = self { 36 | if strongSelf.state != .ended { 37 | strongSelf.state = .failed 38 | } 39 | } 40 | }), selector: #selector(TimerTargetWrapper.timerEvent), userInfo: nil, repeats: false) 41 | self.timer = timer 42 | RunLoop.main.add(timer, forMode: .commonModes) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Display/ViewControllerPreviewing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import SwiftSignalKit 4 | 5 | @available(iOSApplicationExtension 9.0, *) 6 | private final class ViewControllerPeekContent: PeekControllerContent { 7 | private let controller: ViewController 8 | private let menu: [PeekControllerMenuItem] 9 | 10 | init(controller: ViewController) { 11 | self.controller = controller 12 | var menu: [PeekControllerMenuItem] = [] 13 | for item in controller.previewActionItems { 14 | menu.append(PeekControllerMenuItem(title: item.title, color: .accent, action: { [weak controller] in 15 | if let controller = controller, let item = item as? UIPreviewAction { 16 | item.handler(item, controller) 17 | } 18 | })) 19 | } 20 | self.menu = menu 21 | } 22 | 23 | func presentation() -> PeekControllerContentPresentation { 24 | return .contained 25 | } 26 | 27 | func menuActivation() -> PeerkControllerMenuActivation { 28 | return .drag 29 | } 30 | 31 | func menuItems() -> [PeekControllerMenuItem] { 32 | return self.menu 33 | } 34 | 35 | func node() -> PeekControllerContentNode & ASDisplayNode { 36 | return ViewControllerPeekContentNode(controller: self.controller) 37 | } 38 | 39 | func topAccessoryNode() -> ASDisplayNode? { 40 | return nil 41 | } 42 | 43 | func isEqual(to: PeekControllerContent) -> Bool { 44 | if let to = to as? ViewControllerPeekContent { 45 | return self.controller === to.controller 46 | } else { 47 | return false 48 | } 49 | } 50 | } 51 | 52 | private final class ViewControllerPeekContentNode: ASDisplayNode, PeekControllerContentNode { 53 | private let controller: ViewController 54 | private var hasValidLayout = false 55 | 56 | init(controller: ViewController) { 57 | self.controller = controller 58 | 59 | super.init() 60 | } 61 | 62 | func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { 63 | if !self.hasValidLayout { 64 | self.hasValidLayout = true 65 | self.controller.view.frame = CGRect(origin: CGPoint(), size: size) 66 | 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) 67 | self.controller.setIgnoreAppearanceMethodInvocations(true) 68 | self.view.addSubview(self.controller.view) 69 | self.controller.setIgnoreAppearanceMethodInvocations(false) 70 | self.controller.viewWillAppear(false) 71 | self.controller.viewDidAppear(false) 72 | } else { 73 | 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) 74 | } 75 | 76 | return size 77 | } 78 | 79 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 80 | if self.bounds.contains(point) { 81 | return self.view 82 | } 83 | return nil 84 | } 85 | } 86 | 87 | @available(iOSApplicationExtension 9.0, *) 88 | final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreviewing { 89 | weak var delegateImpl: UIViewControllerPreviewingDelegate? 90 | var delegate: UIViewControllerPreviewingDelegate { 91 | return self.delegateImpl! 92 | } 93 | let recognizer: PeekControllerGestureRecognizer 94 | var previewingGestureRecognizerForFailureRelationship: UIGestureRecognizer { 95 | return self.recognizer 96 | } 97 | let sourceView: UIView 98 | let node: ASDisplayNode 99 | 100 | var sourceRect: CGRect = CGRect() 101 | 102 | init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void) { 103 | self.delegateImpl = delegate 104 | self.sourceView = sourceView 105 | self.node = node 106 | var contentAtPointImpl: ((CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?)? 107 | self.recognizer = PeekControllerGestureRecognizer(contentAtPoint: { point in 108 | return contentAtPointImpl?(point) 109 | }, present: { content, sourceNode in 110 | let controller = PeekController(theme: theme, content: content, sourceNode: { 111 | return sourceNode 112 | }) 113 | present(controller, nil) 114 | return controller 115 | }) 116 | 117 | node.view.addGestureRecognizer(self.recognizer) 118 | 119 | super.init() 120 | 121 | contentAtPointImpl = { [weak self] point in 122 | if let strongSelf = self, let delegate = strongSelf.delegateImpl { 123 | if let controller = delegate.previewingContext(strongSelf, viewControllerForLocation: point) as? ViewController { 124 | return .single((strongSelf.node, ViewControllerPeekContent(controller: controller))) 125 | } 126 | } 127 | return nil 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Display/ViewControllerTracingNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | 4 | private final class ViewControllerTracingNodeView: UITracingLayerView { 5 | private var inHitTest = false 6 | var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? 7 | 8 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 9 | if self.inHitTest { 10 | return super.hitTest(point, with: event) 11 | } else { 12 | self.inHitTest = true 13 | let result = self.hitTestImpl?(point, event) 14 | self.inHitTest = false 15 | return result 16 | } 17 | } 18 | } 19 | 20 | open class ViewControllerTracingNode: ASDisplayNode { 21 | override public init() { 22 | super.init() 23 | 24 | self.setViewBlock({ 25 | return ViewControllerTracingNodeView() 26 | }) 27 | } 28 | 29 | override open func didLoad() { 30 | super.didLoad() 31 | 32 | (self.view as! ViewControllerTracingNodeView).hitTestImpl = { [weak self] point, event in 33 | return self?.hitTest(point, with: event) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Display/WindowCoveringView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | open class WindowCoveringView: UIView { 5 | open func updateLayout(_ size: CGSize) { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Display/WindowInputAccessoryHeightProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public protocol WindowInputAccessoryHeightProvider: class { 5 | func getWindowInputAccessoryHeight() -> CGFloat 6 | } 7 | -------------------------------------------------------------------------------- /Display/WindowPanRecognizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class WindowPanRecognizer: UIGestureRecognizer { 4 | public var began: ((CGPoint) -> Void)? 5 | public var moved: ((CGPoint) -> Void)? 6 | public var ended: ((CGPoint, CGPoint?) -> Void)? 7 | 8 | private var previousPoints: [(CGPoint, Double)] = [] 9 | 10 | override public func reset() { 11 | super.reset() 12 | 13 | self.previousPoints.removeAll() 14 | } 15 | 16 | private func addPoint(_ point: CGPoint) { 17 | self.previousPoints.append((point, CACurrentMediaTime())) 18 | if self.previousPoints.count > 6 { 19 | self.previousPoints.removeFirst() 20 | } 21 | } 22 | 23 | private func estimateVerticalVelocity() -> CGFloat { 24 | let timestamp = CACurrentMediaTime() 25 | var sum: CGFloat = 0.0 26 | var count = 0 27 | if self.previousPoints.count > 1 { 28 | for i in 1 ..< self.previousPoints.count { 29 | if self.previousPoints[i].1 >= timestamp - 0.1 { 30 | sum += (self.previousPoints[i].0.y - self.previousPoints[i - 1].0.y) / CGFloat(self.previousPoints[i].1 - self.previousPoints[i - 1].1) 31 | count += 1 32 | } 33 | } 34 | } 35 | 36 | if count != 0 { 37 | return sum / CGFloat(count * 5) 38 | } else { 39 | return 0.0 40 | } 41 | } 42 | 43 | override public func touchesBegan(_ touches: Set, with event: UIEvent) { 44 | super.touchesBegan(touches, with: event) 45 | 46 | if let touch = touches.first { 47 | let location = touch.location(in: self.view) 48 | self.addPoint(location) 49 | self.began?(location) 50 | } 51 | } 52 | 53 | override public func touchesMoved(_ touches: Set, with event: UIEvent) { 54 | super.touchesMoved(touches, with: event) 55 | 56 | if let touch = touches.first { 57 | let location = touch.location(in: self.view) 58 | self.addPoint(location) 59 | self.moved?(location) 60 | } 61 | } 62 | 63 | override public func touchesEnded(_ touches: Set, with event: UIEvent) { 64 | super.touchesEnded(touches, with: event) 65 | 66 | if let touch = touches.first { 67 | let location = touch.location(in: self.view) 68 | self.addPoint(location) 69 | self.ended?(location, CGPoint(x: 0.0, y: self.estimateVerticalVelocity())) 70 | } 71 | } 72 | 73 | override public func touchesCancelled(_ touches: Set, with event: UIEvent) { 74 | super.touchesCancelled(touches, with: event) 75 | 76 | if let touch = touches.first { 77 | self.ended?(touch.location(in: self.view), nil) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /DisplayTests/DisplayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayTests.swift 3 | // DisplayTests 4 | // 5 | // Created by Peter on 29/07/15. 6 | // Copyright © 2015 Telegram. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Display 11 | 12 | class DisplayTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /DisplayTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | --------------------------------------------------------------------------------