import Foundation import UIKit import AsyncDisplayKit import SwiftSignalKit public enum TooltipControllerContent: Equatable { case text(String) case attributedText(NSAttributedString) case iconAndText(UIImage, String) var text: String { switch self { case let .text(text), let .iconAndText(_, text): return text case let .attributedText(text): return text.string } } var image: UIImage? { if case let .iconAndText(image, _) = self { return image } return nil } } public enum SourceAndRect { case node(() -> (ASDisplayNode, CGRect)?) case view(() -> (UIView, CGRect)?) func globalRect() -> CGRect? { switch self { case let .node(node): if let (sourceNode, sourceRect) = node() { return sourceNode.view.convert(sourceRect, to: nil) } case let .view(view): if let (sourceView, sourceRect) = view() { return sourceView.convert(sourceRect, to: nil) } } return nil } } public final class TooltipControllerPresentationArguments { public let sourceAndRect: SourceAndRect public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?) { self.sourceAndRect = .node(sourceNodeAndRect) } public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?) { self.sourceAndRect = .view(sourceViewAndRect) } } open class TooltipController: ViewController, StandalonePresentableController { private var controllerNode: TooltipControllerNode { return self.displayNode as! TooltipControllerNode } public private(set) var content: TooltipControllerContent private let baseFontSize: CGFloat open func updateContent(_ content: TooltipControllerContent, animated: Bool, extendTimer: Bool, arrowOnBottom: Bool = true) { if self.content != content { self.content = content if self.isNodeLoaded { self.controllerNode.updateText(self.content.text, transition: animated ? .animated(duration: 0.25, curve: .easeInOut) : .immediate) self.controllerNode.arrowOnBottom = arrowOnBottom if extendTimer, self.timeoutTimer != nil { self.timeoutTimer?.invalidate() self.timeoutTimer = nil self.beginTimeout() } } } } private let timeout: Double private let dismissByTapOutside: Bool private let dismissByTapOutsideSource: Bool private let dismissImmediatelyOnLayoutUpdate: Bool private var timeoutTimer: SwiftSignalKit.Timer? private var layout: ContainerViewLayout? private var initialArrowOnBottom: Bool public var dismissed: ((Bool) -> Void)? public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true) { self.content = content self.baseFontSize = baseFontSize self.timeout = timeout self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource self.dismissImmediatelyOnLayoutUpdate = dismissImmediatelyOnLayoutUpdate self.initialArrowOnBottom = arrowOnBottom super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.timeoutTimer?.invalidate() } override open func loadDisplayNode() { self.displayNode = TooltipControllerNode(content: self.content, baseFontSize: self.baseFontSize, dismiss: { [weak self] tappedInside in self?.dismiss(tappedInside: tappedInside) }, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource) self.controllerNode.arrowOnBottom = self.initialArrowOnBottom self.displayNodeDidLoad() } override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.controllerNode.animateIn() self.beginTimeout() } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) if self.layout != nil && self.layout! != layout { if self.dismissImmediatelyOnLayoutUpdate { self.dismissImmediately() } else { self.dismiss() } } else { self.layout = layout if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect() { self.controllerNode.sourceRect = sourceRect } else { self.controllerNode.sourceRect = nil } self.controllerNode.containerLayoutUpdated(layout, transition: transition) } } override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.controllerNode.animateIn() self.beginTimeout() } private func beginTimeout() { if self.timeoutTimer == nil { let timeoutTimer = SwiftSignalKit.Timer(timeout: self.timeout, repeat: false, completion: { [weak self] in if let strongSelf = self { strongSelf.dismissed?(false) strongSelf.controllerNode.animateOut { self?.presentingViewController?.dismiss(animated: false) } } }, queue: Queue.mainQueue()) self.timeoutTimer = timeoutTimer timeoutTimer.start() } } private func dismiss(tappedInside: Bool, completion: (() -> Void)? = nil) { self.dismissed?(tappedInside) self.controllerNode.animateOut { [weak self] in self?.presentingViewController?.dismiss(animated: false) completion?() } } override open func dismiss(completion: (() -> Void)? = nil) { self.dismiss(tappedInside: false, completion: completion) } open func dismissImmediately() { self.dismissed?(false) self.controllerNode.hide() self.presentingViewController?.dismiss(animated: false) } }